From 98535f5e51efbb0a1ff20cbca3486aad970c503a Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 09:44:41 -0800 Subject: [PATCH 1/8] Search for PROJ data folder during startup --- pyogrio/_err.pxd | 1 + pyogrio/_err.pyx | 18 ++++++++++- pyogrio/_ogr.pxd | 4 ++- pyogrio/_ogr.pyx | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ pyogrio/core.py | 3 ++ 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/pyogrio/_err.pxd b/pyogrio/_err.pxd index ec469469..53d52a13 100644 --- a/pyogrio/_err.pxd +++ b/pyogrio/_err.pxd @@ -1,3 +1,4 @@ cdef object exc_check() cdef int exc_wrap_int(int retval) except -1 +cdef int exc_wrap_ogrerr(int retval) except -1 cdef void *exc_wrap_pointer(void *ptr) except NULL diff --git a/pyogrio/_err.pyx b/pyogrio/_err.pyx index abf2cac4..c1b28aa4 100644 --- a/pyogrio/_err.pyx +++ b/pyogrio/_err.pyx @@ -1,7 +1,9 @@ # ported from fiona::_err.pyx from enum import IntEnum -from pyogrio._ogr cimport * +from pyogrio._ogr cimport ( + CE_None, CE_Debug, CE_Warning, CE_Failure, CE_Fatal, CPLErrorReset, + CPLGetLastErrorType, CPLGetLastErrorNo, CPLGetLastErrorMsg, OGRErr) # CPL Error types as an enum. @@ -182,6 +184,8 @@ cdef void *exc_wrap_pointer(void *ptr) except NULL: cdef int exc_wrap_int(int err) except -1: """Wrap a GDAL/OGR function that returns CPLErr or OGRErr (int) Raises an exception if a non-fatal error has be set. + + Copied from Fiona (_err.pyx). """ if err: exc = exc_check() @@ -191,3 +195,15 @@ cdef int exc_wrap_int(int err) except -1: # no error message from GDAL raise CPLE_BaseError(-1, -1, "Unspecified OGR / GDAL error") return err + + +cdef int exc_wrap_ogrerr(int err) except -1: + """Wrap a function that returns OGRErr (int) but does not use the + CPL error stack. + + Adapted from Fiona (_err.pyx). + """ + if err != 0: + raise CPLE_BaseError(3, err, f"OGR Error code {err}") + + return err diff --git a/pyogrio/_ogr.pxd b/pyogrio/_ogr.pxd index 373fc0e2..6e5b967a 100644 --- a/pyogrio/_ogr.pxd +++ b/pyogrio/_ogr.pxd @@ -161,11 +161,13 @@ cdef extern from "ogr_srs_api.h": ctypedef void* OGRSpatialReferenceH int OSRAutoIdentifyEPSG(OGRSpatialReferenceH srs) + OGRErr OSRExportToWkt(OGRSpatialReferenceH srs, char **params) const char* OSRGetAuthorityName(OGRSpatialReferenceH srs, const char *key) const char* OSRGetAuthorityCode(OGRSpatialReferenceH srs, const char *key) - OGRErr OSRExportToWkt(OGRSpatialReferenceH srs, char **params) + OGRErr OSRImportFromEPSG(OGRSpatialReferenceH srs, int code) int OSRSetFromUserInput(OGRSpatialReferenceH srs, const char *pszDef) + void OSRSetPROJSearchPaths(const char *const *paths) OGRSpatialReferenceH OSRNewSpatialReference(const char *wkt) void OSRRelease(OGRSpatialReferenceH srs) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index 35afecf7..99d37073 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -1,3 +1,11 @@ +import os +import sys +import warnings + +from pyogrio._err cimport exc_wrap_int, exc_wrap_ogrerr +from pyogrio._err import CPLE_BaseError + + cdef get_string(const char *c_str, str encoding="UTF-8"): """Get Python string from a char * @@ -113,3 +121,77 @@ def ogr_list_drivers(): return drivers + +cdef void set_proj_search_path(str path): + """Set PROJ library data file search path for use in GDAL.""" + cdef char **paths = NULL + cdef const char *path_c = NULL + path_b = path.encode("utf-8") + path_c = path_b + paths = CSLAddString(paths, path_c) + OSRSetPROJSearchPaths(paths) + + +cdef char has_proj_data(): + """Verify if PROJ library data files are loaded by GDAL. + + Returns + ------- + bool + True if a test spatial reference object could be created, which verifies + that data files are correctly loaded. + + Adapted from Fiona (_env.pyx). + """ + cdef OGRSpatialReferenceH srs = OSRNewSpatialReference(NULL) + + try: + exc_wrap_ogrerr(exc_wrap_int(OSRImportFromEPSG(srs, 4326))) + except CPLE_BaseError: + return 0 + else: + return 1 + finally: + if srs != NULL: + OSRRelease(srs) + + +def init_proj_data(): + """Set Proj search directories in the following precedence: + - PROJ_LIB env var + - wheel copy of proj + - default install of proj found by GDAL + - search other well-known paths + + Adapted from Fiona (env.py, _env.pyx). + """ + + if "PROJ_LIB" in os.environ: + set_proj_search_path(os.environ["PROJ_LIB"]) + # verify that this now resolves + if not has_proj_data(): + raise ValueError("PROJ_LIB did not resolve to a path containing PROJ data files") + return + + # wheels are packaged to include PROJ data files at pyogrio/proj_data + wheel_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "proj_data")) + if os.path.exists(wheel_dir): + set_proj_search_path(wheel_dir) + # verify that this now resolves + if not has_proj_data(): + raise ValueError("Could not correctly detect PROJ data files installed by pyogrio wheel") + return + + # GDAL correctly found PROJ based on compiled-in paths + if has_proj_data(): + return + + wk_path = os.path.join(sys.prefix, 'share', 'proj') + if os.path.exists(wk_path): + set_proj_search_path(wk_path) + # verify that this now resolves + if not has_proj_data(): + raise ValueError(f"Found PROJ data directory at {wk_path} but it does not appear to correctly contain PROJ data files") + return + + warnings.warn("Could not detect PROJ data files. Set PROJ_LIB environment variable to the correct path.", RuntimeWarning) diff --git a/pyogrio/core.py b/pyogrio/core.py index 4178d818..b59656df 100644 --- a/pyogrio/core.py +++ b/pyogrio/core.py @@ -9,9 +9,12 @@ ogr_list_drivers, set_gdal_config_options as _set_gdal_config_options, get_gdal_config_option as _get_gdal_config_option, + init_proj_data as _init_proj_data, ) from pyogrio._io import ogr_list_layers, ogr_read_bounds, ogr_read_info + _init_proj_data() + __gdal_version__ = get_gdal_version() __gdal_version_string__ = get_gdal_version_string() From 77557bd786d60f820a4db6f17b783a1d1fda313b Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 10:05:52 -0800 Subject: [PATCH 2/8] Add search for gdal_data --- pyogrio/_ogr.pxd | 1 + pyogrio/_ogr.pyx | 64 +++++++++++++++++++++++++++++++++++++++++++----- pyogrio/core.py | 2 ++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/pyogrio/_ogr.pxd b/pyogrio/_ogr.pxd index 6e5b967a..8014b012 100644 --- a/pyogrio/_ogr.pxd +++ b/pyogrio/_ogr.pxd @@ -8,6 +8,7 @@ cdef extern from "cpl_conv.h": void* CPLMalloc(size_t) void CPLFree(void *ptr) + const char* CPLFindFile(const char *pszClass, const char *filename) const char* CPLGetConfigOption(const char* key, const char* value) void CPLSetConfigOption(const char* key, const char* value) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index 99d37073..e0a8847e 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -132,8 +132,20 @@ cdef void set_proj_search_path(str path): OSRSetPROJSearchPaths(paths) +cdef char has_gdal_data(): + """Verify that GDAL library data files are correctly found. + + Adapted from Fiona (_env.pyx). + """ + + if CPLFindFile("gdal", "header.dxf") != NULL: + return True + + return False + + cdef char has_proj_data(): - """Verify if PROJ library data files are loaded by GDAL. + """Verify that PROJ library data files are loaded by GDAL. Returns ------- @@ -156,12 +168,52 @@ cdef char has_proj_data(): OSRRelease(srs) + +def init_gdal_data(): + """Set GDAL data search directories in the following precedence: + - GDAL_DATA env var + - wheel copy of gdal_data + - other well-known paths under sys.prefix + + Adapted from Fiona (env.py, _env.pyx). + """ + + if "GDAL_DATA" in os.environ: + set_gdal_config_options({"GDAL_DATA": os.environ["GDAL_DATA"]}) + if not has_gdal_data(): + raise ValueError("GDAL_DATA does not resolve to a directory that contains GDAL data files") + return + + # wheels are packaged to include PROJ data files at pyogrio/gdal_data + wheel_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "gdal_data")) + if os.path.exists(wheel_path): + set_gdal_config_options({"GDAL_DATA": wheel_path}) + if not has_gdal_data(): + raise ValueError("Could not correctly detect GDAL data files installed by pyogrio wheel") + return + + # GDAL correctly found data files from compiled-in paths + if has_gdal_data(): + return + + wk_path = os.path.join(sys.prefix, 'share', 'gdal') + if os.path.exists(wk_path): + set_gdal_config_options({"GDAL_DATA": wk_path}) + if not has_gdal_data(): + raise ValueError(f"Found GDAL data directory at {wk_path} but it does not appear to correctly contain GDAL data files") + return + + warnings.warn("Could not detect GDAL data files. Set GDAL_DATA environment variable to the correct path.", RuntimeWarning) + + + + def init_proj_data(): """Set Proj search directories in the following precedence: - PROJ_LIB env var - - wheel copy of proj + - wheel copy of proj_data - default install of proj found by GDAL - - search other well-known paths + - search other well-known paths under sys.prefix Adapted from Fiona (env.py, _env.pyx). """ @@ -174,9 +226,9 @@ def init_proj_data(): return # wheels are packaged to include PROJ data files at pyogrio/proj_data - wheel_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "proj_data")) - if os.path.exists(wheel_dir): - set_proj_search_path(wheel_dir) + wheel_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "proj_data")) + if os.path.exists(wheel_path): + set_proj_search_path(wheel_path) # verify that this now resolves if not has_proj_data(): raise ValueError("Could not correctly detect PROJ data files installed by pyogrio wheel") diff --git a/pyogrio/core.py b/pyogrio/core.py index b59656df..11761c73 100644 --- a/pyogrio/core.py +++ b/pyogrio/core.py @@ -9,10 +9,12 @@ ogr_list_drivers, set_gdal_config_options as _set_gdal_config_options, get_gdal_config_option as _get_gdal_config_option, + init_gdal_data as _init_gdal_data, init_proj_data as _init_proj_data, ) from pyogrio._io import ogr_list_layers, ogr_read_bounds, ogr_read_info + _init_gdal_data() _init_proj_data() __gdal_version__ = get_gdal_version() From 7f54d57ffe90858088fe7a3819d4970260abff00 Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 10:08:27 -0800 Subject: [PATCH 3/8] Docstring fix --- pyogrio/_ogr.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index e0a8847e..734a72d7 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -173,6 +173,7 @@ def init_gdal_data(): """Set GDAL data search directories in the following precedence: - GDAL_DATA env var - wheel copy of gdal_data + - default install of GDAL data files - other well-known paths under sys.prefix Adapted from Fiona (env.py, _env.pyx). From ea2bd5477573a2cecb10af1fd64f8a273066adda Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 10:14:36 -0800 Subject: [PATCH 4/8] Add note to installation instructions --- docs/source/install.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/install.md b/docs/source/install.md index cfcd6c23..00c3913b 100644 --- a/docs/source/install.md +++ b/docs/source/install.md @@ -107,3 +107,16 @@ Also see `.github/test-windows.yml` for additional ideas if you run into problem Windows is minimally tested; we are currently unable to get automated tests working on our Windows CI. + +## GDAL and PROJ data files + +GDAL requires certain files to be present within a GDAL data folder, as well +as a PROJ data folder. These folders are normally detected automatically. + +If you have an unusual installation of GDAL and PROJ, you may need to set +additional environment variables at **runtime** in order for these to be +correctly detected by GDAL: + +- set `GDAL_DATA` to the folder containing the GDAL data files (e.g., contains `header.dxf`) + within the installation of GDAL that is used by Pyogrio. +- set `PROJ_LIB` to the folder containing the PROJ data files (e.g., contains `proj.db`) From ab127c94d5ff12b2b96264d09b984d9025528343 Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 14:26:56 -0800 Subject: [PATCH 5/8] PR feedback --- pyogrio/_ogr.pyx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index 734a72d7..fa6eb2fe 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -168,7 +168,6 @@ cdef char has_proj_data(): OSRRelease(srs) - def init_gdal_data(): """Set GDAL data search directories in the following precedence: - GDAL_DATA env var @@ -185,7 +184,7 @@ def init_gdal_data(): raise ValueError("GDAL_DATA does not resolve to a directory that contains GDAL data files") return - # wheels are packaged to include PROJ data files at pyogrio/gdal_data + # wheels are packaged to include GDAL data files at pyogrio/gdal_data wheel_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "gdal_data")) if os.path.exists(wheel_path): set_gdal_config_options({"GDAL_DATA": wheel_path}) @@ -207,8 +206,6 @@ def init_gdal_data(): warnings.warn("Could not detect GDAL data files. Set GDAL_DATA environment variable to the correct path.", RuntimeWarning) - - def init_proj_data(): """Set Proj search directories in the following precedence: - PROJ_LIB env var From 8d3f39748f6693bf07182e4ccb9a517fef6073e6 Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 14:55:18 -0800 Subject: [PATCH 6/8] Remove specific checks for GDAL_DATA, PROJ_LIB --- pyogrio/_ogr.pyx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index fa6eb2fe..9fb9e57e 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -170,20 +170,13 @@ cdef char has_proj_data(): def init_gdal_data(): """Set GDAL data search directories in the following precedence: - - GDAL_DATA env var - wheel copy of gdal_data - - default install of GDAL data files + - default detection by GDAL, including GDAL_DATA (detected automatically by GDAL) - other well-known paths under sys.prefix Adapted from Fiona (env.py, _env.pyx). """ - if "GDAL_DATA" in os.environ: - set_gdal_config_options({"GDAL_DATA": os.environ["GDAL_DATA"]}) - if not has_gdal_data(): - raise ValueError("GDAL_DATA does not resolve to a directory that contains GDAL data files") - return - # wheels are packaged to include GDAL data files at pyogrio/gdal_data wheel_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "gdal_data")) if os.path.exists(wheel_path): @@ -210,19 +203,12 @@ def init_proj_data(): """Set Proj search directories in the following precedence: - PROJ_LIB env var - wheel copy of proj_data - - default install of proj found by GDAL + - default detection by GDAL, including PROJ_LIB (detected automatically by GDAL) - search other well-known paths under sys.prefix Adapted from Fiona (env.py, _env.pyx). """ - if "PROJ_LIB" in os.environ: - set_proj_search_path(os.environ["PROJ_LIB"]) - # verify that this now resolves - if not has_proj_data(): - raise ValueError("PROJ_LIB did not resolve to a path containing PROJ data files") - return - # wheels are packaged to include PROJ data files at pyogrio/proj_data wheel_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "proj_data")) if os.path.exists(wheel_path): From 8bed720326627cea34e5e898b6417dbafcbf3262 Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 15:10:41 -0800 Subject: [PATCH 7/8] PR feedback --- pyogrio/_ogr.pyx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index 9fb9e57e..99da449e 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -185,7 +185,7 @@ def init_gdal_data(): raise ValueError("Could not correctly detect GDAL data files installed by pyogrio wheel") return - # GDAL correctly found data files from compiled-in paths + # GDAL correctly found data files from GDAL_DATA or compiled-in paths if has_gdal_data(): return @@ -201,7 +201,6 @@ def init_gdal_data(): def init_proj_data(): """Set Proj search directories in the following precedence: - - PROJ_LIB env var - wheel copy of proj_data - default detection by GDAL, including PROJ_LIB (detected automatically by GDAL) - search other well-known paths under sys.prefix @@ -218,7 +217,7 @@ def init_proj_data(): raise ValueError("Could not correctly detect PROJ data files installed by pyogrio wheel") return - # GDAL correctly found PROJ based on compiled-in paths + # GDAL correctly found data files from PROJ_LIB or compiled-in paths if has_proj_data(): return From e92897313a3125e7b3b02a1e9d8d51a91de13298 Mon Sep 17 00:00:00 2001 From: "Brendan C. Ward" Date: Wed, 9 Mar 2022 15:37:09 -0800 Subject: [PATCH 8/8] PR feedback --- pyogrio/_ogr.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyogrio/_ogr.pyx b/pyogrio/_ogr.pyx index 99da449e..51b9088c 100644 --- a/pyogrio/_ogr.pyx +++ b/pyogrio/_ogr.pyx @@ -145,7 +145,7 @@ cdef char has_gdal_data(): cdef char has_proj_data(): - """Verify that PROJ library data files are loaded by GDAL. + """Verify that PROJ library data files are correctly found. Returns ------- @@ -202,7 +202,7 @@ def init_gdal_data(): def init_proj_data(): """Set Proj search directories in the following precedence: - wheel copy of proj_data - - default detection by GDAL, including PROJ_LIB (detected automatically by GDAL) + - default detection by PROJ, including PROJ_LIB (detected automatically by PROJ) - search other well-known paths under sys.prefix Adapted from Fiona (env.py, _env.pyx). @@ -217,7 +217,7 @@ def init_proj_data(): raise ValueError("Could not correctly detect PROJ data files installed by pyogrio wheel") return - # GDAL correctly found data files from PROJ_LIB or compiled-in paths + # PROJ correctly found data files from PROJ_LIB or compiled-in paths if has_proj_data(): return