Skip to content

Commit 1dfbb23

Browse files
committed
Python bindings: add gdal.run() context manager to run GDAL algorithms
Fixes #11832
1 parent 5ac6247 commit 1dfbb23

File tree

4 files changed

+193
-59
lines changed

4 files changed

+193
-59
lines changed

autotest/utilities/test_gdal.py

+51
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,54 @@ def test_gdal_algorithm_getter_setter():
439439

440440
with pytest.raises(Exception):
441441
alg["no-mask"] = "bar"
442+
443+
444+
def test_gdal_run():
445+
446+
with pytest.raises(
447+
Exception, match="i_do_not_exist is not a valid sub-algorithm of gdal"
448+
):
449+
with gdal.run("i_do_not_exist"):
450+
pass
451+
452+
with pytest.raises(
453+
Exception, match="i_do_not_exist is not a valid sub-algorithm of gdal"
454+
):
455+
with gdal.run(["i_do_not_exist"]):
456+
pass
457+
458+
with pytest.raises(
459+
Exception, match="Wrong type for algorithm. Expected string or list of string"
460+
):
461+
with gdal.run(None):
462+
pass
463+
464+
with gdal.run("raster info", {"input": "../gcore/data/byte.tif"}) as res:
465+
assert len(res["bands"]) == 1
466+
467+
with gdal.run("gdal raster info", input="../gcore/data/byte.tif") as res:
468+
assert len(res["bands"]) == 1
469+
470+
with gdal.run(
471+
"raster info", {"input": "../gcore/data/byte.tif"}, optimize_single_output=False
472+
) as res:
473+
assert len(res) == 1
474+
assert res["output-string"].startswith("{")
475+
476+
with gdal.run(
477+
["gdal", "raster", "reproject"],
478+
input="../gcore/data/byte.tif",
479+
output_format="MEM",
480+
dst_crs="EPSG:4326",
481+
) as ds:
482+
assert ds.GetSpatialRef().GetAuthorityCode(None) == "4326"
483+
484+
with gdal.run(
485+
["raster", "reproject"],
486+
{
487+
"input": "../gcore/data/byte.tif",
488+
"output-format": "MEM",
489+
"dst-crs": "EPSG:4326",
490+
},
491+
) as ds:
492+
assert ds.GetSpatialRef().GetAuthorityCode(None) == "4326"

doc/source/programs/gdal_cli_from_python.rst

+17-59
Original file line numberDiff line numberDiff line change
@@ -12,79 +12,53 @@ Raster commands
1212
.. code-block:: python
1313
1414
from osgeo import gdal
15-
import json
1615
1716
gdal.UseExceptions()
18-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["info"]
19-
alg["input"] = "byte.tif"
20-
alg.Run()
21-
info = json.loads(alg["output-string"])
22-
print(info)
17+
with gdal.run(["raster", "info"], {"input": "byte.tif"}) as info:
18+
print(info)
2319
2420
2521
* Converting a georeferenced netCDF file to cloud-optimized GeoTIFF
2622

2723
.. code-block:: python
2824
2925
from osgeo import gdal
26+
3027
gdal.UseExceptions()
31-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["convert"]
32-
alg["input"] = "in.nc"
33-
alg["output"] = "out.tif"
34-
alg["output-format"] = "COG"
35-
alg["overwrite"] = True # if the output file may exist
36-
alg.Run()
37-
alg.Finalize() # ensure output dataset is closed
28+
with gdal.run(["raster", "convert"], {"input": "in.tif", "output": "out.tif", "output-format": "COG", "overwrite": True}):
29+
pass
3830
3931
or
4032

4133
.. code-block:: python
4234
4335
from osgeo import gdal
36+
4437
gdal.UseExceptions()
45-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["convert"]
46-
alg.ParseRunAndFinalize(["--input=in.nc", "--output-format=COG", "--output=out.tif", "--overwrite"])
38+
with gdal.run(["raster", "convert"], input="in.tif", output="out.tif", output_format="COG", overwrite=True):
39+
pass
4740
4841
4942
* Reprojecting a GeoTIFF file to a Deflate compressed tiled GeoTIFF file
5043

5144
.. code-block:: python
5245
5346
from osgeo import gdal
54-
gdal.UseExceptions()
55-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["reproject"]
56-
alg["input"] = "in.tif"
57-
alg["output"] = "out.tif"
58-
alg["dst-crs"] = "EPSG:4326"
59-
alg["creation-options"] = ["TILED=YES", "COMPRESS=DEFLATE"]
60-
alg["overwrite"] = True # if the output file may exist
61-
alg.Run()
62-
alg.Finalize() # ensure output dataset is closed
63-
64-
or
65-
66-
.. code-block:: python
6747
68-
from osgeo import gdal
6948
gdal.UseExceptions()
70-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["reproject"]
71-
alg.ParseRunAndFinalize(["--input=in.tif", "--output=out.tif", "--dst-crs=EPSG:4326", "--co=TILED=YES,COMPRESS=DEFLATE", "--overwrite"])
49+
with gdal.run(["raster", "reproject"], {"input": "in.tif", "output": "out.tif", "dst-crs": "EPSG:4326", "creation-options": { "TILED": "YES", "COMPRESS": "DEFLATE"} }):
50+
pass
7251
7352
7453
* Reprojecting a (possibly in-memory) dataset to a in-memory dataset
7554

7655
.. code-block:: python
7756
7857
from osgeo import gdal
79-
gdal.UseExceptions()
80-
alg = gdal.GetGlobalAlgorithmRegistry()["raster"]["reproject"]
81-
alg["input"] = input_ds
82-
alg["output"] = "dummy_name"
83-
alg["output-format"] = "MEM"
84-
alg["dst-crs"] = "EPSG:4326"
85-
alg.Run()
86-
output_ds = alg["output"].GetDataset()
8758
59+
gdal.UseExceptions()
60+
with gdal.run(["raster", "reproject"], {"input": "in.tif", "output-format": "MEM", "dst-crs": "EPSG:4326"}) as ds:
61+
print(ds.ReadAsArray())
8862
8963
9064
Vector commands
@@ -95,34 +69,18 @@ Vector commands
9569
.. code-block:: python
9670
9771
from osgeo import gdal
98-
import json
9972
10073
gdal.UseExceptions()
101-
alg = gdal.GetGlobalAlgorithmRegistry()["vector"]["info"]
102-
alg["input"] = "poly.gpkg"
103-
alg.Run()
104-
info = json.loads(alg["output-string"])
105-
print(info)
74+
with gdal.run(["raster", "info"], {"input": "poly.gpkg"}) as info:
75+
print(info)
10676
10777
10878
* Converting a shapefile to a GeoPackage
10979

11080
.. code-block:: python
11181
11282
from osgeo import gdal
113-
gdal.UseExceptions()
114-
alg = gdal.GetGlobalAlgorithmRegistry()["vector"]["convert"]
115-
alg["input"] = "in.shp"
116-
alg["output"] = "out.gpkg"
117-
alg["overwrite"] = True # if the output file may exist
118-
alg.Run()
119-
alg.Finalize() # ensure output dataset is closed
120-
121-
or
122-
123-
.. code-block:: python
12483
125-
from osgeo import gdal
12684
gdal.UseExceptions()
127-
alg = gdal.GetGlobalAlgorithmRegistry()["vector"]["convert"]
128-
alg.ParseRunAndFinalize(["--input=in.shp", "--output=out.gpkg", "--overwrite"])
85+
with gdal.run(["raster", "convert"], {"input": "in.shp", "output": "out.gpkg", "overwrite": True}):
86+
pass

gcore/gdalalgorithm.cpp

+14
Original file line numberDiff line numberDiff line change
@@ -1941,6 +1941,20 @@ bool GDALAlgorithm::ValidateArguments()
19411941
if (m_specialActionRequested)
19421942
return true;
19431943

1944+
// If only --output=format=MEM is specified and not --output,
1945+
// then set empty name for --output.
1946+
auto outputArg = GetArg(GDAL_ARG_NAME_OUTPUT);
1947+
auto outputFormatArg = GetArg(GDAL_ARG_NAME_OUTPUT_FORMAT);
1948+
if (outputArg && outputFormatArg && outputFormatArg->IsExplicitlySet() &&
1949+
!outputArg->IsExplicitlySet() &&
1950+
outputFormatArg->GetType() == GAAT_STRING &&
1951+
EQUAL(outputFormatArg->Get<std::string>().c_str(), "MEM") &&
1952+
outputArg->GetType() == GAAT_DATASET &&
1953+
(outputArg->Get<GDALArgDatasetValue>().GetInputFlags() & GADV_NAME))
1954+
{
1955+
outputArg->Get<GDALArgDatasetValue>().Set("");
1956+
}
1957+
19441958
// The method may emit several errors if several constraints are not met.
19451959
bool ret = true;
19461960
std::map<std::string, std::string> mutualExclusionGroupUsed;

swig/include/python/gdal_python.i

+111
Original file line numberDiff line numberDiff line change
@@ -5165,6 +5165,117 @@ def quiet_warnings():
51655165
finally:
51665166
PopErrorHandler()
51675167

5168+
5169+
@contextlib.contextmanager
5170+
def run(algorithm=None, arguments={}, progress=None, optimize_single_output=True, finalize=True, **kwargs):
5171+
"""Run a GDAL algorithm as a context manager
5172+
5173+
.. versionadded: 3.11
5174+
5175+
Parameters
5176+
----------
5177+
algorithm: str or list[str]
5178+
Path to the algorithm. For example "raster info", or ["raster", "info"].
5179+
arguments: dict
5180+
Input arguments of the algorithm. For example {"format": "json", "input": "byte.tif"}
5181+
progress: callable
5182+
Progress function whose arguments are a progress ratio, a string and a user data
5183+
optimize_single_output: bool
5184+
Whether to return a single value when there is a single output argument, and to
5185+
deserialize automatically JSON responses as a dict, and return a osgeo.gdal.Dataset
5186+
when possible.
5187+
finalize: bool
5188+
Whether to run :py:func:`gdal.Algorithm.Finalize` when the context is released.
5189+
kwargs:
5190+
Instead of using the ``arguments`` parameter, it is possible to pass
5191+
algorithm arguments directly as named parameters of gdal.run().
5192+
If the named argument has dash characters in it, the corresponding
5193+
parameter must replace them with an underscore character.
5194+
For example ``dst_crs`` as a a parameter of gdal.run(), instead of
5195+
``dst-crs`` which is the name to use on the command line.
5196+
5197+
Returns
5198+
-------
5199+
A context manager with the output arguments of the algorithm
5200+
5201+
Example
5202+
-------
5203+
5204+
>>> with gdal.run(["raster", "info"], {"input": "byte.tif"}) as res:
5205+
... print(res["bands"])
5206+
5207+
>>> with gdal.run("raster reproject", input="byte.tif", output_format="MEM", dst_crs="EPSG:4326") as ds
5208+
... print(ds.ReadAsArray())
5209+
"""
5210+
5211+
if isinstance(algorithm, list):
5212+
alg = GetGlobalAlgorithmRegistry()
5213+
parent_name = "gdal"
5214+
for i, v in enumerate(algorithm):
5215+
if i == 0 and v == "gdal":
5216+
continue
5217+
subalg = alg[v]
5218+
if not subalg:
5219+
raise Exception(f"{v} is not a valid sub-algorithm of {parent_name}")
5220+
alg = subalg
5221+
parent_name = alg.GetName()
5222+
elif isinstance(algorithm, str):
5223+
if algorithm.startswith("gdal "):
5224+
algorithm = algorithm[len("gdal "):]
5225+
alg = GetGlobalAlgorithmRegistry()
5226+
parent_name = "gdal"
5227+
for i, v in enumerate(algorithm.split(' ')):
5228+
if i == 0 and v == "gdal":
5229+
continue
5230+
subalg = alg[v]
5231+
if not subalg:
5232+
raise Exception(f"{v} is not a valid sub-algorithm of {parent_name}")
5233+
alg = subalg
5234+
parent_name = alg.GetName()
5235+
else:
5236+
raise Exception("Wrong type for algorithm. Expected string or list of string")
5237+
5238+
for k in arguments:
5239+
alg[k.replace('_', '-')] = arguments[k]
5240+
5241+
for k in kwargs:
5242+
alg[k.replace('_', '-')] = kwargs[k]
5243+
5244+
assert alg.Run(progress)
5245+
5246+
count_output = 0
5247+
if optimize_single_output:
5248+
for name in alg.GetArgNames():
5249+
arg = alg.GetArg(name)
5250+
if arg.IsOutput():
5251+
count_output += 1
5252+
5253+
res = {}
5254+
for name in alg.GetArgNames():
5255+
arg = alg.GetArg(name)
5256+
if arg.IsOutput():
5257+
val = alg[name]
5258+
if name == "output-string" and count_output == 1:
5259+
if (val.startswith('{') and (val.endswith('}') or val.endswith('}\n'))) or \
5260+
(val.startswith('[') and (val.endswith(']') or val.endswith(']\n'))):
5261+
import json
5262+
res = json.loads(val)
5263+
else:
5264+
res = val
5265+
elif count_output == 1:
5266+
if arg.GetType() == GAAT_DATASET:
5267+
res = val.GetDataset()
5268+
else:
5269+
res = val
5270+
else:
5271+
res[name] = val
5272+
5273+
yield res
5274+
5275+
if finalize:
5276+
assert alg.Finalize()
5277+
5278+
51685279
%}
51695280

51705281

0 commit comments

Comments
 (0)