Skip to content

Commit 8889f2a

Browse files
ENH: register custom error handler to suppress printing error messages to stderr (#236)
1 parent 7c3ad3d commit 8889f2a

File tree

5 files changed

+50
-2
lines changed

5 files changed

+50
-2
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
specifying a mask manually for missing values in `write` (#219)
1313
- Standardized 3-dimensional geometry type labels from "2.5D <type>" to
1414
"<type> Z" for consistency with well-known text (WKT) formats (#234)
15+
- Failure error messages from GDAL are no longer printed to stderr (they were
16+
already translated into Python exceptions as well) (#236).
1517

1618
## 0.5.1 (2023-01-26)
1719

pyogrio/_err.pyx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ from enum import IntEnum
33

44
from pyogrio._ogr cimport (
55
CE_None, CE_Debug, CE_Warning, CE_Failure, CE_Fatal, CPLErrorReset,
6-
CPLGetLastErrorType, CPLGetLastErrorNo, CPLGetLastErrorMsg, OGRErr)
6+
CPLGetLastErrorType, CPLGetLastErrorNo, CPLGetLastErrorMsg, OGRErr,
7+
CPLErr, CPLErrorHandler, CPLDefaultErrorHandler, CPLPushErrorHandler)
78

89

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

209210
return err
211+
212+
213+
cdef void error_handler(CPLErr err_class, int err_no, const char* err_msg) nogil:
214+
"""Custom CPL error handler to match the Python behaviour.
215+
216+
Generally we want to suppress error printing to stderr (behaviour of the
217+
default GDAL error handler) because we already raise a Python exception
218+
that includes the error message.
219+
"""
220+
if err_class == CE_Fatal:
221+
# If the error class is CE_Fatal, we want to have a message issued
222+
# because the CPL support code does an abort() before any exception
223+
# can be generated
224+
CPLDefaultErrorHandler(err_class, err_no, err_msg)
225+
return
226+
227+
elif err_class == CE_Failure:
228+
# For Failures, do nothing as those are explicitly caught
229+
# with error return codes and translated into Python exceptions
230+
return
231+
232+
# Fall back to the default handler for non-failure messages since
233+
# they won't be translated into exceptions.
234+
CPLDefaultErrorHandler(err_class, err_no, err_msg)
235+
236+
237+
def _register_error_handler():
238+
CPLPushErrorHandler(<CPLErrorHandler>error_handler)

pyogrio/_ogr.pxd

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ cdef extern from "cpl_conv.h":
1414
void CPLSetConfigOption(const char* key, const char* value)
1515

1616

17-
cdef extern from "cpl_error.h":
17+
cdef extern from "cpl_error.h" nogil:
1818
ctypedef enum CPLErr:
1919
CE_None
2020
CE_Debug
@@ -27,6 +27,11 @@ cdef extern from "cpl_error.h":
2727
const char* CPLGetLastErrorMsg()
2828
int CPLGetLastErrorType()
2929

30+
ctypedef void (*CPLErrorHandler)(CPLErr, int, const char*)
31+
void CPLDefaultErrorHandler(CPLErr, int, const char *)
32+
void CPLPushErrorHandler(CPLErrorHandler handler)
33+
void CPLPopErrorHandler()
34+
3035

3136
cdef extern from "cpl_string.h":
3237
char** CSLAddNameValue(char **list, const char *name, const char *value)

pyogrio/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
remove_virtual_file,
1818
_register_drivers,
1919
)
20+
from pyogrio._err import _register_error_handler
2021
from pyogrio._io import ogr_list_layers, ogr_read_bounds, ogr_read_info
2122

2223
_init_gdal_data()
2324
_init_proj_data()
2425
_register_drivers()
26+
_register_error_handler()
2527

2628
__gdal_version__ = get_gdal_version()
2729
__gdal_version_string__ = get_gdal_version_string()

pyogrio/tests/test_core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
get_gdal_config_option,
1313
get_gdal_data_path,
1414
)
15+
from pyogrio.errors import DataSourceError
1516

1617
from pyogrio._env import GDALEnv
1718

@@ -288,3 +289,12 @@ def test_reset_config_options():
288289

289290
set_gdal_config_options({"foo": None})
290291
assert get_gdal_config_option("foo") is None
292+
293+
294+
def test_error_handling(capfd):
295+
# an operation that triggers a GDAL Failure
296+
# -> error translated into Python exception + not printed to stderr
297+
with pytest.raises(DataSourceError, match="No such file or directory"):
298+
read_info("non-existent.shp")
299+
300+
assert capfd.readouterr().err == ""

0 commit comments

Comments
 (0)