mirror of
https://github.com/rasterio/rasterio.git
synced 2025-12-08 17:36:12 +00:00
Port of Pyopener VSI plugin improvements in Fiona 1.10b2 (#3113)
* Port of Pyopener VSI plugin improvements in Fiona 1.10b2 * Don't use cpp flags for _vsiopener * Update change log * Update rasterio/_vsiopener.pyx Co-authored-by: Alan D. Snow <alansnow21@gmail.com> * Add threading and registration tests --------- Co-authored-by: Alan D. Snow <alansnow21@gmail.com>
This commit is contained in:
parent
f3018c78a4
commit
5d65401be8
@ -6,6 +6,12 @@ Next (TBD)
|
|||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
|
- The Pyopener registry and VSI plugin have been rewritten to avoid filename
|
||||||
|
conflicts and to be compatible with multithreading. Now, a new plugin handler
|
||||||
|
is registered for each instance of using an opener (#3113). Before GDAL 3.9.0
|
||||||
|
plugin handlers cannot not be removed and so it may be observed that the size
|
||||||
|
of the Pyopener registry grows during the execution of a program.
|
||||||
|
- A CSLConstList ctypedef has been added and is used where appropriate (#3113).
|
||||||
- Missing parentheses in the denominators of the max_pixels calculation in
|
- Missing parentheses in the denominators of the max_pixels calculation in
|
||||||
calc() and merge() have been added (#3073, #3076). This constrains memory
|
calc() and merge() have been added (#3073, #3076). This constrains memory
|
||||||
use as originally intended.
|
use as originally intended.
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import threading
|
|||||||
from rasterio._err import CPLE_BaseError
|
from rasterio._err import CPLE_BaseError
|
||||||
from rasterio._err cimport exc_wrap_ogrerr, exc_wrap_int
|
from rasterio._err cimport exc_wrap_ogrerr, exc_wrap_int
|
||||||
from rasterio._filepath cimport install_filepath_plugin
|
from rasterio._filepath cimport install_filepath_plugin
|
||||||
from rasterio._vsiopener cimport install_pyopener_plugin
|
|
||||||
from rasterio._version import gdal_version
|
from rasterio._version import gdal_version
|
||||||
|
|
||||||
from libc.stdio cimport stderr
|
from libc.stdio cimport stderr
|
||||||
@ -378,7 +377,6 @@ cdef class GDALEnv(ConfigEnv):
|
|||||||
GDALAllRegister()
|
GDALAllRegister()
|
||||||
OGRRegisterAll()
|
OGRRegisterAll()
|
||||||
install_filepath_plugin(filepath_plugin)
|
install_filepath_plugin(filepath_plugin)
|
||||||
install_pyopener_plugin(pyopener_plugin)
|
|
||||||
|
|
||||||
if 'GDAL_DATA' in os.environ:
|
if 'GDAL_DATA' in os.environ:
|
||||||
log.debug("GDAL_DATA found in environment.")
|
log.debug("GDAL_DATA found in environment.")
|
||||||
|
|||||||
@ -1,4 +1 @@
|
|||||||
include "gdal.pxi"
|
include "gdal.pxi"
|
||||||
|
|
||||||
cdef int install_pyopener_plugin(VSIFilesystemPluginCallbacksStruct *callbacks_struct)
|
|
||||||
cdef void uninstall_pyopener_plugin(VSIFilesystemPluginCallbacksStruct *callbacks_struct)
|
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
# cython: language_level=3, boundscheck=False
|
|
||||||
# distutils: language = c++
|
|
||||||
"""Bridge between Python file openers and GDAL VSI.
|
"""Bridge between Python file openers and GDAL VSI.
|
||||||
|
|
||||||
Based on _filepath.pyx.
|
Based on _filepath.pyx.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
include "gdal.pxi"
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import stat
|
import stat
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from libc.string cimport memcpy
|
from libc.string cimport memcpy
|
||||||
|
|
||||||
@ -20,10 +17,7 @@ from rasterio.errors import OpenerRegistrationError
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Prefix for all in-memory paths used by GDAL's VSI system
|
cdef str VSI_NS_ROOT = "vsiriopener"
|
||||||
# Except for errors and log messages this shouldn't really be seen by the user
|
|
||||||
cdef str PREFIX = "/vsiriopener/"
|
|
||||||
cdef bytes PREFIX_BYTES = PREFIX.encode("utf-8")
|
|
||||||
|
|
||||||
# This is global state for the Python filesystem plugin. It currently only
|
# This is global state for the Python filesystem plugin. It currently only
|
||||||
# contains path -> PyOpenerBase (or subclass) instances. This is used by
|
# contains path -> PyOpenerBase (or subclass) instances. This is used by
|
||||||
@ -36,38 +30,12 @@ _OPEN_FILE_EXIT_STACKS = ContextVar("open_file_exit_stacks")
|
|||||||
_OPEN_FILE_EXIT_STACKS.set({})
|
_OPEN_FILE_EXIT_STACKS.set({})
|
||||||
|
|
||||||
|
|
||||||
cdef int install_pyopener_plugin(VSIFilesystemPluginCallbacksStruct *callbacks_struct):
|
# When an opener is registered for a path, this structure captures the
|
||||||
"""Install handlers for python file openers if it isn't already installed."""
|
# path and unique registration instance. VSI stat, read_dir, and open
|
||||||
cdef char **registered_prefixes = VSIGetFileSystemsPrefixes()
|
# calls have access to the struct instance.
|
||||||
cdef int prefix_index = CSLFindString(registered_prefixes, PREFIX_BYTES)
|
cdef struct FSData:
|
||||||
CSLDestroy(registered_prefixes)
|
char *path
|
||||||
|
char *uuid
|
||||||
if prefix_index < 0:
|
|
||||||
log.debug("Installing Python opener handler plugin...")
|
|
||||||
callbacks_struct = VSIAllocFilesystemPluginCallbacksStruct()
|
|
||||||
callbacks_struct.open = <VSIFilesystemPluginOpenCallback>pyopener_open
|
|
||||||
callbacks_struct.eof = <VSIFilesystemPluginEofCallback>pyopener_eof
|
|
||||||
callbacks_struct.tell = <VSIFilesystemPluginTellCallback>pyopener_tell
|
|
||||||
callbacks_struct.seek = <VSIFilesystemPluginSeekCallback>pyopener_seek
|
|
||||||
callbacks_struct.read = <VSIFilesystemPluginReadCallback>pyopener_read
|
|
||||||
callbacks_struct.write = <VSIFilesystemPluginWriteCallback>pyopener_write
|
|
||||||
callbacks_struct.flush = <VSIFilesystemPluginFlushCallback>pyopener_flush
|
|
||||||
callbacks_struct.close = <VSIFilesystemPluginCloseCallback>pyopener_close
|
|
||||||
callbacks_struct.read_dir = <VSIFilesystemPluginReadDirCallback>pyopener_read_dir
|
|
||||||
callbacks_struct.stat = <VSIFilesystemPluginStatCallback>pyopener_stat
|
|
||||||
callbacks_struct.pUserData = <void*>_OPENER_REGISTRY
|
|
||||||
retval = VSIInstallPluginHandler(PREFIX_BYTES, callbacks_struct)
|
|
||||||
VSIFreeFilesystemPluginCallbacksStruct(callbacks_struct)
|
|
||||||
return retval
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
cdef void uninstall_pyopener_plugin(VSIFilesystemPluginCallbacksStruct *callbacks_struct):
|
|
||||||
if callbacks_struct is not NULL:
|
|
||||||
callbacks_struct.pUserData = NULL
|
|
||||||
VSIFreeFilesystemPluginCallbacksStruct(callbacks_struct)
|
|
||||||
callbacks_struct = NULL
|
|
||||||
|
|
||||||
|
|
||||||
cdef int pyopener_stat(
|
cdef int pyopener_stat(
|
||||||
@ -77,14 +45,20 @@ cdef int pyopener_stat(
|
|||||||
int nFlags
|
int nFlags
|
||||||
) with gil:
|
) with gil:
|
||||||
"""Provides POSIX stat data to GDAL from a Python filesystem."""
|
"""Provides POSIX stat data to GDAL from a Python filesystem."""
|
||||||
# Convert the given filename to a registry key.
|
cdef FSData *fsdata = <FSData *>pUserData
|
||||||
# Reminder: openers are registered by URI scheme, authority, and
|
path = fsdata.path.decode("utf-8")
|
||||||
# *directory* path.
|
uuid = fsdata.uuid.decode("utf-8")
|
||||||
|
key = (Path(path), uuid)
|
||||||
urlpath = pszFilename.decode("utf-8")
|
urlpath = pszFilename.decode("utf-8")
|
||||||
key = Path(urlpath).parent
|
|
||||||
|
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get()
|
||||||
log.debug("Looking up opener in pyopener_stat: registry=%r, key=%r", registry, key)
|
log.debug(
|
||||||
|
"Looking up opener in pyopener_stat: urlpath=%r, registry=%r, key=%r",
|
||||||
|
urlpath,
|
||||||
|
registry,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_opener = registry[key]
|
file_opener = registry[key]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
@ -94,15 +68,15 @@ cdef int pyopener_stat(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if file_opener.isfile(urlpath):
|
if file_opener.isfile(urlpath):
|
||||||
fmode = 0o170000 | stat.S_IFREG
|
fmode = stat.S_IFREG
|
||||||
elif file_opener.isdir(urlpath):
|
elif file_opener.isdir(urlpath):
|
||||||
fmode = 0o170000 | stat.S_IFDIR
|
fmode = stat.S_IFDIR
|
||||||
else:
|
else:
|
||||||
# No such file or directory.
|
# No such file or directory.
|
||||||
return -1
|
return -1
|
||||||
size = file_opener.size(urlpath)
|
size = file_opener.size(urlpath)
|
||||||
mtime = file_opener.mtime(urlpath)
|
mtime = file_opener.mtime(urlpath)
|
||||||
except (FileNotFoundError, KeyError):
|
except (FileNotFoundError, KeyError) as err:
|
||||||
# No such file or directory.
|
# No such file or directory.
|
||||||
return -1
|
return -1
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@ -116,17 +90,64 @@ cdef int pyopener_stat(
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
cdef int pyopener_unlink(
|
||||||
|
void *pUserData,
|
||||||
|
const char *pszFilename,
|
||||||
|
) with gil:
|
||||||
|
"""Unlink a file from a Python filesystem."""
|
||||||
|
cdef FSData *fsdata = <FSData *>pUserData
|
||||||
|
path = fsdata.path.decode("utf-8")
|
||||||
|
uuid = fsdata.uuid.decode("utf-8")
|
||||||
|
key = (Path(path), uuid)
|
||||||
|
urlpath = pszFilename.decode("utf-8")
|
||||||
|
|
||||||
|
registry = _OPENER_REGISTRY.get()
|
||||||
|
log.debug(
|
||||||
|
"Looking up opener in pyopener_unlink: urlpath=%r, registry=%r, key=%r",
|
||||||
|
urlpath,
|
||||||
|
registry,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_opener = registry[key]
|
||||||
|
except KeyError as err:
|
||||||
|
errmsg = f"Opener not found: {repr(err)}".encode("utf-8")
|
||||||
|
CPLError(CE_Failure, <CPLErrorNum>4, <const char *>"%s", <const char *>errmsg)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_opener.rm(urlpath)
|
||||||
|
return 0
|
||||||
|
except (FileNotFoundError, KeyError) as err:
|
||||||
|
# No such file or directory.
|
||||||
|
return -1
|
||||||
|
except Exception as err:
|
||||||
|
errmsg = f"Opener failed to determine file info: {err!r}".encode("utf-8")
|
||||||
|
CPLError(CE_Failure, <CPLErrorNum>4, <const char *>"%s", <const char *>errmsg)
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
cdef char ** pyopener_read_dir(
|
cdef char ** pyopener_read_dir(
|
||||||
void *pUserData,
|
void *pUserData,
|
||||||
const char *pszDirname,
|
const char *pszDirname,
|
||||||
int nMaxFiles
|
int nMaxFiles
|
||||||
) with gil:
|
) with gil:
|
||||||
"""Provides a directory listing to GDAL from a Python filesystem."""
|
"""Provides a directory listing to GDAL from a Python filesystem."""
|
||||||
|
cdef FSData *fsdata = <FSData *>pUserData
|
||||||
|
path = fsdata.path.decode("utf-8")
|
||||||
|
uuid = fsdata.uuid.decode("utf-8")
|
||||||
|
key = (Path(path), uuid)
|
||||||
urlpath = pszDirname.decode("utf-8")
|
urlpath = pszDirname.decode("utf-8")
|
||||||
key = Path(urlpath)
|
|
||||||
|
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get()
|
||||||
log.debug("Looking up opener in pyopener_read_dir: registry=%r, key=%r", registry, key)
|
log.debug(
|
||||||
|
"Looking up opener in pyopener_read_dir: urlpath=%r, registry=%r, key=%r",
|
||||||
|
urlpath,
|
||||||
|
registry,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_opener = registry[key]
|
file_opener = registry[key]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
@ -137,8 +158,7 @@ cdef char ** pyopener_read_dir(
|
|||||||
try:
|
try:
|
||||||
# GDAL wants relative file names.
|
# GDAL wants relative file names.
|
||||||
contents = [Path(item).name for item in file_opener.ls(urlpath)]
|
contents = [Path(item).name for item in file_opener.ls(urlpath)]
|
||||||
log.debug("Looking for dir contents: urlpath=%r, contents=%r", urlpath, contents)
|
except (FileNotFoundError, KeyError) as err:
|
||||||
except (FileNotFoundError, KeyError):
|
|
||||||
# No such file or directory.
|
# No such file or directory.
|
||||||
return NULL
|
return NULL
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@ -166,12 +186,24 @@ cdef void* pyopener_open(
|
|||||||
GDAL may call this function multiple times per filename and each
|
GDAL may call this function multiple times per filename and each
|
||||||
result must be seperately seekable.
|
result must be seperately seekable.
|
||||||
"""
|
"""
|
||||||
|
cdef FSData *fsdata = <FSData *>pUserData
|
||||||
|
path = fsdata.path.decode("utf-8")
|
||||||
|
uuid = fsdata.uuid.decode("utf-8")
|
||||||
|
key = (Path(path), uuid)
|
||||||
urlpath = pszFilename.decode("utf-8")
|
urlpath = pszFilename.decode("utf-8")
|
||||||
|
|
||||||
mode = pszAccess.decode("utf-8")
|
mode = pszAccess.decode("utf-8")
|
||||||
key = Path(urlpath).parent
|
if not "b" in mode:
|
||||||
|
mode += "b"
|
||||||
|
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get()
|
||||||
log.debug("Looking up opener in pyopener_open: registry=%r, key=%r", registry, key)
|
log.debug(
|
||||||
|
"Looking up opener in pyopener_open: urlpath=%r, registry=%r, key=%r",
|
||||||
|
urlpath,
|
||||||
|
registry,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_opener = registry[key]
|
file_opener = registry[key]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
@ -202,7 +234,6 @@ cdef void* pyopener_open(
|
|||||||
try:
|
try:
|
||||||
file_obj = stack.enter_context(file_obj)
|
file_obj = stack.enter_context(file_obj)
|
||||||
except (AttributeError, TypeError) as err:
|
except (AttributeError, TypeError) as err:
|
||||||
log.error("File object is not a context manager: file_obj=%r", file_obj)
|
|
||||||
errmsg = f"Opener failed to open file with arguments ({repr(urlpath)}, {repr(mode)}): {repr(err)}".encode("utf-8")
|
errmsg = f"Opener failed to open file with arguments ({repr(urlpath)}, {repr(mode)}): {repr(err)}".encode("utf-8")
|
||||||
CPLError(CE_Failure, <CPLErrorNum>4, <const char *>"%s", <const char *>errmsg)
|
CPLError(CE_Failure, <CPLErrorNum>4, <const char *>"%s", <const char *>errmsg)
|
||||||
return NULL
|
return NULL
|
||||||
@ -210,10 +241,9 @@ cdef void* pyopener_open(
|
|||||||
errmsg = "OpenFile didn't resolve".encode("utf-8")
|
errmsg = "OpenFile didn't resolve".encode("utf-8")
|
||||||
return NULL
|
return NULL
|
||||||
else:
|
else:
|
||||||
exit_stacks = _OPEN_FILE_EXIT_STACKS.get()
|
exit_stacks = _OPEN_FILE_EXIT_STACKS.get({})
|
||||||
exit_stacks[file_obj] = stack
|
exit_stacks[file_obj] = stack
|
||||||
_OPEN_FILE_EXIT_STACKS.set(exit_stacks)
|
_OPEN_FILE_EXIT_STACKS.set(exit_stacks)
|
||||||
log.debug("Returning: file_obj=%r", file_obj)
|
|
||||||
return <void *>file_obj
|
return <void *>file_obj
|
||||||
|
|
||||||
|
|
||||||
@ -244,7 +274,7 @@ cdef size_t pyopener_read(void *pFile, void *pBuffer, size_t nSize, size_t nCoun
|
|||||||
cdef int num_bytes = len(python_data)
|
cdef int num_bytes = len(python_data)
|
||||||
# NOTE: We have to cast to char* first, otherwise Cython doesn't do the conversion properly
|
# NOTE: We have to cast to char* first, otherwise Cython doesn't do the conversion properly
|
||||||
memcpy(pBuffer, <void*><unsigned char*>python_data, num_bytes)
|
memcpy(pBuffer, <void*><unsigned char*>python_data, num_bytes)
|
||||||
return <size_t>(num_bytes // nSize)
|
return <size_t>(num_bytes / nSize)
|
||||||
|
|
||||||
|
|
||||||
cdef size_t pyopener_write(void *pFile, void *pBuffer, size_t nSize, size_t nCount) with gil:
|
cdef size_t pyopener_write(void *pFile, void *pBuffer, size_t nSize, size_t nCount) with gil:
|
||||||
@ -253,12 +283,16 @@ cdef size_t pyopener_write(void *pFile, void *pBuffer, size_t nSize, size_t nCou
|
|||||||
cdef object file_obj = <object>pFile
|
cdef object file_obj = <object>pFile
|
||||||
buffer_len = nSize * nCount
|
buffer_len = nSize * nCount
|
||||||
cdef unsigned char [:] buff_view = <unsigned char[:buffer_len]>pBuffer
|
cdef unsigned char [:] buff_view = <unsigned char[:buffer_len]>pBuffer
|
||||||
log.debug("Writing data: file_obj=%r, buff_view=%r, buffer_len=%r", file_obj, buff_view, buffer_len)
|
log.debug(
|
||||||
|
"Writing data: file_obj=%r, buff_view=%r, buffer_len=%r",
|
||||||
|
file_obj,
|
||||||
|
buff_view,
|
||||||
|
buffer_len)
|
||||||
try:
|
try:
|
||||||
num_bytes = file_obj.write(buff_view)
|
num = file_obj.write(buff_view)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
num_bytes = file_obj.write(str(buff_view))
|
num = file_obj.write(str(buff_view))
|
||||||
return <size_t>(num_bytes // nSize)
|
return <size_t>(num // nSize)
|
||||||
|
|
||||||
|
|
||||||
cdef int pyopener_flush(void *pFile) with gil:
|
cdef int pyopener_flush(void *pFile) with gil:
|
||||||
@ -283,32 +317,82 @@ cdef int pyopener_close(void *pFile) with gil:
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _opener_registration(urlpath, obj):
|
def _opener_registration(urlpath, obj):
|
||||||
key = Path(urlpath).parent
|
cdef char **registered_prefixes = NULL
|
||||||
|
cdef int prefix_index = 0
|
||||||
|
cdef VSIFilesystemPluginCallbacksStruct *callbacks_struct = NULL
|
||||||
|
cdef FSData fsdata
|
||||||
|
cdef char *path_c = NULL
|
||||||
|
cdef char *uuid_c = NULL
|
||||||
|
|
||||||
|
# To resolve issue 1406 we add the opener or filesystem id to the
|
||||||
|
# registry key.
|
||||||
|
kpath = Path(urlpath).parent
|
||||||
|
kid = uuid4().hex
|
||||||
|
key = (kpath, kid)
|
||||||
|
|
||||||
|
path_b = kpath.as_posix().encode("utf-8")
|
||||||
|
path_c = path_b
|
||||||
|
uuid_b = kid.encode("utf-8")
|
||||||
|
uuid_c = uuid_b
|
||||||
|
|
||||||
|
fsdata = FSData(path_c, uuid_c)
|
||||||
|
|
||||||
|
namespace = f"{VSI_NS_ROOT}_{kid}"
|
||||||
|
cdef bytes prefix_bytes = f"/{namespace}/".encode("utf-8")
|
||||||
|
|
||||||
# Might raise.
|
# Might raise.
|
||||||
opener = _create_opener(obj)
|
opener = _create_opener(obj)
|
||||||
|
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get({})
|
||||||
|
|
||||||
if key in registry:
|
if key in registry:
|
||||||
if registry[key] != opener:
|
if registry[key] != opener:
|
||||||
raise OpenerRegistrationError(f"Opener already registered for urlpath.")
|
raise OpenerRegistrationError(f"Opener already registered for urlpath.")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
yield f"{PREFIX}{urlpath}"
|
yield f"/{namespace}/{urlpath}"
|
||||||
finally:
|
finally:
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get()
|
||||||
_ = registry.pop(key, None)
|
_ = registry.pop(key, None)
|
||||||
_OPENER_REGISTRY.set(registry)
|
_OPENER_REGISTRY.set(registry)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Install handler.
|
||||||
|
registered_prefixes = VSIGetFileSystemsPrefixes()
|
||||||
|
prefix_index = CSLFindString(<CSLConstList>registered_prefixes, prefix_bytes)
|
||||||
|
CSLDestroy(registered_prefixes)
|
||||||
|
|
||||||
|
if prefix_index < 0:
|
||||||
|
log.debug("Installing Python opener handler plugin: prefix_bytes=%r", prefix_bytes)
|
||||||
|
callbacks_struct = VSIAllocFilesystemPluginCallbacksStruct()
|
||||||
|
callbacks_struct.open = <VSIFilesystemPluginOpenCallback>pyopener_open
|
||||||
|
callbacks_struct.eof = <VSIFilesystemPluginEofCallback>pyopener_eof
|
||||||
|
callbacks_struct.tell = <VSIFilesystemPluginTellCallback>pyopener_tell
|
||||||
|
callbacks_struct.seek = <VSIFilesystemPluginSeekCallback>pyopener_seek
|
||||||
|
callbacks_struct.read = <VSIFilesystemPluginReadCallback>pyopener_read
|
||||||
|
callbacks_struct.write = <VSIFilesystemPluginWriteCallback>pyopener_write
|
||||||
|
callbacks_struct.flush = <VSIFilesystemPluginFlushCallback>pyopener_flush
|
||||||
|
callbacks_struct.close = <VSIFilesystemPluginCloseCallback>pyopener_close
|
||||||
|
callbacks_struct.read_dir = <VSIFilesystemPluginReadDirCallback>pyopener_read_dir
|
||||||
|
callbacks_struct.stat = <VSIFilesystemPluginStatCallback>pyopener_stat
|
||||||
|
callbacks_struct.unlink = <VSIFilesystemPluginUnlinkCallback>pyopener_unlink
|
||||||
|
callbacks_struct.pUserData = &fsdata
|
||||||
|
retval = VSIInstallPluginHandler(prefix_bytes, callbacks_struct)
|
||||||
|
VSIFreeFilesystemPluginCallbacksStruct(callbacks_struct)
|
||||||
|
|
||||||
registry[key] = opener
|
registry[key] = opener
|
||||||
_OPENER_REGISTRY.set(registry)
|
_OPENER_REGISTRY.set(registry)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield f"{PREFIX}{urlpath}"
|
yield f"/{namespace}/{urlpath}"
|
||||||
finally:
|
finally:
|
||||||
registry = _OPENER_REGISTRY.get()
|
registry = _OPENER_REGISTRY.get()
|
||||||
_ = registry.pop(key, None)
|
_ = registry.pop(key, None)
|
||||||
_OPENER_REGISTRY.set(registry)
|
_OPENER_REGISTRY.set(registry)
|
||||||
|
|
||||||
|
IF (CTE_GDAL_MAJOR_VERSION, CTE_GDAL_MINOR_VERSION) >= (3, 9):
|
||||||
|
retval = VSIRemovePluginHandler(prefix_bytes)
|
||||||
|
|
||||||
|
|
||||||
class _AbstractOpener:
|
class _AbstractOpener:
|
||||||
"""Adapts a Python object to the opener interface."""
|
"""Adapts a Python object to the opener interface."""
|
||||||
@ -385,6 +469,19 @@ class _AbstractOpener:
|
|||||||
Modification timestamp in seconds.
|
Modification timestamp in seconds.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
def rm(self, path):
|
||||||
|
"""Remove a resource.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path : str
|
||||||
|
The identifier/locator for a resource within a filesystem.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
def size(self, path):
|
def size(self, path):
|
||||||
"""Get the size, in bytes, of a resource..
|
"""Get the size, in bytes, of a resource..
|
||||||
|
|
||||||
@ -431,14 +528,16 @@ class _FilesystemOpener(_AbstractOpener):
|
|||||||
def isdir(self, path):
|
def isdir(self, path):
|
||||||
return self._obj.isdir(path)
|
return self._obj.isdir(path)
|
||||||
def ls(self, path):
|
def ls(self, path):
|
||||||
return self._obj.ls(path)
|
# return value of ls() varies between file and zip fsspec filesystems.
|
||||||
|
return [item if isinstance(item, str) else item["filename"] for item in self._obj.ls(path)]
|
||||||
def mtime(self, path):
|
def mtime(self, path):
|
||||||
try:
|
try:
|
||||||
mtime = int(self._obj.modified(path).timestamp())
|
mtime = int(self._obj.modified(path).timestamp())
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
mtime = 0
|
mtime = 0
|
||||||
log.debug("Modification time: mtime=%r", mtime)
|
|
||||||
return mtime
|
return mtime
|
||||||
|
def rm(self, path):
|
||||||
|
return self._obj.rm(path)
|
||||||
def size(self, path):
|
def size(self, path):
|
||||||
return self._obj.size(path)
|
return self._obj.size(path)
|
||||||
|
|
||||||
@ -451,6 +550,8 @@ class _AltFilesystemOpener(_FilesystemOpener):
|
|||||||
return self._obj.is_dir(path)
|
return self._obj.is_dir(path)
|
||||||
def mtime(self, path):
|
def mtime(self, path):
|
||||||
return 0
|
return 0
|
||||||
|
def rm(self, path):
|
||||||
|
self._obj.remove_file(path)
|
||||||
def size(self, path):
|
def size(self, path):
|
||||||
return self._obj.file_size(path)
|
return self._obj.file_size(path)
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,10 @@ cdef extern from "cpl_conv.h" nogil:
|
|||||||
const char *CPLFindFile(const char *pszClass, const char *pszBasename)
|
const char *CPLFindFile(const char *pszClass, const char *pszBasename)
|
||||||
|
|
||||||
|
|
||||||
|
cdef extern from "cpl_port.h":
|
||||||
|
ctypedef char **CSLConstList
|
||||||
|
|
||||||
|
|
||||||
cdef extern from "cpl_error.h" nogil:
|
cdef extern from "cpl_error.h" nogil:
|
||||||
|
|
||||||
ctypedef enum CPLErr:
|
ctypedef enum CPLErr:
|
||||||
@ -142,8 +146,13 @@ cdef extern from "cpl_vsi.h" nogil:
|
|||||||
size_t VSIFWriteL(void *buffer, size_t nSize, size_t nCount, VSILFILE *fp)
|
size_t VSIFWriteL(void *buffer, size_t nSize, size_t nCount, VSILFILE *fp)
|
||||||
int VSIStatL(const char *pszFilename, VSIStatBufL *psStatBuf)
|
int VSIStatL(const char *pszFilename, VSIStatBufL *psStatBuf)
|
||||||
|
|
||||||
cdef extern from "ogr_srs_api.h" nogil:
|
|
||||||
|
|
||||||
|
IF (CTE_GDAL_MAJOR_VERSION, CTE_GDAL_MINOR_VERSION) >= (3, 9):
|
||||||
|
cdef extern from "cpl_vsi.h" nogil:
|
||||||
|
int VSIRemovePluginHandler(const char*)
|
||||||
|
|
||||||
|
|
||||||
|
cdef extern from "ogr_srs_api.h" nogil:
|
||||||
ctypedef int OGRErr
|
ctypedef int OGRErr
|
||||||
ctypedef void * OGRCoordinateTransformationH
|
ctypedef void * OGRCoordinateTransformationH
|
||||||
ctypedef void * OGRSpatialReferenceH
|
ctypedef void * OGRSpatialReferenceH
|
||||||
|
|||||||
18
setup.py
18
setup.py
@ -190,26 +190,24 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# GDAL 2.3 and newer requires C++11
|
cpp11_flag = '-std=c++11'
|
||||||
if (gdal_major_version, gdal_minor_version) >= (2, 3):
|
|
||||||
cpp11_flag = '-std=c++11'
|
|
||||||
|
|
||||||
# 'extra_compile_args' may not be defined
|
# 'extra_compile_args' may not be defined
|
||||||
eca = cpp_ext_options.get('extra_compile_args', [])
|
eca = cpp_ext_options.get('extra_compile_args', [])
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
|
|
||||||
if cpp11_flag not in eca:
|
if cpp11_flag not in eca:
|
||||||
eca.append(cpp11_flag)
|
eca.append(cpp11_flag)
|
||||||
|
|
||||||
eca += [cpp11_flag, '-mmacosx-version-min=10.9', '-stdlib=libc++']
|
eca += [cpp11_flag, '-mmacosx-version-min=10.9', '-stdlib=libc++']
|
||||||
|
|
||||||
# TODO: Windows
|
# TODO: Windows
|
||||||
|
|
||||||
elif cpp11_flag not in eca:
|
elif cpp11_flag not in eca:
|
||||||
eca.append(cpp11_flag)
|
eca.append(cpp11_flag)
|
||||||
|
|
||||||
cpp_ext_options['extra_compile_args'] = eca
|
cpp_ext_options['extra_compile_args'] = eca
|
||||||
|
|
||||||
# Configure optional Cython coverage.
|
# Configure optional Cython coverage.
|
||||||
cythonize_options = {"language_level": sys.version_info[0]}
|
cythonize_options = {"language_level": sys.version_info[0]}
|
||||||
@ -238,7 +236,7 @@ if "clean" not in sys.argv:
|
|||||||
Extension("rasterio._transform", ["rasterio/_transform.pyx"], **ext_options),
|
Extension("rasterio._transform", ["rasterio/_transform.pyx"], **ext_options),
|
||||||
Extension("rasterio._filepath", ["rasterio/_filepath.pyx"], **cpp_ext_options),
|
Extension("rasterio._filepath", ["rasterio/_filepath.pyx"], **cpp_ext_options),
|
||||||
Extension(
|
Extension(
|
||||||
"rasterio._vsiopener", ["rasterio/_vsiopener.pyx"], **cpp_ext_options
|
"rasterio._vsiopener", ["rasterio/_vsiopener.pyx"], **ext_options
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
ext_modules = cythonize(
|
ext_modules = cythonize(
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
import fsspec
|
import fsspec
|
||||||
@ -13,17 +16,6 @@ from rasterio.enums import MaskFlags
|
|||||||
from rasterio.errors import OpenerRegistrationError
|
from rasterio.errors import OpenerRegistrationError
|
||||||
|
|
||||||
|
|
||||||
def test_registration_failure():
|
|
||||||
"""Exception is raised on attempt to register a second opener for a filename and mode."""
|
|
||||||
with pytest.raises(OpenerRegistrationError) as exc_info:
|
|
||||||
with rasterio.open(
|
|
||||||
"tests/data/RGB.byte.tif", opener=io.open
|
|
||||||
) as a, rasterio.open("tests/data/RGB.byte.tif", opener=fsspec.open) as b:
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert exc_info.value.args[0] == "Opener already registered for urlpath."
|
|
||||||
|
|
||||||
|
|
||||||
def test_opener_failure():
|
def test_opener_failure():
|
||||||
"""Use int as an opener :)"""
|
"""Use int as an opener :)"""
|
||||||
with pytest.raises(OpenerRegistrationError) as exc_info:
|
with pytest.raises(OpenerRegistrationError) as exc_info:
|
||||||
@ -195,3 +187,41 @@ def test_opener_tiledb_vfs():
|
|||||||
profile = src.profile
|
profile = src.profile
|
||||||
assert profile["driver"] == "GTiff"
|
assert profile["driver"] == "GTiff"
|
||||||
assert profile["count"] == 3
|
assert profile["count"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_on_overwrite(data):
|
||||||
|
"""Opener can delete dataset when overwriting."""
|
||||||
|
fs = fsspec.filesystem("file")
|
||||||
|
outputfile = os.path.join(str(data), "RGB.byte.tif")
|
||||||
|
|
||||||
|
with rasterio.open(outputfile, opener=fs) as dst:
|
||||||
|
profile = dst.profile
|
||||||
|
|
||||||
|
# No need to write any data, as open() will error if VSI unlinking
|
||||||
|
# isn't implemented.
|
||||||
|
with rasterio.open(outputfile, "w", opener=fs, **profile) as dst:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("opener", [io.open, fsspec.filesystem("file")])
|
||||||
|
def test_opener_registration(opener):
|
||||||
|
"""Opener is correctly registered."""
|
||||||
|
from rasterio._vsiopener import _OPENER_REGISTRY, _opener_registration
|
||||||
|
with _opener_registration("tests/data/RGB.byte.tif", opener) as registered_vsi_path:
|
||||||
|
assert registered_vsi_path.startswith("/vsiriopener_")
|
||||||
|
key = (Path("tests/data"), registered_vsi_path.split("/")[1].split("_")[1])
|
||||||
|
val = _OPENER_REGISTRY.get()[key]
|
||||||
|
assert val.isfile
|
||||||
|
assert val._obj == opener
|
||||||
|
|
||||||
|
|
||||||
|
def test_threads_context():
|
||||||
|
"""Threads have opener registries."""
|
||||||
|
|
||||||
|
def target():
|
||||||
|
with rasterio.open("tests/data/RGB.byte.tif", opener=io.open) as dst:
|
||||||
|
assert dst.count == 3
|
||||||
|
|
||||||
|
thread = Thread(target=target)
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user