pyogrio 0.8.0__cp39-cp39-win_amd64.whl → 0.10.0__cp39-cp39-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 (103) hide show
  1. pyogrio/__init__.py +33 -22
  2. pyogrio/_compat.py +7 -1
  3. pyogrio/_env.py +4 -6
  4. pyogrio/_err.c +460 -445
  5. pyogrio/_err.cp39-win_amd64.pyd +0 -0
  6. pyogrio/_geometry.c +645 -612
  7. pyogrio/_geometry.cp39-win_amd64.pyd +0 -0
  8. pyogrio/_io.c +7764 -7602
  9. pyogrio/_io.cp39-win_amd64.pyd +0 -0
  10. pyogrio/_ogr.c +601 -609
  11. pyogrio/_ogr.cp39-win_amd64.pyd +0 -0
  12. pyogrio/_version.py +3 -3
  13. pyogrio/_vsi.c +7570 -2514
  14. pyogrio/_vsi.cp39-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 +75 -90
  55. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/WHEEL +1 -1
  56. pyogrio.libs/.load-order-pyogrio-0.10.0 +18 -0
  57. pyogrio.libs/{Lerc-62a2c1c74500e7815994b3e49b36750c.dll → Lerc-089e3fef3df84b17326dcddbf1dedaa4.dll} +0 -0
  58. pyogrio.libs/{gdal-2bfc6a9f962a8953b0640db9a272d797.dll → gdal-debee5933f0da7bb90b4bcd009023377.dll} +0 -0
  59. pyogrio.libs/{geos-289d7171bd083dfed1f8a90e4ae57442.dll → geos-ace4c5b5c1f569bb4213e7bbd0b0322e.dll} +0 -0
  60. pyogrio.libs/{geos_c-2a12859cd876719c648f1eb950b7d94c.dll → geos_c-7478ca0a86136b280d9b2d245c6f6627.dll} +0 -0
  61. pyogrio.libs/geotiff-c8fe8a095520a4ea4e465d27e06add3a.dll +0 -0
  62. pyogrio.libs/{iconv-2-f2d9304f8dc4cdd981024b520b73a099.dll → iconv-2-27352d156a5467ca5383d3951093ea5a.dll} +0 -0
  63. pyogrio.libs/{jpeg62-a67b2bf7fd32d34c565ae5bb6d47c224.dll → jpeg62-e56b6f95a95af498f4623b8da4cebd46.dll} +0 -0
  64. pyogrio.libs/{json-c-79a8df7e59952f5c5d594620e4b66c13.dll → json-c-c84940e2654a4f8468bfcf2ce992aa93.dll} +0 -0
  65. pyogrio.libs/libcurl-d69cfd4ad487d53d58743b6778ec85e7.dll +0 -0
  66. pyogrio.libs/{libexpat-fa55f107b678de136400c6d953c3cdde.dll → libexpat-6576a8d02641b6a3dbad35901ec200a7.dll} +0 -0
  67. pyogrio.libs/liblzma-9ee4accb476ec1ae24e924953140273d.dll +0 -0
  68. pyogrio.libs/{libpng16-6227e9a35c2a350ae6b0586079c10b9e.dll → libpng16-7c36142dda59f186f6bb683e8dae2bfe.dll} +0 -0
  69. pyogrio.libs/{msvcp140-46db46e967c8db2cb7a20fc75872a57e.dll → msvcp140-98b3e5b80de1e5e9d1703b786d795623.dll} +0 -0
  70. pyogrio.libs/proj-a408c5327f3fd2f5fabe8c56815beed7.dll +0 -0
  71. pyogrio.libs/{qhull_r-d8840f4ed1f7d452ff9a30237320bcfd.dll → qhull_r-516897f855568caab1ab1fe37912766c.dll} +0 -0
  72. pyogrio.libs/sqlite3-9bc109d8536d5ed9666332fec94485fc.dll +0 -0
  73. pyogrio.libs/{tiff-ffca1ff19d0e95dad39df0078fb037af.dll → tiff-9b3f605fffe0bccc0a964c374ee4f820.dll} +0 -0
  74. pyogrio.libs/{zlib1-aaba6ea052f6d3fa3d84a301e3eb3d30.dll → zlib1-e5af16a15c63f05bd82d90396807ae5b.dll} +0 -0
  75. pyogrio/_err.pxd +0 -4
  76. pyogrio/_err.pyx +0 -250
  77. pyogrio/_geometry.pxd +0 -4
  78. pyogrio/_geometry.pyx +0 -129
  79. pyogrio/_io.pxd +0 -0
  80. pyogrio/_io.pyx +0 -2738
  81. pyogrio/_ogr.pxd +0 -441
  82. pyogrio/_ogr.pyx +0 -346
  83. pyogrio/_vsi.pxd +0 -4
  84. pyogrio/_vsi.pyx +0 -140
  85. pyogrio/arrow_bridge.h +0 -115
  86. pyogrio/gdal_data/bag_template.xml +0 -201
  87. pyogrio/gdal_data/gmlasconf.xml +0 -169
  88. pyogrio/gdal_data/gmlasconf.xsd +0 -1066
  89. pyogrio/gdal_data/netcdf_config.xsd +0 -143
  90. pyogrio/gdal_data/template_tiles.mapml +0 -28
  91. pyogrio/tests/fixtures/test_datetime.geojson +0 -7
  92. pyogrio/tests/fixtures/test_datetime_tz.geojson +0 -8
  93. pyogrio/tests/fixtures/test_fgdb.gdb.zip +0 -0
  94. pyogrio/tests/fixtures/test_nested.geojson +0 -18
  95. pyogrio/tests/fixtures/test_ogr_types_list.geojson +0 -12
  96. pyogrio-0.8.0.dist-info/DELVEWHEEL +0 -2
  97. pyogrio.libs/.load-order-pyogrio-0.8.0 +0 -18
  98. pyogrio.libs/geotiff-d1c0fcc3c454409ad8be61ff04a7422c.dll +0 -0
  99. pyogrio.libs/libcurl-7fef9869f6520a5fbdb2bc9ce4c496cc.dll +0 -0
  100. pyogrio.libs/liblzma-5a1f648afc3d4cf36e3aef2266d55143.dll +0 -0
  101. pyogrio.libs/proj-74051a73897c9fa6d7bfef4561688568.dll +0 -0
  102. pyogrio.libs/sqlite3-fe7a86058d1c5658d1f9106228a7fd83.dll +0 -0
  103. {pyogrio-0.8.0.dist-info → pyogrio-0.10.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,17 @@
1
+ import contextlib
1
2
  import os
2
3
  from pathlib import Path
3
- import contextlib
4
- from zipfile import ZipFile, ZIP_DEFLATED
5
-
6
- import pytest
4
+ from zipfile import ZIP_DEFLATED, ZipFile
7
5
 
8
6
  import pyogrio
9
7
  import pyogrio.raw
10
- from pyogrio.util import vsi_path, get_vsi_path_or_buffer
8
+ from pyogrio._compat import HAS_PYPROJ
9
+ from pyogrio.util import get_vsi_path_or_buffer, vsi_path
10
+
11
+ import pytest
11
12
 
12
13
  try:
13
- import geopandas # NOQA
14
+ import geopandas # noqa: F401
14
15
 
15
16
  has_geopandas = True
16
17
  except ImportError:
@@ -32,9 +33,11 @@ def change_cwd(path):
32
33
  [
33
34
  # local file paths that should be passed through as is
34
35
  ("data.gpkg", "data.gpkg"),
36
+ (Path("data.gpkg"), "data.gpkg"),
35
37
  ("/home/user/data.gpkg", "/home/user/data.gpkg"),
36
38
  (r"C:\User\Documents\data.gpkg", r"C:\User\Documents\data.gpkg"),
37
39
  ("file:///home/user/data.gpkg", "/home/user/data.gpkg"),
40
+ ("/home/folder # with hash/data.gpkg", "/home/folder # with hash/data.gpkg"),
38
41
  # cloud URIs
39
42
  ("https://testing/data.gpkg", "/vsicurl/https://testing/data.gpkg"),
40
43
  ("s3://testing/data.gpkg", "/vsis3/testing/data.gpkg"),
@@ -83,6 +86,8 @@ def change_cwd(path):
83
86
  "s3://testing/test.zip!a/b/item.shp",
84
87
  "/vsizip/vsis3/testing/test.zip/a/b/item.shp",
85
88
  ),
89
+ ("/vsimem/data.gpkg", "/vsimem/data.gpkg"),
90
+ (Path("/vsimem/data.gpkg"), "/vsimem/data.gpkg"),
86
91
  ],
87
92
  )
88
93
  def test_vsi_path(path, expected):
@@ -237,6 +242,9 @@ def test_detect_zip_path(tmp_path, naturalearth_lowres):
237
242
  path = tmp_path / "test.zip"
238
243
  with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=5) as out:
239
244
  for ext in ["dbf", "prj", "shp", "shx"]:
245
+ if not HAS_PYPROJ and ext == "prj":
246
+ continue
247
+
240
248
  filename = f"test1.{ext}"
241
249
  out.write(tmp_path / filename, filename)
242
250
 
@@ -278,6 +286,7 @@ def test_url():
278
286
  assert len(result[0]) == 177
279
287
 
280
288
 
289
+ @pytest.mark.network
281
290
  @pytest.mark.skipif(not has_geopandas, reason="GeoPandas not available")
282
291
  def test_url_dataframe():
283
292
  url = "https://raw.githubusercontent.com/geopandas/pyogrio/main/pyogrio/tests/fixtures/naturalearth_lowres/naturalearth_lowres.shp"
@@ -333,19 +342,23 @@ def test_uri_s3_dataframe(aws_env_setup):
333
342
  assert len(df) == 67
334
343
 
335
344
 
336
- def test_get_vsi_path_or_buffer_obj_to_string():
337
- path = Path("/tmp/test.gpkg")
338
- assert get_vsi_path_or_buffer(path) == str(path)
345
+ @pytest.mark.parametrize(
346
+ "path, expected",
347
+ [
348
+ (Path("/tmp/test.gpkg"), str(Path("/tmp/test.gpkg"))),
349
+ (Path("/vsimem/test.gpkg"), "/vsimem/test.gpkg"),
350
+ ],
351
+ )
352
+ def test_get_vsi_path_or_buffer_obj_to_string(path, expected):
353
+ """Verify that get_vsi_path_or_buffer retains forward slashes in /vsimem paths.
354
+
355
+ The /vsimem paths should keep forward slashes for GDAL to recognize them as such.
356
+ However, on Windows systems, forward slashes are by default replaced by backslashes,
357
+ so this test verifies that this doesn't happen for /vsimem paths.
358
+ """
359
+ assert get_vsi_path_or_buffer(path) == expected
339
360
 
340
361
 
341
362
  def test_get_vsi_path_or_buffer_fixtures_to_string(tmp_path):
342
363
  path = tmp_path / "test.gpkg"
343
364
  assert get_vsi_path_or_buffer(path) == str(path)
344
-
345
-
346
- @pytest.mark.parametrize(
347
- "raw_path", ["/vsimem/test.shp.zip", "/vsizip//vsimem/test.shp.zip"]
348
- )
349
- def test_vsimem_path_exception(raw_path):
350
- with pytest.raises(ValueError, match=""):
351
- vsi_path(raw_path)
@@ -1,33 +1,36 @@
1
1
  import contextlib
2
2
  import ctypes
3
- from io import BytesIO
4
3
  import json
5
4
  import sys
5
+ from io import BytesIO
6
+ from zipfile import ZipFile
6
7
 
7
8
  import numpy as np
8
9
  from numpy import array_equal
9
- import pytest
10
10
 
11
11
  import pyogrio
12
12
  from pyogrio import (
13
- list_layers,
13
+ __gdal_version__,
14
+ get_gdal_config_option,
14
15
  list_drivers,
16
+ list_layers,
15
17
  read_info,
16
18
  set_gdal_config_options,
17
- get_gdal_config_option,
18
- __gdal_version__,
19
19
  )
20
- from pyogrio._compat import HAS_SHAPELY, HAS_PYARROW
21
- from pyogrio.raw import read, write, open_arrow
22
- from pyogrio.errors import DataSourceError, DataLayerError, FeatureError
20
+ from pyogrio._compat import HAS_PYARROW, HAS_SHAPELY
21
+ from pyogrio.errors import DataLayerError, DataSourceError, FeatureError
22
+ from pyogrio.raw import open_arrow, read, write
23
23
  from pyogrio.tests.conftest import (
24
- DRIVERS,
25
24
  DRIVER_EXT,
25
+ DRIVERS,
26
26
  prepare_testfile,
27
- requires_pyarrow_api,
28
27
  requires_arrow_api,
28
+ requires_pyarrow_api,
29
+ requires_shapely,
29
30
  )
30
31
 
32
+ import pytest
33
+
31
34
  try:
32
35
  import shapely
33
36
  except ImportError:
@@ -116,6 +119,29 @@ def test_read_no_geometry(naturalearth_lowres):
116
119
  assert geometry is None
117
120
 
118
121
 
122
+ @requires_shapely
123
+ def test_read_no_geometry__mask(naturalearth_lowres):
124
+ geometry, fields = read(
125
+ naturalearth_lowres,
126
+ read_geometry=False,
127
+ mask=shapely.Point(-105, 55),
128
+ )[2:]
129
+
130
+ assert np.array_equal(fields[3], ["CAN"])
131
+ assert geometry is None
132
+
133
+
134
+ def test_read_no_geometry__bbox(naturalearth_lowres):
135
+ geometry, fields = read(
136
+ naturalearth_lowres,
137
+ read_geometry=False,
138
+ bbox=(-109.0, 55.0, -109.0, 55.0),
139
+ )[2:]
140
+
141
+ assert np.array_equal(fields[3], ["CAN"])
142
+ assert geometry is None
143
+
144
+
119
145
  def test_read_no_geometry_no_columns_no_fids(naturalearth_lowres):
120
146
  with pytest.raises(
121
147
  ValueError,
@@ -255,9 +281,7 @@ def test_read_bbox_where(naturalearth_lowres_all_ext):
255
281
  assert np.array_equal(fields[3], ["CAN"])
256
282
 
257
283
 
258
- @pytest.mark.skipif(
259
- not HAS_SHAPELY, reason="Shapely is required for mask functionality"
260
- )
284
+ @requires_shapely
261
285
  @pytest.mark.parametrize(
262
286
  "mask",
263
287
  [
@@ -271,17 +295,13 @@ def test_read_mask_invalid(naturalearth_lowres, mask):
271
295
  read(naturalearth_lowres, mask=mask)
272
296
 
273
297
 
274
- @pytest.mark.skipif(
275
- not HAS_SHAPELY, reason="Shapely is required for mask functionality"
276
- )
298
+ @requires_shapely
277
299
  def test_read_bbox_mask_invalid(naturalearth_lowres):
278
300
  with pytest.raises(ValueError, match="cannot set both 'bbox' and 'mask'"):
279
301
  read(naturalearth_lowres, bbox=(-85, 8, -80, 10), mask=shapely.Point(-105, 55))
280
302
 
281
303
 
282
- @pytest.mark.skipif(
283
- not HAS_SHAPELY, reason="Shapely is required for mask functionality"
284
- )
304
+ @requires_shapely
285
305
  @pytest.mark.parametrize(
286
306
  "mask,expected",
287
307
  [
@@ -316,9 +336,7 @@ def test_read_mask(naturalearth_lowres_all_ext, mask, expected):
316
336
  assert len(geometry) == len(expected)
317
337
 
318
338
 
319
- @pytest.mark.skipif(
320
- not HAS_SHAPELY, reason="Shapely is required for mask functionality"
321
- )
339
+ @requires_shapely
322
340
  def test_read_mask_sql(naturalearth_lowres_all_ext):
323
341
  fields = read(
324
342
  naturalearth_lowres_all_ext,
@@ -329,9 +347,7 @@ def test_read_mask_sql(naturalearth_lowres_all_ext):
329
347
  assert np.array_equal(fields[3], ["CAN"])
330
348
 
331
349
 
332
- @pytest.mark.skipif(
333
- not HAS_SHAPELY, reason="Shapely is required for mask functionality"
334
- )
350
+ @requires_shapely
335
351
  def test_read_mask_where(naturalearth_lowres_all_ext):
336
352
  fields = read(
337
353
  naturalearth_lowres_all_ext,
@@ -603,13 +619,80 @@ def test_write_no_geom_no_fields():
603
619
  __gdal_version__ < (3, 6, 0),
604
620
  reason="OpenFileGDB write support only available for GDAL >= 3.6.0",
605
621
  )
606
- def test_write_openfilegdb(tmp_path, naturalearth_lowres):
607
- meta, _, geometry, field_data = read(naturalearth_lowres)
622
+ @pytest.mark.parametrize(
623
+ "write_int64",
624
+ [
625
+ False,
626
+ pytest.param(
627
+ True,
628
+ marks=pytest.mark.skipif(
629
+ __gdal_version__ < (3, 9, 0),
630
+ reason="OpenFileGDB write support for int64 values for GDAL >= 3.9.0",
631
+ ),
632
+ ),
633
+ ],
634
+ )
635
+ def test_write_openfilegdb(tmp_path, write_int64):
636
+ # Point(0, 0)
637
+ expected_geometry = np.array(
638
+ [bytes.fromhex("010100000000000000000000000000000000000000")] * 3, dtype=object
639
+ )
640
+ expected_field_data = [
641
+ np.array([True, False, True], dtype="bool"),
642
+ np.array([1, 2, 3], dtype="int16"),
643
+ np.array([1, 2, 3], dtype="int32"),
644
+ np.array([1, 2, 3], dtype="int64"),
645
+ np.array([1, 2, 3], dtype="float32"),
646
+ np.array([1, 2, 3], dtype="float64"),
647
+ ]
648
+ expected_fields = ["bool", "int16", "int32", "int64", "float32", "float64"]
649
+ expected_meta = {
650
+ "geometry_type": "Point",
651
+ "crs": "EPSG:4326",
652
+ "fields": expected_fields,
653
+ }
608
654
 
609
655
  filename = tmp_path / "test.gdb"
610
- write(filename, geometry, field_data, driver="OpenFileGDB", **meta)
611
656
 
612
- assert filename.exists()
657
+ # int64 is not supported without additional config: https://gdal.org/en/latest/drivers/vector/openfilegdb.html#bit-integer-field-support
658
+ # it is converted to float64 by default and raises a warning
659
+ # (for GDAL >= 3.9.0 only)
660
+ write_params = (
661
+ {"TARGET_ARCGIS_VERSION": "ARCGIS_PRO_3_2_OR_LATER"} if write_int64 else {}
662
+ )
663
+
664
+ if write_int64 or __gdal_version__ < (3, 9, 0):
665
+ ctx = contextlib.nullcontext()
666
+ else:
667
+ ctx = pytest.warns(
668
+ RuntimeWarning, match="Integer64 will be written as a Float64"
669
+ )
670
+
671
+ with ctx:
672
+ write(
673
+ filename,
674
+ expected_geometry,
675
+ expected_field_data,
676
+ driver="OpenFileGDB",
677
+ **expected_meta,
678
+ **write_params,
679
+ )
680
+
681
+ meta, _, geometry, field_data = read(filename)
682
+
683
+ if not write_int64:
684
+ expected_field_data[3] = expected_field_data[3].astype("float64")
685
+
686
+ # bool types are converted to int32
687
+ expected_field_data[0] = expected_field_data[0].astype("int32")
688
+
689
+ assert meta["crs"] == expected_meta["crs"]
690
+ assert np.array_equal(meta["fields"], expected_meta["fields"])
691
+
692
+ assert np.array_equal(geometry, expected_geometry)
693
+ for i in range(len(expected_field_data)):
694
+ assert field_data[i].dtype == expected_field_data[i].dtype
695
+ assert np.array_equal(field_data[i], expected_field_data[i])
613
696
 
614
697
 
615
698
  @pytest.mark.parametrize("ext", DRIVERS)
@@ -743,7 +826,7 @@ def assert_equal_result(result1, result2):
743
826
 
744
827
  assert np.array_equal(meta1["fields"], meta2["fields"])
745
828
  assert np.array_equal(index1, index2)
746
- assert all([np.array_equal(f1, f2) for f1, f2 in zip(field_data1, field_data2)])
829
+ assert all(np.array_equal(f1, f2) for f1, f2 in zip(field_data1, field_data2))
747
830
 
748
831
  if HAS_SHAPELY:
749
832
  # a plain `assert np.array_equal(geometry1, geometry2)` doesn't work
@@ -794,6 +877,12 @@ def test_read_from_file_like(tmp_path, naturalearth_lowres, driver, ext):
794
877
  assert_equal_result((meta, index, geometry, field_data), result2)
795
878
 
796
879
 
880
+ def test_read_from_nonseekable_bytes(nonseekable_bytes):
881
+ meta, _, geometry, _ = read(nonseekable_bytes)
882
+ assert meta["fields"].shape == (0,)
883
+ assert len(geometry) == 1
884
+
885
+
797
886
  @pytest.mark.parametrize("ext", ["gpkg", "fgb"])
798
887
  def test_read_write_data_types_numeric(tmp_path, ext):
799
888
  # Point(0, 0)
@@ -809,13 +898,13 @@ def test_read_write_data_types_numeric(tmp_path, ext):
809
898
  np.array([1, 2, 3], dtype="float64"),
810
899
  ]
811
900
  fields = ["bool", "int16", "int32", "int64", "float32", "float64"]
812
- meta = dict(geometry_type="Point", crs="EPSG:4326", spatial_index=False)
901
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "spatial_index": False}
813
902
 
814
903
  filename = tmp_path / f"test.{ext}"
815
904
  write(filename, geometry, field_data, fields, **meta)
816
905
  result = read(filename)[3]
817
- assert all([np.array_equal(f1, f2) for f1, f2 in zip(result, field_data)])
818
- assert all([f1.dtype == f2.dtype for f1, f2 in zip(result, field_data)])
906
+ assert all(np.array_equal(f1, f2) for f1, f2 in zip(result, field_data))
907
+ assert all(f1.dtype == f2.dtype for f1, f2 in zip(result, field_data))
819
908
 
820
909
  # other integer data types that don't roundtrip exactly
821
910
  # these are generally promoted to a larger integer type except for uint64
@@ -866,7 +955,7 @@ def test_read_write_datetime(tmp_path):
866
955
  geometry = np.array(
867
956
  [bytes.fromhex("010100000000000000000000000000000000000000")] * 2, dtype=object
868
957
  )
869
- meta = dict(geometry_type="Point", crs="EPSG:4326", spatial_index=False)
958
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "spatial_index": False}
870
959
 
871
960
  filename = tmp_path / "test.gpkg"
872
961
  write(filename, geometry, field_data, fields, **meta)
@@ -889,7 +978,7 @@ def test_read_write_int64_large(tmp_path, ext):
889
978
  )
890
979
  field_data = [np.array([1, 2192502720, -5], dtype="int64")]
891
980
  fields = ["overflow_int64"]
892
- meta = dict(geometry_type="Point", crs="EPSG:4326", spatial_index=False)
981
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "spatial_index": False}
893
982
 
894
983
  filename = tmp_path / f"test.{ext}"
895
984
  write(filename, geometry, field_data, fields, **meta)
@@ -912,17 +1001,17 @@ def test_read_data_types_numeric_with_null(test_gpkg_nulls):
912
1001
  assert field.dtype == "float64"
913
1002
 
914
1003
 
915
- def test_read_unsupported_types(test_ogr_types_list):
916
- fields = read(test_ogr_types_list)[3]
1004
+ def test_read_unsupported_types(list_field_values_file):
1005
+ fields = read(list_field_values_file)[3]
917
1006
  # list field gets skipped, only integer field is read
918
1007
  assert len(fields) == 1
919
1008
 
920
- fields = read(test_ogr_types_list, columns=["int64"])[3]
1009
+ fields = read(list_field_values_file, columns=["int64"])[3]
921
1010
  assert len(fields) == 1
922
1011
 
923
1012
 
924
- def test_read_datetime_millisecond(test_datetime):
925
- field = read(test_datetime)[3][0]
1013
+ def test_read_datetime_millisecond(datetime_file):
1014
+ field = read(datetime_file)[3][0]
926
1015
  assert field.dtype == "datetime64[ms]"
927
1016
  assert field[0] == np.datetime64("2020-01-01 09:00:00.123")
928
1017
  assert field[1] == np.datetime64("2020-01-01 10:00:00.000")
@@ -951,13 +1040,14 @@ def test_read_unsupported_ext_with_prefix(tmp_path):
951
1040
  assert field_data[0] == "data1"
952
1041
 
953
1042
 
954
- def test_read_datetime_as_string(test_datetime_tz):
955
- field = read(test_datetime_tz)[3][0]
1043
+ def test_read_datetime_as_string(datetime_tz_file):
1044
+ field = read(datetime_tz_file)[3][0]
956
1045
  assert field.dtype == "datetime64[ms]"
957
1046
  # timezone is ignored in numpy layer
958
1047
  assert field[0] == np.datetime64("2020-01-01 09:00:00.123")
959
1048
  assert field[1] == np.datetime64("2020-01-01 10:00:00.000")
960
- field = read(test_datetime_tz, datetime_as_string=True)[3][0]
1049
+
1050
+ field = read(datetime_tz_file, datetime_as_string=True)[3][0]
961
1051
  assert field.dtype == "object"
962
1052
  # GDAL doesn't return strings in ISO format (yet)
963
1053
  assert field[0] == "2020/01/01 09:00:00.123-05"
@@ -973,7 +1063,7 @@ def test_read_write_null_geometry(tmp_path, ext):
973
1063
  )
974
1064
  field_data = [np.array([1, 2], dtype="int32")]
975
1065
  fields = ["col"]
976
- meta = dict(geometry_type="Point", crs="EPSG:4326")
1066
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326"}
977
1067
  if ext == "gpkg":
978
1068
  meta["spatial_index"] = False
979
1069
 
@@ -993,12 +1083,12 @@ def test_write_float_nan_null(tmp_path, dtype):
993
1083
  )
994
1084
  field_data = [np.array([1.5, np.nan], dtype=dtype)]
995
1085
  fields = ["col"]
996
- meta = dict(geometry_type="Point", crs="EPSG:4326")
1086
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326"}
997
1087
  filename = tmp_path / "test.geojson"
998
1088
 
999
1089
  # default nan_as_null=True
1000
1090
  write(filename, geometry, field_data, fields, **meta)
1001
- with open(filename, "r") as f:
1091
+ with open(filename) as f:
1002
1092
  content = f.read()
1003
1093
  assert '{ "col": null }' in content
1004
1094
 
@@ -1010,7 +1100,7 @@ def test_write_float_nan_null(tmp_path, dtype):
1010
1100
  ctx = contextlib.nullcontext()
1011
1101
  with ctx:
1012
1102
  write(filename, geometry, field_data, fields, **meta, nan_as_null=False)
1013
- with open(filename, "r") as f:
1103
+ with open(filename) as f:
1014
1104
  content = f.read()
1015
1105
  assert '"properties": { }' in content
1016
1106
 
@@ -1024,7 +1114,7 @@ def test_write_float_nan_null(tmp_path, dtype):
1024
1114
  nan_as_null=False,
1025
1115
  WRITE_NON_FINITE_VALUES="YES",
1026
1116
  )
1027
- with open(filename, "r") as f:
1117
+ with open(filename) as f:
1028
1118
  content = f.read()
1029
1119
  assert '{ "col": NaN }' in content
1030
1120
 
@@ -1043,7 +1133,7 @@ def test_write_float_nan_null_arrow(tmp_path):
1043
1133
  )
1044
1134
  field_data = [np.array([1.5, np.nan], dtype="float64")]
1045
1135
  fields = ["col"]
1046
- meta = dict(geometry_type="Point", crs="EPSG:4326")
1136
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326"}
1047
1137
  fname = tmp_path / "test.arrow"
1048
1138
 
1049
1139
  # default nan_as_null=True
@@ -1146,6 +1236,27 @@ def test_write_memory_existing_unsupported(naturalearth_lowres):
1146
1236
  write(buffer, geometry, field_data, driver="GeoJSON", layer="test", **meta)
1147
1237
 
1148
1238
 
1239
+ def test_write_open_file_handle(tmp_path, naturalearth_lowres):
1240
+ """Verify that writing to an open file handle is not currently supported"""
1241
+
1242
+ meta, _, geometry, field_data = read(naturalearth_lowres)
1243
+
1244
+ # verify it fails for regular file handle
1245
+ with pytest.raises(
1246
+ NotImplementedError, match="writing to an open file handle is not yet supported"
1247
+ ):
1248
+ with open(tmp_path / "test.geojson", "wb") as f:
1249
+ write(f, geometry, field_data, driver="GeoJSON", layer="test", **meta)
1250
+
1251
+ # verify it fails for ZipFile
1252
+ with pytest.raises(
1253
+ NotImplementedError, match="writing to an open file handle is not yet supported"
1254
+ ):
1255
+ with ZipFile(tmp_path / "test.geojson.zip", "w") as z:
1256
+ with z.open("test.geojson", "w") as f:
1257
+ write(f, geometry, field_data, driver="GeoJSON", layer="test", **meta)
1258
+
1259
+
1149
1260
  @pytest.mark.parametrize("ext", ["fgb", "gpkg", "geojson"])
1150
1261
  @pytest.mark.parametrize(
1151
1262
  "read_encoding,write_encoding",
@@ -1182,7 +1293,7 @@ def test_encoding_io(tmp_path, ext, read_encoding, write_encoding):
1182
1293
  np.array([mandarin], dtype=object),
1183
1294
  ]
1184
1295
  fields = [arabic, cree, mandarin]
1185
- meta = dict(geometry_type="Point", crs="EPSG:4326", encoding=write_encoding)
1296
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "encoding": write_encoding}
1186
1297
 
1187
1298
  filename = tmp_path / f"test.{ext}"
1188
1299
  write(filename, geometry, field_data, fields, **meta)
@@ -1232,7 +1343,7 @@ def test_encoding_io_shapefile(tmp_path, read_encoding, write_encoding):
1232
1343
  # character level) by GDAL when output to shapefile, so we have to truncate
1233
1344
  # before writing
1234
1345
  fields = [arabic[:5], cree[:3], mandarin]
1235
- meta = dict(geometry_type="Point", crs="EPSG:4326", encoding="UTF-8")
1346
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "encoding": "UTF-8"}
1236
1347
 
1237
1348
  filename = tmp_path / "test.shp"
1238
1349
  # NOTE: GDAL automatically creates a cpg file with the encoding name, which
@@ -1277,7 +1388,7 @@ def test_non_utf8_encoding_io(tmp_path, ext, encoded_text):
1277
1388
  field_data = [np.array([text], dtype=object)]
1278
1389
 
1279
1390
  fields = [text]
1280
- meta = dict(geometry_type="Point", crs="EPSG:4326", encoding=encoding)
1391
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "encoding": encoding}
1281
1392
 
1282
1393
  filename = tmp_path / f"test.{ext}"
1283
1394
  write(filename, geometry, field_data, fields, **meta)
@@ -1307,7 +1418,7 @@ def test_non_utf8_encoding_io_shapefile(tmp_path, encoded_text):
1307
1418
  field_data = [np.array([text], dtype=object)]
1308
1419
 
1309
1420
  fields = [text]
1310
- meta = dict(geometry_type="Point", crs="EPSG:4326", encoding=encoding)
1421
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326", "encoding": encoding}
1311
1422
 
1312
1423
  filename = tmp_path / "test.shp"
1313
1424
  write(filename, geometry, field_data, fields, **meta)
@@ -1357,7 +1468,7 @@ def test_write_with_mask(tmp_path):
1357
1468
  field_data = [np.array([1, 2, 3], dtype="int32")]
1358
1469
  field_mask = [np.array([False, True, False])]
1359
1470
  fields = ["col"]
1360
- meta = dict(geometry_type="Point", crs="EPSG:4326")
1471
+ meta = {"geometry_type": "Point", "crs": "EPSG:4326"}
1361
1472
 
1362
1473
  filename = tmp_path / "test.geojson"
1363
1474
  write(filename, geometry, field_data, fields, field_mask, **meta)
@@ -0,0 +1,56 @@
1
+ from pathlib import Path
2
+
3
+ from pyogrio import vsi_listtree, vsi_unlink
4
+ from pyogrio.raw import read, write
5
+ from pyogrio.util import vsimem_rmtree_toplevel
6
+
7
+ import pytest
8
+
9
+
10
+ def test_vsimem_rmtree_toplevel(naturalearth_lowres):
11
+ # Prepare test data in /vsimem/
12
+ meta, _, geometry, field_data = read(naturalearth_lowres)
13
+ meta["spatial_index"] = False
14
+ meta["geometry_type"] = "MultiPolygon"
15
+ test_dir_path = Path(f"/vsimem/test/{naturalearth_lowres.stem}.gpkg")
16
+ test_dir2_path = Path(f"/vsimem/test2/test2/{naturalearth_lowres.stem}.gpkg")
17
+
18
+ write(test_dir_path, geometry, field_data, **meta)
19
+ write(test_dir2_path, geometry, field_data, **meta)
20
+
21
+ # Check if everything was created properly with listtree
22
+ files = vsi_listtree("/vsimem/")
23
+ assert test_dir_path.as_posix() in files
24
+ assert test_dir2_path.as_posix() in files
25
+
26
+ # Test deleting parent dir of file in single directory
27
+ vsimem_rmtree_toplevel(test_dir_path)
28
+ files = vsi_listtree("/vsimem/")
29
+ assert test_dir_path.parent.as_posix() not in files
30
+ assert test_dir2_path.as_posix() in files
31
+
32
+ # Test deleting top-level dir of file in a subdirectory
33
+ vsimem_rmtree_toplevel(test_dir2_path)
34
+ assert test_dir2_path.as_posix() not in vsi_listtree("/vsimem/")
35
+
36
+
37
+ def test_vsimem_rmtree_toplevel_error(naturalearth_lowres):
38
+ # Prepare test data in /vsimem
39
+ meta, _, geometry, field_data = read(naturalearth_lowres)
40
+ meta["spatial_index"] = False
41
+ meta["geometry_type"] = "MultiPolygon"
42
+ test_file_path = Path(f"/vsimem/pyogrio_test_{naturalearth_lowres.stem}.gpkg")
43
+
44
+ write(test_file_path, geometry, field_data, **meta)
45
+ assert test_file_path.as_posix() in vsi_listtree("/vsimem/")
46
+
47
+ # Deleting parent dir of non-existent file should raise an error.
48
+ with pytest.raises(FileNotFoundError, match="Path does not exist"):
49
+ vsimem_rmtree_toplevel("/vsimem/test/non-existent.gpkg")
50
+
51
+ # File should still be there
52
+ assert test_file_path.as_posix() in vsi_listtree("/vsimem/")
53
+
54
+ # Cleanup.
55
+ vsi_unlink(test_file_path)
56
+ assert test_file_path not in vsi_listtree("/vsimem/")