Commit 03f88c0b authored by Kirill Smelkov's avatar Kirill Smelkov

errors: Take .__cause__ into account

A Python error can have links to other errors by means of both .Unwrap()
and .__cause__ . These ways are both explicit and so should be treated
by e.g. errors.Is as present in error's error chain.

It is a bit unclear, at least initially, how to linearise and order
error chain traversal in divergence points - for exception objects where
both .Unwrap() and .__cause__ are !None. However more closer look
suggests linearisation rule to traverse into .__cause__ after going
through .Unwrap() part - please see details in documentation added into
_error.pyx

-> Teach errors.Is to do this traversal, and this way now e.g. exception
raised as

	raise X from Y

will be treated by errors.Is as being both X and Y, even if any of X or Y
also has its own error chain via .Unwrap().

Top-level documentation is TODO.
parent 337de0d7
...@@ -29,14 +29,132 @@ from golang cimport pyerror, nil, topyexc ...@@ -29,14 +29,132 @@ from golang cimport pyerror, nil, topyexc
from golang import b as pyb from golang import b as pyb
from golang cimport errors from golang cimport errors
# Taking both .Unwrap() and .__cause__ into account
#
# Contrary to Go and Pyx/C++ cases, in Python errors could link to each other
# by both .Unwrap() and .__cause__ . Below we show that both error links have
# to be taken into account when building an error's chain, and how:
#
# Consider the following cases
#
# 1. X -> X
# 2. X -> error
# 3. error -> X
# 4. error -> error
#
# where
#
# "->" means link via .__cause__,
# X - exception that does not provide .Unwrap()
# error - exception that provides .Unwrap()
#
# 1: Since the cause is explicit we want errors.Unwrap to follow "->" link,
# so that e.g. errors.Is works correctly.
#
# 2: The same.
#
# 3: Consider e.g.
#
# e1 w→ e2 w→ e3 w→ denotes what .Unwrap() returns
# | -> denotes .__cause__
# v
# X
#
# this picture is a result of
#
# try:
# # call something that raises regular exc X
# except X:
# err = dosmth_pygo()
# raise err from x
#
# due to the logic in code we a) want to keep X in unwrap sequence of err
# (it was explicitly specified as cause), and b) we want X to be in the
# end of unwrap sequence of err, because due to the logic it is the most
# inner cause:
#
# e1 w→ e2 w→ e3
# | w
# v |
# X ← ← ← ← ← ←
#
# 4: Consider e.g.
#
# e11 w→ e12 w→ e13
# |
# v
# e21 w→ e22
#
# Similarly to 3 we want err2 to be kept in unwrap sequence of err1 and to
# be appended there into tail:
#
# e11 w→ e12 w→ e13
# | ← ← ← ← ← ← ← w
# v↓
# e21 w→ e22
#
# 1-4 suggest the following rules: let U(e) be the list with full error chain
# of e, starting from e itself. For example for `err = e1 w→ e2 w→ e3`,
# `U(err) = [e1, e2, e3]`
#
# we can deduce recursive formula for U based on 1-4:
#
# - U(nil) = [] ; obvious
#
# - for e: U(e) = [e] ; obvious
# .w = nil
# .__cause__ = nil
#
# - for e: U(e) = [e] + U(e.w) ; regular go-style wrapping
# .w != nil
# .__cause__ = nil
#
# - for e: U(e) = [e] + U(e.__cause__) ; cases 1 & 2
# .w = nil
# .__cause__ != nil
#
# - for e: U(e) = [e] + U(e.w) + U(e.__cause__) ; ex. cases 3 & 4
# .w != nil
# .__cause__ != nil
#
# the formula for cases 3 & 4 is the general one and works for all cases:
#
# U(nil) = []
# U(e) = [e] + U(e.w) + U(e.__cause__)
#
#
# e.g. consider how it works for:
#
# e1 w→ e2 w→ e3
# | | w
# v v |
# X1← ← X2← ← ←
#
# U(e1) = [e1] + U(e2) + U(X1)
# = [e1] + {[e2] + U(e3) + U(X2)} + [X1]
# = [e1] + {[e2] + [e3] + [X2]} + [X1]
# = [e1, e2, e3, X2, X1]
#
# --------
#
# Implementation: with errors.Unwrap we cannot use U directly to implement
# unwrapping because it requires to keep unwrapping iterator state and
# errors.Unwrap API returns just an error instance, nothing more. For this
# reason python version of errors package, does not expose errors.Unwrap, and
# internally uses errors._UnwrapIter, which returns iterator through an
# error's error chain.
def pyNew(text): # -> error def pyNew(text): # -> error
"""New creates new error with provided text.""" """New creates new error with provided text."""
return pyerror.from_error(errors_New_pyexc(pyb(text))) return pyerror.from_error(errors_New_pyexc(pyb(text)))
def _pyUnwrap(err): # -> error def _pyUnwrapIter(err): # -> iter(error)
"""_Unwrap tries to unwrap error. """_UnwrapIter returns iterator through err's error chain.
This iteration takes both .Unwrap() and .__cause__ into account.
See "Taking both .Unwrap() and .__cause__ into account" in internal overview.
""" """
if err is None: if err is None:
return return
...@@ -46,18 +164,33 @@ def _pyUnwrap(err): # -> error ...@@ -46,18 +164,33 @@ def _pyUnwrap(err): # -> error
cdef pyerror pye cdef pyerror pye
cdef error e cdef error e
# + U(e.w)
if type(err) is not pyerror: if type(err) is not pyerror:
# err is python-level error (pyerror-based or just BaseException child) # err is python-level error (pyerror-based or just BaseException child)
eunwrap = getattr(err, 'Unwrap', _missing) eunwrap = getattr(err, 'Unwrap', _missing)
pyw = None pyw = None
if eunwrap is not _missing: if eunwrap is not _missing:
pyw = eunwrap() pyw = eunwrap()
return pyw if pyw is not None:
yield pyw
for _ in _pyUnwrapIter(pyw):
yield _
else: else:
# err is wrapper around C-level error # err is wrapper around C-level error
pye = err pye = err
e = errors_Unwrap_pyexc(pye.err) e = pye.err
return pyerror.from_error(e) while 1:
e = errors_Unwrap_pyexc(e)
if e == nil:
break
yield pyerror.from_error(e)
# + U(e.__cause__)
pycause = getattr(err, '__cause__', None)
if pycause is not None:
yield pycause
for _ in _pyUnwrapIter(pycause):
yield _
def pyIs(err, target): # -> bool def pyIs(err, target): # -> bool
...@@ -72,6 +205,7 @@ def pyIs(err, target): # -> bool ...@@ -72,6 +205,7 @@ def pyIs(err, target): # -> bool
if target is None: if target is None:
return (err is None) return (err is None)
wit = _pyUnwrapIter(err)
while 1: while 1:
if err is None: if err is None:
return False return False
...@@ -80,7 +214,7 @@ def pyIs(err, target): # -> bool ...@@ -80,7 +214,7 @@ def pyIs(err, target): # -> bool
if err == target: if err == target:
return True return True
err = _pyUnwrap(err) err = next(wit, None)
# ---- misc ---- # ---- misc ----
......
...@@ -87,9 +87,25 @@ class XWrap(BaseException): # NOTE does not inherit from error ...@@ -87,9 +87,25 @@ class XWrap(BaseException): # NOTE does not inherit from error
(a.text == b.text) and (a.err == b.err) (a.text == b.text) and (a.err == b.err)
# XExc is custom error class that does not inherit from error, nor provides
# .Error() nor .Unwrap().
class XExc(Exception): # NOTE does not inherit from error
def __init__(xerr, text):
xerr.text = text
def __str__(xerr): return xerr.text
def __repr__(xerr): return 'XExc("%s")' % xerr.text
# no .Error()
# no .Unwrap()
def __eq__(a, b):
return (type(a) is type(b)) and \
(a.text == b.text)
# Unwrap1(e) is approximate for `errors.Unwrap(e)` in Go. # Unwrap1(e) is approximate for `errors.Unwrap(e)` in Go.
def Unwrap1(e): def Unwrap1(e):
return _errors._pyUnwrap(e) wit = _errors._pyUnwrapIter(e)
return next(wit, None)
# test for golang.error class. # test for golang.error class.
...@@ -182,6 +198,7 @@ def test_new(): ...@@ -182,6 +198,7 @@ def test_new():
E(1) E(1)
# verify Unwrap for simple linear cases.
def test_unwrap(): def test_unwrap():
E = errors.New E = errors.New
Ec = error_mkchain Ec = error_mkchain
...@@ -214,6 +231,7 @@ def test_unwrap(): ...@@ -214,6 +231,7 @@ def test_unwrap():
assert Unwrap1(e1) is None assert Unwrap1(e1) is None
# verify Is for simple linear cases.
def test_is(): def test_is():
E = errors.New E = errors.New
Ec = error_mkchain Ec = error_mkchain
...@@ -255,3 +273,149 @@ def test_is(): ...@@ -255,3 +273,149 @@ def test_is():
assert errors.Is(ewrap, w2) == False assert errors.Is(ewrap, w2) == False
w2 = XWrap("qqq", Ec(["abc", "привет"])) w2 = XWrap("qqq", Ec(["abc", "привет"]))
assert errors.Is(ewrap, w2) == True assert errors.Is(ewrap, w2) == True
# ---- Unwrap/Is in the presence of both .Unwrap and .__cause__ ----
# U returns [] with e error chain built via e.Unwrap and e.__cause__ recursion.
def U(e):
if e is None:
return []
return [e] + U(getattr(e,'Unwrap',lambda:None)()) + U(getattr(e,'__cause__',None))
# Uunwrap returns [] with e error chain built via errors.Unwrap recursion.
def Uunwrap(e):
return [e] + list(_errors._pyUnwrapIter(e))
# verifyUnwrap verifies errors.UnwrapIter via comparing its result with direct
# recursion through .Unwrap and .__cause__ .
def verifyUnwrap(e, estrvok):
assert [str(_) for _ in U(e)] == estrvok
assert Uunwrap(e) == U(e)
# verify how errors.Unwrap and errors.Is handle unwrapping when .__cause__ is also !None.
def test_unwrap_with_cause():
E = errors.New
Ec = error_mkchain
V = verifyUnwrap
V(E("abc"), ["abc"])
V(E("hello world"), ["hello world"])
# e1 w→ e2 w→ e3 e1 w→ e2 w→ e3
# | -> | w
# v v |
# X X ← ← ← ← ← ←
e1 = Ec(["1", "2", "3"])
x = XExc("x")
e1.__cause__ = x
V(e1, ["1: 2: 3", "2: 3", "3", "x"])
assert errors.Is(e1, Ec(["1", "2", "3"])) == True
assert errors.Is(e1, Ec(["2", "3"])) == True
assert errors.Is(e1, Ec(["2", "4"])) == False
assert errors.Is(e1, E("3")) == True
assert errors.Is(e1, XExc("x")) == True
assert errors.Is(e1, XExc("y")) == False
# e11 w→ e12 w→ e13 e11 w→ e12 w→ e13
# | | ← ← ← ← ← ← ← w
# v -> v↓
# e21 w→ e22 e21 w→ e22
e11 = Ec(["11", "12", "13"])
e21 = Ec(["21", "22"])
e11.__cause__ = e21
V(e11, ["11: 12: 13", "12: 13", "13", "21: 22", "22"])
assert errors.Is(e11, Ec(["11", "12", "13"])) == True
assert errors.Is(e11, Ec(["12", "13"])) == True
assert errors.Is(e11, Ec(["12", "14"])) == False
assert errors.Is(e11, E("13")) == True
assert errors.Is(e11, Ec(["11", "12", "13", "21", "22"])) == False
assert errors.Is(e11, Ec(["11", "12", "13", "21"])) == False
assert errors.Is(e11, Ec(["21", "22"])) == True
assert errors.Is(e11, E("22")) == True
assert errors.Is(e11, Ec(["21", "22", "23"])) == False
assert errors.Is(e11, Ec(["21", "23"])) == False
# e1 w→ e2 w→ e3
# | | w
# v v |
# X1← ← X2← ← ←
e2 = Ec(["2", "3"])
e1 = EErrorWrap("1", e2)
x1 = XExc("x1")
x2 = XExc("x2")
e1.__cause__ = x1
e2.__cause__ = x2
V(e1, ["1: 2: 3", "2: 3", "3", "x2", "x1"])
assert errors.Is(e1, EErrorWrap("1", Ec(["2", "3"]))) == True
assert errors.Is(e1, EErrorWrap("1", Ec(["2", "4"]))) == False
assert errors.Is(e1, EErrorWrap("0", Ec(["2", "3"]))) == False
assert errors.Is(e1, Ec(["2", "3"])) == True
assert errors.Is(e1, Ec(["2", "4"])) == False
assert errors.Is(e1, XExc("x1")) == True
assert errors.Is(e1, XExc("x2")) == True
assert errors.Is(e1, XExc("x3")) == False
# e11 w→ e12 w→ e13
# | | w
# v v .← ← ←'
# X1 e21 w→ e22
# `← ← ← ← ← ←'
e13 = XExc("13")
e12 = EErrorWrap("12", e13)
e11 = XWrap("11", e12)
x1 = XExc("x1")
e21 = Ec(["21", "22"])
e11.__cause__ = x1
e12.__cause__ = e21
V(e11, ["11: 12: 13", "12: 13", "13", "21: 22", "22", "x1"])
assert errors.Is(e11, EErrorWrap("12", XExc("13"))) == True
assert errors.Is(e11, EErrorWrap("12", XExc("14"))) == False
assert errors.Is(e11, EErrorWrap("xx", XExc("13"))) == False
assert errors.Is(e11, XExc("13")) == True
assert errors.Is(e11, XExc("x1")) == True
assert errors.Is(e11, XExc("y")) == False
assert errors.Is(e11, Ec(["21", "22"])) == True
assert errors.Is(e11, E("22")) == True
assert errors.Is(e11, Ec(["21", "23"])) == False
assert errors.Is(e11, E("23")) == False
# X1 w→ X2
# | w
# v .← ←'
# e1 w→ e2 w→ e3
x2 = XExc("x2")
x1 = XWrap("x1", x2)
e1 = Ec(["1", "2", "3"])
x1.__cause__ = e1
V(x1, ["x1: x2", "x2", "1: 2: 3", "2: 3", "3"])
assert errors.Is(x1, XExc("x2")) == True
assert errors.Is(x1, XExc("x3")) == False
assert errors.Is(x1, XWrap("x1", XExc("x2"))) == True
assert errors.Is(x1, XWrap("x1", XExc("x3"))) == False
assert errors.Is(x1, XWrap("y1", XExc("x2"))) == False
assert errors.Is(x1, Ec(["1", "2", "3"])) == True
assert errors.Is(x1, Ec(["2", "3"])) == True
assert errors.Is(x1, E("3")) == True
assert errors.Is(x1, Ec(["2", "4"])) == False
# X11 w→ X12
# |
# v
# X21 w→ X22
x12 = XExc("x12")
x11 = XWrap("x11", x12)
x22 = XExc("x22")
x21 = XWrap("x21", x22)
x11.__cause__ = x21
V(x11, ["x11: x12", "x12", "x21: x22", "x22"])
assert errors.Is(x11, XExc("x12")) == True
assert errors.Is(x11, XExc("x13")) == False
assert errors.Is(x11, XWrap("x11", XExc("x12"))) == True
assert errors.Is(x11, XWrap("y11", XExc("x12"))) == False
assert errors.Is(x11, XExc("x22")) == True
assert errors.Is(x11, XExc("x23")) == False
assert errors.Is(x11, XWrap("x21", XExc("x22"))) == True
assert errors.Is(x11, XWrap("y21", XExc("x22"))) == False
assert errors.Is(x11, XWrap("x11", XWrap("x21", XExc("x22")))) == False
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment