Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: register custom error handler to suppress printing error messages to stderr #236

Merged
merged 10 commits into from
Apr 4, 2023
31 changes: 30 additions & 1 deletion pyogrio/_err.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ from enum import IntEnum

from pyogrio._ogr cimport (
CE_None, CE_Debug, CE_Warning, CE_Failure, CE_Fatal, CPLErrorReset,
CPLGetLastErrorType, CPLGetLastErrorNo, CPLGetLastErrorMsg, OGRErr)
CPLGetLastErrorType, CPLGetLastErrorNo, CPLGetLastErrorMsg, OGRErr,
CPLErr, CPLErrorHandler, CPLDefaultErrorHandler, CPLPushErrorHandler)


# CPL Error types as an enum.
Expand Down Expand Up @@ -207,3 +208,31 @@ cdef int exc_wrap_ogrerr(int err) except -1:
raise CPLE_BaseError(3, err, f"OGR Error code {err}")

return err


cdef void error_handler(CPLErr err_class, int err_no, const char* err_msg) nogil:
"""Custom CPL error handler to match the Python behaviour.

Generally we want to suppress error printing to stderr (behaviour of the
default GDAL error handler) because we already raise a Python exception
that includes the error message.
"""
if err_class == CE_Fatal:
# If the error class is CE_Fatal, we want to have a message issued
# because the CPL support code does an abort() before any exception
# can be generated
CPLDefaultErrorHandler(err_class, err_no, err_msg)
return

elif err_class == CE_Failure:
# For Failures, do nothing as those are explicitly catched
jorisvandenbossche marked this conversation as resolved.
Show resolved Hide resolved
# with error return codes and translated into Python exceptions
return

# Fall back to the default handler for non-failure messages since
# they won't be translated into exceptions.
CPLDefaultErrorHandler(err_class, err_no, err_msg)


def _register_error_handler():
CPLPushErrorHandler(<CPLErrorHandler>error_handler)
7 changes: 6 additions & 1 deletion pyogrio/_ogr.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ cdef extern from "cpl_conv.h":
void CPLSetConfigOption(const char* key, const char* value)


cdef extern from "cpl_error.h":
cdef extern from "cpl_error.h" nogil:
brendan-ward marked this conversation as resolved.
Show resolved Hide resolved
ctypedef enum CPLErr:
CE_None
CE_Debug
Expand All @@ -27,6 +27,11 @@ cdef extern from "cpl_error.h":
const char* CPLGetLastErrorMsg()
int CPLGetLastErrorType()

ctypedef void (*CPLErrorHandler)(CPLErr, int, const char*)
void CPLDefaultErrorHandler(CPLErr, int, const char *)
void CPLPushErrorHandler(CPLErrorHandler handler)
void CPLPopErrorHandler()


cdef extern from "cpl_string.h":
char** CSLAddNameValue(char **list, const char *name, const char *value)
Expand Down
2 changes: 2 additions & 0 deletions pyogrio/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
remove_virtual_file,
_register_drivers,
)
from pyogrio._err import _register_error_handler
from pyogrio._io import ogr_list_layers, ogr_read_bounds, ogr_read_info

_init_gdal_data()
_init_proj_data()
_register_drivers()
_register_error_handler()

__gdal_version__ = get_gdal_version()
__gdal_version_string__ = get_gdal_version_string()
Expand Down
10 changes: 10 additions & 0 deletions pyogrio/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
get_gdal_config_option,
get_gdal_data_path,
)
from pyogrio.errors import DataSourceError

from pyogrio._env import GDALEnv

Expand Down Expand Up @@ -288,3 +289,12 @@ def test_reset_config_options():

set_gdal_config_options({"foo": None})
assert get_gdal_config_option("foo") is None


def test_error_handling(capfd):
# an operation that triggers a GDAL Failure
# -> error translated into Python exception + not printed to stderr
with pytest.raises(DataSourceError, match="No such file or directory"):
read_info("non-existent.shp")

assert capfd.readouterr().err == ""