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
__version__ = "0.0.5"
__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
import inspect, sys
......@@ -295,7 +295,7 @@ if six.PY2:
import golang._patch.ipython_py2
# ---- go + channels, panic, etc... ----
# ---- go + channels, panic, error, etc... ----
from ._golang import \
pygo as go, \
......@@ -305,5 +305,6 @@ from ._golang import \
pynilchan as nilchan, \
_PanicError, \
pypanic as panic, \
pyerror as error, \
pyb as b, \
pyu as u
......@@ -22,7 +22,7 @@
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 cython cimport final, internal
from cython.operator cimport typeid
......@@ -75,13 +75,7 @@ cdef class PyContext:
def err(PyContext pyctx): # -> error
with nogil:
err = pyctx.ctx.err()
if err == nil:
return None
if err.eq(canceled):
return pycanceled
if err.eq(deadlineExceeded):
return pydeadlineExceeded
return RuntimeError(err.Error())
return pyerror.from_error(err)
# value returns value associated with key, or None, if context has no key.
#
......@@ -152,10 +146,10 @@ cdef PyContext _pybackground = _newPyCtx(background())
# 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.
pydeadlineExceeded = RuntimeError(deadlineExceeded.Error())
pydeadlineExceeded = pyerror.from_error(deadlineExceeded)
# 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 -*-
# cython: language_level=2
# cython: c_string_type=str, c_string_encoding=utf8
# distutils: language=c++
#
# Copyright (C) 2020 Nexedi SA and Contributors.
......@@ -23,7 +24,21 @@
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
......
......@@ -235,3 +235,16 @@ cdef class pychan:
cdef pychan from_chan_int (chan[int] ch)
@staticmethod
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):
qobj = pyu(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
def assertCtx(ctx, children, deadline=None, err=None, done=False):
assert isinstance(ctx, _context.PyContext)
assert ctx.deadline() == deadline
assert ctx.err() is err
assert ctx.err() == err
ctxdone = ctx.done()
assert ready(ctxdone) == done
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 @@
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._errors_test import pyerror_mkchain as error_mkchain
from pytest import raises
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):
_()
# test_error lives in errors_test.py
# verify b, u
def test_strings():
testv = (
......
......@@ -40,6 +40,7 @@ def test_golang_builtins():
assert go is golang.go
assert chan is golang.chan
assert select is golang.select
assert error is golang.error
assert b is golang.b
assert u is golang.u
......
......@@ -255,6 +255,8 @@ setup(
['golang/_cxx_test.pyx',
'golang/cxx_test.cpp']),
Ext('golang._errors',
['golang/_errors.pyx']),
Ext('golang._errors_test',
['golang/_errors_test.pyx',
'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