pyogrio 0.9.0__cp311-cp311-manylinux_2_28_aarch64.whl → 0.10.0__cp311-cp311-manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyogrio might be problematic. Click here for more details.

Files changed (74) hide show
  1. pyogrio/__init__.py +20 -13
  2. pyogrio/_compat.py +7 -1
  3. pyogrio/_env.py +4 -6
  4. pyogrio/_err.cpython-311-aarch64-linux-gnu.so +0 -0
  5. pyogrio/_geometry.cpython-311-aarch64-linux-gnu.so +0 -0
  6. pyogrio/_io.cpython-311-aarch64-linux-gnu.so +0 -0
  7. pyogrio/_ogr.cpython-311-aarch64-linux-gnu.so +0 -0
  8. pyogrio/_version.py +3 -3
  9. pyogrio/_vsi.cpython-311-aarch64-linux-gnu.so +0 -0
  10. pyogrio/core.py +86 -20
  11. pyogrio/errors.py +9 -16
  12. pyogrio/gdal_data/GDAL-targets-release.cmake +3 -3
  13. pyogrio/gdal_data/GDAL-targets.cmake +1 -1
  14. pyogrio/gdal_data/GDALConfig.cmake +0 -1
  15. pyogrio/gdal_data/GDALConfigVersion.cmake +3 -3
  16. pyogrio/gdal_data/MM_m_idofic.csv +321 -0
  17. pyogrio/gdal_data/gdaltileindex.xsd +269 -0
  18. pyogrio/gdal_data/gdalvrt.xsd +130 -22
  19. pyogrio/gdal_data/ogrinfo_output.schema.json +23 -0
  20. pyogrio/gdal_data/ogrvrt.xsd +3 -0
  21. pyogrio/gdal_data/pci_datum.txt +222 -155
  22. pyogrio/gdal_data/pci_ellips.txt +90 -38
  23. pyogrio/gdal_data/vcpkg.spdx.json +26 -26
  24. pyogrio/gdal_data/vcpkg_abi_info.txt +25 -25
  25. pyogrio/geopandas.py +32 -24
  26. pyogrio/proj_data/proj-config-version.cmake +2 -2
  27. pyogrio/proj_data/proj-targets.cmake +1 -1
  28. pyogrio/proj_data/proj.db +0 -0
  29. pyogrio/proj_data/proj4-targets.cmake +1 -1
  30. pyogrio/proj_data/projjson.schema.json +1 -1
  31. pyogrio/proj_data/vcpkg.spdx.json +20 -20
  32. pyogrio/proj_data/vcpkg_abi_info.txt +13 -13
  33. pyogrio/raw.py +46 -30
  34. pyogrio/tests/conftest.py +206 -12
  35. pyogrio/tests/fixtures/README.md +32 -13
  36. pyogrio/tests/fixtures/curve.gpkg +0 -0
  37. pyogrio/tests/fixtures/{test_multisurface.gpkg → curvepolygon.gpkg} +0 -0
  38. pyogrio/tests/fixtures/line_zm.gpkg +0 -0
  39. pyogrio/tests/fixtures/multisurface.gpkg +0 -0
  40. pyogrio/tests/test_arrow.py +178 -24
  41. pyogrio/tests/test_core.py +162 -72
  42. pyogrio/tests/test_geopandas_io.py +239 -99
  43. pyogrio/tests/test_path.py +29 -17
  44. pyogrio/tests/test_raw_io.py +165 -54
  45. pyogrio/tests/test_util.py +56 -0
  46. pyogrio/util.py +54 -30
  47. {pyogrio-0.9.0.dist-info → pyogrio-0.10.0.dist-info}/LICENSE +1 -1
  48. {pyogrio-0.9.0.dist-info → pyogrio-0.10.0.dist-info}/METADATA +37 -8
  49. {pyogrio-0.9.0.dist-info → pyogrio-0.10.0.dist-info}/RECORD +198 -214
  50. {pyogrio-0.9.0.dist-info → pyogrio-0.10.0.dist-info}/WHEEL +1 -1
  51. pyogrio.libs/{libgdal-6ff0914e.so.34.3.8.5 → libgdal-b0847c7b.so.35.3.9.1} +0 -0
  52. pyogrio/_err.pxd +0 -4
  53. pyogrio/_err.pyx +0 -250
  54. pyogrio/_geometry.pxd +0 -4
  55. pyogrio/_geometry.pyx +0 -129
  56. pyogrio/_io.pxd +0 -0
  57. pyogrio/_io.pyx +0 -2742
  58. pyogrio/_ogr.pxd +0 -444
  59. pyogrio/_ogr.pyx +0 -346
  60. pyogrio/_vsi.pxd +0 -4
  61. pyogrio/_vsi.pyx +0 -140
  62. pyogrio/arrow_bridge.h +0 -115
  63. pyogrio/gdal_data/bag_template.xml +0 -201
  64. pyogrio/gdal_data/gmlasconf.xml +0 -169
  65. pyogrio/gdal_data/gmlasconf.xsd +0 -1066
  66. pyogrio/gdal_data/netcdf_config.xsd +0 -143
  67. pyogrio/gdal_data/template_tiles.mapml +0 -28
  68. pyogrio/tests/fixtures/poly_not_enough_points.shp.zip +0 -0
  69. pyogrio/tests/fixtures/test_datetime.geojson +0 -7
  70. pyogrio/tests/fixtures/test_datetime_tz.geojson +0 -8
  71. pyogrio/tests/fixtures/test_fgdb.gdb.zip +0 -0
  72. pyogrio/tests/fixtures/test_nested.geojson +0 -18
  73. pyogrio/tests/fixtures/test_ogr_types_list.geojson +0 -12
  74. {pyogrio-0.9.0.dist-info → pyogrio-0.10.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,23 @@
1
1
  import contextlib
2
+ import locale
3
+ import warnings
2
4
  from datetime import datetime
3
5
  from io import BytesIO
4
- import locale
6
+ from zipfile import ZipFile
5
7
 
6
8
  import numpy as np
7
- import pytest
8
9
 
9
- from pyogrio import list_layers, list_drivers, read_info, __gdal_version__
10
+ from pyogrio import (
11
+ __gdal_version__,
12
+ list_drivers,
13
+ list_layers,
14
+ read_info,
15
+ vsi_listtree,
16
+ vsi_unlink,
17
+ )
18
+ from pyogrio._compat import HAS_ARROW_WRITE_API, HAS_PYPROJ, PANDAS_GE_15
10
19
  from pyogrio.errors import DataLayerError, DataSourceError, FeatureError, GeometryError
11
- from pyogrio.geopandas import read_dataframe, write_dataframe, PANDAS_GE_20
20
+ from pyogrio.geopandas import PANDAS_GE_20, read_dataframe, write_dataframe
12
21
  from pyogrio.raw import (
13
22
  DRIVERS_NO_MIXED_DIMENSIONS,
14
23
  DRIVERS_NO_MIXED_SINGLE_MULTI,
@@ -16,27 +25,29 @@ from pyogrio.raw import (
16
25
  from pyogrio.tests.conftest import (
17
26
  ALL_EXTS,
18
27
  DRIVERS,
19
- requires_pyarrow_api,
28
+ START_FID,
20
29
  requires_arrow_write_api,
21
30
  requires_gdal_geos,
31
+ requires_pyarrow_api,
32
+ requires_pyproj,
22
33
  )
23
- from pyogrio._compat import PANDAS_GE_15, HAS_ARROW_WRITE_API
24
34
 
25
- try:
26
- import pandas as pd
27
- from pandas.testing import (
28
- assert_frame_equal,
29
- assert_index_equal,
30
- assert_series_equal,
31
- )
35
+ import pytest
32
36
 
37
+ try:
33
38
  import geopandas as gp
39
+ import pandas as pd
34
40
  from geopandas.array import from_wkt
35
- from geopandas.testing import assert_geodataframe_equal
36
41
 
37
42
  import shapely # if geopandas is present, shapely is expected to be present
38
43
  from shapely.geometry import Point
39
44
 
45
+ from geopandas.testing import assert_geodataframe_equal
46
+ from pandas.testing import (
47
+ assert_index_equal,
48
+ assert_series_equal,
49
+ )
50
+
40
51
  except ImportError:
41
52
  pass
42
53
 
@@ -124,7 +135,8 @@ def test_read_csv_platform_encoding(tmp_path):
124
135
  def test_read_dataframe(naturalearth_lowres_all_ext):
125
136
  df = read_dataframe(naturalearth_lowres_all_ext)
126
137
 
127
- assert df.crs == "EPSG:4326"
138
+ if HAS_PYPROJ:
139
+ assert df.crs == "EPSG:4326"
128
140
  assert len(df) == 177
129
141
  assert df.columns.tolist() == [
130
142
  "pop_est",
@@ -142,14 +154,13 @@ def test_read_dataframe_vsi(naturalearth_lowres_vsi, use_arrow):
142
154
 
143
155
 
144
156
  @pytest.mark.parametrize(
145
- "columns, fid_as_index, exp_len", [(None, False, 2), ([], True, 2), ([], False, 0)]
157
+ "columns, fid_as_index, exp_len", [(None, False, 3), ([], True, 3), ([], False, 0)]
146
158
  )
147
159
  def test_read_layer_without_geometry(
148
- test_fgdb_vsi, columns, fid_as_index, use_arrow, exp_len
160
+ no_geometry_file, columns, fid_as_index, use_arrow, exp_len
149
161
  ):
150
162
  result = read_dataframe(
151
- test_fgdb_vsi,
152
- layer="basetable",
163
+ no_geometry_file,
153
164
  columns=columns,
154
165
  fid_as_index=fid_as_index,
155
166
  use_arrow=use_arrow,
@@ -195,38 +206,62 @@ def test_read_no_geometry_no_columns_no_fids(naturalearth_lowres, use_arrow):
195
206
  )
196
207
 
197
208
 
198
- def test_read_force_2d(test_fgdb_vsi, use_arrow):
199
- with pytest.warns(
200
- UserWarning, match=r"Measured \(M\) geometry types are not supported"
201
- ):
202
- df = read_dataframe(test_fgdb_vsi, layer="test_lines", max_features=1)
203
- assert df.iloc[0].geometry.has_z
209
+ def test_read_force_2d(tmp_path, use_arrow):
210
+ filename = tmp_path / "test.gpkg"
204
211
 
205
- df = read_dataframe(
206
- test_fgdb_vsi,
207
- layer="test_lines",
208
- force_2d=True,
209
- max_features=1,
210
- use_arrow=use_arrow,
211
- )
212
- assert not df.iloc[0].geometry.has_z
212
+ # create a GPKG with 3D point values
213
+ expected = gp.GeoDataFrame(
214
+ geometry=[Point(0, 0, 0), Point(1, 1, 0)], crs="EPSG:4326"
215
+ )
216
+ write_dataframe(expected, filename)
217
+
218
+ df = read_dataframe(filename)
219
+ assert df.iloc[0].geometry.has_z
220
+
221
+ df = read_dataframe(
222
+ filename,
223
+ force_2d=True,
224
+ max_features=1,
225
+ use_arrow=use_arrow,
226
+ )
227
+ assert not df.iloc[0].geometry.has_z
228
+
229
+
230
+ def test_read_layer(tmp_path, use_arrow):
231
+ filename = tmp_path / "test.gpkg"
213
232
 
233
+ # create a multilayer GPKG
234
+ expected1 = gp.GeoDataFrame(geometry=[Point(0, 0)], crs="EPSG:4326")
235
+ write_dataframe(
236
+ expected1,
237
+ filename,
238
+ layer="layer1",
239
+ )
240
+
241
+ expected2 = gp.GeoDataFrame(geometry=[Point(1, 1)], crs="EPSG:4326")
242
+ write_dataframe(expected2, filename, layer="layer2", append=True)
243
+
244
+ assert np.array_equal(
245
+ list_layers(filename), [["layer1", "Point"], ["layer2", "Point"]]
246
+ )
214
247
 
215
- @pytest.mark.filterwarnings("ignore: Measured")
216
- @pytest.mark.filterwarnings("ignore: More than one layer found in")
217
- def test_read_layer(test_fgdb_vsi, use_arrow):
218
- layers = list_layers(test_fgdb_vsi)
219
- kwargs = {"use_arrow": use_arrow, "read_geometry": False, "max_features": 1}
248
+ kwargs = {"use_arrow": use_arrow, "max_features": 1}
220
249
 
221
- # The first layer is read by default (NOTE: first layer has no features)
222
- df = read_dataframe(test_fgdb_vsi, **kwargs)
223
- df2 = read_dataframe(test_fgdb_vsi, layer=layers[0][0], **kwargs)
224
- assert_frame_equal(df, df2)
250
+ # The first layer is read by default, which will warn when there are multiple
251
+ # layers
252
+ with pytest.warns(UserWarning, match="More than one layer found"):
253
+ df = read_dataframe(filename, **kwargs)
225
254
 
226
- # Reading a specific layer should return that layer.
255
+ assert_geodataframe_equal(df, expected1)
256
+
257
+ # Reading a specific layer by name should return that layer.
227
258
  # Detected here by a known column.
228
- df = read_dataframe(test_fgdb_vsi, layer="test_lines", **kwargs)
229
- assert "RIVER_MILE" in df.columns
259
+ df = read_dataframe(filename, layer="layer2", **kwargs)
260
+ assert_geodataframe_equal(df, expected2)
261
+
262
+ # Reading a specific layer by index should return that layer
263
+ df = read_dataframe(filename, layer=1, **kwargs)
264
+ assert_geodataframe_equal(df, expected2)
230
265
 
231
266
 
232
267
  def test_read_layer_invalid(naturalearth_lowres_all_ext, use_arrow):
@@ -234,22 +269,19 @@ def test_read_layer_invalid(naturalearth_lowres_all_ext, use_arrow):
234
269
  read_dataframe(naturalearth_lowres_all_ext, layer="wrong", use_arrow=use_arrow)
235
270
 
236
271
 
237
- @pytest.mark.filterwarnings("ignore: Measured")
238
- def test_read_datetime(test_fgdb_vsi, use_arrow):
239
- df = read_dataframe(
240
- test_fgdb_vsi, layer="test_lines", use_arrow=use_arrow, max_features=1
241
- )
272
+ def test_read_datetime(datetime_file, use_arrow):
273
+ df = read_dataframe(datetime_file, use_arrow=use_arrow)
242
274
  if PANDAS_GE_20:
243
275
  # starting with pandas 2.0, it preserves the passed datetime resolution
244
- assert df.SURVEY_DAT.dtype.name == "datetime64[ms]"
276
+ assert df.col.dtype.name == "datetime64[ms]"
245
277
  else:
246
- assert df.SURVEY_DAT.dtype.name == "datetime64[ns]"
278
+ assert df.col.dtype.name == "datetime64[ns]"
247
279
 
248
280
 
249
281
  @pytest.mark.filterwarnings("ignore: Non-conformant content for record 1 in column ")
250
282
  @pytest.mark.requires_arrow_write_api
251
- def test_read_datetime_tz(test_datetime_tz, tmp_path, use_arrow):
252
- df = read_dataframe(test_datetime_tz)
283
+ def test_read_datetime_tz(datetime_tz_file, tmp_path, use_arrow):
284
+ df = read_dataframe(datetime_tz_file)
253
285
  # Make the index non-consecutive to test this case as well. Added for issue
254
286
  # https://github.com/geopandas/pyogrio/issues/324
255
287
  df = df.set_index(np.array([0, 2]))
@@ -319,14 +351,17 @@ def test_read_write_datetime_tz_with_nulls(tmp_path, use_arrow):
319
351
  assert_geodataframe_equal(df, result)
320
352
 
321
353
 
322
- def test_read_null_values(test_fgdb_vsi, use_arrow):
323
- df = read_dataframe(
324
- test_fgdb_vsi, layer="basetable_2", use_arrow=use_arrow, read_geometry=False
325
- )
354
+ def test_read_null_values(tmp_path, use_arrow):
355
+ filename = tmp_path / "test_null_values_no_geometry.gpkg"
356
+
357
+ # create a GPKG with no geometries and only null values
358
+ expected = pd.DataFrame({"col": [None, None]})
359
+ write_dataframe(expected, filename)
360
+
361
+ df = read_dataframe(filename, use_arrow=use_arrow, read_geometry=False)
326
362
 
327
363
  # make sure that Null values are preserved
328
- assert df.SEGMENT_NAME.isnull().max()
329
- assert df.loc[df.SEGMENT_NAME.isnull()].SEGMENT_NAME.iloc[0] is None
364
+ assert np.array_equal(df.col.values, expected.col.values)
330
365
 
331
366
 
332
367
  def test_read_fid_as_index(naturalearth_lowres_all_ext, use_arrow):
@@ -344,12 +379,9 @@ def test_read_fid_as_index(naturalearth_lowres_all_ext, use_arrow):
344
379
  fid_as_index=True,
345
380
  **kwargs,
346
381
  )
347
- if naturalearth_lowres_all_ext.suffix in [".gpkg"]:
348
- # File format where fid starts at 1
349
- assert_index_equal(df.index, pd.Index([3, 4], name="fid"))
350
- else:
351
- # File format where fid starts at 0
352
- assert_index_equal(df.index, pd.Index([2, 3], name="fid"))
382
+ fids_expected = pd.Index([2, 3], name="fid")
383
+ fids_expected += START_FID[naturalearth_lowres_all_ext.suffix]
384
+ assert_index_equal(df.index, fids_expected)
353
385
 
354
386
 
355
387
  def test_read_fid_as_index_only(naturalearth_lowres, use_arrow):
@@ -605,17 +637,22 @@ def test_read_fids_arrow_warning_old_gdal(naturalearth_lowres_all_ext):
605
637
  assert len(df) == 1
606
638
 
607
639
 
608
- def test_read_fids_force_2d(test_fgdb_vsi):
609
- with pytest.warns(
610
- UserWarning, match=r"Measured \(M\) geometry types are not supported"
611
- ):
612
- df = read_dataframe(test_fgdb_vsi, layer="test_lines", fids=[22])
613
- assert len(df) == 1
614
- assert df.iloc[0].geometry.has_z
640
+ def test_read_fids_force_2d(tmp_path):
641
+ filename = tmp_path / "test.gpkg"
615
642
 
616
- df = read_dataframe(test_fgdb_vsi, layer="test_lines", force_2d=True, fids=[22])
617
- assert len(df) == 1
618
- assert not df.iloc[0].geometry.has_z
643
+ # create a GPKG with 3D point values
644
+ expected = gp.GeoDataFrame(
645
+ geometry=[Point(0, 0, 0), Point(1, 1, 0)], crs="EPSG:4326"
646
+ )
647
+ write_dataframe(expected, filename)
648
+
649
+ df = read_dataframe(filename, fids=[1])
650
+ assert_geodataframe_equal(df, expected.iloc[:1])
651
+
652
+ df = read_dataframe(filename, force_2d=True, fids=[1])
653
+ assert np.array_equal(
654
+ df.geometry.values, shapely.force_2d(expected.iloc[:1].geometry.values)
655
+ )
619
656
 
620
657
 
621
658
  @pytest.mark.parametrize("skip_features", [10, 200])
@@ -769,7 +806,7 @@ def test_read_sql_invalid(naturalearth_lowres_all_ext, use_arrow):
769
806
  )
770
807
 
771
808
  with pytest.raises(
772
- ValueError, match="'sql' paramater cannot be combined with 'layer'"
809
+ ValueError, match="'sql' parameter cannot be combined with 'layer'"
773
810
  ):
774
811
  read_dataframe(
775
812
  naturalearth_lowres_all_ext,
@@ -924,9 +961,9 @@ def test_write_csv_encoding(tmp_path, encoding):
924
961
  write_dataframe(df, csv_pyogrio_path, encoding=encoding)
925
962
 
926
963
  # Check if the text files written both ways can be read again and give same result.
927
- with open(csv_path, "r", encoding=encoding) as csv:
964
+ with open(csv_path, encoding=encoding) as csv:
928
965
  csv_str = csv.read()
929
- with open(csv_pyogrio_path, "r", encoding=encoding) as csv_pyogrio:
966
+ with open(csv_pyogrio_path, encoding=encoding) as csv_pyogrio:
930
967
  csv_pyogrio_str = csv_pyogrio.read()
931
968
  assert csv_str == csv_pyogrio_str
932
969
 
@@ -960,7 +997,7 @@ def test_write_dataframe(tmp_path, naturalearth_lowres, ext, use_arrow):
960
997
  if DRIVERS[ext] in DRIVERS_NO_MIXED_SINGLE_MULTI:
961
998
  assert list(geometry_types) == ["MultiPolygon"]
962
999
  else:
963
- assert set(geometry_types) == set(["MultiPolygon", "Polygon"])
1000
+ assert set(geometry_types) == {"MultiPolygon", "Polygon"}
964
1001
 
965
1002
  # Coordinates are not precisely equal when written to JSON
966
1003
  # dtypes do not necessarily round-trip precisely through JSON
@@ -1062,6 +1099,23 @@ def test_write_empty_dataframe(tmp_path, ext, use_arrow):
1062
1099
  assert_geodataframe_equal(df, expected)
1063
1100
 
1064
1101
 
1102
+ def test_write_empty_geometry(tmp_path):
1103
+ expected = gp.GeoDataFrame({"x": [0]}, geometry=from_wkt(["POINT EMPTY"]), crs=4326)
1104
+ filename = tmp_path / "test.gpkg"
1105
+
1106
+ # Check that no warning is raised with GeoSeries.notna()
1107
+ with warnings.catch_warnings():
1108
+ warnings.simplefilter("error", UserWarning)
1109
+ if not HAS_PYPROJ:
1110
+ warnings.filterwarnings("ignore", message="'crs' was not provided.")
1111
+ write_dataframe(expected, filename)
1112
+ assert filename.exists()
1113
+
1114
+ # Xref GH-436: round-tripping possible with GPKG but not others
1115
+ df = read_dataframe(filename)
1116
+ assert_geodataframe_equal(df, expected)
1117
+
1118
+
1065
1119
  @pytest.mark.parametrize("ext", [".geojsonl", ".geojsons"])
1066
1120
  @pytest.mark.requires_arrow_write_api
1067
1121
  def test_write_read_empty_dataframe_unsupported(tmp_path, ext, use_arrow):
@@ -1161,7 +1215,7 @@ def test_write_dataframe_gdal_options(
1161
1215
  df,
1162
1216
  outfilename2,
1163
1217
  use_arrow=use_arrow,
1164
- layer_options=dict(spatial_index=spatial_index),
1218
+ layer_options={"spatial_index": spatial_index},
1165
1219
  )
1166
1220
  assert outfilename2.exists() is True
1167
1221
  index_filename2 = tmp_path / "test2.qix"
@@ -1207,7 +1261,7 @@ def test_write_dataframe_gdal_options_dataset(tmp_path, naturalearth_lowres, use
1207
1261
  df,
1208
1262
  test_no_contents_filename2,
1209
1263
  use_arrow=use_arrow,
1210
- dataset_options=dict(add_gpkg_ogr_contents=False),
1264
+ dataset_options={"add_gpkg_ogr_contents": False},
1211
1265
  )
1212
1266
  assert "gpkg_ogr_contents" not in _get_gpkg_table_names(test_no_contents_filename2)
1213
1267
 
@@ -1320,7 +1374,8 @@ def test_write_dataframe_promote_to_multi_layer_geom_type(
1320
1374
  ".shp",
1321
1375
  None,
1322
1376
  "Point",
1323
- "Could not add feature to layer at index|Error while writing batch to OGR layer",
1377
+ "Could not add feature to layer at index|Error while writing batch to OGR "
1378
+ "layer",
1324
1379
  ),
1325
1380
  ],
1326
1381
  )
@@ -1443,6 +1498,7 @@ def test_write_dataframe_infer_geometry_with_nulls(tmp_path, geoms, ext, use_arr
1443
1498
  "ignore: You will likely lose important projection information"
1444
1499
  )
1445
1500
  @pytest.mark.requires_arrow_write_api
1501
+ @requires_pyproj
1446
1502
  def test_custom_crs_io(tmp_path, naturalearth_lowres_all_ext, use_arrow):
1447
1503
  df = read_dataframe(naturalearth_lowres_all_ext)
1448
1504
  # project Belgium to a custom Albers Equal Area projection
@@ -1517,6 +1573,22 @@ def test_write_read_null(tmp_path, use_arrow):
1517
1573
  assert result_gdf["object_str"][2] is None
1518
1574
 
1519
1575
 
1576
+ @pytest.mark.requires_arrow_write_api
1577
+ def test_write_read_vsimem(naturalearth_lowres_vsi, use_arrow):
1578
+ path, _ = naturalearth_lowres_vsi
1579
+ mem_path = f"/vsimem/{path.name}"
1580
+
1581
+ input = read_dataframe(path, use_arrow=use_arrow)
1582
+ assert len(input) == 177
1583
+
1584
+ try:
1585
+ write_dataframe(input, mem_path, use_arrow=use_arrow)
1586
+ result = read_dataframe(mem_path, use_arrow=use_arrow)
1587
+ assert len(result) == 177
1588
+ finally:
1589
+ vsi_unlink(mem_path)
1590
+
1591
+
1520
1592
  @pytest.mark.parametrize(
1521
1593
  "wkt,geom_types",
1522
1594
  [
@@ -1529,7 +1601,7 @@ def test_write_read_null(tmp_path, use_arrow):
1529
1601
  ["2.5D MultiLineString", "MultiLineString Z"],
1530
1602
  ),
1531
1603
  (
1532
- "MultiPolygon Z (((0 0 0, 0 1 0, 1 1 0, 0 0 0)), ((1 1 1, 1 2 1, 2 2 1, 1 1 1)))",
1604
+ "MultiPolygon Z (((0 0 0, 0 1 0, 1 1 0, 0 0 0)), ((1 1 1, 1 2 1, 2 2 1, 1 1 1)))", # noqa: E501
1533
1605
  ["2.5D MultiPolygon", "MultiPolygon Z"],
1534
1606
  ),
1535
1607
  (
@@ -1572,7 +1644,7 @@ def test_write_geometry_z_types(tmp_path, wkt, geom_types, use_arrow):
1572
1644
  "MultiPolygon Z",
1573
1645
  False,
1574
1646
  [
1575
- "MultiPolygon Z (((0 0 0, 0 1 0, 1 1 0, 0 0 0)), ((1 1 1, 1 2 1, 2 2 1, 1 1 1)))"
1647
+ "MultiPolygon Z (((0 0 0, 0 1 0, 1 1 0, 0 0 0)), ((1 1 1, 1 2 1, 2 2 1, 1 1 1)))" # noqa: E501
1576
1648
  ],
1577
1649
  ),
1578
1650
  (
@@ -1653,7 +1725,7 @@ def test_write_geometry_z_types_auto(
1653
1725
  ("ignore", None),
1654
1726
  ],
1655
1727
  )
1656
- def test_read_invalid_shp(data_dir, use_arrow, on_invalid, message):
1728
+ def test_read_invalid_poly_ring(tmp_path, use_arrow, on_invalid, message):
1657
1729
  if on_invalid == "raise":
1658
1730
  handler = pytest.raises(shapely.errors.GEOSException, match=message)
1659
1731
  elif on_invalid == "warn":
@@ -1663,33 +1735,54 @@ def test_read_invalid_shp(data_dir, use_arrow, on_invalid, message):
1663
1735
  else:
1664
1736
  raise ValueError(f"unknown value for on_invalid: {on_invalid}")
1665
1737
 
1738
+ # create a GeoJSON file with an invalid exterior ring
1739
+ invalid_geojson = """{
1740
+ "type": "FeatureCollection",
1741
+ "features": [
1742
+ {
1743
+ "type": "Feature",
1744
+ "properties": {},
1745
+ "geometry": {
1746
+ "type": "Polygon",
1747
+ "coordinates": [ [ [0, 0], [0, 0] ] ]
1748
+ }
1749
+ }
1750
+ ]
1751
+ }"""
1752
+
1753
+ filename = tmp_path / "test.geojson"
1754
+ with open(filename, "w") as f:
1755
+ _ = f.write(invalid_geojson)
1756
+
1666
1757
  with handler:
1667
1758
  df = read_dataframe(
1668
- data_dir / "poly_not_enough_points.shp.zip",
1759
+ filename,
1669
1760
  use_arrow=use_arrow,
1670
1761
  on_invalid=on_invalid,
1671
1762
  )
1672
1763
  df.geometry.isnull().all()
1673
1764
 
1674
1765
 
1675
- def test_read_multisurface(data_dir, use_arrow):
1766
+ def test_read_multisurface(multisurface_file, use_arrow):
1676
1767
  if use_arrow:
1768
+ # TODO: revisit once https://github.com/geopandas/pyogrio/issues/478
1769
+ # is resolved.
1770
+ pytest.skip("Shapely + GEOS 3.13 crashes in from_wkb for this case")
1771
+
1677
1772
  with pytest.raises(shapely.errors.GEOSException):
1678
1773
  # TODO(Arrow)
1679
1774
  # shapely fails parsing the WKB
1680
- read_dataframe(data_dir / "test_multisurface.gpkg", use_arrow=True)
1775
+ read_dataframe(multisurface_file, use_arrow=True)
1681
1776
  else:
1682
- df = read_dataframe(data_dir / "test_multisurface.gpkg")
1777
+ df = read_dataframe(multisurface_file)
1683
1778
 
1684
1779
  # MultiSurface should be converted to MultiPolygon
1685
1780
  assert df.geometry.type.tolist() == ["MultiPolygon"]
1686
1781
 
1687
1782
 
1688
- def test_read_dataset_kwargs(data_dir, use_arrow):
1689
- filename = data_dir / "test_nested.geojson"
1690
-
1783
+ def test_read_dataset_kwargs(nested_geojson_file, use_arrow):
1691
1784
  # by default, nested data are not flattened
1692
- df = read_dataframe(filename, use_arrow=use_arrow)
1785
+ df = read_dataframe(nested_geojson_file, use_arrow=use_arrow)
1693
1786
 
1694
1787
  expected = gp.GeoDataFrame(
1695
1788
  {
@@ -1702,7 +1795,9 @@ def test_read_dataset_kwargs(data_dir, use_arrow):
1702
1795
 
1703
1796
  assert_geodataframe_equal(df, expected)
1704
1797
 
1705
- df = read_dataframe(filename, use_arrow=use_arrow, FLATTEN_NESTED_ATTRIBUTES="YES")
1798
+ df = read_dataframe(
1799
+ nested_geojson_file, use_arrow=use_arrow, FLATTEN_NESTED_ATTRIBUTES="YES"
1800
+ )
1706
1801
 
1707
1802
  expected = gp.GeoDataFrame(
1708
1803
  {
@@ -1904,6 +1999,9 @@ def test_write_memory(naturalearth_lowres, driver):
1904
1999
  check_dtype=not is_json,
1905
2000
  )
1906
2001
 
2002
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2003
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2004
+
1907
2005
 
1908
2006
  def test_write_memory_driver_required(naturalearth_lowres):
1909
2007
  df = read_dataframe(naturalearth_lowres)
@@ -1916,6 +2014,9 @@ def test_write_memory_driver_required(naturalearth_lowres):
1916
2014
  ):
1917
2015
  write_dataframe(df.head(1), buffer, driver=None, layer="test")
1918
2016
 
2017
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2018
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2019
+
1919
2020
 
1920
2021
  @pytest.mark.parametrize("driver", ["ESRI Shapefile", "OpenFileGDB"])
1921
2022
  def test_write_memory_unsupported_driver(naturalearth_lowres, driver):
@@ -1931,6 +2032,9 @@ def test_write_memory_unsupported_driver(naturalearth_lowres, driver):
1931
2032
  ):
1932
2033
  write_dataframe(df, buffer, driver=driver, layer="test")
1933
2034
 
2035
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2036
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2037
+
1934
2038
 
1935
2039
  @pytest.mark.parametrize("driver", ["GeoJSON", "GPKG"])
1936
2040
  def test_write_memory_append_unsupported(naturalearth_lowres, driver):
@@ -1943,6 +2047,9 @@ def test_write_memory_append_unsupported(naturalearth_lowres, driver):
1943
2047
  ):
1944
2048
  write_dataframe(df.head(1), buffer, driver=driver, layer="test", append=True)
1945
2049
 
2050
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2051
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2052
+
1946
2053
 
1947
2054
  def test_write_memory_existing_unsupported(naturalearth_lowres):
1948
2055
  df = read_dataframe(naturalearth_lowres)
@@ -1954,6 +2061,33 @@ def test_write_memory_existing_unsupported(naturalearth_lowres):
1954
2061
  ):
1955
2062
  write_dataframe(df.head(1), buffer, driver="GeoJSON", layer="test")
1956
2063
 
2064
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2065
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2066
+
2067
+
2068
+ def test_write_open_file_handle(tmp_path, naturalearth_lowres):
2069
+ """Verify that writing to an open file handle is not currently supported"""
2070
+
2071
+ df = read_dataframe(naturalearth_lowres)
2072
+
2073
+ # verify it fails for regular file handle
2074
+ with pytest.raises(
2075
+ NotImplementedError, match="writing to an open file handle is not yet supported"
2076
+ ):
2077
+ with open(tmp_path / "test.geojson", "wb") as f:
2078
+ write_dataframe(df.head(1), f)
2079
+
2080
+ # verify it fails for ZipFile
2081
+ with pytest.raises(
2082
+ NotImplementedError, match="writing to an open file handle is not yet supported"
2083
+ ):
2084
+ with ZipFile(tmp_path / "test.geojson.zip", "w") as z:
2085
+ with z.open("test.geojson", "w") as f:
2086
+ write_dataframe(df.head(1), f)
2087
+
2088
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2089
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2090
+
1957
2091
 
1958
2092
  @pytest.mark.parametrize("ext", ["gpkg", "geojson"])
1959
2093
  def test_non_utf8_encoding_io(tmp_path, ext, encoded_text):
@@ -2045,7 +2179,8 @@ def test_non_utf8_encoding_io_shapefile(tmp_path, encoded_text, use_arrow):
2045
2179
 
2046
2180
 
2047
2181
  def test_encoding_read_option_collision_shapefile(naturalearth_lowres, use_arrow):
2048
- """Providing both encoding parameter and ENCODING open option (even if blank) is not allowed"""
2182
+ """Providing both encoding parameter and ENCODING open option
2183
+ (even if blank) is not allowed."""
2049
2184
 
2050
2185
  with pytest.raises(
2051
2186
  ValueError, match='cannot provide both encoding parameter and "ENCODING" option'
@@ -2056,7 +2191,8 @@ def test_encoding_read_option_collision_shapefile(naturalearth_lowres, use_arrow
2056
2191
 
2057
2192
 
2058
2193
  def test_encoding_write_layer_option_collision_shapefile(tmp_path, encoded_text):
2059
- """Providing both encoding parameter and ENCODING layer creation option (even if blank) is not allowed"""
2194
+ """Providing both encoding parameter and ENCODING layer creation option
2195
+ (even if blank) is not allowed."""
2060
2196
  encoding, text = encoded_text
2061
2197
 
2062
2198
  output_path = tmp_path / "test.shp"
@@ -2064,7 +2200,10 @@ def test_encoding_write_layer_option_collision_shapefile(tmp_path, encoded_text)
2064
2200
 
2065
2201
  with pytest.raises(
2066
2202
  ValueError,
2067
- match='cannot provide both encoding parameter and "ENCODING" layer creation option',
2203
+ match=(
2204
+ 'cannot provide both encoding parameter and "ENCODING" layer creation '
2205
+ "option"
2206
+ ),
2068
2207
  ):
2069
2208
  write_dataframe(
2070
2209
  df, output_path, encoding=encoding, layer_options={"ENCODING": ""}
@@ -2102,7 +2241,8 @@ def test_non_utf8_encoding_shapefile_sql(tmp_path, use_arrow):
2102
2241
 
2103
2242
  @pytest.mark.requires_arrow_write_api
2104
2243
  def test_write_kml_file_coordinate_order(tmp_path, use_arrow):
2105
- # confirm KML coordinates are written in lon, lat order even if CRS axis specifies otherwise
2244
+ # confirm KML coordinates are written in lon, lat order even if CRS axis
2245
+ # specifies otherwise
2106
2246
  points = [Point(10, 20), Point(30, 40), Point(50, 60)]
2107
2247
  gdf = gp.GeoDataFrame(geometry=points, crs="EPSG:4326")
2108
2248
  output_path = tmp_path / "test.kml"