From 3cc38d676266e8da335226f927d19f778e2da774 Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Tue, 22 Sep 2026 15:50:47 +0300 Subject: [PATCH 1/4] Python bindings: Invalidate Band and Dataset child refs when Dataset closes --- swig/include/python/gdal_python.i | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/swig/include/python/gdal_python.i b/swig/include/python/gdal_python.i index 9544ef7013ef..13d13f31d01d 100644 --- a/swig/include/python/gdal_python.i +++ b/swig/include/python/gdal_python.i @@ -1346,6 +1346,31 @@ void wrapper_VSIGetMemFileBuffer(const char *utf8_string, GByte **out, vsi_l_off self._parent_ds()._add_child_ref(val) %} +%feature("pythonappend") GetSampleOverview %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + +%feature("pythonappend") GetDefaultRAT %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + +%feature("pythonappend") GetRasterColorTable %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + +%feature("pythonappend") GetColorTable %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + +%feature("pythonappend") GetDataset %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + %feature("shadow") ComputeStatistics %{ def ComputeStatistics(self, *args, **kwargs): """ComputeStatistics(Band self, bool approx_ok, callback=None, callback_data=None) @@ -2604,6 +2629,14 @@ def ComputeInterBandCovarianceMatrix(self, self._add_child_ref(val) %} +%feature("pythonappend") GetFieldDomain %{ + self._add_child_ref(val) +%} + +%feature("pythonappend") GetStyleTable %{ + self._add_child_ref(val) +%} + } %extend GDALMajorObjectShadow { From 3644e7753aef9b12364dc3be79d4d2474e090798 Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Tue, 22 Sep 2026 15:50:47 +0300 Subject: [PATCH 2/4] Python bindings: Invalidate Layer child refs when Dataset closes --- swig/include/python/ogr_python.i | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/swig/include/python/ogr_python.i b/swig/include/python/ogr_python.i index 338abaa123b5..15b1debb9b51 100644 --- a/swig/include/python/ogr_python.i +++ b/swig/include/python/ogr_python.i @@ -496,6 +496,16 @@ def _WarnIfUserHasNotSpecifiedIfUsingExceptions(): %} +%feature("pythonappend") GetSpatialFilter %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + +%feature("pythonappend") GetStyleTable %{ + if hasattr(self, '_parent_ds') and self._parent_ds(): + self._parent_ds()._add_child_ref(val) +%} + } %extend OGRFeatureShadow { From 0a736909c9f4546dd8eeb8bf6b5cc81823561ced Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Tue, 22 Sep 2026 15:50:47 +0300 Subject: [PATCH 3/4] autotest: Add lifetime tests for Dataset child refs --- autotest/gcore/basic_test.py | 98 +++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/autotest/gcore/basic_test.py b/autotest/gcore/basic_test.py index 115027a4618e..652345e4ff52 100755 --- a/autotest/gcore/basic_test.py +++ b/autotest/gcore/basic_test.py @@ -19,7 +19,7 @@ import gdaltest import pytest -from osgeo import gdal, osr +from osgeo import gdal, ogr, osr ############################################################################### @@ -1029,6 +1029,102 @@ def test_ovr_band_use_after_dataset_close(): ovr.Checksum() +def test_sample_ovr_band_use_after_dataset_close(): + with gdal.Open("data/byte_with_ovr.tif") as ds: + ovr = ds.GetRasterBand(1).GetSampleOverview(0) + + # Make sure ds.__exit__() invalidation has propagated to sample overviews + + with pytest.raises(Exception): + ovr.Checksum() + + +@pytest.mark.require_driver("HFA") +def test_rat_use_after_dataset_close(): + with gdal.Open("data/rat.img") as ds: + rat = ds.GetRasterBand(1).GetDefaultRAT() + + # Make sure ds.__exit__() invalidation has propagated to the RAT + + with pytest.raises(Exception): + rat.GetRowCount() + + +@pytest.mark.require_driver("BMP") +def test_color_table_use_after_dataset_close(): + with gdal.Open("data/8bit_pal.bmp") as ds: + ct = ds.GetRasterBand(1).GetRasterColorTable() + + # Make sure ds.__exit__() invalidation has propagated to the color table + + with pytest.raises(Exception): + ct.GetCount() + + +def test_field_domain_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + ds.AddFieldDomain( + ogr.CreateRangeFieldDomain( + "dom", "", ogr.OFTInteger, ogr.OFSTNone, 1, True, 2, True + ) + ) + dom = ds.GetFieldDomain("dom") + + # Make sure ds.__exit__() invalidation has propagated to the field domain + + with pytest.raises(Exception): + dom.GetName() + + +def test_style_table_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + st = ogr.StyleTable() + st.AddStyle("style", "PEN(c:#FF0000)") + ds.SetStyleTable(st) + style_table = ds.GetStyleTable() + + # Make sure ds.__exit__() invalidation has propagated to the style table + + with pytest.raises(Exception): + style_table.Find("style") + + +def test_layer_style_table_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + lyr = ds.CreateLayer("lyr") + st = ogr.StyleTable() + st.AddStyle("style", "PEN(c:#FF0000)") + lyr.SetStyleTable(st) + style_table = lyr.GetStyleTable() + + # Make sure ds.__exit__() invalidation has propagated to the layer style table + + with pytest.raises(Exception): + style_table.Find("style") + + +def test_spatial_filter_use_after_dataset_close(): + with gdal.OpenEx("../ogr/data/poly.shp") as ds: + lyr = ds.GetLayer(0) + lyr.SetSpatialFilterRect(0, 0, 1, 1) + filter_geom = lyr.GetSpatialFilter() + + # Make sure ds.__exit__() invalidation has propagated to the spatial filter + + with pytest.raises(Exception): + filter_geom.ExportToWkt() + + +def test_band_dataset_use_after_dataset_close(): + with gdal.Open("data/byte.tif") as ds: + band_ds = ds.GetRasterBand(1).GetDataset() + + # Make sure ds.__exit__() invalidation has propagated to Band.GetDataset() + + with pytest.raises(Exception): + assert band_ds.RasterXSize + + @pytest.mark.slow() def test_checksum_more_than_2billion_pixels(): From 5fdf8333817ac2d030e6494f13200b676f938c2a Mon Sep 17 00:00:00 2001 From: Alon Faraj Date: Tue, 22 Sep 2026 18:02:14 +0300 Subject: [PATCH 4/4] autotest: Move OGR child ref tests and match expected error --- autotest/gcore/basic_test.py | 76 ++++++++-------------------------- autotest/ogr/ogr_basic_test.py | 70 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/autotest/gcore/basic_test.py b/autotest/gcore/basic_test.py index 652345e4ff52..c996a46d5ed9 100755 --- a/autotest/gcore/basic_test.py +++ b/autotest/gcore/basic_test.py @@ -19,7 +19,7 @@ import gdaltest import pytest -from osgeo import gdal, ogr, osr +from osgeo import gdal, osr ############################################################################### @@ -1035,7 +1035,10 @@ def test_sample_ovr_band_use_after_dataset_close(): # Make sure ds.__exit__() invalidation has propagated to sample overviews - with pytest.raises(Exception): + with pytest.raises( + Exception, + match=r"in method 'Band_Checksum', argument 1 of type 'GDALRasterBandShadow \*'", + ): ovr.Checksum() @@ -1046,7 +1049,10 @@ def test_rat_use_after_dataset_close(): # Make sure ds.__exit__() invalidation has propagated to the RAT - with pytest.raises(Exception): + with pytest.raises( + Exception, + match=r"in method 'RasterAttributeTable_GetRowCount', argument 1 of type 'GDALRasterAttributeTableShadow \*'", + ): rat.GetRowCount() @@ -1057,71 +1063,23 @@ def test_color_table_use_after_dataset_close(): # Make sure ds.__exit__() invalidation has propagated to the color table - with pytest.raises(Exception): + with pytest.raises( + Exception, + match=r"in method 'ColorTable_GetCount', argument 1 of type 'GDALColorTableShadow \*'", + ): ct.GetCount() -def test_field_domain_use_after_dataset_close(): - with gdal.GetDriverByName("MEM").CreateVector("") as ds: - ds.AddFieldDomain( - ogr.CreateRangeFieldDomain( - "dom", "", ogr.OFTInteger, ogr.OFSTNone, 1, True, 2, True - ) - ) - dom = ds.GetFieldDomain("dom") - - # Make sure ds.__exit__() invalidation has propagated to the field domain - - with pytest.raises(Exception): - dom.GetName() - - -def test_style_table_use_after_dataset_close(): - with gdal.GetDriverByName("MEM").CreateVector("") as ds: - st = ogr.StyleTable() - st.AddStyle("style", "PEN(c:#FF0000)") - ds.SetStyleTable(st) - style_table = ds.GetStyleTable() - - # Make sure ds.__exit__() invalidation has propagated to the style table - - with pytest.raises(Exception): - style_table.Find("style") - - -def test_layer_style_table_use_after_dataset_close(): - with gdal.GetDriverByName("MEM").CreateVector("") as ds: - lyr = ds.CreateLayer("lyr") - st = ogr.StyleTable() - st.AddStyle("style", "PEN(c:#FF0000)") - lyr.SetStyleTable(st) - style_table = lyr.GetStyleTable() - - # Make sure ds.__exit__() invalidation has propagated to the layer style table - - with pytest.raises(Exception): - style_table.Find("style") - - -def test_spatial_filter_use_after_dataset_close(): - with gdal.OpenEx("../ogr/data/poly.shp") as ds: - lyr = ds.GetLayer(0) - lyr.SetSpatialFilterRect(0, 0, 1, 1) - filter_geom = lyr.GetSpatialFilter() - - # Make sure ds.__exit__() invalidation has propagated to the spatial filter - - with pytest.raises(Exception): - filter_geom.ExportToWkt() - - def test_band_dataset_use_after_dataset_close(): with gdal.Open("data/byte.tif") as ds: band_ds = ds.GetRasterBand(1).GetDataset() # Make sure ds.__exit__() invalidation has propagated to Band.GetDataset() - with pytest.raises(Exception): + with pytest.raises( + Exception, + match=r"in method 'Dataset_RasterXSize_get', argument 1 of type 'GDALDatasetShadow \*'", + ): assert band_ds.RasterXSize diff --git a/autotest/ogr/ogr_basic_test.py b/autotest/ogr/ogr_basic_test.py index 329de5a0e9f7..5b8cfc23c599 100755 --- a/autotest/ogr/ogr_basic_test.py +++ b/autotest/ogr/ogr_basic_test.py @@ -1384,3 +1384,73 @@ def test_ogr_setpoint_grows_geometry(): assert g.GetPointCount() == 16 g.SetPointZM(20, 1, 1, 1, 1) + + +############################################################################### +# Test that objects borrowed from a Dataset are invalidated when it closes + + +def test_field_domain_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + ds.AddFieldDomain( + ogr.CreateRangeFieldDomain( + "dom", "", ogr.OFTInteger, ogr.OFSTNone, 1, True, 2, True + ) + ) + dom = ds.GetFieldDomain("dom") + + # Make sure ds.__exit__() invalidation has propagated to the field domain + + with pytest.raises( + Exception, + match=r"in method 'FieldDomain_GetName', argument 1 of type 'OGRFieldDomainShadow \*'", + ): + dom.GetName() + + +def test_style_table_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + st = ogr.StyleTable() + st.AddStyle("style", "PEN(c:#FF0000)") + ds.SetStyleTable(st) + style_table = ds.GetStyleTable() + + # Make sure ds.__exit__() invalidation has propagated to the style table + + with pytest.raises( + Exception, + match=r"in method 'StyleTable_Find', argument 1 of type 'OGRStyleTableShadow \*'", + ): + style_table.Find("style") + + +def test_layer_style_table_use_after_dataset_close(): + with gdal.GetDriverByName("MEM").CreateVector("") as ds: + lyr = ds.CreateLayer("lyr") + st = ogr.StyleTable() + st.AddStyle("style", "PEN(c:#FF0000)") + lyr.SetStyleTable(st) + style_table = lyr.GetStyleTable() + + # Make sure ds.__exit__() invalidation has propagated to the layer style table + + with pytest.raises( + Exception, + match=r"in method 'StyleTable_Find', argument 1 of type 'OGRStyleTableShadow \*'", + ): + style_table.Find("style") + + +def test_spatial_filter_use_after_dataset_close(): + with gdal.OpenEx("data/poly.shp") as ds: + lyr = ds.GetLayer(0) + lyr.SetSpatialFilterRect(0, 0, 1, 1) + filter_geom = lyr.GetSpatialFilter() + + # Make sure ds.__exit__() invalidation has propagated to the spatial filter + + with pytest.raises( + Exception, + match=r"in method 'Geometry_ExportToWkt', argument 1 of type 'OGRGeometryShadow \*'", + ): + filter_geom.ExportToWkt()