Commit 17798442 authored by Kirill Smelkov's avatar Kirill Smelkov

golang: Expose error at Py level

The first step to expose errors and error chaining to Python:

- Add pyerror that wraps a pyx/nogil C-level error and is exposed as golang.error at py level.
- py errors must be compared by ==, not by "is"
- Add (py) errors.New to create a new error from text.
- a C-level error that has .Unwrap, is exposed with .Unwrap at py level,
  but full py-level chaining will be implemented in a follow-up patch.
- py error does not support inheritance yet.

Top-level documentation is TODO.
parent fd95c88a
...@@ -34,7 +34,7 @@ from __future__ import print_function, absolute_import ...@@ -34,7 +34,7 @@ from __future__ import print_function, absolute_import
__version__ = "0.0.5" __version__ = "0.0.5"
__all__ = ['go', 'chan', 'select', 'default', 'nilchan', 'defer', 'panic', __all__ = ['go', 'chan', 'select', 'default', 'nilchan', 'defer', 'panic',
'recover', 'func', 'b', 'u', 'gimport'] 'recover', 'func', 'error', 'b', 'u', 'gimport']
from golang._gopath import gimport # make gimport available from golang from golang._gopath import gimport # make gimport available from golang
import inspect, sys import inspect, sys
...@@ -295,7 +295,7 @@ if six.PY2: ...@@ -295,7 +295,7 @@ if six.PY2:
import golang._patch.ipython_py2 import golang._patch.ipython_py2
# ---- go + channels, panic, etc... ---- # ---- go + channels, panic, error, etc... ----
from ._golang import \ from ._golang import \
pygo as go, \ pygo as go, \
...@@ -305,5 +305,6 @@ from ._golang import \ ...@@ -305,5 +305,6 @@ from ._golang import \
pynilchan as nilchan, \ pynilchan as nilchan, \
_PanicError, \ _PanicError, \
pypanic as panic, \ pypanic as panic, \
pyerror as error, \
pyb as b, \ pyb as b, \
pyu as u pyu as u
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
from __future__ import print_function, absolute_import from __future__ import print_function, absolute_import
from golang cimport pychan, nil, _interface, gobject, newref, adoptref, topyexc from golang cimport pychan, pyerror, nil, _interface, gobject, newref, adoptref, topyexc
from golang cimport cxx, time from golang cimport cxx, time
from cython cimport final, internal from cython cimport final, internal
from cython.operator cimport typeid from cython.operator cimport typeid
...@@ -75,13 +75,7 @@ cdef class PyContext: ...@@ -75,13 +75,7 @@ cdef class PyContext:
def err(PyContext pyctx): # -> error def err(PyContext pyctx): # -> error
with nogil: with nogil:
err = pyctx.ctx.err() err = pyctx.ctx.err()
if err == nil: return pyerror.from_error(err)
return None
if err.eq(canceled):
return pycanceled
if err.eq(deadlineExceeded):
return pydeadlineExceeded
return RuntimeError(err.Error())
# value returns value associated with key, or None, if context has no key. # value returns value associated with key, or None, if context has no key.
# #
...@@ -152,10 +146,10 @@ cdef PyContext _pybackground = _newPyCtx(background()) ...@@ -152,10 +146,10 @@ cdef PyContext _pybackground = _newPyCtx(background())
# canceled is the error returned by Context.err when context is canceled. # canceled is the error returned by Context.err when context is canceled.
pycanceled = RuntimeError(canceled.Error()) pycanceled = pyerror.from_error(canceled)
# deadlineExceeded is the error returned by Context.err when time goes past context's deadline. # deadlineExceeded is the error returned by Context.err when time goes past context's deadline.
pydeadlineExceeded = RuntimeError(deadlineExceeded.Error()) pydeadlineExceeded = pyerror.from_error(deadlineExceeded)
# with_cancel creates new context that can be canceled on its own. # with_cancel creates new context that can be canceled on its own.
......
# -*- coding: utf-8 -*-
# cython: language_level=2
# cython: c_string_type=str, c_string_encoding=utf8
# distutils: language=c++
#
# Copyright (C) 2020 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""_errors.pyx implements errors.pyx - see _errors.pxd for package overview."""
from __future__ import print_function, absolute_import
from golang cimport pyerror, nil, topyexc
from golang import b as pyb
from golang cimport errors
def pyNew(text): # -> error
"""New creates new error with provided text."""
return pyerror.from_error(errors_New_pyexc(pyb(text)))
# ---- misc ----
cdef nogil:
error errors_New_pyexc(const char* text) except +topyexc:
return errors.New(text)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# cython: language_level=2 # cython: language_level=2
# cython: c_string_type=str, c_string_encoding=utf8
# distutils: language=c++ # distutils: language=c++
# #
# Copyright (C) 2020 Nexedi SA and Contributors. # Copyright (C) 2020 Nexedi SA and Contributors.
...@@ -23,7 +24,21 @@ ...@@ -23,7 +24,21 @@
from __future__ import print_function, absolute_import from __future__ import print_function, absolute_import
from golang cimport topyexc from golang cimport error, pyerror, nil, topyexc
from golang cimport errors, fmt
# pyerror_mkchain creates error chain from [] of text.
def pyerror_mkchain(textv):
cdef error err
cdef const char *s
for text in reversed(textv):
if err == nil:
err = errors.New(text)
else:
s = text
err = fmt.errorf("%s: %w", s, err)
return pyerror.from_error(err)
# errors_test.cpp # errors_test.cpp
......
...@@ -235,3 +235,16 @@ cdef class pychan: ...@@ -235,3 +235,16 @@ cdef class pychan:
cdef pychan from_chan_int (chan[int] ch) cdef pychan from_chan_int (chan[int] ch)
@staticmethod @staticmethod
cdef pychan from_chan_double (chan[double] ch) cdef pychan from_chan_double (chan[double] ch)
# pyerror wraps an error into python object.
#
# There can be multiple pyerror(s) wrapping a particular raw error object.
# Nil C-level error corresponds to None at Python-level.
cdef class pyerror(Exception):
cdef error err # raw error object
# pyerror.from_error returns pyerror wrapping pyx/nogil-level error.
# from_error(nil) -> returns None.
@staticmethod
cdef object from_error (error err) # -> pyerror | None
...@@ -831,3 +831,94 @@ def pyqq(obj): ...@@ -831,3 +831,94 @@ def pyqq(obj):
qobj = pyu(qobj) qobj = pyu(qobj)
return qobj return qobj
# ---- error ----
from golang cimport errors
from libcpp.typeinfo cimport type_info
from cython.operator cimport typeid
from libc.string cimport strcmp
# _frompyx indicates that a constructor is called from pyx code
cdef object _frompyx = object()
cdef class pyerror(Exception):
# pyerror <- error
@staticmethod
cdef object from_error(error err):
if err == nil:
return None
cdef pyerror pyerr = pyerror.__new__(pyerror, _frompyx)
pyerr.err = err
return pyerr
def __cinit__(pyerror pyerr, *argv):
pyerr.err = nil
pyerr.args = ()
if len(argv)==1 and argv[0] is _frompyx:
return # keep .err=nil - the object is being created via pyerror.from_error
pyerr.args = argv
# pyerror("abc") call
if type(pyerr) is pyerror:
arg, = argv
pyerr.err = errors_New_pyexc(pyb(arg))
return
raise TypeError("subclassing error is not supported yet")
def __dealloc__(pyerror pyerr):
pyerr.err = nil
def Error(pyerror pyerr):
"""Error returns string that represents the error."""
assert pyerr.err != nil
return pyerr.err.Error()
def Unwrap(pyerror pyerr):
"""Unwrap tries to extract wrapped error."""
w = errors_Unwrap_pyexc(pyerr.err)
return pyerror.from_error(w)
# pyerror == pyerror
def __hash__(pyerror pyerr):
# TODO use std::hash directly
cdef const type_info* typ = &typeid(pyerr.err._ptr()[0])
return hash(typ.name()) ^ hash(pyerr.err.Error())
def __ne__(pyerror a, object rhs):
return not (a == rhs)
def __eq__(pyerror a, object rhs):
if type(a) is not type(rhs):
return False
cdef pyerror b = rhs
cdef const type_info* atype = &typeid(a.err._ptr()[0])
cdef const type_info* btype = &typeid(b.err._ptr()[0])
if strcmp(atype.name(), btype.name()) != 0:
return False
# XXX hack instead of dynamic == (not available in C++)
return (a.err.Error() == b.err.Error())
def __str__(pyerror pyerr):
return pyerr.Error()
def __repr__(pyerror pyerr):
typ = type(pyerr)
cdef const type_info* ctype = &typeid(pyerr.err._ptr()[0])
# TODO demangle type name (e.g. abi::__cxa_demangle)
return "<%s.%s object ctype=%s error=%s>" % (typ.__module__, typ.__name__, ctype.name(), pyqq(pyerr.Error()))
# ---- misc ----
cdef nogil:
error errors_New_pyexc(const char* text) except +topyexc:
return errors.New(text)
error errors_Unwrap_pyexc(error err) except +topyexc:
return errors.Unwrap(err)
...@@ -29,7 +29,7 @@ from golang.time_test import dt ...@@ -29,7 +29,7 @@ from golang.time_test import dt
def assertCtx(ctx, children, deadline=None, err=None, done=False): def assertCtx(ctx, children, deadline=None, err=None, done=False):
assert isinstance(ctx, _context.PyContext) assert isinstance(ctx, _context.PyContext)
assert ctx.deadline() == deadline assert ctx.deadline() == deadline
assert ctx.err() is err assert ctx.err() == err
ctxdone = ctx.done() ctxdone = ctx.done()
assert ready(ctxdone) == done assert ready(ctxdone) == done
tctxAssertChildren(ctx, children) tctxAssertChildren(ctx, children)
......
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Package errors mirrors Go package errors.
- `New` creates new error with provided text.
See also https://golang.org/pkg/errors for Go errors package documentation.
"""
from __future__ import print_function, absolute_import
from golang._errors import \
pyNew as New
...@@ -20,6 +20,88 @@ ...@@ -20,6 +20,88 @@
from __future__ import print_function, absolute_import from __future__ import print_function, absolute_import
from golang import error, b
from golang import errors
from golang.golang_test import import_pyx_tests from golang.golang_test import import_pyx_tests
from golang._errors_test import pyerror_mkchain as error_mkchain
from pytest import raises
import_pyx_tests("golang._errors_test") import_pyx_tests("golang._errors_test")
# assertEeq asserts that `e1 == e2`, `not (e1 != e2)`, `hash(e1) == hash(e2)`,
# and symmetrically.
def assertEeq(e1, e2):
assert e1 == e2
assert e2 == e1
assert not (e1 != e2)
assert not (e2 != e1)
assert hash(e1) == hash(e2)
assert hash(e2) == hash(e1)
# assertEne asserts that `e1 != e2`, `not (e1 == e2)` and symmetrically.
def assertEne(e1, e2):
assert e1 != e2
assert e2 != e1
assert not (e1 == e2)
assert not (e2 == e1)
# cannot - generally speaking there could be hash collisions
#assert hash(e1) != hash(e2)
# test for golang.error class.
def test_error():
assert error_mkchain([]) is None
e = error_mkchain(["abc"])
assert type(e) is error
assert e.Error() == "abc"
assert str(e) == "abc"
assert repr(e).endswith(' error="abc">')
assert e.Unwrap() is None
assertEeq(e, e)
e1 = e
e = error_mkchain(["привет", "abc"])
assert type(e) is error
assert e.Error() == "привет: abc"
assert str(e) == "привет: abc"
assert repr(e).endswith(' error="привет: abc">')
e1_ = e.Unwrap()
assert type(e1_) is type(e1)
assertEeq(e1_, e1)
assert e1_.Unwrap() is None
assertEeq(e, e)
assertEne(e, e1)
e2 = e
# create an error from py via error() call
with raises(ValueError): error()
with raises(ValueError): error("hello", "world")
for x in ["hello мир", u"hello мир", b("hello мир")]:
e = error(x)
assert type(e) is error
assert e.Error() == "hello мир"
assert str(e) == "hello мир"
assert repr(e).endswith(' error="hello мир">')
assert e.Unwrap() is None
assertEeq(e, e)
def test_new():
E = errors.New
for x in ["мир", u"мир", b("мир")]:
err = E(x)
assert type(err) is error
assertEeq(err, E("мир"))
assertEne(err, E("def"))
assertEeq(err, error("мир"))
assertEeq(err, error(u"мир"))
assertEeq(err, error(b("мир")))
with raises(TypeError):
E(1)
...@@ -1553,6 +1553,9 @@ def bench_defer(b): ...@@ -1553,6 +1553,9 @@ def bench_defer(b):
_() _()
# test_error lives in errors_test.py
# verify b, u # verify b, u
def test_strings(): def test_strings():
testv = ( testv = (
......
...@@ -40,6 +40,7 @@ def test_golang_builtins(): ...@@ -40,6 +40,7 @@ def test_golang_builtins():
assert go is golang.go assert go is golang.go
assert chan is golang.chan assert chan is golang.chan
assert select is golang.select assert select is golang.select
assert error is golang.error
assert b is golang.b assert b is golang.b
assert u is golang.u assert u is golang.u
......
...@@ -255,6 +255,8 @@ setup( ...@@ -255,6 +255,8 @@ setup(
['golang/_cxx_test.pyx', ['golang/_cxx_test.pyx',
'golang/cxx_test.cpp']), 'golang/cxx_test.cpp']),
Ext('golang._errors',
['golang/_errors.pyx']),
Ext('golang._errors_test', Ext('golang._errors_test',
['golang/_errors_test.pyx', ['golang/_errors_test.pyx',
'golang/errors_test.cpp']), 'golang/errors_test.cpp']),
......
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