Skip to content

Commit 89a66b6

Browse files
committed
Introduce GqlError.find_by_gql_status
Return the first GqlError in the cause chain with the given GQL status. This method traverses this GQLErorrs's :attr:`__cause__` chain, starting with this error itself, and returns the first error that has the given GQL status. If no error matches, :data:`None` is returned. Example: ```python def invalid_syntax(err: GqlError) -> bool: return err.find_by_gql_status("42001") is not None ```
1 parent 449d7cf commit 89a66b6

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

docs/source/api.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,7 +1960,9 @@ GQL Errors
19601960
==========
19611961
.. autoexception:: neo4j.exceptions.GqlError()
19621962
:show-inheritance:
1963-
:members: gql_status, message, gql_status_description, gql_raw_classification, gql_classification, diagnostic_record, __cause__
1963+
:members:
1964+
gql_status, message, gql_status_description, gql_raw_classification, gql_classification, diagnostic_record,
1965+
find_by_gql_status, __cause__
19641966

19651967
.. autoclass:: neo4j.exceptions.GqlErrorClassification()
19661968
:show-inheritance:
@@ -2002,7 +2004,7 @@ Server-side errors
20022004

20032005
.. autoexception:: neo4j.exceptions.Neo4jError()
20042006
:show-inheritance:
2005-
:members: message, code, is_retriable, is_retryable
2007+
:members: message, code, is_retryable
20062008

20072009
.. autoexception:: neo4j.exceptions.ClientError()
20082010
:show-inheritance:

src/neo4j/exceptions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ class GqlError(Exception):
219219
Instead, only subclasses are raised.
220220
Further, it is used as the :attr:`__cause__` of GqlError subclasses.
221221
222+
Sometimes it is helpful or necessary to traverse the cause chain of
223+
GQLErrors to fully understand or appropriately handle the error. In such
224+
cases, users can either traverse the :attr:`__cause__` attribute of the
225+
error(s) or use the helper method :meth:`.find_by_gql_status`. Note that
226+
:attr:`__cause__` is a standard attribute of all Python
227+
:class:`Exception`s. Therefore, the cause chain may also contain other
228+
types besides GqlError.
229+
222230
.. versionadded: 5.26
223231
224232
.. versionchanged:: 6.0 Stabilized from preview.
@@ -427,6 +435,36 @@ def _get_status_diagnostic_record(self) -> dict[str, _t.Any]:
427435
self._status_diagnostic_record = dict(_UNKNOWN_GQL_DIAGNOSTIC_RECORD)
428436
return self._status_diagnostic_record
429437

438+
def find_by_gql_status(self, status: str) -> GqlError | None:
439+
"""
440+
Return the first GqlError in the cause chain with the given GQL status.
441+
442+
This method traverses this GQLErorrs's :attr:`__cause__` chain,
443+
starting with this error itself, and returns the first error that has
444+
the given GQL status. If no error matches, :data:`None` is returned.
445+
446+
Example::
447+
448+
def invalid_syntax(err: GqlError) -> bool:
449+
return err.find_by_gql_status("42001") is not None
450+
451+
:param status: The GQL status to search for.
452+
453+
:returns: The first matching error or :data:`None`.
454+
455+
.. versionadded:: 6.0
456+
"""
457+
if self.gql_status == status:
458+
return self
459+
460+
cause = self.__cause__
461+
while cause is not None:
462+
if isinstance(cause, GqlError) and cause.gql_status == status:
463+
return cause
464+
cause = getattr(cause, "__cause__", None)
465+
466+
return None
467+
430468
def __str__(self):
431469
return (
432470
f"{{gql_status: {self.gql_status}}} "

tests/unit/common/test_exceptions.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,3 +868,33 @@ def test_deprecated_setter(attr):
868868
setattr(error, attr, obj)
869869

870870
assert getattr(error, attr) is not obj
871+
872+
873+
@pytest.mark.parametrize("insert_after", range(-1, 3))
874+
def test_find_by_gql_status(insert_after: int) -> None:
875+
error_to_find = _make_test_gql_error("12345")
876+
877+
root = None
878+
if insert_after == -1:
879+
root = error_to_find = _make_test_gql_error("12345")
880+
for i in range(3):
881+
root = _make_test_gql_error(f"{i + 2}2345", cause=root)
882+
if i == insert_after:
883+
root = error_to_find = _make_test_gql_error("12345", cause=root)
884+
885+
if root is None:
886+
raise RuntimeError("unreachable, loop is not empty")
887+
888+
assert root.find_by_gql_status("12345") is error_to_find
889+
890+
891+
def test_find_by_gql_status_no_match() -> None:
892+
root = None
893+
for i in range(3):
894+
root = _make_test_gql_error(f"{i + 1}2345", cause=root)
895+
896+
if root is None:
897+
raise RuntimeError("unreachable, loop is not empty")
898+
899+
for status in ("2345", "02345", "42345", "54321"):
900+
assert root.find_by_gql_status(status) is None

0 commit comments

Comments
 (0)