pyogrio 0.8.0__cp311-cp311-win_amd64.whl → 0.10.0__cp311-cp311-win_amd64.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 (101) hide show
  1. pyogrio/__init__.py +26 -18
  2. pyogrio/_compat.py +7 -1
  3. pyogrio/_env.py +4 -6
  4. pyogrio/_err.c +460 -445
  5. pyogrio/_err.cp311-win_amd64.pyd +0 -0
  6. pyogrio/_geometry.c +645 -612
  7. pyogrio/_geometry.cp311-win_amd64.pyd +0 -0
  8. pyogrio/_io.c +7764 -7602
  9. pyogrio/_io.cp311-win_amd64.pyd +0 -0
  10. pyogrio/_ogr.c +601 -609
  11. pyogrio/_ogr.cp311-win_amd64.pyd +0 -0
  12. pyogrio/_version.py +3 -3
  13. pyogrio/_vsi.c +7570 -2514
  14. pyogrio/_vsi.cp311-win_amd64.pyd +0 -0
  15. pyogrio/core.py +86 -20
  16. pyogrio/errors.py +9 -16
  17. pyogrio/gdal_data/GDAL-targets.cmake +1 -1
  18. pyogrio/gdal_data/GDALConfig.cmake +0 -1
  19. pyogrio/gdal_data/GDALConfigVersion.cmake +3 -3
  20. pyogrio/gdal_data/MM_m_idofic.csv +321 -0
  21. pyogrio/gdal_data/gdaltileindex.xsd +269 -0
  22. pyogrio/gdal_data/gdalvrt.xsd +130 -22
  23. pyogrio/gdal_data/ogrinfo_output.schema.json +23 -0
  24. pyogrio/gdal_data/ogrvrt.xsd +3 -0
  25. pyogrio/gdal_data/pci_datum.txt +222 -155
  26. pyogrio/gdal_data/pci_ellips.txt +90 -38
  27. pyogrio/gdal_data/vcpkg.spdx.json +21 -21
  28. pyogrio/gdal_data/vcpkg_abi_info.txt +27 -27
  29. pyogrio/geopandas.py +44 -27
  30. pyogrio/proj_data/proj-config-version.cmake +2 -2
  31. pyogrio/proj_data/proj-targets.cmake +1 -1
  32. pyogrio/proj_data/proj.db +0 -0
  33. pyogrio/proj_data/proj4-targets.cmake +1 -1
  34. pyogrio/proj_data/projjson.schema.json +1 -1
  35. pyogrio/proj_data/vcpkg.spdx.json +17 -17
  36. pyogrio/proj_data/vcpkg_abi_info.txt +15 -15
  37. pyogrio/raw.py +46 -30
  38. pyogrio/tests/conftest.py +206 -12
  39. pyogrio/tests/fixtures/README.md +32 -13
  40. pyogrio/tests/fixtures/curve.gpkg +0 -0
  41. pyogrio/tests/fixtures/{test_multisurface.gpkg → curvepolygon.gpkg} +0 -0
  42. pyogrio/tests/fixtures/line_zm.gpkg +0 -0
  43. pyogrio/tests/fixtures/multisurface.gpkg +0 -0
  44. pyogrio/tests/test_arrow.py +178 -24
  45. pyogrio/tests/test_core.py +162 -72
  46. pyogrio/tests/test_geopandas_io.py +341 -96
  47. pyogrio/tests/test_path.py +30 -17
  48. pyogrio/tests/test_raw_io.py +165 -54
  49. pyogrio/tests/test_util.py +56 -0
  50. pyogrio/util.py +55 -31
  51. pyogrio-0.10.0.dist-info/DELVEWHEEL +2 -0
  52. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/LICENSE +1 -1
  53. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/METADATA +37 -8
  54. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/RECORD +74 -89
  55. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/WHEEL +1 -1
  56. pyogrio.libs/{Lerc-62a2c1c74500e7815994b3e49b36750c.dll → Lerc-089e3fef3df84b17326dcddbf1dedaa4.dll} +0 -0
  57. pyogrio.libs/{gdal-2bfc6a9f962a8953b0640db9a272d797.dll → gdal-debee5933f0da7bb90b4bcd009023377.dll} +0 -0
  58. pyogrio.libs/{geos-289d7171bd083dfed1f8a90e4ae57442.dll → geos-ace4c5b5c1f569bb4213e7bbd0b0322e.dll} +0 -0
  59. pyogrio.libs/{geos_c-2a12859cd876719c648f1eb950b7d94c.dll → geos_c-7478ca0a86136b280d9b2d245c6f6627.dll} +0 -0
  60. pyogrio.libs/geotiff-c8fe8a095520a4ea4e465d27e06add3a.dll +0 -0
  61. pyogrio.libs/{iconv-2-f2d9304f8dc4cdd981024b520b73a099.dll → iconv-2-27352d156a5467ca5383d3951093ea5a.dll} +0 -0
  62. pyogrio.libs/{jpeg62-a67b2bf7fd32d34c565ae5bb6d47c224.dll → jpeg62-e56b6f95a95af498f4623b8da4cebd46.dll} +0 -0
  63. pyogrio.libs/{json-c-79a8df7e59952f5c5d594620e4b66c13.dll → json-c-c84940e2654a4f8468bfcf2ce992aa93.dll} +0 -0
  64. pyogrio.libs/libcurl-d69cfd4ad487d53d58743b6778ec85e7.dll +0 -0
  65. pyogrio.libs/{libexpat-fa55f107b678de136400c6d953c3cdde.dll → libexpat-6576a8d02641b6a3dbad35901ec200a7.dll} +0 -0
  66. pyogrio.libs/liblzma-9ee4accb476ec1ae24e924953140273d.dll +0 -0
  67. pyogrio.libs/{libpng16-6227e9a35c2a350ae6b0586079c10b9e.dll → libpng16-7c36142dda59f186f6bb683e8dae2bfe.dll} +0 -0
  68. pyogrio.libs/{msvcp140-46db46e967c8db2cb7a20fc75872a57e.dll → msvcp140-98b3e5b80de1e5e9d1703b786d795623.dll} +0 -0
  69. pyogrio.libs/proj-a408c5327f3fd2f5fabe8c56815beed7.dll +0 -0
  70. pyogrio.libs/{qhull_r-d8840f4ed1f7d452ff9a30237320bcfd.dll → qhull_r-516897f855568caab1ab1fe37912766c.dll} +0 -0
  71. pyogrio.libs/sqlite3-9bc109d8536d5ed9666332fec94485fc.dll +0 -0
  72. pyogrio.libs/{tiff-ffca1ff19d0e95dad39df0078fb037af.dll → tiff-9b3f605fffe0bccc0a964c374ee4f820.dll} +0 -0
  73. pyogrio.libs/{zlib1-aaba6ea052f6d3fa3d84a301e3eb3d30.dll → zlib1-e5af16a15c63f05bd82d90396807ae5b.dll} +0 -0
  74. pyogrio/_err.pxd +0 -4
  75. pyogrio/_err.pyx +0 -250
  76. pyogrio/_geometry.pxd +0 -4
  77. pyogrio/_geometry.pyx +0 -129
  78. pyogrio/_io.pxd +0 -0
  79. pyogrio/_io.pyx +0 -2738
  80. pyogrio/_ogr.pxd +0 -441
  81. pyogrio/_ogr.pyx +0 -346
  82. pyogrio/_vsi.pxd +0 -4
  83. pyogrio/_vsi.pyx +0 -140
  84. pyogrio/arrow_bridge.h +0 -115
  85. pyogrio/gdal_data/bag_template.xml +0 -201
  86. pyogrio/gdal_data/gmlasconf.xml +0 -169
  87. pyogrio/gdal_data/gmlasconf.xsd +0 -1066
  88. pyogrio/gdal_data/netcdf_config.xsd +0 -143
  89. pyogrio/gdal_data/template_tiles.mapml +0 -28
  90. pyogrio/tests/fixtures/test_datetime.geojson +0 -7
  91. pyogrio/tests/fixtures/test_datetime_tz.geojson +0 -8
  92. pyogrio/tests/fixtures/test_fgdb.gdb.zip +0 -0
  93. pyogrio/tests/fixtures/test_nested.geojson +0 -18
  94. pyogrio/tests/fixtures/test_ogr_types_list.geojson +0 -12
  95. pyogrio-0.8.0.dist-info/DELVEWHEEL +0 -2
  96. pyogrio.libs/geotiff-d1c0fcc3c454409ad8be61ff04a7422c.dll +0 -0
  97. pyogrio.libs/libcurl-7fef9869f6520a5fbdb2bc9ce4c496cc.dll +0 -0
  98. pyogrio.libs/liblzma-5a1f648afc3d4cf36e3aef2266d55143.dll +0 -0
  99. pyogrio.libs/proj-74051a73897c9fa6d7bfef4561688568.dll +0 -0
  100. pyogrio.libs/sqlite3-fe7a86058d1c5658d1f9106228a7fd83.dll +0 -0
  101. {pyogrio-0.8.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, 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"
211
+
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
204
228
 
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
213
229
 
230
+ def test_read_layer(tmp_path, use_arrow):
231
+ filename = tmp_path / "test.gpkg"
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
+ )
247
+
248
+ kwargs = {"use_arrow": use_arrow, "max_features": 1}
214
249
 
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}
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)
220
254
 
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)
255
+ assert_geodataframe_equal(df, expected1)
225
256
 
226
- # Reading a specific layer should return that layer.
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
  (
@@ -1641,24 +1713,76 @@ def test_write_geometry_z_types_auto(
1641
1713
  assert_geodataframe_equal(gdf, result_gdf)
1642
1714
 
1643
1715
 
1644
- def test_read_multisurface(data_dir, use_arrow):
1716
+ @pytest.mark.parametrize(
1717
+ "on_invalid, message",
1718
+ [
1719
+ (
1720
+ "warn",
1721
+ "Invalid WKB: geometry is returned as None. IllegalArgumentException: "
1722
+ "Invalid number of points in LinearRing found 2 - must be 0 or >=",
1723
+ ),
1724
+ ("raise", "Invalid number of points in LinearRing found 2 - must be 0 or >="),
1725
+ ("ignore", None),
1726
+ ],
1727
+ )
1728
+ def test_read_invalid_poly_ring(tmp_path, use_arrow, on_invalid, message):
1729
+ if on_invalid == "raise":
1730
+ handler = pytest.raises(shapely.errors.GEOSException, match=message)
1731
+ elif on_invalid == "warn":
1732
+ handler = pytest.warns(match=message)
1733
+ elif on_invalid == "ignore":
1734
+ handler = contextlib.nullcontext()
1735
+ else:
1736
+ raise ValueError(f"unknown value for on_invalid: {on_invalid}")
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
+
1757
+ with handler:
1758
+ df = read_dataframe(
1759
+ filename,
1760
+ use_arrow=use_arrow,
1761
+ on_invalid=on_invalid,
1762
+ )
1763
+ df.geometry.isnull().all()
1764
+
1765
+
1766
+ def test_read_multisurface(multisurface_file, use_arrow):
1645
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
+
1646
1772
  with pytest.raises(shapely.errors.GEOSException):
1647
1773
  # TODO(Arrow)
1648
1774
  # shapely fails parsing the WKB
1649
- read_dataframe(data_dir / "test_multisurface.gpkg", use_arrow=True)
1775
+ read_dataframe(multisurface_file, use_arrow=True)
1650
1776
  else:
1651
- df = read_dataframe(data_dir / "test_multisurface.gpkg")
1777
+ df = read_dataframe(multisurface_file)
1652
1778
 
1653
1779
  # MultiSurface should be converted to MultiPolygon
1654
1780
  assert df.geometry.type.tolist() == ["MultiPolygon"]
1655
1781
 
1656
1782
 
1657
- def test_read_dataset_kwargs(data_dir, use_arrow):
1658
- filename = data_dir / "test_nested.geojson"
1659
-
1783
+ def test_read_dataset_kwargs(nested_geojson_file, use_arrow):
1660
1784
  # by default, nested data are not flattened
1661
- df = read_dataframe(filename, use_arrow=use_arrow)
1785
+ df = read_dataframe(nested_geojson_file, use_arrow=use_arrow)
1662
1786
 
1663
1787
  expected = gp.GeoDataFrame(
1664
1788
  {
@@ -1671,7 +1795,9 @@ def test_read_dataset_kwargs(data_dir, use_arrow):
1671
1795
 
1672
1796
  assert_geodataframe_equal(df, expected)
1673
1797
 
1674
- 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
+ )
1675
1801
 
1676
1802
  expected = gp.GeoDataFrame(
1677
1803
  {
@@ -1873,6 +1999,9 @@ def test_write_memory(naturalearth_lowres, driver):
1873
1999
  check_dtype=not is_json,
1874
2000
  )
1875
2001
 
2002
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2003
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2004
+
1876
2005
 
1877
2006
  def test_write_memory_driver_required(naturalearth_lowres):
1878
2007
  df = read_dataframe(naturalearth_lowres)
@@ -1885,6 +2014,9 @@ def test_write_memory_driver_required(naturalearth_lowres):
1885
2014
  ):
1886
2015
  write_dataframe(df.head(1), buffer, driver=None, layer="test")
1887
2016
 
2017
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2018
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2019
+
1888
2020
 
1889
2021
  @pytest.mark.parametrize("driver", ["ESRI Shapefile", "OpenFileGDB"])
1890
2022
  def test_write_memory_unsupported_driver(naturalearth_lowres, driver):
@@ -1900,6 +2032,9 @@ def test_write_memory_unsupported_driver(naturalearth_lowres, driver):
1900
2032
  ):
1901
2033
  write_dataframe(df, buffer, driver=driver, layer="test")
1902
2034
 
2035
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2036
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2037
+
1903
2038
 
1904
2039
  @pytest.mark.parametrize("driver", ["GeoJSON", "GPKG"])
1905
2040
  def test_write_memory_append_unsupported(naturalearth_lowres, driver):
@@ -1912,6 +2047,9 @@ def test_write_memory_append_unsupported(naturalearth_lowres, driver):
1912
2047
  ):
1913
2048
  write_dataframe(df.head(1), buffer, driver=driver, layer="test", append=True)
1914
2049
 
2050
+ # Check temp file was cleaned up. Filter, as gdal keeps cache files in /vsimem/.
2051
+ assert vsi_listtree("/vsimem/", pattern="pyogrio_*") == []
2052
+
1915
2053
 
1916
2054
  def test_write_memory_existing_unsupported(naturalearth_lowres):
1917
2055
  df = read_dataframe(naturalearth_lowres)
@@ -1923,6 +2061,33 @@ def test_write_memory_existing_unsupported(naturalearth_lowres):
1923
2061
  ):
1924
2062
  write_dataframe(df.head(1), buffer, driver="GeoJSON", layer="test")
1925
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
+
1926
2091
 
1927
2092
  @pytest.mark.parametrize("ext", ["gpkg", "geojson"])
1928
2093
  def test_non_utf8_encoding_io(tmp_path, ext, encoded_text):
@@ -2014,7 +2179,8 @@ def test_non_utf8_encoding_io_shapefile(tmp_path, encoded_text, use_arrow):
2014
2179
 
2015
2180
 
2016
2181
  def test_encoding_read_option_collision_shapefile(naturalearth_lowres, use_arrow):
2017
- """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."""
2018
2184
 
2019
2185
  with pytest.raises(
2020
2186
  ValueError, match='cannot provide both encoding parameter and "ENCODING" option'
@@ -2025,7 +2191,8 @@ def test_encoding_read_option_collision_shapefile(naturalearth_lowres, use_arrow
2025
2191
 
2026
2192
 
2027
2193
  def test_encoding_write_layer_option_collision_shapefile(tmp_path, encoded_text):
2028
- """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."""
2029
2196
  encoding, text = encoded_text
2030
2197
 
2031
2198
  output_path = tmp_path / "test.shp"
@@ -2033,7 +2200,10 @@ def test_encoding_write_layer_option_collision_shapefile(tmp_path, encoded_text)
2033
2200
 
2034
2201
  with pytest.raises(
2035
2202
  ValueError,
2036
- 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
+ ),
2037
2207
  ):
2038
2208
  write_dataframe(
2039
2209
  df, output_path, encoding=encoding, layer_options={"ENCODING": ""}
@@ -2067,3 +2237,78 @@ def test_non_utf8_encoding_shapefile_sql(tmp_path, use_arrow):
2067
2237
  )
2068
2238
  assert actual.columns[0] == mandarin
2069
2239
  assert actual[mandarin].values[0] == mandarin
2240
+
2241
+
2242
+ @pytest.mark.requires_arrow_write_api
2243
+ def test_write_kml_file_coordinate_order(tmp_path, use_arrow):
2244
+ # confirm KML coordinates are written in lon, lat order even if CRS axis
2245
+ # specifies otherwise
2246
+ points = [Point(10, 20), Point(30, 40), Point(50, 60)]
2247
+ gdf = gp.GeoDataFrame(geometry=points, crs="EPSG:4326")
2248
+ output_path = tmp_path / "test.kml"
2249
+ write_dataframe(
2250
+ gdf, output_path, layer="tmp_layer", driver="KML", use_arrow=use_arrow
2251
+ )
2252
+
2253
+ gdf_in = read_dataframe(output_path, use_arrow=use_arrow)
2254
+
2255
+ assert np.array_equal(gdf_in.geometry.values, points)
2256
+
2257
+ if "LIBKML" in list_drivers():
2258
+ # test appending to the existing file only if LIBKML is available
2259
+ # as it appears to fall back on LIBKML driver when appending.
2260
+ points_append = [Point(70, 80), Point(90, 100), Point(110, 120)]
2261
+ gdf_append = gp.GeoDataFrame(geometry=points_append, crs="EPSG:4326")
2262
+
2263
+ write_dataframe(
2264
+ gdf_append,
2265
+ output_path,
2266
+ layer="tmp_layer",
2267
+ driver="KML",
2268
+ use_arrow=use_arrow,
2269
+ append=True,
2270
+ )
2271
+ # force_2d used to only compare xy geometry as z-dimension is undesirably
2272
+ # introduced when the kml file is over-written.
2273
+ gdf_in_appended = read_dataframe(
2274
+ output_path, use_arrow=use_arrow, force_2d=True
2275
+ )
2276
+
2277
+ assert np.array_equal(gdf_in_appended.geometry.values, points + points_append)
2278
+
2279
+
2280
+ @pytest.mark.requires_arrow_write_api
2281
+ def test_write_geojson_rfc7946_coordinates(tmp_path, use_arrow):
2282
+ points = [Point(10, 20), Point(30, 40), Point(50, 60)]
2283
+ gdf = gp.GeoDataFrame(geometry=points, crs="EPSG:4326")
2284
+ output_path = tmp_path / "test.geojson"
2285
+ write_dataframe(
2286
+ gdf,
2287
+ output_path,
2288
+ layer="tmp_layer",
2289
+ driver="GeoJSON",
2290
+ RFC7946=True,
2291
+ use_arrow=use_arrow,
2292
+ )
2293
+
2294
+ gdf_in = read_dataframe(output_path, use_arrow=use_arrow)
2295
+
2296
+ assert np.array_equal(gdf_in.geometry.values, points)
2297
+
2298
+ # test appending to the existing file
2299
+
2300
+ points_append = [Point(70, 80), Point(90, 100), Point(110, 120)]
2301
+ gdf_append = gp.GeoDataFrame(geometry=points_append, crs="EPSG:4326")
2302
+
2303
+ write_dataframe(
2304
+ gdf_append,
2305
+ output_path,
2306
+ layer="tmp_layer",
2307
+ driver="GeoJSON",
2308
+ RFC7946=True,
2309
+ use_arrow=use_arrow,
2310
+ append=True,
2311
+ )
2312
+
2313
+ gdf_in_appended = read_dataframe(output_path, use_arrow=use_arrow)
2314
+ assert np.array_equal(gdf_in_appended.geometry.values, points + points_append)