Skip to content

Commit 8c0a31e

Browse files
authored
Install new VSI plugin handler for every instance of using an opener (#1408)
* Set defaults for pyopener registry contextvar get() Resolves #1406 * Install a new VSI plugin handler for every instance Plugin handler installation is implicit and hidden from the API user, but could be exposed in the future if needed. Resolves #1406 * Add opener support to listdir(), listlayers(), and remove() This involves adding GDAL callback support for unlinking VSI files and also a major stat() bug fix. * Adjust expectation of number of files * Clean up * More cleanup and change log update
1 parent 4509d75 commit 8c0a31e

File tree

11 files changed

+552
-138
lines changed

11 files changed

+552
-138
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ All issue numbers are relative to https://github.com/Toblerity/Fiona/issues.
88

99
Bug fixes:
1010

11+
- The Pyopener registry and VSI plugin have been rewritten to avoid filename
12+
conflicts and to be compatible with multithreading. Now, a new plugin handler
13+
is registered for each instance of using an opener (#1408). Before GDAL 3.9.0
14+
plugin handlers cannot not be removed and so it may be observed that the size
15+
of the Pyopener registry grows during the execution of a program.
1116
- A CSLConstList ctypedef has been added and is used where appropriate (#1404).
1217
- Fiona model objects have a informative, printable representation again (#).
1318

fiona/__init__.py

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def func(*args, **kwds):
324324
log.debug("Registering opener: raw_dataset_path=%r, opener=%r", raw_dataset_path, opener)
325325
vsi_path_ctx = _opener_registration(raw_dataset_path, opener)
326326
registered_vsi_path = stack.enter_context(vsi_path_ctx)
327-
log.debug("Registered vsi path: registered_vsi_path%r", registered_vsi_path)
327+
log.debug("Registered vsi path: registered_vsi_path=%r", registered_vsi_path)
328328
path = _UnparsedPath(registered_vsi_path)
329329
else:
330330
if vfs:
@@ -386,7 +386,7 @@ def func(*args, **kwds):
386386

387387

388388
@ensure_env_with_credentials
389-
def remove(path_or_collection, driver=None, layer=None):
389+
def remove(path_or_collection, driver=None, layer=None, opener=None):
390390
"""Delete an OGR data source or one of its layers.
391391
392392
If no layer is specified, the entire dataset and all of its layers
@@ -396,6 +396,19 @@ def remove(path_or_collection, driver=None, layer=None):
396396
----------
397397
path_or_collection : str, pathlib.Path, or Collection
398398
The target Collection or its path.
399+
opener : callable or obj, optional
400+
A custom dataset opener which can serve GDAL's virtual
401+
filesystem machinery via Python file-like objects. The
402+
underlying file-like object is obtained by calling *opener* with
403+
(*fp*, *mode*) or (*fp*, *mode* + "b") depending on the format
404+
driver's native mode. *opener* must return a Python file-like
405+
object that provides read, seek, tell, and close methods. Note:
406+
only one opener at a time per fp, mode pair is allowed.
407+
408+
Alternatively, opener may be a filesystem object from a package
409+
like fsspec that provides the following methods: isdir(),
410+
isfile(), ls(), mtime(), open(), and size(). The exact interface
411+
is defined in the fiona._vsiopener._AbstractOpener class.
399412
driver : str, optional
400413
The name of a driver to be used for deletion, optional. Can
401414
usually be detected.
@@ -414,21 +427,37 @@ def remove(path_or_collection, driver=None, layer=None):
414427
"""
415428
if isinstance(path_or_collection, Collection):
416429
collection = path_or_collection
417-
path = collection.path
430+
raw_dataset_path = collection.path
418431
driver = collection.driver
419432
collection.close()
420-
elif isinstance(path_or_collection, Path):
421-
path = str(path_or_collection)
433+
422434
else:
423-
path = path_or_collection
424-
if layer is None:
425-
_remove(path, driver)
435+
fp = path_or_collection
436+
if hasattr(fp, "path") and hasattr(fp, "fs"):
437+
log.debug("Detected fp is an OpenFile: fp=%r", fp)
438+
raw_dataset_path = fp.path
439+
opener = fp.fs.open
440+
else:
441+
raw_dataset_path = os.fspath(fp)
442+
443+
if opener:
444+
log.debug("Registering opener: raw_dataset_path=%r, opener=%r", raw_dataset_path, opener)
445+
with _opener_registration(raw_dataset_path, opener) as registered_vsi_path:
446+
log.debug("Registered vsi path: registered_vsi_path=%r", registered_vsi_path)
447+
if layer is None:
448+
_remove(registered_vsi_path, driver)
449+
else:
450+
_remove_layer(registered_vsi_path, layer, driver)
426451
else:
427-
_remove_layer(path, layer, driver)
452+
pobj = _parse_path(raw_dataset_path)
453+
if layer is None:
454+
_remove(_vsi_path(pobj), driver)
455+
else:
456+
_remove_layer(_vsi_path(pobj), layer, driver)
428457

429458

430459
@ensure_env_with_credentials
431-
def listdir(fp):
460+
def listdir(fp, opener=None):
432461
"""Lists the datasets in a directory or archive file.
433462
434463
Archive files must be prefixed like "zip://" or "tar://".
@@ -437,6 +466,19 @@ def listdir(fp):
437466
----------
438467
fp : str or pathlib.Path
439468
Directory or archive path.
469+
opener : callable or obj, optional
470+
A custom dataset opener which can serve GDAL's virtual
471+
filesystem machinery via Python file-like objects. The
472+
underlying file-like object is obtained by calling *opener* with
473+
(*fp*, *mode*) or (*fp*, *mode* + "b") depending on the format
474+
driver's native mode. *opener* must return a Python file-like
475+
object that provides read, seek, tell, and close methods. Note:
476+
only one opener at a time per fp, mode pair is allowed.
477+
478+
Alternatively, opener may be a filesystem object from a package
479+
like fsspec that provides the following methods: isdir(),
480+
isfile(), ls(), mtime(), open(), and size(). The exact interface
481+
is defined in the fiona._vsiopener._AbstractOpener class.
440482
441483
Returns
442484
-------
@@ -449,18 +491,25 @@ def listdir(fp):
449491
If the input is not a str or Path.
450492
451493
"""
452-
if isinstance(fp, Path):
453-
fp = str(fp)
454-
455-
if not isinstance(fp, str):
456-
raise TypeError("invalid path: %r" % fp)
494+
if hasattr(fp, "path") and hasattr(fp, "fs"):
495+
log.debug("Detected fp is an OpenFile: fp=%r", fp)
496+
raw_dataset_path = fp.path
497+
opener = fp.fs.open
498+
else:
499+
raw_dataset_path = os.fspath(fp)
457500

458-
pobj = _parse_path(fp)
459-
return _listdir(_vsi_path(pobj))
501+
if opener:
502+
log.debug("Registering opener: raw_dataset_path=%r, opener=%r", raw_dataset_path, opener)
503+
with _opener_registration(raw_dataset_path, opener) as registered_vsi_path:
504+
log.debug("Registered vsi path: registered_vsi_path=%r", registered_vsi_path)
505+
return _listdir(registered_vsi_path)
506+
else:
507+
pobj = _parse_path(raw_dataset_path)
508+
return _listdir(_vsi_path(pobj))
460509

461510

462511
@ensure_env_with_credentials
463-
def listlayers(fp, vfs=None, **kwargs):
512+
def listlayers(fp, opener=None, vfs=None, **kwargs):
464513
"""Lists the layers (collections) in a dataset.
465514
466515
Archive files must be prefixed like "zip://" or "tar://".
@@ -469,6 +518,19 @@ def listlayers(fp, vfs=None, **kwargs):
469518
----------
470519
fp : str, pathlib.Path, or file-like object
471520
A dataset identifier or file object containing a dataset.
521+
opener : callable or obj, optional
522+
A custom dataset opener which can serve GDAL's virtual
523+
filesystem machinery via Python file-like objects. The
524+
underlying file-like object is obtained by calling *opener* with
525+
(*fp*, *mode*) or (*fp*, *mode* + "b") depending on the format
526+
driver's native mode. *opener* must return a Python file-like
527+
object that provides read, seek, tell, and close methods. Note:
528+
only one opener at a time per fp, mode pair is allowed.
529+
530+
Alternatively, opener may be a filesystem object from a package
531+
like fsspec that provides the following methods: isdir(),
532+
isfile(), ls(), mtime(), open(), and size(). The exact interface
533+
is defined in the fiona._vsiopener._AbstractOpener class.
472534
vfs : str
473535
This is a deprecated parameter. A URI scheme such as "zip://"
474536
should be used instead.
@@ -486,18 +548,26 @@ def listlayers(fp, vfs=None, **kwargs):
486548
If the input is not a str, Path, or file object.
487549
488550
"""
551+
if vfs and not isinstance(vfs, str):
552+
raise TypeError(f"invalid vfs: {vfs!r}")
553+
489554
if hasattr(fp, 'read'):
490555
with MemoryFile(fp.read()) as memfile:
491556
return _listlayers(memfile.name, **kwargs)
492-
else:
493-
if isinstance(fp, Path):
494-
fp = str(fp)
495557

496-
if not isinstance(fp, str):
497-
raise TypeError(f"invalid path: {fp!r}")
498-
if vfs and not isinstance(vfs, str):
499-
raise TypeError(f"invalid vfs: {vfs!r}")
558+
if hasattr(fp, "path") and hasattr(fp, "fs"):
559+
log.debug("Detected fp is an OpenFile: fp=%r", fp)
560+
raw_dataset_path = fp.path
561+
opener = fp.fs.open
562+
else:
563+
raw_dataset_path = os.fspath(fp)
500564

565+
if opener:
566+
log.debug("Registering opener: raw_dataset_path=%r, opener=%r", raw_dataset_path, opener)
567+
with _opener_registration(raw_dataset_path, opener) as registered_vsi_path:
568+
log.debug("Registered vsi path: registered_vsi_path=%r", registered_vsi_path)
569+
return _listlayers(registered_vsi_path, **kwargs)
570+
else:
501571
if vfs:
502572
warnings.warn(
503573
"The vfs keyword argument is deprecated and will be removed in 2.0. "
@@ -506,10 +576,10 @@ def listlayers(fp, vfs=None, **kwargs):
506576
stacklevel=2,
507577
)
508578
pobj_vfs = _parse_path(vfs)
509-
pobj_path = _parse_path(fp)
579+
pobj_path = _parse_path(raw_dataset_path)
510580
pobj = _ParsedPath(pobj_path.path, pobj_vfs.path, pobj_vfs.scheme)
511581
else:
512-
pobj = _parse_path(fp)
582+
pobj = _parse_path(raw_dataset_path)
513583

514584
return _listlayers(_vsi_path(pobj), **kwargs)
515585

fiona/_env.pxd

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
include "gdal.pxi"
22

33

4-
cdef extern from "ogr_srs_api.h":
5-
void OSRSetPROJSearchPaths(const char *const *papszPaths)
6-
void OSRGetPROJVersion (int *pnMajor, int *pnMinor, int *pnPatch)
7-
8-
94
cdef class ConfigEnv(object):
105
cdef public object options
116

fiona/_env.pyx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import threading
1717

1818
from fiona._err cimport exc_wrap_int, exc_wrap_ogrerr
1919
from fiona._err import CPLE_BaseError
20-
from fiona._vsiopener cimport install_pyopener_plugin
2120
from fiona.errors import EnvError
2221

2322
level_map = {
@@ -406,10 +405,8 @@ cdef class GDALEnv(ConfigEnv):
406405
if not self._have_registered_drivers:
407406
with threading.Lock():
408407
if not self._have_registered_drivers:
409-
410408
GDALAllRegister()
411409
OGRRegisterAll()
412-
install_pyopener_plugin(pyopener_plugin)
413410

414411
if 'GDAL_DATA' in os.environ:
415412
log.debug("GDAL_DATA found in environment.")

fiona/_err.pxd

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
from libc.stdio cimport *
2-
3-
cdef extern from "cpl_vsi.h":
4-
5-
ctypedef FILE VSILFILE
1+
include "gdal.pxi"
62

7-
cdef extern from "ogr_core.h":
8-
9-
ctypedef int OGRErr
3+
from libc.stdio cimport *
104

115
cdef get_last_error_msg()
126
cdef int exc_wrap_int(int retval) except -1
137
cdef OGRErr exc_wrap_ogrerr(OGRErr retval) except -1
148
cdef void *exc_wrap_pointer(void *ptr) except NULL
159
cdef VSILFILE *exc_wrap_vsilfile(VSILFILE *f) except NULL
10+
11+
cdef class StackChecker:
12+
cdef object error_stack
13+
cdef int exc_wrap_int(self, int retval) except -1
14+
cdef void *exc_wrap_pointer(self, void *ptr) except NULL

0 commit comments

Comments
 (0)