Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialize by using __new__ whenever possible #73

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ parallel = true
[report]
show_missing = true
precision = 2
omit = *migrations*
omit =
*migrations*
tests/bad*.py
12 changes: 6 additions & 6 deletions .github/workflows/github-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
toxpython: 'python3.8'
python_arch: 'x64'
tox_env: 'py38'
os: 'macos-latest'
os: 'macos-13'
- name: 'py39 (ubuntu)'
python: '3.9'
toxpython: 'python3.9'
Expand All @@ -54,7 +54,7 @@ jobs:
toxpython: 'python3.9'
python_arch: 'x64'
tox_env: 'py39'
os: 'macos-latest'
os: 'macos-13'
- name: 'py310 (ubuntu)'
python: '3.10'
toxpython: 'python3.10'
Expand All @@ -72,7 +72,7 @@ jobs:
toxpython: 'python3.10'
python_arch: 'x64'
tox_env: 'py310'
os: 'macos-latest'
os: 'macos-13'
- name: 'py311 (ubuntu)'
python: '3.11'
toxpython: 'python3.11'
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:
toxpython: 'pypy3.8'
python_arch: 'x64'
tox_env: 'pypy38'
os: 'macos-latest'
os: 'macos-13'
- name: 'pypy39 (ubuntu)'
python: 'pypy-3.9'
toxpython: 'pypy3.9'
Expand All @@ -144,7 +144,7 @@ jobs:
toxpython: 'pypy3.9'
python_arch: 'x64'
tox_env: 'pypy39'
os: 'macos-latest'
os: 'macos-13'
- name: 'pypy310 (ubuntu)'
python: 'pypy-3.10'
toxpython: 'pypy3.10'
Expand All @@ -162,7 +162,7 @@ jobs:
toxpython: 'pypy3.10'
python_arch: 'x64'
tox_env: 'pypy310'
os: 'macos-latest'
os: 'macos-13'
steps:
- uses: actions/checkout@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Changelog

* Add support for PyPy3.5-5.7.1-beta. Previously ``AttributeError:
'Frame' object has no attribute 'clear'`` could be raised. See PyPy
issue `#2532 <https://foss.heptapod.net/pypy/pypy/-/issues/2532>`_.
issue `#2532 <https://github.com/pypy/pypy/issues/2532>`_.

1.3.1 (2017-03-27)
~~~~~~~~~~~~~~~~~~
Expand Down
32 changes: 20 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Raising

::

>>> from six import reraise
>>> from tblib.decorators import reraise
>>> reraise(*pickle.loads(s1))
Traceback (most recent call last):
...
Expand Down Expand Up @@ -433,22 +433,26 @@ json.JSONDecoder::
{'tb_frame': {'f_code': {'co_filename': '<doctest README.rst[...]>',
'co_name': '<module>'},
'f_globals': {'__name__': '__main__'},
'f_lineno': 5},
'f_lineno': 5,
'f_locals': {}},
'tb_lineno': 2,
'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
'co_name': 'inner_2'},
'f_globals': {'__name__': '__main__'},
'f_lineno': 2},
'f_lineno': 2,
'f_locals': {}},
'tb_lineno': 2,
'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
'co_name': 'inner_1'},
'f_globals': {'__name__': '__main__'},
'f_lineno': 2},
'f_lineno': 2,
'f_locals': {}},
'tb_lineno': 2,
'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
'co_name': 'inner_0'},
'f_globals': {'__name__': '__main__'},
'f_lineno': 2},
'f_lineno': 2,
'f_locals': {}},
'tb_lineno': 2,
'tb_next': None}}}}

Expand Down Expand Up @@ -503,7 +507,7 @@ tblib.Traceback.from_string
File "...examples.py", line 10, in func_c
func_d()
File "...examples.py", line 14, in func_d
raise Exception("Guessing time !")
raise Exception('Guessing time !')
Exception: fail


Expand Down Expand Up @@ -534,7 +538,7 @@ If you use the ``strict=False`` option then parsing is a bit more lax::
File "...examples.py", line 10, in func_c
func_d()
File "...examples.py", line 14, in func_d
raise Exception("Guessing time !")
raise Exception('Guessing time !')
Exception: fail

tblib.decorators.return_error
Expand Down Expand Up @@ -607,6 +611,8 @@ Not very useful is it? Let's sort this out::
i.reraise()
File "...tblib...decorators.py", line ..., in reraise
reraise(self.exc_type, self.exc_value, self.traceback)
File "...tblib...decorators.py", line ..., in reraise
raise value.with_traceback(tb)
File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
return func(*args, **kwargs)
File "...tblib...decorators.py", line ..., in apply_with_return_error
Expand All @@ -618,7 +624,7 @@ Not very useful is it? Let's sort this out::
File "...examples.py", line 10, in func_c
func_d()
File "...examples.py", line 14, in func_d
raise Exception("Guessing time !")
raise Exception('Guessing time !')
Exception: Guessing time !
<BLANKLINE>
>>> pool.terminate()
Expand Down Expand Up @@ -660,11 +666,13 @@ What if we have a local call stack ?
local_0()
File "<doctest README.rst[...]>", line 6, in local_0
i.reraise()
File "...tblib...decorators.py", line 20, in reraise
File "...tblib...decorators.py", line ..., in reraise
reraise(self.exc_type, self.exc_value, self.traceback)
File "...tblib...decorators.py", line 27, in return_exceptions_wrapper
File "...tblib...decorators.py", line ..., in reraise
raise value.with_traceback(tb)
File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
return func(*args, **kwargs)
File "...tblib...decorators.py", line 47, in apply_with_return_error
File "...tblib...decorators.py", line ..., in apply_with_return_error
return args[0](*args[1:])
File "...tests...examples.py", line 2, in func_a
func_b()
Expand All @@ -673,7 +681,7 @@ What if we have a local call stack ?
File "...tests...examples.py", line 10, in func_c
func_d()
File "...tests...examples.py", line 14, in func_d
raise Exception("Guessing time !")
raise Exception('Guessing time !')
Exception: Guessing time !
<BLANKLINE>

Expand Down
25 changes: 24 additions & 1 deletion src/tblib/pickling_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ def pickle_traceback(tb, *, get_locals=None):
)


def unpickle_exception_with_new(func, args, cause, tb, context, suppress_context, notes):
inst = func.__new__(func)
if args is not None:
inst.args = args
inst.__cause__ = cause
inst.__traceback__ = tb
inst.__context__ = context
inst.__suppress_context__ = suppress_context
if notes is not None:
inst.__notes__ = notes
return inst


# Note: Older versions of tblib will generate pickle archives that call unpickle_exception() with
# fewer arguments. We assign default values to some of the arguments to support this.
def unpickle_exception(func, args, cause, tb, context=None, suppress_context=False, notes=None):
Expand Down Expand Up @@ -49,8 +62,18 @@ def pickle_exception(obj):
assert isinstance(rv, tuple)
assert len(rv) >= 2

# Use __new__ whenever there is no customization by __reduce__ and
# __reduce_ex__. Note that OSError and descendants are known to require
# using a constructor, otherwise they do not set the errno, strerror and other
# attributes.
use_new = (
obj.__class__.__reduce__ is BaseException.__reduce__
and obj.__class__.__reduce_ex__ is BaseException.__reduce_ex__
and not isinstance(obj, OSError)
)

return (
unpickle_exception,
unpickle_exception_with_new if use_new else unpickle_exception,
rv[:2]
+ (
obj.__cause__,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_issue65.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pickle

from tblib import pickling_support


class HTTPrettyError(Exception):
pass


class UnmockedError(HTTPrettyError):
def __init__(self):
super().__init__('No mocking was registered, and real connections are not allowed (httpretty.allow_net_connect = False).')


def test_65():
pickling_support.install()

try:
raise UnmockedError
except Exception as e:
exc = e

exc = pickle.loads(pickle.dumps(exc))

assert isinstance(exc, UnmockedError)
assert exc.args == ('No mocking was registered, and real connections are not allowed (httpretty.allow_net_connect = False).',)
assert exc.__traceback__ is not None
92 changes: 92 additions & 0 deletions tests/test_pickle_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,95 @@ def func(my_arg='2'):

exc = pickle.loads(pickle.dumps(exc, protocol=protocol))
assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1}


class CustomWithAttributesException(Exception):
def __init__(self, message, arg1, arg2, arg3):
super().__init__(message)
self.values12 = (arg1, arg2)
self.value3 = arg3


def test_custom_with_attributes():
try:
raise CustomWithAttributesException('bar', 1, 2, 3)
except Exception as e:
exc = e

tblib.pickling_support.install(exc)
exc = pickle.loads(pickle.dumps(exc))

assert isinstance(exc, CustomWithAttributesException)
assert exc.args == ('bar',)
assert exc.values12 == (1, 2)
assert exc.value3 == 3
assert exc.__traceback__ is not None


class CustomReduceException(Exception):
def __init__(self, message, arg1, arg2, arg3):
super().__init__(message)
self.values12 = (arg1, arg2)
self.value3 = arg3

def __reduce__(self):
return self.__class__, self.args + self.values12 + (self.value3,)


def test_custom_reduce():
try:
raise CustomReduceException('foo', 1, 2, 3)
except Exception as e:
exc = e

tblib.pickling_support.install(exc)
exc = pickle.loads(pickle.dumps(exc))

assert isinstance(exc, CustomReduceException)
assert exc.args == ('foo',)
assert exc.values12 == (1, 2)
assert exc.value3 == 3
assert exc.__traceback__ is not None


class CustomReduceExException(Exception):
def __init__(self, message, arg1, arg2, protocol):
super().__init__(message)
self.values12 = (arg1, arg2)
self.value3 = protocol

def __reduce_ex__(self, protocol):
return self.__class__, self.args + self.values12 + (self.value3,)


@pytest.mark.parametrize('protocol', [None, *list(range(1, pickle.HIGHEST_PROTOCOL + 1))])
def test_custom_reduce_ex(protocol):
try:
raise CustomReduceExException('foo', 1, 2, 3)
except Exception as e:
exc = e

tblib.pickling_support.install(exc)
exc = pickle.loads(pickle.dumps(exc, protocol=protocol))

assert isinstance(exc, CustomReduceExException)
assert exc.args == ('foo',)
assert exc.values12 == (1, 2)
assert exc.value3 == 3
assert exc.__traceback__ is not None


def test_oserror():
try:
raise OSError(13, 'Permission denied')
except Exception as e:
exc = e

tblib.pickling_support.install(exc)
exc = pickle.loads(pickle.dumps(exc))

assert isinstance(exc, OSError)
assert exc.args == (13, 'Permission denied')
assert exc.errno == 13
assert exc.strerror == 'Permission denied'
assert exc.__traceback__ is not None
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ deps =
pytest
pytest-cov
commands =
{posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests}
{posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests README.rst}

[testenv:check]
deps =
Expand Down