summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Watson <cjwatson@debian.org>2023-06-28 22:44:07 +0100
committerColin Watson <cjwatson@debian.org>2023-06-28 22:44:35 +0100
commit774ff05868118a5799a9680d2092a28a50fc498e (patch)
tree6bb71e2b676218859ff9905cab7f69953c88d901
parent5a36ecbfad41504ff3c6a87284ab730fa4de97ac (diff)
parent0f96ae90f194236823aa97e709f6b4935fbd69b3 (diff)
Update upstream source from tag 'upstream/2.0.0'
Update to upstream version '2.0.0' with Debian dir 79d168e267a2c1a4fccee991712d1b9eb825f67e
-rw-r--r--.appveyor.yml78
-rw-r--r--.bumpversion.cfg15
-rw-r--r--.cookiecutterrc83
-rw-r--r--.editorconfig9
-rw-r--r--.github/workflows/github-actions.yml185
-rw-r--r--.gitignore63
-rw-r--r--.pre-commit-config.yaml22
-rw-r--r--.travis.yml56
-rw-r--r--CHANGELOG.rst8
-rw-r--r--CONTRIBUTING.rst11
-rw-r--r--LICENSE2
-rw-r--r--MANIFEST.in9
-rw-r--r--PKG-INFO1482
-rw-r--r--README.rst29
-rwxr-xr-xci/appveyor-download.py109
-rw-r--r--ci/appveyor-with-compiler.cmd23
-rwxr-xr-xci/bootstrap.py78
-rw-r--r--ci/requirements.txt4
-rw-r--r--ci/templates/.appveyor.yml49
-rw-r--r--ci/templates/.github/workflows/github-actions.yml65
-rw-r--r--ci/templates/.travis.yml46
-rw-r--r--debian/changelog5
-rw-r--r--docs/conf.py20
-rw-r--r--docs/index.rst2
-rw-r--r--pyproject.toml58
-rw-r--r--pytest.ini30
-rw-r--r--setup.cfg35
-rwxr-xr-x[-rw-r--r--]setup.py37
-rw-r--r--src/tblib.egg-info/PKG-INFO1482
-rw-r--r--src/tblib.egg-info/SOURCES.txt13
-rw-r--r--src/tblib/__init__.py119
-rw-r--r--src/tblib/cpython.py80
-rw-r--r--src/tblib/decorators.py2
-rw-r--r--src/tblib/pickling_support.py10
-rw-r--r--tests/badmodule.py2
-rw-r--r--tests/examples.py8
-rw-r--r--tests/test_issue30.py6
-rw-r--r--tests/test_pickle_exception.py30
-rw-r--r--tests/test_tblib.py161
-rw-r--r--tox.ini44
40 files changed, 2199 insertions, 2371 deletions
diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index 71d0450..0000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-version: '{branch}-{build}'
-build: off
-environment:
- matrix:
- - TOXENV: check
- TOXPYTHON: C:\Python36\python.exe
- PYTHON_HOME: C:\Python36
- PYTHON_VERSION: '3.6'
- PYTHON_ARCH: '32'
- - TOXENV: py27,codecov
- TOXPYTHON: C:\Python27\python.exe
- PYTHON_HOME: C:\Python27
- PYTHON_VERSION: '2.7'
- PYTHON_ARCH: '32'
- - TOXENV: py27,codecov
- TOXPYTHON: C:\Python27-x64\python.exe
- PYTHON_HOME: C:\Python27-x64
- PYTHON_VERSION: '2.7'
- PYTHON_ARCH: '64'
- WINDOWS_SDK_VERSION: v7.0
- - TOXENV: py35,codecov
- TOXPYTHON: C:\Python35\python.exe
- PYTHON_HOME: C:\Python35
- PYTHON_VERSION: '3.5'
- PYTHON_ARCH: '32'
- - TOXENV: py35,codecov
- TOXPYTHON: C:\Python35-x64\python.exe
- PYTHON_HOME: C:\Python35-x64
- PYTHON_VERSION: '3.5'
- PYTHON_ARCH: '64'
- - TOXENV: py36,codecov
- TOXPYTHON: C:\Python36\python.exe
- PYTHON_HOME: C:\Python36
- PYTHON_VERSION: '3.6'
- PYTHON_ARCH: '32'
- - TOXENV: py36,codecov
- TOXPYTHON: C:\Python36-x64\python.exe
- PYTHON_HOME: C:\Python36-x64
- PYTHON_VERSION: '3.6'
- PYTHON_ARCH: '64'
- - TOXENV: py37,codecov
- TOXPYTHON: C:\Python37\python.exe
- PYTHON_HOME: C:\Python37
- PYTHON_VERSION: '3.7'
- PYTHON_ARCH: '32'
- - TOXENV: py37,codecov
- TOXPYTHON: C:\Python37-x64\python.exe
- PYTHON_HOME: C:\Python37-x64
- PYTHON_VERSION: '3.7'
- PYTHON_ARCH: '64'
- - TOXENV: py38,codecov
- TOXPYTHON: C:\Python38\python.exe
- PYTHON_HOME: C:\Python38
- PYTHON_VERSION: '3.8'
- PYTHON_ARCH: '32'
- - TOXENV: py38,codecov
- TOXPYTHON: C:\Python38-x64\python.exe
- PYTHON_HOME: C:\Python38-x64
- PYTHON_VERSION: '3.8'
- PYTHON_ARCH: '64'
-init:
- - ps: echo $env:TOXENV
- - ps: ls C:\Python*
-install:
- - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt'
- - '%PYTHON_HOME%\Scripts\virtualenv --version'
- - '%PYTHON_HOME%\Scripts\easy_install --version'
- - '%PYTHON_HOME%\Scripts\pip --version'
- - '%PYTHON_HOME%\Scripts\tox --version'
-test_script:
- - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox
-on_failure:
- - ps: dir "env:"
- - ps: get-content .tox\*\log\*
-
-### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker):
-# on_finish:
-# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 1d8622c..09d7051 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 1.7.0
+current_version = 2.0.0
commit = True
tag = True
@@ -7,9 +7,13 @@ tag = True
search = version='{current_version}'
replace = version='{new_version}'
-[bumpversion:file:README.rst]
-search = v{current_version}.
-replace = v{new_version}.
+[bumpversion:file (badge):README.rst]
+search = /v{current_version}.svg
+replace = /v{new_version}.svg
+
+[bumpversion:file (link):README.rst]
+search = /v{current_version}...master
+replace = /v{new_version}...master
[bumpversion:file:docs/conf.py]
search = version = release = '{current_version}'
@@ -19,3 +23,6 @@ replace = version = release = '{new_version}'
search = __version__ = '{current_version}'
replace = __version__ = '{new_version}'
+[bumpversion:file:.cookiecutterrc]
+search = version: {current_version}
+replace = version: {new_version}
diff --git a/.cookiecutterrc b/.cookiecutterrc
index 1836a3d..5fea57e 100644
--- a/.cookiecutterrc
+++ b/.cookiecutterrc
@@ -1,52 +1,47 @@
# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher)
-cookiecutter:
- full_name: Ionel Cristian Mărieș
+default_context:
+ allow_tests_inside_package: 'no'
+ c_extension_function: '-'
+ c_extension_module: '-'
+ c_extension_optional: 'no'
+ c_extension_support: 'no'
+ codacy: 'no'
+ codacy_projectid: '-'
+ codeclimate: 'no'
+ codecov: 'yes'
+ command_line_interface: 'no'
+ command_line_interface_bin_name: '-'
+ coveralls: 'no'
+ distribution_name: tblib
email: contact@ionelmc.ro
- website: https://blog.ionelmc.ro/
+ formatter_quote_style: single
+ full_name: Ionel Cristian Mărieș
+ github_actions: 'yes'
+ github_actions_osx: 'yes'
+ github_actions_windows: 'yes'
+ license: BSD 2-Clause License
+ package_name: tblib
+ pre_commit: 'yes'
project_name: tblib
- repo_name: python-tblib
+ project_short_description: Traceback serialization library.
+ pypi_badge: 'yes'
+ pypi_disable_upload: 'no'
+ release_date: '2020-07-24'
repo_hosting: github.com
repo_hosting_domain: github.com
+ repo_main_branch: master
+ repo_name: python-tblib
repo_username: ionelmc
- package_name: tblib
- distribution_name: tblib
- project_short_description: Traceback serialization library.
- release_date: '2020-03-07'
- year_from: '2013'
- year_to: '2'
- version: 1.6.0
- license: BSD 2-Clause License
- c_extension_support: no
- c_extension_optional: no
- c_extension_module: '-'
- c_extension_function: '-'
- c_extension_test_pypi: no
- c_extension_test_pypi_username: '-'
- test_matrix_configurator: no
- test_matrix_separate_coverage: no
- test_runner: pytest
- setup_py_uses_test_runner: no
- setup_py_uses_setuptools_scm: no
- pypi_badge: yes
- pypi_disable_upload: no
- allow_tests_inside_package: no
- linter: flake8
- command_line_interface: no
- command_line_interface_bin_name: '-'
- coveralls: no
- coveralls_token: '-'
- codecov: yes
- landscape: no
- scrutinizer: no
- codacy: no
- codacy_projectid: '-'
- codeclimate: no
- sphinx_docs: yes
- sphinx_theme: sphinx-py3doc-enhanced-theme
- sphinx_doctest: no
+ scrutinizer: 'no'
+ setup_py_uses_setuptools_scm: 'no'
+ sphinx_docs: 'yes'
sphinx_docs_hosting: https://python-tblib.readthedocs.io/
- travis: yes
- travis_osx: no
- appveyor: yes
- requiresio: yes
+ sphinx_doctest: 'no'
+ sphinx_theme: sphinx-py3doc-enhanced-theme
+ test_matrix_separate_coverage: 'no'
+ version: 2.0.0
+ version_manager: bump2version
+ website: https://blog.ionelmc.ro/
+ year_from: '2013'
+ year_to: '2022'
diff --git a/.editorconfig b/.editorconfig
index 6eb7567..586c736 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,12 +2,19 @@
root = true
[*]
+# Use Unix-style newlines for most files (except Windows files, see below).
end_of_line = lf
trim_trailing_whitespace = true
-insert_final_newline = true
indent_style = space
+insert_final_newline = true
indent_size = 4
charset = utf-8
[*.{bat,cmd,ps1}]
end_of_line = crlf
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[*.tsv]
+indent_style = tab
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
new file mode 100644
index 0000000..51985bc
--- /dev/null
+++ b/.github/workflows/github-actions.yml
@@ -0,0 +1,185 @@
+name: build
+on: [push, pull_request]
+jobs:
+ test:
+ name: ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: 'check'
+ python: '3.11'
+ toxpython: 'python3.11'
+ tox_env: 'check'
+ os: 'ubuntu-latest'
+ - name: 'docs'
+ python: '3.11'
+ toxpython: 'python3.11'
+ tox_env: 'docs'
+ os: 'ubuntu-latest'
+ - name: 'py37 (ubuntu)'
+ python: '3.7'
+ toxpython: 'python3.7'
+ python_arch: 'x64'
+ tox_env: 'py37'
+ os: 'ubuntu-latest'
+ - name: 'py37 (windows)'
+ python: '3.7'
+ toxpython: 'python3.7'
+ python_arch: 'x64'
+ tox_env: 'py37'
+ os: 'windows-latest'
+ - name: 'py37 (macos)'
+ python: '3.7'
+ toxpython: 'python3.7'
+ python_arch: 'x64'
+ tox_env: 'py37'
+ os: 'macos-latest'
+ - name: 'py38 (ubuntu)'
+ python: '3.8'
+ toxpython: 'python3.8'
+ python_arch: 'x64'
+ tox_env: 'py38'
+ os: 'ubuntu-latest'
+ - name: 'py38 (windows)'
+ python: '3.8'
+ toxpython: 'python3.8'
+ python_arch: 'x64'
+ tox_env: 'py38'
+ os: 'windows-latest'
+ - name: 'py38 (macos)'
+ python: '3.8'
+ toxpython: 'python3.8'
+ python_arch: 'x64'
+ tox_env: 'py38'
+ os: 'macos-latest'
+ - name: 'py39 (ubuntu)'
+ python: '3.9'
+ toxpython: 'python3.9'
+ python_arch: 'x64'
+ tox_env: 'py39'
+ os: 'ubuntu-latest'
+ - name: 'py39 (windows)'
+ python: '3.9'
+ toxpython: 'python3.9'
+ python_arch: 'x64'
+ tox_env: 'py39'
+ os: 'windows-latest'
+ - name: 'py39 (macos)'
+ python: '3.9'
+ toxpython: 'python3.9'
+ python_arch: 'x64'
+ tox_env: 'py39'
+ os: 'macos-latest'
+ - name: 'py310 (ubuntu)'
+ python: '3.10'
+ toxpython: 'python3.10'
+ python_arch: 'x64'
+ tox_env: 'py310'
+ os: 'ubuntu-latest'
+ - name: 'py310 (windows)'
+ python: '3.10'
+ toxpython: 'python3.10'
+ python_arch: 'x64'
+ tox_env: 'py310'
+ os: 'windows-latest'
+ - name: 'py310 (macos)'
+ python: '3.10'
+ toxpython: 'python3.10'
+ python_arch: 'x64'
+ tox_env: 'py310'
+ os: 'macos-latest'
+ - name: 'py311 (ubuntu)'
+ python: '3.11'
+ toxpython: 'python3.11'
+ python_arch: 'x64'
+ tox_env: 'py311'
+ os: 'ubuntu-latest'
+ - name: 'py311 (windows)'
+ python: '3.11'
+ toxpython: 'python3.11'
+ python_arch: 'x64'
+ tox_env: 'py311'
+ os: 'windows-latest'
+ - name: 'py311 (macos)'
+ python: '3.11'
+ toxpython: 'python3.11'
+ python_arch: 'x64'
+ tox_env: 'py311'
+ os: 'macos-latest'
+ - name: 'pypy37 (ubuntu)'
+ python: 'pypy-3.7'
+ toxpython: 'pypy3.7'
+ python_arch: 'x64'
+ tox_env: 'pypy37'
+ os: 'ubuntu-latest'
+ - name: 'pypy37 (windows)'
+ python: 'pypy-3.7'
+ toxpython: 'pypy3.7'
+ python_arch: 'x64'
+ tox_env: 'pypy37'
+ os: 'windows-latest'
+ - name: 'pypy37 (macos)'
+ python: 'pypy-3.7'
+ toxpython: 'pypy3.7'
+ python_arch: 'x64'
+ tox_env: 'pypy37'
+ os: 'macos-latest'
+ - name: 'pypy38 (ubuntu)'
+ python: 'pypy-3.8'
+ toxpython: 'pypy3.8'
+ python_arch: 'x64'
+ tox_env: 'pypy38'
+ os: 'ubuntu-latest'
+ - name: 'pypy38 (windows)'
+ python: 'pypy-3.8'
+ toxpython: 'pypy3.8'
+ python_arch: 'x64'
+ tox_env: 'pypy38'
+ os: 'windows-latest'
+ - name: 'pypy38 (macos)'
+ python: 'pypy-3.8'
+ toxpython: 'pypy3.8'
+ python_arch: 'x64'
+ tox_env: 'pypy38'
+ os: 'macos-latest'
+ - name: 'pypy39 (ubuntu)'
+ python: 'pypy-3.9'
+ toxpython: 'pypy3.9'
+ python_arch: 'x64'
+ tox_env: 'pypy39'
+ os: 'ubuntu-latest'
+ - name: 'pypy39 (windows)'
+ python: 'pypy-3.9'
+ toxpython: 'pypy3.9'
+ python_arch: 'x64'
+ tox_env: 'pypy39'
+ os: 'windows-latest'
+ - name: 'pypy39 (macos)'
+ python: 'pypy-3.9'
+ toxpython: 'pypy3.9'
+ python_arch: 'x64'
+ tox_env: 'pypy39'
+ os: 'macos-latest'
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python }}
+ architecture: ${{ matrix.python_arch }}
+ - name: install dependencies
+ run: |
+ python -mpip install --progress-bar=off -r ci/requirements.txt
+ virtualenv --version
+ pip --version
+ tox --version
+ pip list --format=freeze
+ - name: test
+ env:
+ TOXPYTHON: '${{ matrix.toxpython }}'
+ run: >
+ tox -e ${{ matrix.tox_env }} -v
diff --git a/.gitignore b/.gitignore
index dfe5838..77973dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,51 +1,67 @@
*.py[cod]
__pycache__
+# Temp files
+.*.sw[po]
+*~
+*.bak
+.DS_Store
+
# C extensions
*.so
-# Packages
+# Build and package files
*.egg
*.egg-info
-dist
-build
-eggs
+.bootstrap
+.build
+.cache
.eggs
-parts
+.env
+.installed.cfg
+.ve
bin
-var
-sdist
-wheelhouse
+build
develop-eggs
-.installed.cfg
+dist
+eggs
lib
lib64
-venv*/
-pyvenv*/
+parts
pip-wheel-metadata/
+pyvenv*/
+sdist
+var
+venv*/
+wheelhouse
# Installer logs
pip-log.txt
# Unit test / coverage reports
+.benchmarks
.coverage
-.tox
.coverage.*
+.pytest
.pytest_cache/
-nosetests.xml
+.tox
coverage.xml
htmlcov
+nosetests.xml
# Translations
*.mo
-# Mr Developer
+# Buildout
.mr.developer.cfg
-.project
-.pydevproject
-.idea
+
+# IDE project files
*.iml
*.komodoproject
+.idea
+.project
+.pydevproject
+.vscode
# Complexity
output/*.html
@@ -54,18 +70,5 @@ output/*/index.html
# Sphinx
docs/_build
-.DS_Store
-*~
-.*.sw[po]
-.build
-.ve
-.env
-.cache
-.pytest
-.benchmarks
-.bootstrap
-.appveyor.token
-*.bak
-
# Mypy Cache
.mypy_cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..1831efd
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+# To install the git pre-commit hooks run:
+# pre-commit install --install-hooks
+# To update the versions:
+# pre-commit autoupdate
+exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|tests/bad.*.py)(/|$)'
+# Note the order is intentional to avoid multiple passes of the hooks
+repos:
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.274
+ hooks:
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix, --show-fixes]
+ - repo: https://github.com/psf/black
+ rev: 23.3.0
+ hooks:
+ - id: black
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: debug-statements
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 0e947cb..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-language: python
-dist: xenial
-cache: false
-env:
- global:
- - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so
- - SEGFAULT_SIGNALS=all
-matrix:
- include:
- - python: '3.6'
- env:
- - TOXENV=check
- - python: '3.6'
- env:
- - TOXENV=docs
- - env:
- - TOXENV=py27,codecov
- python: '2.7'
- - env:
- - TOXENV=py35,codecov
- python: '3.5'
- - env:
- - TOXENV=py36,codecov
- python: '3.6'
- - env:
- - TOXENV=py37,codecov
- python: '3.7'
- - env:
- - TOXENV=py38,codecov
- python: '3.8'
- - env:
- - TOXENV=pypy,codecov
- python: 'pypy'
- - env:
- - TOXENV=pypy3,codecov
- - TOXPYTHON=pypy3
- python: 'pypy3'
-before_install:
- - python --version
- - uname -a
- - lsb_release -a || true
-install:
- - python -mpip install --progress-bar=off tox -rci/requirements.txt
- - virtualenv --version
- - easy_install --version
- - pip --version
- - tox --version
-script:
- - tox -v
-after_failure:
- - more .tox/log/* | cat
- - more .tox/*/log/* | cat
-notifications:
- email:
- on_success: never
- on_failure: always
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c3c5aeb..9a34ad5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,12 @@
Changelog
=========
+2.0.0 (2023-06-22)
+~~~~~~~~~~~~~~~~~~
+
+* Removed support for legacy Pythons (2.7 and 3.6) and added Python 3.11 in the test grid.
+* Some cleanups and refactors (mostly from ruff).
+
1.7.0 (2020-07-24)
~~~~~~~~~~~~~~~~~~
@@ -36,7 +42,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://bitbucket.org/pypy/pypy/issues/2532/pypy3-attributeerror-frame-object-has-no>`_.
+ issue `#2532 <https://foss.heptapod.net/pypy/pypy/-/issues/2532>`_.
1.3.1 (2017-03-27)
~~~~~~~~~~~~~~~~~~
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 1d6bfc5..a89ed3a 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -41,7 +41,7 @@ To set up `python-tblib` for local development:
(look for the "Fork" button).
2. Clone your fork locally::
- git clone git@github.com:ionelmc/python-tblib.git
+ git clone git@github.com:YOURGITHUBNAME/python-tblib.git
3. Create a branch for local development::
@@ -68,15 +68,12 @@ If you need some code review or feedback while you're developing the code just m
For merging, you should:
-1. Include passing tests (run ``tox``) [1]_.
+1. Include passing tests (run ``tox``).
2. Update documentation when there's new API, functionality etc.
3. Add a note to ``CHANGELOG.rst`` about the changes.
4. Add yourself to ``AUTHORS.rst``.
-.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will
- `run the tests <https://travis-ci.org/ionelmc/python-tblib/pull_requests>`_ for each change you add in the pull request.
- It will be slower though ...
Tips
----
@@ -85,6 +82,6 @@ To run a subset of tests::
tox -e envname -- pytest -k test_myfeature
-To run all the test environments in *parallel* (you need to ``pip install detox``)::
+To run all the test environments in *parallel*::
- detox
+ tox -p auto
diff --git a/LICENSE b/LICENSE
index 7dac254..151345b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
BSD 2-Clause License
-Copyright (c) 2013-2020, Ionel Cristian Mărieș. All rights reserved.
+Copyright (c) 2013-2022, Ionel Cristian Mărieș. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
diff --git a/MANIFEST.in b/MANIFEST.in
index 8b9e93d..d0dac9c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,9 +4,14 @@ graft ci
graft tests
include .bumpversion.cfg
-include .coveragerc
include .cookiecutterrc
+include .coveragerc
include .editorconfig
+include .github/workflows/github-actions.yml
+include .pre-commit-config.yaml
+include .readthedocs.yml
+include pytest.ini
+include tox.ini
include AUTHORS.rst
include CHANGELOG.rst
@@ -14,6 +19,4 @@ include CONTRIBUTING.rst
include LICENSE
include README.rst
-include tox.ini .travis.yml .appveyor.yml .readthedocs.yml
-
global-exclude *.py[cod] __pycache__/* *.so *.dylib
diff --git a/PKG-INFO b/PKG-INFO
index 122805d..b15d380 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: tblib
-Version: 1.7.0
+Version: 2.0.0
Summary: Traceback serialization library.
Home-page: https://github.com/ionelmc/python-tblib
Author: Ionel Cristian Mărieș
@@ -9,738 +9,7 @@ License: BSD-2-Clause
Project-URL: Documentation, https://python-tblib.readthedocs.io/
Project-URL: Changelog, https://python-tblib.readthedocs.io/en/latest/changelog.html
Project-URL: Issue Tracker, https://github.com/ionelmc/python-tblib/issues
-Description: ========
- Overview
- ========
-
-
-
- Serialization library for Exceptions and Tracebacks.
-
- * Free software: BSD license
-
- It allows you to:
-
- * `Pickle <https://docs.python.org/3/library/pickle.html>`_ tracebacks and raise exceptions
- with pickled tracebacks in different processes. This allows better error handling when running
- code over multiple processes (imagine multiprocessing, billiard, futures, celery etc).
- * Create traceback objects from strings (the ``from_string`` method). *No pickling is used*.
- * Serialize tracebacks to/from plain dicts (the ``from_dict`` and ``to_dict`` methods). *No pickling is used*.
- * Raise the tracebacks created from the aforementioned sources.
- * Pickle an Exception together with its traceback and exception chain
- (``raise ... from ...``) *(Python 3 only)*
-
- **Again, note that using the pickle support is completely optional. You are solely responsible for
- security problems should you decide to use the pickle support.**
-
- Installation
- ============
-
- ::
-
- pip install tblib
-
- Documentation
- =============
-
- .. contents::
- :local:
-
- Pickling tracebacks
- ~~~~~~~~~~~~~~~~~~~
-
- **Note**: The traceback objects that come out are stripped of some attributes (like variables). But you'll be able to raise exceptions with
- those tracebacks or print them - that should cover 99% of the usecases.
-
- ::
-
- >>> from tblib import pickling_support
- >>> pickling_support.install()
- >>> import pickle, sys
- >>> def inner_0():
- ... raise Exception('fail')
- ...
- >>> def inner_1():
- ... inner_0()
- ...
- >>> def inner_2():
- ... inner_1()
- ...
- >>> try:
- ... inner_2()
- ... except:
- ... s1 = pickle.dumps(sys.exc_info())
- ...
- >>> len(s1) > 1
- True
- >>> try:
- ... inner_2()
- ... except:
- ... s2 = pickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
- ...
- >>> len(s2) > 1
- True
-
- >>> try:
- ... import cPickle
- ... except ImportError:
- ... import pickle as cPickle
- >>> try:
- ... inner_2()
- ... except:
- ... s3 = cPickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
- ...
- >>> len(s3) > 1
- True
-
- Unpickling tracebacks
- ~~~~~~~~~~~~~~~~~~~~~
-
- ::
-
- >>> pickle.loads(s1)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- >>> pickle.loads(s2)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- >>> pickle.loads(s3)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- Raising
- ~~~~~~~
-
- ::
-
- >>> from six import reraise
- >>> reraise(*pickle.loads(s1))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
- >>> reraise(*pickle.loads(s2))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
- >>> reraise(*pickle.loads(s3))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- Pickling Exceptions together with their traceback and chain (Python 3 only)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- ::
-
- >>> try: # doctest: +SKIP
- ... try:
- ... 1 / 0
- ... except Exception as e:
- ... raise Exception("foo") from e
- ... except Exception as e:
- ... s = pickle.dumps(e)
- >>> raise pickle.loads(s) # doctest: +SKIP
- Traceback (most recent call last):
- File "<doctest README.rst[16]>", line 3, in <module>
- 1 / 0
- ZeroDivisionError: division by zero
-
- The above exception was the direct cause of the following exception:
-
- Traceback (most recent call last):
- File "<doctest README.rst[17]>", line 1, in <module>
- raise pickle.loads(s)
- File "<doctest README.rst[16]>", line 5, in <module>
- raise Exception("foo") from e
- Exception: foo
-
- BaseException subclasses defined after calling ``pickling_support.install()`` will
- **not** retain their traceback and exception chain pickling.
- To cover custom Exceptions, there are three options:
-
- 1. Use ``@pickling_support.install`` as a decorator for each custom Exception
-
- .. code-block:: python
-
- >>> from tblib import pickling_support
- >>> # Declare all imports of your package's dependencies
- >>> import numpy # doctest: +SKIP
-
- >>> pickling_support.install() # install for all modules imported so far
-
- >>> @pickling_support.install
- ... class CustomError(Exception):
- ... pass
-
- Eventual subclasses of ``CustomError`` will need to be decorated again.
-
- 2. Invoke ``pickling_support.install()`` after all modules have been imported and all
- Exception subclasses have been declared
-
- .. code-block:: python
-
- >>> # Declare all imports of your package's dependencies
- >>> import numpy # doctest: +SKIP
- >>> from tblib import pickling_support
-
- >>> # Declare your own custom Exceptions
- >>> class CustomError(Exception):
- ... pass
-
- >>> # Finally, install tblib
- >>> pickling_support.install()
-
- 3. Selectively install tblib for Exception instances just before they are pickled
-
- .. code-block:: python
-
- pickling_support.install(<Exception instance>, [Exception instance], ...)
-
- The above will install tblib pickling for all listed exceptions as well as any other
- exceptions in their exception chains.
-
- For example, one could write a wrapper to be used with
- `ProcessPoolExecutor <https://docs.python.org/3/library/concurrent.futures.html>`_,
- `Dask.distributed <https://distributed.dask.org/>`_, or similar libraries:
-
- ::
-
- >>> from tblib import pickling_support
- >>> def wrapper(func, *args, **kwargs):
- ... try:
- ... return func(*args, **kwargs)
- ... except Exception as e:
- ... pickling_support.install(e)
- ... raise
-
- What if we have a local stack, does it show correctly ?
- -------------------------------------------------------
-
- Yes it does::
-
- >>> exc_info = pickle.loads(s3)
- >>> def local_0():
- ... reraise(*exc_info)
- ...
- >>> def local_1():
- ... local_0()
- ...
- >>> def local_2():
- ... local_1()
- ...
- >>> local_2()
- Traceback (most recent call last):
- File "...doctest.py", line ..., in __run
- compileflags, 1) in test.globs
- File "<doctest README.rst[24]>", line 1, in <module>
- local_2()
- File "<doctest README.rst[23]>", line 2, in local_2
- local_1()
- File "<doctest README.rst[22]>", line 2, in local_1
- local_0()
- File "<doctest README.rst[21]>", line 2, in local_0
- reraise(*exc_info)
- File "<doctest README.rst[11]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- It also supports more contrived scenarios
- -----------------------------------------
-
- Like tracebacks with syntax errors::
-
- >>> from tblib import Traceback
- >>> from examples import bad_syntax
- >>> try:
- ... bad_syntax()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[58]>", line 1, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[57]>", line 2, in <module>
- bad_syntax()
- File "...tests...examples.py", line 18, in bad_syntax
- import badsyntax
- File "...tests...badsyntax.py", line 5
- is very bad
- ^
- SyntaxError: invalid syntax
-
- Or other import failures::
-
- >>> from examples import bad_module
- >>> try:
- ... bad_module()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[61]>", line 1, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[60]>", line 2, in <module>
- bad_module()
- File "...tests...examples.py", line 23, in bad_module
- import badmodule
- File "...tests...badmodule.py", line 3, in <module>
- raise Exception("boom!")
- Exception: boom!
-
- Or a traceback that's caused by exceeding the recursion limit (here we're
- forcing the type and value to have consistency across platforms)::
-
- >>> def f(): f()
- >>> try:
- ... f()
- ... except RuntimeError:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(RuntimeError, RuntimeError("maximum recursion depth exceeded"), tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- ...
- RuntimeError: maximum recursion depth exceeded
-
- Reference
- ~~~~~~~~~
-
- tblib.Traceback
- ---------------
-
- It is used by the ``pickling_support``. You can use it too if you want more flexibility::
-
- >>> from tblib import Traceback
- >>> try:
- ... inner_2()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[21]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[21]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- tblib.Traceback.to_dict
- ```````````````````````
-
- You can use the ``to_dict`` method and the ``from_dict`` classmethod to
- convert a Traceback into and from a dictionary serializable by the stdlib
- json.JSONDecoder::
-
- >>> import json
- >>> from pprint import pprint
- >>> try:
- ... inner_2()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ... tb_dict = tb.to_dict()
- ... pprint(tb_dict)
- {'tb_frame': {'f_code': {'co_filename': '<doctest README.rst[...]>',
- 'co_name': '<module>'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 5},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_2'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_1'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_0'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': None}}}}
-
- tblib.Traceback.from_dict
- `````````````````````````
-
- Building on the previous example::
-
- >>> tb_json = json.dumps(tb_dict)
- >>> tb = Traceback.from_dict(json.loads(tb_json))
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[21]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[21]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- tblib.Traceback.from_string
- ```````````````````````````
-
- ::
-
- >>> tb = Traceback.from_string("""
- ... File "skipped.py", line 123, in func_123
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """)
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[42]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: fail
-
-
- If you use the ``strict=False`` option then parsing is a bit more lax::
-
- >>> tb = Traceback.from_string("""
- ... File "bogus.py", line 123, in bogus
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """, strict=False)
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[42]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "bogus.py", line 123, in bogus
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: fail
-
- tblib.decorators.return_error
- -----------------------------
-
- ::
-
- >>> from tblib.decorators import return_error
- >>> inner_2r = return_error(inner_2)
- >>> e = inner_2r()
- >>> e
- <tblib.decorators.Error object at ...>
- >>> e.reraise()
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[26]>", line 1, in <module>
- e.reraise()
- File "...tblib...decorators.py", line 19, in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line 25, in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- How's this useful? Imagine you're using multiprocessing like this::
-
- # Note that Python 3.4 and later will show the remote traceback (but as a string sadly) so we skip testing this.
- >>> import traceback
- >>> from multiprocessing import Pool
- >>> from examples import func_a
- >>> pool = Pool() # doctest: +SKIP
- >>> try: # doctest: +SKIP
- ... for i in pool.map(func_a, range(5)):
- ... print(i)
- ... except:
- ... print(traceback.format_exc())
- ...
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 2, in <module>
- for i in pool.map(func_a, range(5)):
- File "...multiprocessing...pool.py", line ..., in map
- ...
- File "...multiprocessing...pool.py", line ..., in get
- ...
- Exception: Guessing time !
- <BLANKLINE>
- >>> pool.terminate() # doctest: +SKIP
-
- Not very useful is it? Let's sort this out::
-
- >>> from tblib.decorators import apply_with_return_error, Error
- >>> from itertools import repeat
- >>> pool = Pool()
- >>> try:
- ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
- ... if isinstance(i, Error):
- ... i.reraise()
- ... else:
- ... print(i)
- ... except:
- ... print(traceback.format_exc())
- ...
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 4, in <module>
- i.reraise()
- File "...tblib...decorators.py", line ..., in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "...tblib...decorators.py", line ..., in apply_with_return_error
- return args[0](*args[1:])
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: Guessing time !
- <BLANKLINE>
- >>> pool.terminate()
-
- Much better !
-
- What if we have a local call stack ?
- ````````````````````````````````````
-
- ::
-
- >>> def local_0():
- ... pool = Pool()
- ... try:
- ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
- ... if isinstance(i, Error):
- ... i.reraise()
- ... else:
- ... print(i)
- ... finally:
- ... pool.close()
- ...
- >>> def local_1():
- ... local_0()
- ...
- >>> def local_2():
- ... local_1()
- ...
- >>> try:
- ... local_2()
- ... except:
- ... print(traceback.format_exc())
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 2, in <module>
- local_2()
- File "<doctest README.rst[...]>", line 2, in local_2
- local_1()
- File "<doctest README.rst[...]>", line 2, in local_1
- local_0()
- File "<doctest README.rst[...]>", line 6, in local_0
- i.reraise()
- File "...tblib...decorators.py", line 20, in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line 27, in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "...tblib...decorators.py", line 47, in apply_with_return_error
- return args[0](*args[1:])
- File "...tests...examples.py", line 2, in func_a
- func_b()
- File "...tests...examples.py", line 6, in func_b
- func_c()
- File "...tests...examples.py", line 10, in func_c
- func_d()
- File "...tests...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: Guessing time !
- <BLANKLINE>
-
- Other weird stuff
- `````````````````
-
- Clearing traceback works (Python 3.4 and up)::
-
- >>> tb = Traceback.from_string("""
- ... File "skipped.py", line 123, in func_123
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """)
- >>> import traceback, sys
- >>> if sys.version_info > (3, 4):
- ... traceback.clear_frames(tb)
-
- Credits
- =======
-
- * `mitsuhiko/jinja2 <https://github.com/mitsuhiko/jinja2>`_ for figuring a way to create traceback objects.
-
-
- Changelog
- =========
-
- 1.7.0 (2020-07-24)
- ~~~~~~~~~~~~~~~~~~
-
- * Add more attributes to ``Frame`` and ``Code`` objects for pytest compatibility. Contributed by Ivanq in
- `#58 <https://github.com/ionelmc/python-tblib/pull/58>`_.
-
- 1.6.0 (2019-12-07)
- ~~~~~~~~~~~~~~~~~~
-
- * When pickling an Exception, also pickle its traceback and the Exception chain
- (``raise ... from ...``). Contributed by Guido Imperiale in
- `#53 <https://github.com/ionelmc/python-tblib/issues/53>`_.
-
- 1.5.0 (2019-10-23)
- ~~~~~~~~~~~~~~~~~~
-
- * Added support for Python 3.8. Contributed by Victor Stinner in
- `#42 <https://github.com/ionelmc/python-tblib/issues/42>`_.
- * Removed support for end of life Python 3.4.
- * Few CI improvements and fixes.
-
- 1.4.0 (2019-05-02)
- ~~~~~~~~~~~~~~~~~~
-
- * Removed support for end of life Python 3.3.
- * Fixed tests for Python 3.7. Contributed by Elliott Sales de Andrade in
- `#36 <https://github.com/ionelmc/python-tblib/issues/36>`_.
- * Fixed compatibility issue with Twised (``twisted.python.failure.Failure`` expected a ``co_code`` attribute).
-
- 1.3.2 (2017-04-09)
- ~~~~~~~~~~~~~~~~~~
-
- * 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://bitbucket.org/pypy/pypy/issues/2532/pypy3-attributeerror-frame-object-has-no>`_.
-
- 1.3.1 (2017-03-27)
- ~~~~~~~~~~~~~~~~~~
-
- * Fixed handling for tracebacks due to exceeding the recursion limit.
- Fixes `#15 <https://github.com/ionelmc/python-tblib/issues/15>`_.
-
- 1.3.0 (2016-03-08)
- ~~~~~~~~~~~~~~~~~~
-
- * Added ``Traceback.from_string``.
-
- 1.2.0 (2015-12-18)
- ~~~~~~~~~~~~~~~~~~
-
- * Fixed handling for tracebacks from generators and other internal improvements
- and optimizations. Contributed by DRayX in `#10 <https://github.com/ionelmc/python-tblib/issues/10>`_
- and `#11 <https://github.com/ionelmc/python-tblib/pull/11>`_.
-
- 1.1.0 (2015-07-27)
- ~~~~~~~~~~~~~~~~~~
-
- * Added support for Python 2.6. Contributed by Arcadiy Ivanov in
- `#8 <https://github.com/ionelmc/python-tblib/pull/8>`_.
-
- 1.0.0 (2015-03-30)
- ~~~~~~~~~~~~~~~~~~
-
- * Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks.
- Contributed by beckjake in `#5 <https://github.com/ionelmc/python-tblib/pull/5>`_.
-
Keywords: traceback,debugging,exceptions
-Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
@@ -748,13 +17,752 @@ Classifier: Operating System :: Unix
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Utilities
-Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Requires-Python: >=3.7
+License-File: LICENSE
+License-File: AUTHORS.rst
+
+========
+Overview
+========
+
+
+
+Serialization library for Exceptions and Tracebacks.
+
+* Free software: BSD license
+
+It allows you to:
+
+* `Pickle <https://docs.python.org/3/library/pickle.html>`_ tracebacks and raise exceptions
+ with pickled tracebacks in different processes. This allows better error handling when running
+ code over multiple processes (imagine multiprocessing, billiard, futures, celery etc).
+* Create traceback objects from strings (the ``from_string`` method). *No pickling is used*.
+* Serialize tracebacks to/from plain dicts (the ``from_dict`` and ``to_dict`` methods). *No pickling is used*.
+* Raise the tracebacks created from the aforementioned sources.
+* Pickle an Exception together with its traceback and exception chain
+ (``raise ... from ...``) *(Python 3 only)*
+
+**Again, note that using the pickle support is completely optional. You are solely responsible for
+security problems should you decide to use the pickle support.**
+
+Installation
+============
+
+::
+
+ pip install tblib
+
+Documentation
+=============
+
+.. contents::
+ :local:
+
+Pickling tracebacks
+~~~~~~~~~~~~~~~~~~~
+
+**Note**: The traceback objects that come out are stripped of some attributes (like variables). But you'll be able to raise exceptions with
+those tracebacks or print them - that should cover 99% of the usecases.
+
+::
+
+ >>> from tblib import pickling_support
+ >>> pickling_support.install()
+ >>> import pickle, sys
+ >>> def inner_0():
+ ... raise Exception('fail')
+ ...
+ >>> def inner_1():
+ ... inner_0()
+ ...
+ >>> def inner_2():
+ ... inner_1()
+ ...
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s1 = pickle.dumps(sys.exc_info())
+ ...
+ >>> len(s1) > 1
+ True
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s2 = pickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
+ ...
+ >>> len(s2) > 1
+ True
+
+ >>> try:
+ ... import cPickle
+ ... except ImportError:
+ ... import pickle as cPickle
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s3 = cPickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
+ ...
+ >>> len(s3) > 1
+ True
+
+Unpickling tracebacks
+~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ >>> pickle.loads(s1)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+ >>> pickle.loads(s2)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+ >>> pickle.loads(s3)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+Raising
+~~~~~~~
+
+::
+
+ >>> from six import reraise
+ >>> reraise(*pickle.loads(s1))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+ >>> reraise(*pickle.loads(s2))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+ >>> reraise(*pickle.loads(s3))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+Pickling Exceptions together with their traceback and chain (Python 3 only)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ >>> try: # doctest: +SKIP
+ ... try:
+ ... 1 / 0
+ ... except Exception as e:
+ ... raise Exception("foo") from e
+ ... except Exception as e:
+ ... s = pickle.dumps(e)
+ >>> raise pickle.loads(s) # doctest: +SKIP
+ Traceback (most recent call last):
+ File "<doctest README.rst[16]>", line 3, in <module>
+ 1 / 0
+ ZeroDivisionError: division by zero
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ File "<doctest README.rst[17]>", line 1, in <module>
+ raise pickle.loads(s)
+ File "<doctest README.rst[16]>", line 5, in <module>
+ raise Exception("foo") from e
+ Exception: foo
+
+BaseException subclasses defined after calling ``pickling_support.install()`` will
+**not** retain their traceback and exception chain pickling.
+To cover custom Exceptions, there are three options:
+
+1. Use ``@pickling_support.install`` as a decorator for each custom Exception
+
+ .. code-block:: python
+
+ >>> from tblib import pickling_support
+ >>> # Declare all imports of your package's dependencies
+ >>> import numpy # doctest: +SKIP
+
+ >>> pickling_support.install() # install for all modules imported so far
+
+ >>> @pickling_support.install
+ ... class CustomError(Exception):
+ ... pass
+
+ Eventual subclasses of ``CustomError`` will need to be decorated again.
+
+2. Invoke ``pickling_support.install()`` after all modules have been imported and all
+ Exception subclasses have been declared
+
+ .. code-block:: python
+
+ >>> # Declare all imports of your package's dependencies
+ >>> import numpy # doctest: +SKIP
+ >>> from tblib import pickling_support
+
+ >>> # Declare your own custom Exceptions
+ >>> class CustomError(Exception):
+ ... pass
+
+ >>> # Finally, install tblib
+ >>> pickling_support.install()
+
+3. Selectively install tblib for Exception instances just before they are pickled
+
+ .. code-block:: python
+
+ pickling_support.install(<Exception instance>, [Exception instance], ...)
+
+ The above will install tblib pickling for all listed exceptions as well as any other
+ exceptions in their exception chains.
+
+ For example, one could write a wrapper to be used with
+ `ProcessPoolExecutor <https://docs.python.org/3/library/concurrent.futures.html>`_,
+ `Dask.distributed <https://distributed.dask.org/>`_, or similar libraries:
+
+::
+
+ >>> from tblib import pickling_support
+ >>> def wrapper(func, *args, **kwargs):
+ ... try:
+ ... return func(*args, **kwargs)
+ ... except Exception as e:
+ ... pickling_support.install(e)
+ ... raise
+
+What if we have a local stack, does it show correctly ?
+-------------------------------------------------------
+
+Yes it does::
+
+ >>> exc_info = pickle.loads(s3)
+ >>> def local_0():
+ ... reraise(*exc_info)
+ ...
+ >>> def local_1():
+ ... local_0()
+ ...
+ >>> def local_2():
+ ... local_1()
+ ...
+ >>> local_2()
+ Traceback (most recent call last):
+ File "...doctest.py", line ..., in __run
+ compileflags, 1) in test.globs
+ File "<doctest README.rst[24]>", line 1, in <module>
+ local_2()
+ File "<doctest README.rst[23]>", line 2, in local_2
+ local_1()
+ File "<doctest README.rst[22]>", line 2, in local_1
+ local_0()
+ File "<doctest README.rst[21]>", line 2, in local_0
+ reraise(*exc_info)
+ File "<doctest README.rst[11]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+It also supports more contrived scenarios
+-----------------------------------------
+
+Like tracebacks with syntax errors::
+
+ >>> from tblib import Traceback
+ >>> from examples import bad_syntax
+ >>> try:
+ ... bad_syntax()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[58]>", line 1, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[57]>", line 2, in <module>
+ bad_syntax()
+ File "...tests...examples.py", line 18, in bad_syntax
+ import badsyntax
+ File "...tests...badsyntax.py", line 5
+ is very bad
+ ^
+ SyntaxError: invalid syntax
+
+Or other import failures::
+
+ >>> from examples import bad_module
+ >>> try:
+ ... bad_module()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[61]>", line 1, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[60]>", line 2, in <module>
+ bad_module()
+ File "...tests...examples.py", line 23, in bad_module
+ import badmodule
+ File "...tests...badmodule.py", line 3, in <module>
+ raise Exception("boom!")
+ Exception: boom!
+
+Or a traceback that's caused by exceeding the recursion limit (here we're
+forcing the type and value to have consistency across platforms)::
+
+ >>> def f(): f()
+ >>> try:
+ ... f()
+ ... except RuntimeError:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(RuntimeError, RuntimeError("maximum recursion depth exceeded"), tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ ...
+ RuntimeError: maximum recursion depth exceeded
+
+Reference
+~~~~~~~~~
+
+tblib.Traceback
+---------------
+
+It is used by the ``pickling_support``. You can use it too if you want more flexibility::
+
+ >>> from tblib import Traceback
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[21]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[21]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+tblib.Traceback.to_dict
+```````````````````````
+
+You can use the ``to_dict`` method and the ``from_dict`` classmethod to
+convert a Traceback into and from a dictionary serializable by the stdlib
+json.JSONDecoder::
+
+ >>> import json
+ >>> from pprint import pprint
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ... tb_dict = tb.to_dict()
+ ... pprint(tb_dict)
+ {'tb_frame': {'f_code': {'co_filename': '<doctest README.rst[...]>',
+ 'co_name': '<module>'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 5},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_2'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_1'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_0'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': None}}}}
+
+tblib.Traceback.from_dict
+`````````````````````````
+
+Building on the previous example::
+
+ >>> tb_json = json.dumps(tb_dict)
+ >>> tb = Traceback.from_dict(json.loads(tb_json))
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[21]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[21]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+tblib.Traceback.from_string
+```````````````````````````
+
+::
+
+ >>> tb = Traceback.from_string("""
+ ... File "skipped.py", line 123, in func_123
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """)
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[42]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: fail
+
+
+If you use the ``strict=False`` option then parsing is a bit more lax::
+
+ >>> tb = Traceback.from_string("""
+ ... File "bogus.py", line 123, in bogus
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """, strict=False)
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[42]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "bogus.py", line 123, in bogus
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: fail
+
+tblib.decorators.return_error
+-----------------------------
+
+::
+
+ >>> from tblib.decorators import return_error
+ >>> inner_2r = return_error(inner_2)
+ >>> e = inner_2r()
+ >>> e
+ <tblib.decorators.Error object at ...>
+ >>> e.reraise()
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[26]>", line 1, in <module>
+ e.reraise()
+ File "...tblib...decorators.py", line 19, in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line 25, in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+How's this useful? Imagine you're using multiprocessing like this::
+
+ # Note that Python 3.4 and later will show the remote traceback (but as a string sadly) so we skip testing this.
+ >>> import traceback
+ >>> from multiprocessing import Pool
+ >>> from examples import func_a
+ >>> pool = Pool() # doctest: +SKIP
+ >>> try: # doctest: +SKIP
+ ... for i in pool.map(func_a, range(5)):
+ ... print(i)
+ ... except:
+ ... print(traceback.format_exc())
+ ...
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 2, in <module>
+ for i in pool.map(func_a, range(5)):
+ File "...multiprocessing...pool.py", line ..., in map
+ ...
+ File "...multiprocessing...pool.py", line ..., in get
+ ...
+ Exception: Guessing time !
+ <BLANKLINE>
+ >>> pool.terminate() # doctest: +SKIP
+
+Not very useful is it? Let's sort this out::
+
+ >>> from tblib.decorators import apply_with_return_error, Error
+ >>> from itertools import repeat
+ >>> pool = Pool()
+ >>> try:
+ ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
+ ... if isinstance(i, Error):
+ ... i.reraise()
+ ... else:
+ ... print(i)
+ ... except:
+ ... print(traceback.format_exc())
+ ...
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 4, in <module>
+ i.reraise()
+ File "...tblib...decorators.py", line ..., in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "...tblib...decorators.py", line ..., in apply_with_return_error
+ return args[0](*args[1:])
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: Guessing time !
+ <BLANKLINE>
+ >>> pool.terminate()
+
+Much better !
+
+What if we have a local call stack ?
+````````````````````````````````````
+
+::
+
+ >>> def local_0():
+ ... pool = Pool()
+ ... try:
+ ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
+ ... if isinstance(i, Error):
+ ... i.reraise()
+ ... else:
+ ... print(i)
+ ... finally:
+ ... pool.close()
+ ...
+ >>> def local_1():
+ ... local_0()
+ ...
+ >>> def local_2():
+ ... local_1()
+ ...
+ >>> try:
+ ... local_2()
+ ... except:
+ ... print(traceback.format_exc())
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 2, in <module>
+ local_2()
+ File "<doctest README.rst[...]>", line 2, in local_2
+ local_1()
+ File "<doctest README.rst[...]>", line 2, in local_1
+ local_0()
+ File "<doctest README.rst[...]>", line 6, in local_0
+ i.reraise()
+ File "...tblib...decorators.py", line 20, in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line 27, in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "...tblib...decorators.py", line 47, in apply_with_return_error
+ return args[0](*args[1:])
+ File "...tests...examples.py", line 2, in func_a
+ func_b()
+ File "...tests...examples.py", line 6, in func_b
+ func_c()
+ File "...tests...examples.py", line 10, in func_c
+ func_d()
+ File "...tests...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: Guessing time !
+ <BLANKLINE>
+
+Other weird stuff
+`````````````````
+
+Clearing traceback works (Python 3.4 and up)::
+
+ >>> tb = Traceback.from_string("""
+ ... File "skipped.py", line 123, in func_123
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """)
+ >>> import traceback, sys
+ >>> if sys.version_info > (3, 4):
+ ... traceback.clear_frames(tb)
+
+Credits
+=======
+
+* `mitsuhiko/jinja2 <https://github.com/mitsuhiko/jinja2>`_ for figuring a way to create traceback objects.
+
+
+Changelog
+=========
+
+2.0.0 (2023-06-22)
+~~~~~~~~~~~~~~~~~~
+
+* Removed support for legacy Pythons (2.7 and 3.6) and added Python 3.11 in the test grid.
+* Some cleanups and refactors (mostly from ruff).
+
+1.7.0 (2020-07-24)
+~~~~~~~~~~~~~~~~~~
+
+* Add more attributes to ``Frame`` and ``Code`` objects for pytest compatibility. Contributed by Ivanq in
+ `#58 <https://github.com/ionelmc/python-tblib/pull/58>`_.
+
+1.6.0 (2019-12-07)
+~~~~~~~~~~~~~~~~~~
+
+* When pickling an Exception, also pickle its traceback and the Exception chain
+ (``raise ... from ...``). Contributed by Guido Imperiale in
+ `#53 <https://github.com/ionelmc/python-tblib/issues/53>`_.
+
+1.5.0 (2019-10-23)
+~~~~~~~~~~~~~~~~~~
+
+* Added support for Python 3.8. Contributed by Victor Stinner in
+ `#42 <https://github.com/ionelmc/python-tblib/issues/42>`_.
+* Removed support for end of life Python 3.4.
+* Few CI improvements and fixes.
+
+1.4.0 (2019-05-02)
+~~~~~~~~~~~~~~~~~~
+
+* Removed support for end of life Python 3.3.
+* Fixed tests for Python 3.7. Contributed by Elliott Sales de Andrade in
+ `#36 <https://github.com/ionelmc/python-tblib/issues/36>`_.
+* Fixed compatibility issue with Twised (``twisted.python.failure.Failure`` expected a ``co_code`` attribute).
+
+1.3.2 (2017-04-09)
+~~~~~~~~~~~~~~~~~~
+
+* 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>`_.
+
+1.3.1 (2017-03-27)
+~~~~~~~~~~~~~~~~~~
+
+* Fixed handling for tracebacks due to exceeding the recursion limit.
+ Fixes `#15 <https://github.com/ionelmc/python-tblib/issues/15>`_.
+
+1.3.0 (2016-03-08)
+~~~~~~~~~~~~~~~~~~
+
+* Added ``Traceback.from_string``.
+
+1.2.0 (2015-12-18)
+~~~~~~~~~~~~~~~~~~
+
+* Fixed handling for tracebacks from generators and other internal improvements
+ and optimizations. Contributed by DRayX in `#10 <https://github.com/ionelmc/python-tblib/issues/10>`_
+ and `#11 <https://github.com/ionelmc/python-tblib/pull/11>`_.
+
+1.1.0 (2015-07-27)
+~~~~~~~~~~~~~~~~~~
+
+* Added support for Python 2.6. Contributed by Arcadiy Ivanov in
+ `#8 <https://github.com/ionelmc/python-tblib/pull/8>`_.
+
+1.0.0 (2015-03-30)
+~~~~~~~~~~~~~~~~~~
+
+* Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks.
+ Contributed by beckjake in `#5 <https://github.com/ionelmc/python-tblib/pull/5>`_.
diff --git a/README.rst b/README.rst
index 3d003da..1a296cb 100644
--- a/README.rst
+++ b/README.rst
@@ -10,31 +10,22 @@ Overview
* - docs
- |docs|
* - tests
- - | |travis| |appveyor| |requires|
+ - | |github-actions| |requires|
| |codecov|
* - package
- | |version| |wheel| |supported-versions| |supported-implementations|
| |commits-since|
-
-.. |docs| image:: https://codecov.io/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master
- :target: https://readthedocs.org/projects/python-tblib
+.. |docs| image:: https://readthedocs.org/projects/python-tblib/badge/?style=flat
+ :target: https://python-tblib.readthedocs.io/
:alt: Documentation Status
-.. |travis| image:: https://api.travis-ci.org/ionelmc/python-tblib.svg?branch=master
- :alt: Travis-CI Build Status
- :target: https://travis-ci.org/ionelmc/python-tblib
-
-.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/ionelmc/python-tblib?branch=master&svg=true
- :alt: AppVeyor Build Status
- :target: https://ci.appveyor.com/project/ionelmc/python-tblib
-
-.. |requires| image:: https://requires.io/github/ionelmc/python-tblib/requirements.svg?branch=master
- :alt: Requirements Status
- :target: https://requires.io/github/ionelmc/python-tblib/requirements/?branch=master
+.. |github-actions| image:: https://github.com/ionelmc/python-tblib/actions/workflows/github-actions.yml/badge.svg
+ :alt: GitHub Actions Build Status
+ :target: https://github.com/ionelmc/python-tblib/actions
-.. |codecov| image:: https://codecov.io/github/ionelmc/python-tblib/coverage.svg?branch=master
+.. |codecov| image:: https://codecov.io/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master
:alt: Coverage Status
- :target: https://codecov.io/github/ionelmc/python-tblib
+ :target: https://app.codecov.io/github/ionelmc/python-tblib
.. |version| image:: https://img.shields.io/pypi/v/tblib.svg
:alt: PyPI Package latest release
@@ -52,9 +43,9 @@ Overview
:alt: Supported implementations
:target: https://pypi.org/project/tblib
-.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v1.7.0.svg
+.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v2.0.0.svg
:alt: Commits since latest release
- :target: https://github.com/ionelmc/python-tblib/compare/v1.7.0...master
+ :target: https://github.com/ionelmc/python-tblib/compare/v2.0.0...master
.. end-badges
diff --git a/ci/appveyor-download.py b/ci/appveyor-download.py
deleted file mode 100755
index 8373863..0000000
--- a/ci/appveyor-download.py
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/env python
-"""
-Use the AppVeyor API to download Windows artifacts.
-
-Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py
-# Licensed under the Apache License: https://www.apache.org/licenses/LICENSE-2.0
-# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-"""
-from __future__ import unicode_literals
-
-import argparse
-import os
-import zipfile
-
-import requests
-
-
-def make_auth_headers():
- """Make the authentication headers needed to use the Appveyor API."""
- path = os.path.expanduser("~/.appveyor.token")
- if not os.path.exists(path):
- raise RuntimeError(
- "Please create a file named `.appveyor.token` in your home directory. "
- "You can get the token from https://ci.appveyor.com/api-token"
- )
- with open(path) as f:
- token = f.read().strip()
-
- headers = {
- 'Authorization': 'Bearer {}'.format(token),
- }
- return headers
-
-
-def download_latest_artifacts(account_project, build_id):
- """Download all the artifacts from the latest build."""
- if build_id is None:
- url = "https://ci.appveyor.com/api/projects/{}".format(account_project)
- else:
- url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id)
- build = requests.get(url, headers=make_auth_headers()).json()
- jobs = build['build']['jobs']
- print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs)))
-
- for job in jobs:
- name = job['name']
- print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job))
-
- url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId'])
- response = requests.get(url, headers=make_auth_headers())
- artifacts = response.json()
-
- for artifact in artifacts:
- is_zip = artifact['type'] == "Zip"
- filename = artifact['fileName']
- print(u" {0}, {1} bytes".format(filename, artifact['size']))
-
- url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename)
- download_url(url, filename, make_auth_headers())
-
- if is_zip:
- unpack_zipfile(filename)
- os.remove(filename)
-
-
-def ensure_dirs(filename):
- """Make sure the directories exist for `filename`."""
- dirname = os.path.dirname(filename)
- if dirname and not os.path.exists(dirname):
- os.makedirs(dirname)
-
-
-def download_url(url, filename, headers):
- """Download a file from `url` to `filename`."""
- ensure_dirs(filename)
- response = requests.get(url, headers=headers, stream=True)
- if response.status_code == 200:
- with open(filename, 'wb') as f:
- for chunk in response.iter_content(16 * 1024):
- f.write(chunk)
- else:
- print(u" Error downloading {}: {}".format(url, response))
-
-
-def unpack_zipfile(filename):
- """Unpack a zipfile, using the names in the zip."""
- with open(filename, 'rb') as fzip:
- z = zipfile.ZipFile(fzip)
- for name in z.namelist():
- print(u" extracting {}".format(name))
- ensure_dirs(name)
- z.extract(name)
-
-
-parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.')
-parser.add_argument('--id',
- metavar='PROJECT_ID',
- default='ionelmc/python-tblib',
- help='Project ID in AppVeyor.')
-parser.add_argument('build',
- nargs='?',
- metavar='BUILD_ID',
- help='Build ID in AppVeyor. Eg: master-123')
-
-if __name__ == "__main__":
- # import logging
- # logging.basicConfig(level="DEBUG")
- args = parser.parse_args()
- download_latest_artifacts(args.id, args.build)
diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd
deleted file mode 100644
index 289585f..0000000
--- a/ci/appveyor-with-compiler.cmd
+++ /dev/null
@@ -1,23 +0,0 @@
-:: Very simple setup:
-:: - if WINDOWS_SDK_VERSION is set then activate the SDK.
-:: - disable the WDK if it's around.
-
-SET COMMAND_TO_RUN=%*
-SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
-SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf"
-ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH%
-
-IF EXIST %WIN_WDK% (
- REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/
- REN %WIN_WDK% 0wdf
-)
-IF "%WINDOWS_SDK_VERSION%"=="" GOTO main
-
-SET DISTUTILS_USE_SDK=1
-SET MSSdk=1
-"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
-CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
-
-:main
-ECHO Executing: %COMMAND_TO_RUN%
-CALL %COMMAND_TO_RUN% || EXIT 1
diff --git a/ci/bootstrap.py b/ci/bootstrap.py
index 2597983..f3c9a7e 100755
--- a/ci/bootstrap.py
+++ b/ci/bootstrap.py
@@ -1,64 +1,57 @@
#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import print_function
-from __future__ import unicode_literals
-
import os
+import pathlib
import subprocess
import sys
-from os.path import abspath
-from os.path import dirname
-from os.path import exists
-from os.path import join
-base_path = dirname(dirname(abspath(__file__)))
+base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent
+templates_path = base_path / 'ci' / 'templates'
def check_call(args):
- print("+", *args)
+ print('+', *args)
subprocess.check_call(args)
def exec_in_env():
- env_path = join(base_path, ".tox", "bootstrap")
- if sys.platform == "win32":
- bin_path = join(env_path, "Scripts")
+ env_path = base_path / '.tox' / 'bootstrap'
+ if sys.platform == 'win32':
+ bin_path = env_path / 'Scripts'
else:
- bin_path = join(env_path, "bin")
- if not exists(env_path):
+ bin_path = env_path / 'bin'
+ if not env_path.exists():
import subprocess
- print("Making bootstrap env in: {0} ...".format(env_path))
+ print(f'Making bootstrap env in: {env_path} ...')
try:
- check_call([sys.executable, "-m", "venv", env_path])
+ check_call([sys.executable, '-m', 'venv', env_path])
except subprocess.CalledProcessError:
try:
- check_call([sys.executable, "-m", "virtualenv", env_path])
+ check_call([sys.executable, '-m', 'virtualenv', env_path])
except subprocess.CalledProcessError:
- check_call(["virtualenv", env_path])
- print("Installing `jinja2` into bootstrap environment...")
- check_call([join(bin_path, "pip"), "install", "jinja2", "tox"])
- python_executable = join(bin_path, "python")
- if not os.path.exists(python_executable):
- python_executable += '.exe'
+ check_call(['virtualenv', env_path])
+ print('Installing `jinja2` into bootstrap environment...')
+ check_call([bin_path / 'pip', 'install', 'jinja2', 'tox'])
+ python_executable = bin_path / 'python'
+ if not python_executable.exists():
+ python_executable = python_executable.with_suffix('.exe')
+
+ print(f'Re-executing with: {python_executable}')
+ print('+ exec', python_executable, __file__, '--no-env')
+ os.execv(python_executable, [python_executable, __file__, '--no-env'])
- print("Re-executing with: {0}".format(python_executable))
- print("+ exec", python_executable, __file__, "--no-env")
- os.execv(python_executable, [python_executable, __file__, "--no-env"])
def main():
import jinja2
- print("Project path: {0}".format(base_path))
+ print(f'Project path: {base_path}')
jinja = jinja2.Environment(
- loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")),
+ loader=jinja2.FileSystemLoader(str(templates_path)),
trim_blocks=True,
lstrip_blocks=True,
- keep_trailing_newline=True
+ keep_trailing_newline=True,
)
-
tox_environments = [
line.strip()
# 'tox' need not be installed globally, but must be importable
@@ -69,21 +62,22 @@ def main():
for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines()
]
tox_environments = [line for line in tox_environments if line.startswith('py')]
+ for template in templates_path.rglob('*'):
+ if template.is_file():
+ template_path = template.relative_to(templates_path).as_posix()
+ destination = base_path / template_path
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ destination.write_text(jinja.get_template(template_path).render(tox_environments=tox_environments))
+ print(f'Wrote {template_path}')
+ print('DONE.')
- for name in os.listdir(join("ci", "templates")):
- with open(join(base_path, name), "w") as fh:
- fh.write(jinja.get_template(name).render(tox_environments=tox_environments))
- print("Wrote {}".format(name))
- print("DONE.")
-
-if __name__ == "__main__":
+if __name__ == '__main__':
args = sys.argv[1:]
- if args == ["--no-env"]:
+ if args == ['--no-env']:
main()
elif not args:
exec_in_env()
else:
- print("Unexpected arguments {0}".format(args), file=sys.stderr)
+ print(f'Unexpected arguments: {args}', file=sys.stderr)
sys.exit(1)
-
diff --git a/ci/requirements.txt b/ci/requirements.txt
index b2a21e5..a1708f4 100644
--- a/ci/requirements.txt
+++ b/ci/requirements.txt
@@ -1,4 +1,6 @@
virtualenv>=16.6.0
pip>=19.1.1
setuptools>=18.0.1
-six>=1.12.0
+six>=1.14.0
+tox
+twine
diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml
deleted file mode 100644
index bb4a055..0000000
--- a/ci/templates/.appveyor.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-version: '{branch}-{build}'
-build: off
-environment:
- matrix:
- - TOXENV: check
- TOXPYTHON: C:\Python36\python.exe
- PYTHON_HOME: C:\Python36
- PYTHON_VERSION: '3.6'
- PYTHON_ARCH: '32'
-{% for env in tox_environments %}
-{% if env.startswith(('py2', 'py3')) %}
- - TOXENV: {{ env }},codecov{{ "" }}
- TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe
- PYTHON_HOME: C:\Python{{ env[2:4] }}
- PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}'
- PYTHON_ARCH: '32'
-{% if 'nocov' in env %}
- WHEEL_PATH: .tox/dist
-{% endif %}
- - TOXENV: {{ env }},codecov{{ "" }}
- TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe
- PYTHON_HOME: C:\Python{{ env[2:4] }}-x64
- PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}'
- PYTHON_ARCH: '64'
-{% if 'nocov' in env %}
- WHEEL_PATH: .tox/dist
-{% endif %}
-{% if env.startswith('py2') %}
- WINDOWS_SDK_VERSION: v7.0
-{% endif %}
-{% endif %}{% endfor %}
-init:
- - ps: echo $env:TOXENV
- - ps: ls C:\Python*
-install:
- - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt'
- - '%PYTHON_HOME%\Scripts\virtualenv --version'
- - '%PYTHON_HOME%\Scripts\easy_install --version'
- - '%PYTHON_HOME%\Scripts\pip --version'
- - '%PYTHON_HOME%\Scripts\tox --version'
-test_script:
- - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox
-on_failure:
- - ps: dir "env:"
- - ps: get-content .tox\*\log\*
-
-### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker):
-# on_finish:
-# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml
new file mode 100644
index 0000000..b8e2655
--- /dev/null
+++ b/ci/templates/.github/workflows/github-actions.yml
@@ -0,0 +1,65 @@
+name: build
+on: [push, pull_request]
+jobs:
+ test:
+ name: {{ '${{ matrix.name }}' }}
+ runs-on: {{ '${{ matrix.os }}' }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: 'check'
+ python: '3.11'
+ toxpython: 'python3.11'
+ tox_env: 'check'
+ os: 'ubuntu-latest'
+ - name: 'docs'
+ python: '3.11'
+ toxpython: 'python3.11'
+ tox_env: 'docs'
+ os: 'ubuntu-latest'
+{% for env in tox_environments %}
+{% set prefix = env.split('-')[0] -%}
+{% if prefix.startswith('pypy') %}
+{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %}
+{% set cpython %}pp{{ prefix[4:5] }}{% endset %}
+{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %}
+{% else %}
+{% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %}
+{% set cpython %}cp{{ prefix[2:] }}{% endset %}
+{% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %}
+{% endif %}
+{% for os, python_arch in [
+ ['ubuntu', 'x64'],
+ ['windows', 'x64'],
+ ['macos', 'x64'],
+] %}
+ - name: '{{ env }} ({{ os }})'
+ python: '{{ python }}'
+ toxpython: '{{ toxpython }}'
+ python_arch: '{{ python_arch }}'
+ tox_env: '{{ env }}'
+ os: '{{ os }}-latest'
+{% endfor %}
+{% endfor %}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v4
+ with:
+ python-version: {{ '${{ matrix.python }}' }}
+ architecture: {{ '${{ matrix.python_arch }}' }}
+ - name: install dependencies
+ run: |
+ python -mpip install --progress-bar=off -r ci/requirements.txt
+ virtualenv --version
+ pip --version
+ tox --version
+ pip list --format=freeze
+ - name: test
+ env:
+ TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}'
+ run: >
+ tox -e {{ '${{ matrix.tox_env }}' }} -v
diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml
deleted file mode 100644
index 03110ef..0000000
--- a/ci/templates/.travis.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-language: python
-dist: xenial
-cache: false
-env:
- global:
- - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so
- - SEGFAULT_SIGNALS=all
-matrix:
- include:
- - python: '3.6'
- env:
- - TOXENV=check
- - python: '3.6'
- env:
- - TOXENV=docs
-{%- for env in tox_environments %}{{ '' }}
- - env:
- - TOXENV={{ env }},codecov
-{%- if env.startswith('pypy3') %}{{ '' }}
- - TOXPYTHON=pypy3
- python: 'pypy3'
-{%- elif env.startswith('pypy') %}{{ '' }}
- python: 'pypy'
-{%- else %}{{ '' }}
- python: '{{ '{0[2]}.{0[3]}'.format(env) }}'
-{%- endif %}
-{%- endfor %}{{ '' }}
-before_install:
- - python --version
- - uname -a
- - lsb_release -a || true
-install:
- - python -mpip install --progress-bar=off tox -rci/requirements.txt
- - virtualenv --version
- - easy_install --version
- - pip --version
- - tox --version
-script:
- - tox -v
-after_failure:
- - more .tox/log/* | cat
- - more .tox/*/log/* | cat
-notifications:
- email:
- on_success: never
- on_failure: always
diff --git a/debian/changelog b/debian/changelog
index c32072f..a7b7aae 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,9 @@
-python-tblib (1.7.0-4) UNRELEASED; urgency=medium
+python-tblib (2.0.0-1) UNRELEASED; urgency=medium
* Update watch file format version to 4.
+ * New upstream release.
- -- Colin Watson <cjwatson@debian.org> Mon, 02 Jan 2023 14:02:24 -0000
+ -- Colin Watson <cjwatson@debian.org> Wed, 28 Jun 2023 22:44:24 +0100
python-tblib (1.7.0-3) unstable; urgency=medium
diff --git a/docs/conf.py b/docs/conf.py
index af0256c..2169b97 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-import os
+import sphinx_py3doc_enhanced_theme
extensions = [
'autoapi.extension',
@@ -19,10 +16,10 @@ autoapi_dirs = ['../src']
source_suffix = '.rst'
master_doc = 'index'
project = 'tblib'
-year = '2013-2020'
+year = '2013-2022'
author = 'Ionel Cristian Mărieș'
-copyright = '{0}, {1}'.format(year, author)
-version = release = '1.7.0'
+copyright = f'{year}, {author}'
+version = release = '2.0.0'
pygments_style = 'trac'
templates_path = ['.']
@@ -30,20 +27,19 @@ extlinks = {
'issue': ('https://github.com/ionelmc/python-tblib/issues/%s', '#'),
'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #'),
}
-import sphinx_py3doc_enhanced_theme
-html_theme = "sphinx_py3doc_enhanced_theme"
+html_theme = 'sphinx_py3doc_enhanced_theme'
html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()]
html_theme_options = {
- 'githuburl': 'https://github.com/ionelmc/python-tblib/'
+ 'githuburl': 'https://github.com/ionelmc/python-tblib/',
}
html_use_smartypants = True
html_last_updated_fmt = '%b %d, %Y'
html_split_index = False
html_sidebars = {
- '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'],
+ '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'],
}
-html_short_title = '%s-%s' % (project, version)
+html_short_title = f'{project}-{version}'
napoleon_use_ivar = True
napoleon_use_rtype = False
diff --git a/docs/index.rst b/docs/index.rst
index e55d633..040ea20 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -9,6 +9,7 @@ Contents
installation
usage
contributing
+ autoapi/index
authors
changelog
@@ -18,4 +19,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
-
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c9dd47b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,58 @@
+[build-system]
+requires = [
+ "setuptools>=30.3.0",
+ "wheel",
+]
+
+[tool.ruff.per-file-ignores]
+"ci/*" = ["S"]
+
+[tool.ruff]
+extend-exclude = ["static", "ci/templates"]
+ignore = [
+ "RUF001", # ruff-specific rules ambiguous-unicode-character-string
+ "S101", # flake8-bandit assert
+ "S301", # flake8-bandit pickle
+ "S308", # flake8-bandit suspicious-mark-safe-usage
+ "E501", # pycodestyle line-too-long
+]
+line-length = 140
+select = [
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "DTZ", # flake8-datetimez
+ "E", # pycodestyle errors
+ "EXE", # flake8-executable
+ "F", # pyflakes
+ "I", # isort
+ "INT", # flake8-gettext
+ "PIE", # flake8-pie
+ "PLC", # pylint convention
+ "PLE", # pylint errors
+ "PT", # flake8-pytest-style
+ "PTH", # flake8-use-pathlib
+ "Q", # flake8-quotes
+ "RSE", # flake8-raise
+ "RUF", # ruff-specific rules
+ "S", # flake8-bandit
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+]
+src = ["src", "tests"]
+target-version = "py37"
+
+[tool.ruff.flake8-pytest-style]
+fixture-parentheses = false
+mark-parentheses = false
+
+[tool.ruff.isort]
+forced-separate = ["conftest"]
+force-single-line = true
+
+[tool.black]
+line-length = 140
+target-version = ["py37"]
+skip-string-normalization = true
+
+[tool.ruff.flake8-quotes]
+inline-quotes = "single"
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..5f7ccc6
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,30 @@
+[pytest]
+# If a pytest section is found in one of the possible config files
+# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others,
+# so if you add a pytest config section elsewhere,
+# you will need to delete this section from setup.cfg.
+norecursedirs =
+ migrations
+
+python_files =
+ test_*.py
+ *_test.py
+ tests.py
+addopts =
+ -ra
+ --strict-markers
+ --ignore=tests/badmodule.py
+ --ignore=tests/badsyntax.py
+ --doctest-modules
+ --doctest-glob=\*.rst
+ --tb=short
+testpaths =
+ tests
+
+# Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors
+filterwarnings =
+ error
+# You can add exclusions, some examples:
+# ignore:'tblib' defines default_app_config:PendingDeprecationWarning::
+# ignore:The {{% if:::
+# ignore:Coverage disabled via --no-cov switch!
diff --git a/setup.cfg b/setup.cfg
index 597f8af..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,38 +1,3 @@
-[bdist_wheel]
-universal = 1
-
-[flake8]
-max-line-length = 140
-exclude = */migrations/*
-
-[tool:pytest]
-norecursedirs =
- migrations
-python_files =
- test_*.py
- *_test.py
- tests.py
-addopts =
- -ra
- --strict
- --ignore=tests/badmodule.py
- --ignore=tests/badsyntax.py
- --doctest-modules
- --doctest-continue-on-failure
- --doctest-glob=\*.rst
- --tb=short
-testpaths =
- tests
-
-[tool:isort]
-force_single_line = True
-line_length = 120
-known_first_party = tblib
-default_section = THIRDPARTY
-forced_separate = test_tblib
-not_skip = __init__.py
-skip = migrations
-
[egg_info]
tag_build =
tag_date = 0
diff --git a/setup.py b/setup.py
index 9193b68..f47d250 100644..100755
--- a/setup.py
+++ b/setup.py
@@ -1,43 +1,31 @@
#!/usr/bin/env python
-# -*- encoding: utf-8 -*-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import io
import re
-from glob import glob
-from os.path import basename
-from os.path import dirname
-from os.path import join
-from os.path import splitext
+from pathlib import Path
from setuptools import find_packages
from setuptools import setup
def read(*names, **kwargs):
- with io.open(
- join(dirname(__file__), *names),
- encoding=kwargs.get('encoding', 'utf8')
- ) as fh:
+ with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh:
return fh.read()
setup(
name='tblib',
- version='1.7.0',
+ version='2.0.0',
license='BSD-2-Clause',
description='Traceback serialization library.',
- long_description='%s\n%s' % (
+ long_description='{}\n{}'.format(
re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')),
- re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))
+ re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')),
),
author='Ionel Cristian Mărieș',
author_email='contact@ionelmc.ro',
url='https://github.com/ionelmc/python-tblib',
packages=find_packages('src'),
package_dir={'': 'src'},
- py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')],
+ py_modules=[path.stem for path in Path('src').glob('*.py')],
include_package_data=True,
zip_safe=False,
classifiers=[
@@ -49,12 +37,13 @@ setup(
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Programming Language :: Python',
- 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
# uncomment if you test on these interpreters:
@@ -69,9 +58,11 @@ setup(
'Issue Tracker': 'https://github.com/ionelmc/python-tblib/issues',
},
keywords=[
- 'traceback', 'debugging', 'exceptions',
+ 'traceback',
+ 'debugging',
+ 'exceptions',
],
- python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
+ python_requires='>=3.7',
install_requires=[
# eg: 'aspectlib==1.1.1', 'six>=1.7',
],
diff --git a/src/tblib.egg-info/PKG-INFO b/src/tblib.egg-info/PKG-INFO
index 122805d..b15d380 100644
--- a/src/tblib.egg-info/PKG-INFO
+++ b/src/tblib.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.2
+Metadata-Version: 2.1
Name: tblib
-Version: 1.7.0
+Version: 2.0.0
Summary: Traceback serialization library.
Home-page: https://github.com/ionelmc/python-tblib
Author: Ionel Cristian Mărieș
@@ -9,738 +9,7 @@ License: BSD-2-Clause
Project-URL: Documentation, https://python-tblib.readthedocs.io/
Project-URL: Changelog, https://python-tblib.readthedocs.io/en/latest/changelog.html
Project-URL: Issue Tracker, https://github.com/ionelmc/python-tblib/issues
-Description: ========
- Overview
- ========
-
-
-
- Serialization library for Exceptions and Tracebacks.
-
- * Free software: BSD license
-
- It allows you to:
-
- * `Pickle <https://docs.python.org/3/library/pickle.html>`_ tracebacks and raise exceptions
- with pickled tracebacks in different processes. This allows better error handling when running
- code over multiple processes (imagine multiprocessing, billiard, futures, celery etc).
- * Create traceback objects from strings (the ``from_string`` method). *No pickling is used*.
- * Serialize tracebacks to/from plain dicts (the ``from_dict`` and ``to_dict`` methods). *No pickling is used*.
- * Raise the tracebacks created from the aforementioned sources.
- * Pickle an Exception together with its traceback and exception chain
- (``raise ... from ...``) *(Python 3 only)*
-
- **Again, note that using the pickle support is completely optional. You are solely responsible for
- security problems should you decide to use the pickle support.**
-
- Installation
- ============
-
- ::
-
- pip install tblib
-
- Documentation
- =============
-
- .. contents::
- :local:
-
- Pickling tracebacks
- ~~~~~~~~~~~~~~~~~~~
-
- **Note**: The traceback objects that come out are stripped of some attributes (like variables). But you'll be able to raise exceptions with
- those tracebacks or print them - that should cover 99% of the usecases.
-
- ::
-
- >>> from tblib import pickling_support
- >>> pickling_support.install()
- >>> import pickle, sys
- >>> def inner_0():
- ... raise Exception('fail')
- ...
- >>> def inner_1():
- ... inner_0()
- ...
- >>> def inner_2():
- ... inner_1()
- ...
- >>> try:
- ... inner_2()
- ... except:
- ... s1 = pickle.dumps(sys.exc_info())
- ...
- >>> len(s1) > 1
- True
- >>> try:
- ... inner_2()
- ... except:
- ... s2 = pickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
- ...
- >>> len(s2) > 1
- True
-
- >>> try:
- ... import cPickle
- ... except ImportError:
- ... import pickle as cPickle
- >>> try:
- ... inner_2()
- ... except:
- ... s3 = cPickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
- ...
- >>> len(s3) > 1
- True
-
- Unpickling tracebacks
- ~~~~~~~~~~~~~~~~~~~~~
-
- ::
-
- >>> pickle.loads(s1)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- >>> pickle.loads(s2)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- >>> pickle.loads(s3)
- (<...Exception'>, Exception('fail'...), <traceback object at ...>)
-
- Raising
- ~~~~~~~
-
- ::
-
- >>> from six import reraise
- >>> reraise(*pickle.loads(s1))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
- >>> reraise(*pickle.loads(s2))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
- >>> reraise(*pickle.loads(s3))
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[14]>", line 1, in <module>
- reraise(*pickle.loads(s2))
- File "<doctest README.rst[8]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- Pickling Exceptions together with their traceback and chain (Python 3 only)
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- ::
-
- >>> try: # doctest: +SKIP
- ... try:
- ... 1 / 0
- ... except Exception as e:
- ... raise Exception("foo") from e
- ... except Exception as e:
- ... s = pickle.dumps(e)
- >>> raise pickle.loads(s) # doctest: +SKIP
- Traceback (most recent call last):
- File "<doctest README.rst[16]>", line 3, in <module>
- 1 / 0
- ZeroDivisionError: division by zero
-
- The above exception was the direct cause of the following exception:
-
- Traceback (most recent call last):
- File "<doctest README.rst[17]>", line 1, in <module>
- raise pickle.loads(s)
- File "<doctest README.rst[16]>", line 5, in <module>
- raise Exception("foo") from e
- Exception: foo
-
- BaseException subclasses defined after calling ``pickling_support.install()`` will
- **not** retain their traceback and exception chain pickling.
- To cover custom Exceptions, there are three options:
-
- 1. Use ``@pickling_support.install`` as a decorator for each custom Exception
-
- .. code-block:: python
-
- >>> from tblib import pickling_support
- >>> # Declare all imports of your package's dependencies
- >>> import numpy # doctest: +SKIP
-
- >>> pickling_support.install() # install for all modules imported so far
-
- >>> @pickling_support.install
- ... class CustomError(Exception):
- ... pass
-
- Eventual subclasses of ``CustomError`` will need to be decorated again.
-
- 2. Invoke ``pickling_support.install()`` after all modules have been imported and all
- Exception subclasses have been declared
-
- .. code-block:: python
-
- >>> # Declare all imports of your package's dependencies
- >>> import numpy # doctest: +SKIP
- >>> from tblib import pickling_support
-
- >>> # Declare your own custom Exceptions
- >>> class CustomError(Exception):
- ... pass
-
- >>> # Finally, install tblib
- >>> pickling_support.install()
-
- 3. Selectively install tblib for Exception instances just before they are pickled
-
- .. code-block:: python
-
- pickling_support.install(<Exception instance>, [Exception instance], ...)
-
- The above will install tblib pickling for all listed exceptions as well as any other
- exceptions in their exception chains.
-
- For example, one could write a wrapper to be used with
- `ProcessPoolExecutor <https://docs.python.org/3/library/concurrent.futures.html>`_,
- `Dask.distributed <https://distributed.dask.org/>`_, or similar libraries:
-
- ::
-
- >>> from tblib import pickling_support
- >>> def wrapper(func, *args, **kwargs):
- ... try:
- ... return func(*args, **kwargs)
- ... except Exception as e:
- ... pickling_support.install(e)
- ... raise
-
- What if we have a local stack, does it show correctly ?
- -------------------------------------------------------
-
- Yes it does::
-
- >>> exc_info = pickle.loads(s3)
- >>> def local_0():
- ... reraise(*exc_info)
- ...
- >>> def local_1():
- ... local_0()
- ...
- >>> def local_2():
- ... local_1()
- ...
- >>> local_2()
- Traceback (most recent call last):
- File "...doctest.py", line ..., in __run
- compileflags, 1) in test.globs
- File "<doctest README.rst[24]>", line 1, in <module>
- local_2()
- File "<doctest README.rst[23]>", line 2, in local_2
- local_1()
- File "<doctest README.rst[22]>", line 2, in local_1
- local_0()
- File "<doctest README.rst[21]>", line 2, in local_0
- reraise(*exc_info)
- File "<doctest README.rst[11]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- It also supports more contrived scenarios
- -----------------------------------------
-
- Like tracebacks with syntax errors::
-
- >>> from tblib import Traceback
- >>> from examples import bad_syntax
- >>> try:
- ... bad_syntax()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[58]>", line 1, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[57]>", line 2, in <module>
- bad_syntax()
- File "...tests...examples.py", line 18, in bad_syntax
- import badsyntax
- File "...tests...badsyntax.py", line 5
- is very bad
- ^
- SyntaxError: invalid syntax
-
- Or other import failures::
-
- >>> from examples import bad_module
- >>> try:
- ... bad_module()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[61]>", line 1, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[60]>", line 2, in <module>
- bad_module()
- File "...tests...examples.py", line 23, in bad_module
- import badmodule
- File "...tests...badmodule.py", line 3, in <module>
- raise Exception("boom!")
- Exception: boom!
-
- Or a traceback that's caused by exceeding the recursion limit (here we're
- forcing the type and value to have consistency across platforms)::
-
- >>> def f(): f()
- >>> try:
- ... f()
- ... except RuntimeError:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(RuntimeError, RuntimeError("maximum recursion depth exceeded"), tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- File "<doctest README.rst[32]>", line 1, in f
- def f(): f()
- ...
- RuntimeError: maximum recursion depth exceeded
-
- Reference
- ~~~~~~~~~
-
- tblib.Traceback
- ---------------
-
- It is used by the ``pickling_support``. You can use it too if you want more flexibility::
-
- >>> from tblib import Traceback
- >>> try:
- ... inner_2()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ...
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[21]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[21]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- tblib.Traceback.to_dict
- ```````````````````````
-
- You can use the ``to_dict`` method and the ``from_dict`` classmethod to
- convert a Traceback into and from a dictionary serializable by the stdlib
- json.JSONDecoder::
-
- >>> import json
- >>> from pprint import pprint
- >>> try:
- ... inner_2()
- ... except:
- ... et, ev, tb = sys.exc_info()
- ... tb = Traceback(tb)
- ... tb_dict = tb.to_dict()
- ... pprint(tb_dict)
- {'tb_frame': {'f_code': {'co_filename': '<doctest README.rst[...]>',
- 'co_name': '<module>'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 5},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_2'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_1'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
- 'co_name': 'inner_0'},
- 'f_globals': {'__name__': '__main__'},
- 'f_lineno': 2},
- 'tb_lineno': 2,
- 'tb_next': None}}}}
-
- tblib.Traceback.from_dict
- `````````````````````````
-
- Building on the previous example::
-
- >>> tb_json = json.dumps(tb_dict)
- >>> tb = Traceback.from_dict(json.loads(tb_json))
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[21]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "<doctest README.rst[21]>", line 2, in <module>
- inner_2()
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- tblib.Traceback.from_string
- ```````````````````````````
-
- ::
-
- >>> tb = Traceback.from_string("""
- ... File "skipped.py", line 123, in func_123
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """)
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[42]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: fail
-
-
- If you use the ``strict=False`` option then parsing is a bit more lax::
-
- >>> tb = Traceback.from_string("""
- ... File "bogus.py", line 123, in bogus
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """, strict=False)
- >>> reraise(et, ev, tb.as_traceback())
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[42]>", line 6, in <module>
- reraise(et, ev, tb.as_traceback())
- File "bogus.py", line 123, in bogus
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: fail
-
- tblib.decorators.return_error
- -----------------------------
-
- ::
-
- >>> from tblib.decorators import return_error
- >>> inner_2r = return_error(inner_2)
- >>> e = inner_2r()
- >>> e
- <tblib.decorators.Error object at ...>
- >>> e.reraise()
- Traceback (most recent call last):
- ...
- File "<doctest README.rst[26]>", line 1, in <module>
- e.reraise()
- File "...tblib...decorators.py", line 19, in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line 25, in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "<doctest README.rst[5]>", line 2, in inner_2
- inner_1()
- File "<doctest README.rst[4]>", line 2, in inner_1
- inner_0()
- File "<doctest README.rst[3]>", line 2, in inner_0
- raise Exception('fail')
- Exception: fail
-
- How's this useful? Imagine you're using multiprocessing like this::
-
- # Note that Python 3.4 and later will show the remote traceback (but as a string sadly) so we skip testing this.
- >>> import traceback
- >>> from multiprocessing import Pool
- >>> from examples import func_a
- >>> pool = Pool() # doctest: +SKIP
- >>> try: # doctest: +SKIP
- ... for i in pool.map(func_a, range(5)):
- ... print(i)
- ... except:
- ... print(traceback.format_exc())
- ...
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 2, in <module>
- for i in pool.map(func_a, range(5)):
- File "...multiprocessing...pool.py", line ..., in map
- ...
- File "...multiprocessing...pool.py", line ..., in get
- ...
- Exception: Guessing time !
- <BLANKLINE>
- >>> pool.terminate() # doctest: +SKIP
-
- Not very useful is it? Let's sort this out::
-
- >>> from tblib.decorators import apply_with_return_error, Error
- >>> from itertools import repeat
- >>> pool = Pool()
- >>> try:
- ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
- ... if isinstance(i, Error):
- ... i.reraise()
- ... else:
- ... print(i)
- ... except:
- ... print(traceback.format_exc())
- ...
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 4, in <module>
- i.reraise()
- File "...tblib...decorators.py", line ..., in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "...tblib...decorators.py", line ..., in apply_with_return_error
- return args[0](*args[1:])
- File "...examples.py", line 2, in func_a
- func_b()
- File "...examples.py", line 6, in func_b
- func_c()
- File "...examples.py", line 10, in func_c
- func_d()
- File "...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: Guessing time !
- <BLANKLINE>
- >>> pool.terminate()
-
- Much better !
-
- What if we have a local call stack ?
- ````````````````````````````````````
-
- ::
-
- >>> def local_0():
- ... pool = Pool()
- ... try:
- ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
- ... if isinstance(i, Error):
- ... i.reraise()
- ... else:
- ... print(i)
- ... finally:
- ... pool.close()
- ...
- >>> def local_1():
- ... local_0()
- ...
- >>> def local_2():
- ... local_1()
- ...
- >>> try:
- ... local_2()
- ... except:
- ... print(traceback.format_exc())
- Traceback (most recent call last):
- File "<doctest README.rst[...]>", line 2, in <module>
- local_2()
- File "<doctest README.rst[...]>", line 2, in local_2
- local_1()
- File "<doctest README.rst[...]>", line 2, in local_1
- local_0()
- File "<doctest README.rst[...]>", line 6, in local_0
- i.reraise()
- File "...tblib...decorators.py", line 20, in reraise
- reraise(self.exc_type, self.exc_value, self.traceback)
- File "...tblib...decorators.py", line 27, in return_exceptions_wrapper
- return func(*args, **kwargs)
- File "...tblib...decorators.py", line 47, in apply_with_return_error
- return args[0](*args[1:])
- File "...tests...examples.py", line 2, in func_a
- func_b()
- File "...tests...examples.py", line 6, in func_b
- func_c()
- File "...tests...examples.py", line 10, in func_c
- func_d()
- File "...tests...examples.py", line 14, in func_d
- raise Exception("Guessing time !")
- Exception: Guessing time !
- <BLANKLINE>
-
- Other weird stuff
- `````````````````
-
- Clearing traceback works (Python 3.4 and up)::
-
- >>> tb = Traceback.from_string("""
- ... File "skipped.py", line 123, in func_123
- ... Traceback (most recent call last):
- ... File "tests/examples.py", line 2, in func_a
- ... func_b()
- ... File "tests/examples.py", line 6, in func_b
- ... func_c()
- ... File "tests/examples.py", line 10, in func_c
- ... func_d()
- ... File "tests/examples.py", line 14, in func_d
- ... Doesn't: matter
- ... """)
- >>> import traceback, sys
- >>> if sys.version_info > (3, 4):
- ... traceback.clear_frames(tb)
-
- Credits
- =======
-
- * `mitsuhiko/jinja2 <https://github.com/mitsuhiko/jinja2>`_ for figuring a way to create traceback objects.
-
-
- Changelog
- =========
-
- 1.7.0 (2020-07-24)
- ~~~~~~~~~~~~~~~~~~
-
- * Add more attributes to ``Frame`` and ``Code`` objects for pytest compatibility. Contributed by Ivanq in
- `#58 <https://github.com/ionelmc/python-tblib/pull/58>`_.
-
- 1.6.0 (2019-12-07)
- ~~~~~~~~~~~~~~~~~~
-
- * When pickling an Exception, also pickle its traceback and the Exception chain
- (``raise ... from ...``). Contributed by Guido Imperiale in
- `#53 <https://github.com/ionelmc/python-tblib/issues/53>`_.
-
- 1.5.0 (2019-10-23)
- ~~~~~~~~~~~~~~~~~~
-
- * Added support for Python 3.8. Contributed by Victor Stinner in
- `#42 <https://github.com/ionelmc/python-tblib/issues/42>`_.
- * Removed support for end of life Python 3.4.
- * Few CI improvements and fixes.
-
- 1.4.0 (2019-05-02)
- ~~~~~~~~~~~~~~~~~~
-
- * Removed support for end of life Python 3.3.
- * Fixed tests for Python 3.7. Contributed by Elliott Sales de Andrade in
- `#36 <https://github.com/ionelmc/python-tblib/issues/36>`_.
- * Fixed compatibility issue with Twised (``twisted.python.failure.Failure`` expected a ``co_code`` attribute).
-
- 1.3.2 (2017-04-09)
- ~~~~~~~~~~~~~~~~~~
-
- * 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://bitbucket.org/pypy/pypy/issues/2532/pypy3-attributeerror-frame-object-has-no>`_.
-
- 1.3.1 (2017-03-27)
- ~~~~~~~~~~~~~~~~~~
-
- * Fixed handling for tracebacks due to exceeding the recursion limit.
- Fixes `#15 <https://github.com/ionelmc/python-tblib/issues/15>`_.
-
- 1.3.0 (2016-03-08)
- ~~~~~~~~~~~~~~~~~~
-
- * Added ``Traceback.from_string``.
-
- 1.2.0 (2015-12-18)
- ~~~~~~~~~~~~~~~~~~
-
- * Fixed handling for tracebacks from generators and other internal improvements
- and optimizations. Contributed by DRayX in `#10 <https://github.com/ionelmc/python-tblib/issues/10>`_
- and `#11 <https://github.com/ionelmc/python-tblib/pull/11>`_.
-
- 1.1.0 (2015-07-27)
- ~~~~~~~~~~~~~~~~~~
-
- * Added support for Python 2.6. Contributed by Arcadiy Ivanov in
- `#8 <https://github.com/ionelmc/python-tblib/pull/8>`_.
-
- 1.0.0 (2015-03-30)
- ~~~~~~~~~~~~~~~~~~
-
- * Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks.
- Contributed by beckjake in `#5 <https://github.com/ionelmc/python-tblib/pull/5>`_.
-
Keywords: traceback,debugging,exceptions
-Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
@@ -748,13 +17,752 @@ Classifier: Operating System :: Unix
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Utilities
-Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Requires-Python: >=3.7
+License-File: LICENSE
+License-File: AUTHORS.rst
+
+========
+Overview
+========
+
+
+
+Serialization library for Exceptions and Tracebacks.
+
+* Free software: BSD license
+
+It allows you to:
+
+* `Pickle <https://docs.python.org/3/library/pickle.html>`_ tracebacks and raise exceptions
+ with pickled tracebacks in different processes. This allows better error handling when running
+ code over multiple processes (imagine multiprocessing, billiard, futures, celery etc).
+* Create traceback objects from strings (the ``from_string`` method). *No pickling is used*.
+* Serialize tracebacks to/from plain dicts (the ``from_dict`` and ``to_dict`` methods). *No pickling is used*.
+* Raise the tracebacks created from the aforementioned sources.
+* Pickle an Exception together with its traceback and exception chain
+ (``raise ... from ...``) *(Python 3 only)*
+
+**Again, note that using the pickle support is completely optional. You are solely responsible for
+security problems should you decide to use the pickle support.**
+
+Installation
+============
+
+::
+
+ pip install tblib
+
+Documentation
+=============
+
+.. contents::
+ :local:
+
+Pickling tracebacks
+~~~~~~~~~~~~~~~~~~~
+
+**Note**: The traceback objects that come out are stripped of some attributes (like variables). But you'll be able to raise exceptions with
+those tracebacks or print them - that should cover 99% of the usecases.
+
+::
+
+ >>> from tblib import pickling_support
+ >>> pickling_support.install()
+ >>> import pickle, sys
+ >>> def inner_0():
+ ... raise Exception('fail')
+ ...
+ >>> def inner_1():
+ ... inner_0()
+ ...
+ >>> def inner_2():
+ ... inner_1()
+ ...
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s1 = pickle.dumps(sys.exc_info())
+ ...
+ >>> len(s1) > 1
+ True
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s2 = pickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
+ ...
+ >>> len(s2) > 1
+ True
+
+ >>> try:
+ ... import cPickle
+ ... except ImportError:
+ ... import pickle as cPickle
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... s3 = cPickle.dumps(sys.exc_info(), protocol=pickle.HIGHEST_PROTOCOL)
+ ...
+ >>> len(s3) > 1
+ True
+
+Unpickling tracebacks
+~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ >>> pickle.loads(s1)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+ >>> pickle.loads(s2)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+ >>> pickle.loads(s3)
+ (<...Exception'>, Exception('fail'...), <traceback object at ...>)
+
+Raising
+~~~~~~~
+
+::
+
+ >>> from six import reraise
+ >>> reraise(*pickle.loads(s1))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+ >>> reraise(*pickle.loads(s2))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+ >>> reraise(*pickle.loads(s3))
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[14]>", line 1, in <module>
+ reraise(*pickle.loads(s2))
+ File "<doctest README.rst[8]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+Pickling Exceptions together with their traceback and chain (Python 3 only)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ >>> try: # doctest: +SKIP
+ ... try:
+ ... 1 / 0
+ ... except Exception as e:
+ ... raise Exception("foo") from e
+ ... except Exception as e:
+ ... s = pickle.dumps(e)
+ >>> raise pickle.loads(s) # doctest: +SKIP
+ Traceback (most recent call last):
+ File "<doctest README.rst[16]>", line 3, in <module>
+ 1 / 0
+ ZeroDivisionError: division by zero
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ File "<doctest README.rst[17]>", line 1, in <module>
+ raise pickle.loads(s)
+ File "<doctest README.rst[16]>", line 5, in <module>
+ raise Exception("foo") from e
+ Exception: foo
+
+BaseException subclasses defined after calling ``pickling_support.install()`` will
+**not** retain their traceback and exception chain pickling.
+To cover custom Exceptions, there are three options:
+
+1. Use ``@pickling_support.install`` as a decorator for each custom Exception
+
+ .. code-block:: python
+
+ >>> from tblib import pickling_support
+ >>> # Declare all imports of your package's dependencies
+ >>> import numpy # doctest: +SKIP
+
+ >>> pickling_support.install() # install for all modules imported so far
+
+ >>> @pickling_support.install
+ ... class CustomError(Exception):
+ ... pass
+
+ Eventual subclasses of ``CustomError`` will need to be decorated again.
+
+2. Invoke ``pickling_support.install()`` after all modules have been imported and all
+ Exception subclasses have been declared
+
+ .. code-block:: python
+
+ >>> # Declare all imports of your package's dependencies
+ >>> import numpy # doctest: +SKIP
+ >>> from tblib import pickling_support
+
+ >>> # Declare your own custom Exceptions
+ >>> class CustomError(Exception):
+ ... pass
+
+ >>> # Finally, install tblib
+ >>> pickling_support.install()
+
+3. Selectively install tblib for Exception instances just before they are pickled
+
+ .. code-block:: python
+
+ pickling_support.install(<Exception instance>, [Exception instance], ...)
+
+ The above will install tblib pickling for all listed exceptions as well as any other
+ exceptions in their exception chains.
+
+ For example, one could write a wrapper to be used with
+ `ProcessPoolExecutor <https://docs.python.org/3/library/concurrent.futures.html>`_,
+ `Dask.distributed <https://distributed.dask.org/>`_, or similar libraries:
+
+::
+
+ >>> from tblib import pickling_support
+ >>> def wrapper(func, *args, **kwargs):
+ ... try:
+ ... return func(*args, **kwargs)
+ ... except Exception as e:
+ ... pickling_support.install(e)
+ ... raise
+
+What if we have a local stack, does it show correctly ?
+-------------------------------------------------------
+
+Yes it does::
+
+ >>> exc_info = pickle.loads(s3)
+ >>> def local_0():
+ ... reraise(*exc_info)
+ ...
+ >>> def local_1():
+ ... local_0()
+ ...
+ >>> def local_2():
+ ... local_1()
+ ...
+ >>> local_2()
+ Traceback (most recent call last):
+ File "...doctest.py", line ..., in __run
+ compileflags, 1) in test.globs
+ File "<doctest README.rst[24]>", line 1, in <module>
+ local_2()
+ File "<doctest README.rst[23]>", line 2, in local_2
+ local_1()
+ File "<doctest README.rst[22]>", line 2, in local_1
+ local_0()
+ File "<doctest README.rst[21]>", line 2, in local_0
+ reraise(*exc_info)
+ File "<doctest README.rst[11]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+It also supports more contrived scenarios
+-----------------------------------------
+
+Like tracebacks with syntax errors::
+
+ >>> from tblib import Traceback
+ >>> from examples import bad_syntax
+ >>> try:
+ ... bad_syntax()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[58]>", line 1, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[57]>", line 2, in <module>
+ bad_syntax()
+ File "...tests...examples.py", line 18, in bad_syntax
+ import badsyntax
+ File "...tests...badsyntax.py", line 5
+ is very bad
+ ^
+ SyntaxError: invalid syntax
+
+Or other import failures::
+
+ >>> from examples import bad_module
+ >>> try:
+ ... bad_module()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[61]>", line 1, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[60]>", line 2, in <module>
+ bad_module()
+ File "...tests...examples.py", line 23, in bad_module
+ import badmodule
+ File "...tests...badmodule.py", line 3, in <module>
+ raise Exception("boom!")
+ Exception: boom!
+
+Or a traceback that's caused by exceeding the recursion limit (here we're
+forcing the type and value to have consistency across platforms)::
+
+ >>> def f(): f()
+ >>> try:
+ ... f()
+ ... except RuntimeError:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(RuntimeError, RuntimeError("maximum recursion depth exceeded"), tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ File "<doctest README.rst[32]>", line 1, in f
+ def f(): f()
+ ...
+ RuntimeError: maximum recursion depth exceeded
+
+Reference
+~~~~~~~~~
+
+tblib.Traceback
+---------------
+
+It is used by the ``pickling_support``. You can use it too if you want more flexibility::
+
+ >>> from tblib import Traceback
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ...
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[21]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[21]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+tblib.Traceback.to_dict
+```````````````````````
+
+You can use the ``to_dict`` method and the ``from_dict`` classmethod to
+convert a Traceback into and from a dictionary serializable by the stdlib
+json.JSONDecoder::
+
+ >>> import json
+ >>> from pprint import pprint
+ >>> try:
+ ... inner_2()
+ ... except:
+ ... et, ev, tb = sys.exc_info()
+ ... tb = Traceback(tb)
+ ... tb_dict = tb.to_dict()
+ ... pprint(tb_dict)
+ {'tb_frame': {'f_code': {'co_filename': '<doctest README.rst[...]>',
+ 'co_name': '<module>'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 5},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_2'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_1'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': {'tb_frame': {'f_code': {'co_filename': ...,
+ 'co_name': 'inner_0'},
+ 'f_globals': {'__name__': '__main__'},
+ 'f_lineno': 2},
+ 'tb_lineno': 2,
+ 'tb_next': None}}}}
+
+tblib.Traceback.from_dict
+`````````````````````````
+
+Building on the previous example::
+
+ >>> tb_json = json.dumps(tb_dict)
+ >>> tb = Traceback.from_dict(json.loads(tb_json))
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[21]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "<doctest README.rst[21]>", line 2, in <module>
+ inner_2()
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+tblib.Traceback.from_string
+```````````````````````````
+
+::
+
+ >>> tb = Traceback.from_string("""
+ ... File "skipped.py", line 123, in func_123
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """)
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[42]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: fail
+
+
+If you use the ``strict=False`` option then parsing is a bit more lax::
+
+ >>> tb = Traceback.from_string("""
+ ... File "bogus.py", line 123, in bogus
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """, strict=False)
+ >>> reraise(et, ev, tb.as_traceback())
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[42]>", line 6, in <module>
+ reraise(et, ev, tb.as_traceback())
+ File "bogus.py", line 123, in bogus
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: fail
+
+tblib.decorators.return_error
+-----------------------------
+
+::
+
+ >>> from tblib.decorators import return_error
+ >>> inner_2r = return_error(inner_2)
+ >>> e = inner_2r()
+ >>> e
+ <tblib.decorators.Error object at ...>
+ >>> e.reraise()
+ Traceback (most recent call last):
+ ...
+ File "<doctest README.rst[26]>", line 1, in <module>
+ e.reraise()
+ File "...tblib...decorators.py", line 19, in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line 25, in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "<doctest README.rst[5]>", line 2, in inner_2
+ inner_1()
+ File "<doctest README.rst[4]>", line 2, in inner_1
+ inner_0()
+ File "<doctest README.rst[3]>", line 2, in inner_0
+ raise Exception('fail')
+ Exception: fail
+
+How's this useful? Imagine you're using multiprocessing like this::
+
+ # Note that Python 3.4 and later will show the remote traceback (but as a string sadly) so we skip testing this.
+ >>> import traceback
+ >>> from multiprocessing import Pool
+ >>> from examples import func_a
+ >>> pool = Pool() # doctest: +SKIP
+ >>> try: # doctest: +SKIP
+ ... for i in pool.map(func_a, range(5)):
+ ... print(i)
+ ... except:
+ ... print(traceback.format_exc())
+ ...
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 2, in <module>
+ for i in pool.map(func_a, range(5)):
+ File "...multiprocessing...pool.py", line ..., in map
+ ...
+ File "...multiprocessing...pool.py", line ..., in get
+ ...
+ Exception: Guessing time !
+ <BLANKLINE>
+ >>> pool.terminate() # doctest: +SKIP
+
+Not very useful is it? Let's sort this out::
+
+ >>> from tblib.decorators import apply_with_return_error, Error
+ >>> from itertools import repeat
+ >>> pool = Pool()
+ >>> try:
+ ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
+ ... if isinstance(i, Error):
+ ... i.reraise()
+ ... else:
+ ... print(i)
+ ... except:
+ ... print(traceback.format_exc())
+ ...
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 4, in <module>
+ i.reraise()
+ File "...tblib...decorators.py", line ..., in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line ..., in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "...tblib...decorators.py", line ..., in apply_with_return_error
+ return args[0](*args[1:])
+ File "...examples.py", line 2, in func_a
+ func_b()
+ File "...examples.py", line 6, in func_b
+ func_c()
+ File "...examples.py", line 10, in func_c
+ func_d()
+ File "...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: Guessing time !
+ <BLANKLINE>
+ >>> pool.terminate()
+
+Much better !
+
+What if we have a local call stack ?
+````````````````````````````````````
+
+::
+
+ >>> def local_0():
+ ... pool = Pool()
+ ... try:
+ ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))):
+ ... if isinstance(i, Error):
+ ... i.reraise()
+ ... else:
+ ... print(i)
+ ... finally:
+ ... pool.close()
+ ...
+ >>> def local_1():
+ ... local_0()
+ ...
+ >>> def local_2():
+ ... local_1()
+ ...
+ >>> try:
+ ... local_2()
+ ... except:
+ ... print(traceback.format_exc())
+ Traceback (most recent call last):
+ File "<doctest README.rst[...]>", line 2, in <module>
+ local_2()
+ File "<doctest README.rst[...]>", line 2, in local_2
+ local_1()
+ File "<doctest README.rst[...]>", line 2, in local_1
+ local_0()
+ File "<doctest README.rst[...]>", line 6, in local_0
+ i.reraise()
+ File "...tblib...decorators.py", line 20, in reraise
+ reraise(self.exc_type, self.exc_value, self.traceback)
+ File "...tblib...decorators.py", line 27, in return_exceptions_wrapper
+ return func(*args, **kwargs)
+ File "...tblib...decorators.py", line 47, in apply_with_return_error
+ return args[0](*args[1:])
+ File "...tests...examples.py", line 2, in func_a
+ func_b()
+ File "...tests...examples.py", line 6, in func_b
+ func_c()
+ File "...tests...examples.py", line 10, in func_c
+ func_d()
+ File "...tests...examples.py", line 14, in func_d
+ raise Exception("Guessing time !")
+ Exception: Guessing time !
+ <BLANKLINE>
+
+Other weird stuff
+`````````````````
+
+Clearing traceback works (Python 3.4 and up)::
+
+ >>> tb = Traceback.from_string("""
+ ... File "skipped.py", line 123, in func_123
+ ... Traceback (most recent call last):
+ ... File "tests/examples.py", line 2, in func_a
+ ... func_b()
+ ... File "tests/examples.py", line 6, in func_b
+ ... func_c()
+ ... File "tests/examples.py", line 10, in func_c
+ ... func_d()
+ ... File "tests/examples.py", line 14, in func_d
+ ... Doesn't: matter
+ ... """)
+ >>> import traceback, sys
+ >>> if sys.version_info > (3, 4):
+ ... traceback.clear_frames(tb)
+
+Credits
+=======
+
+* `mitsuhiko/jinja2 <https://github.com/mitsuhiko/jinja2>`_ for figuring a way to create traceback objects.
+
+
+Changelog
+=========
+
+2.0.0 (2023-06-22)
+~~~~~~~~~~~~~~~~~~
+
+* Removed support for legacy Pythons (2.7 and 3.6) and added Python 3.11 in the test grid.
+* Some cleanups and refactors (mostly from ruff).
+
+1.7.0 (2020-07-24)
+~~~~~~~~~~~~~~~~~~
+
+* Add more attributes to ``Frame`` and ``Code`` objects for pytest compatibility. Contributed by Ivanq in
+ `#58 <https://github.com/ionelmc/python-tblib/pull/58>`_.
+
+1.6.0 (2019-12-07)
+~~~~~~~~~~~~~~~~~~
+
+* When pickling an Exception, also pickle its traceback and the Exception chain
+ (``raise ... from ...``). Contributed by Guido Imperiale in
+ `#53 <https://github.com/ionelmc/python-tblib/issues/53>`_.
+
+1.5.0 (2019-10-23)
+~~~~~~~~~~~~~~~~~~
+
+* Added support for Python 3.8. Contributed by Victor Stinner in
+ `#42 <https://github.com/ionelmc/python-tblib/issues/42>`_.
+* Removed support for end of life Python 3.4.
+* Few CI improvements and fixes.
+
+1.4.0 (2019-05-02)
+~~~~~~~~~~~~~~~~~~
+
+* Removed support for end of life Python 3.3.
+* Fixed tests for Python 3.7. Contributed by Elliott Sales de Andrade in
+ `#36 <https://github.com/ionelmc/python-tblib/issues/36>`_.
+* Fixed compatibility issue with Twised (``twisted.python.failure.Failure`` expected a ``co_code`` attribute).
+
+1.3.2 (2017-04-09)
+~~~~~~~~~~~~~~~~~~
+
+* 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>`_.
+
+1.3.1 (2017-03-27)
+~~~~~~~~~~~~~~~~~~
+
+* Fixed handling for tracebacks due to exceeding the recursion limit.
+ Fixes `#15 <https://github.com/ionelmc/python-tblib/issues/15>`_.
+
+1.3.0 (2016-03-08)
+~~~~~~~~~~~~~~~~~~
+
+* Added ``Traceback.from_string``.
+
+1.2.0 (2015-12-18)
+~~~~~~~~~~~~~~~~~~
+
+* Fixed handling for tracebacks from generators and other internal improvements
+ and optimizations. Contributed by DRayX in `#10 <https://github.com/ionelmc/python-tblib/issues/10>`_
+ and `#11 <https://github.com/ionelmc/python-tblib/pull/11>`_.
+
+1.1.0 (2015-07-27)
+~~~~~~~~~~~~~~~~~~
+
+* Added support for Python 2.6. Contributed by Arcadiy Ivanov in
+ `#8 <https://github.com/ionelmc/python-tblib/pull/8>`_.
+
+1.0.0 (2015-03-30)
+~~~~~~~~~~~~~~~~~~
+
+* Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks.
+ Contributed by beckjake in `#5 <https://github.com/ionelmc/python-tblib/pull/5>`_.
diff --git a/src/tblib.egg-info/SOURCES.txt b/src/tblib.egg-info/SOURCES.txt
index f5cb383..0570ca0 100644
--- a/src/tblib.egg-info/SOURCES.txt
+++ b/src/tblib.egg-info/SOURCES.txt
@@ -1,26 +1,24 @@
-.appveyor.yml
.bumpversion.cfg
.cookiecutterrc
.coveragerc
.editorconfig
.gitignore
+.pre-commit-config.yaml
.readthedocs.yml
-.travis.yml
AUTHORS.rst
CHANGELOG.rst
CONTRIBUTING.rst
LICENSE
MANIFEST.in
README.rst
-setup.cfg
+pyproject.toml
+pytest.ini
setup.py
tox.ini
-ci/appveyor-download.py
-ci/appveyor-with-compiler.cmd
+.github/workflows/github-actions.yml
ci/bootstrap.py
ci/requirements.txt
-ci/templates/.appveyor.yml
-ci/templates/.travis.yml
+ci/templates/.github/workflows/github-actions.yml
docs/authors.rst
docs/changelog.rst
docs/conf.py
@@ -32,7 +30,6 @@ docs/requirements.txt
docs/spelling_wordlist.txt
docs/usage.rst
src/tblib/__init__.py
-src/tblib/cpython.py
src/tblib/decorators.py
src/tblib/pickling_support.py
src/tblib.egg-info/PKG-INFO
diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py
index 7e717b4..a9b1084 100644
--- a/src/tblib/__init__.py
+++ b/src/tblib/__init__.py
@@ -1,25 +1,10 @@
import re
import sys
from types import CodeType
-from types import FrameType
-from types import TracebackType
-
-try:
- from __pypy__ import tproxy
-except ImportError:
- tproxy = None
-try:
- from .cpython import tb_set_next
-except ImportError:
- tb_set_next = None
-
-if not tb_set_next and not tproxy:
- raise ImportError("Cannot use tblib. Runtime not supported.")
-
-__version__ = '1.7.0'
+
+__version__ = '2.0.0'
__all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code'
-PY3 = sys.version_info[0] == 3
FRAME_RE = re.compile(r'^\s*File "(?P<co_filename>.+)", line (?P<tb_lineno>\d+)(, in (?P<co_name>.+))?$')
@@ -30,7 +15,7 @@ class _AttrDict(dict):
try:
return self[name]
except KeyError:
- raise AttributeError(name)
+ raise AttributeError(name) from None
# noinspection PyPep8Naming
@@ -42,10 +27,11 @@ class TracebackParseError(Exception):
pass
-class Code(object):
+class Code:
"""
Class that replicates just enough of the builtin Code object to enable serialization and traceback rendering.
"""
+
co_code = None
def __init__(self, code):
@@ -59,28 +45,15 @@ class Code(object):
self.co_flags = 64
self.co_firstlineno = 0
- # noinspection SpellCheckingInspection
- def __tproxy__(self, operation, *args, **kwargs):
- """
- Necessary for PyPy's tproxy.
- """
- if operation in ('__getattribute__', '__getattr__'):
- return getattr(self, args[0])
- else:
- return getattr(self, operation)(*args, **kwargs)
-
-class Frame(object):
+class Frame:
"""
Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering.
"""
+
def __init__(self, frame):
self.f_locals = {}
- self.f_globals = {
- k: v
- for k, v in frame.f_globals.items()
- if k in ("__file__", "__name__")
- }
+ self.f_globals = {k: v for k, v in frame.f_globals.items() if k in ('__file__', '__name__')}
self.f_code = Code(frame.f_code)
self.f_lineno = frame.f_lineno
@@ -92,24 +65,12 @@ class Frame(object):
in turn is called by unittest.TestCase.assertRaises
"""
- # noinspection SpellCheckingInspection
- def __tproxy__(self, operation, *args, **kwargs):
- """
- Necessary for PyPy's tproxy.
- """
- if operation in ('__getattribute__', '__getattr__'):
- if args[0] == 'f_code':
- return tproxy(CodeType, self.f_code.__tproxy__)
- else:
- return getattr(self, args[0])
- else:
- return getattr(self, operation)(*args, **kwargs)
-
-class Traceback(object):
+class Traceback:
"""
Class that wraps builtin Traceback objects.
"""
+
tb_next = None
def __init__(self, tb):
@@ -133,48 +94,43 @@ class Traceback(object):
"""
Convert to a builtin Traceback object that is usable for raising or rendering a stacktrace.
"""
- if tproxy:
- return tproxy(TracebackType, self.__tproxy__)
- if not tb_set_next:
- raise RuntimeError("Unsupported Python interpreter!")
-
current = self
top_tb = None
tb = None
while current:
f_code = current.tb_frame.f_code
code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec')
- if hasattr(code, "replace"):
+ if hasattr(code, 'replace'):
# Python 3.8 and newer
- code = code.replace(co_argcount=0,
- co_filename=f_code.co_filename, co_name=f_code.co_name,
- co_freevars=(), co_cellvars=())
- elif PY3:
- code = CodeType(
- 0, code.co_kwonlyargcount,
- code.co_nlocals, code.co_stacksize, code.co_flags,
- code.co_code, code.co_consts, code.co_names, code.co_varnames,
- f_code.co_filename, f_code.co_name,
- code.co_firstlineno, code.co_lnotab, (), ()
- )
+ code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=())
else:
code = CodeType(
0,
- code.co_nlocals, code.co_stacksize, code.co_flags,
- code.co_code, code.co_consts, code.co_names, code.co_varnames,
- f_code.co_filename.encode(), f_code.co_name.encode(),
- code.co_firstlineno, code.co_lnotab, (), ()
+ code.co_kwonlyargcount,
+ code.co_nlocals,
+ code.co_stacksize,
+ code.co_flags,
+ code.co_code,
+ code.co_consts,
+ code.co_names,
+ code.co_varnames,
+ f_code.co_filename,
+ f_code.co_name,
+ code.co_firstlineno,
+ code.co_lnotab,
+ (),
+ (),
)
# noinspection PyBroadException
try:
- exec(code, dict(current.tb_frame.f_globals), {})
+ exec(code, dict(current.tb_frame.f_globals), {}) # noqa: S102
except Exception:
next_tb = sys.exc_info()[2].tb_next
if top_tb is None:
top_tb = next_tb
if tb is not None:
- tb_set_next(tb, next_tb)
+ tb.tb_next = next_tb
tb = next_tb
del next_tb
@@ -184,22 +140,8 @@ class Traceback(object):
finally:
del top_tb
del tb
- to_traceback = as_traceback
- # noinspection SpellCheckingInspection
- def __tproxy__(self, operation, *args, **kwargs):
- """
- Necessary for PyPy's tproxy.
- """
- if operation in ('__getattribute__', '__getattr__'):
- if args[0] == 'tb_next':
- return self.tb_next and self.tb_next.as_traceback()
- elif args[0] == 'tb_frame':
- return tproxy(FrameType, self.tb_frame.__tproxy__)
- else:
- return getattr(self, args[0])
- else:
- return getattr(self, operation)(*args, **kwargs)
+ to_traceback = as_traceback
def as_dict(self):
"""
@@ -225,6 +167,7 @@ class Traceback(object):
'tb_lineno': self.tb_lineno,
'tb_next': tb_next,
}
+
to_dict = as_dict
@classmethod
@@ -294,4 +237,4 @@ class Traceback(object):
)
return cls(previous)
else:
- raise TracebackParseError("Could not find any frames in %r." % string)
+ raise TracebackParseError('Could not find any frames in %r.' % string)
diff --git a/src/tblib/cpython.py b/src/tblib/cpython.py
deleted file mode 100644
index 5c4bf20..0000000
--- a/src/tblib/cpython.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-Taken verbatim from Jinja2.
-
-https://github.com/mitsuhiko/jinja2/blob/master/jinja2/debug.py#L267
-"""
-import platform
-import sys
-
-
-def _init_ugly_crap():
- """This function implements a few ugly things so that we can patch the
- traceback objects. The function returned allows resetting `tb_next` on
- any python traceback object. Do not attempt to use this on non cpython
- interpreters
- """
- import ctypes
- from types import TracebackType
-
- # figure out side of _Py_ssize_t
- if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
- _Py_ssize_t = ctypes.c_int64
- else:
- _Py_ssize_t = ctypes.c_int
-
- # regular python
- class _PyObject(ctypes.Structure):
- pass
-
- _PyObject._fields_ = [
- ('ob_refcnt', _Py_ssize_t),
- ('ob_type', ctypes.POINTER(_PyObject))
- ]
-
- # python with trace
- if hasattr(sys, 'getobjects'):
- class _PyObject(ctypes.Structure):
- pass
-
- _PyObject._fields_ = [
- ('_ob_next', ctypes.POINTER(_PyObject)),
- ('_ob_prev', ctypes.POINTER(_PyObject)),
- ('ob_refcnt', _Py_ssize_t),
- ('ob_type', ctypes.POINTER(_PyObject))
- ]
-
- class _Traceback(_PyObject):
- pass
-
- _Traceback._fields_ = [
- ('tb_next', ctypes.POINTER(_Traceback)),
- ('tb_frame', ctypes.POINTER(_PyObject)),
- ('tb_lasti', ctypes.c_int),
- ('tb_lineno', ctypes.c_int)
- ]
-
- def tb_set_next(tb, next):
- """Set the tb_next attribute of a traceback object."""
- if not (isinstance(tb, TracebackType) and (next is None or isinstance(next, TracebackType))):
- raise TypeError('tb_set_next arguments must be traceback objects')
- obj = _Traceback.from_address(id(tb))
- if tb.tb_next is not None:
- old = _Traceback.from_address(id(tb.tb_next))
- old.ob_refcnt -= 1
- if next is None:
- obj.tb_next = ctypes.POINTER(_Traceback)()
- else:
- next = _Traceback.from_address(id(next))
- next.ob_refcnt += 1
- obj.tb_next = ctypes.pointer(next)
-
- return tb_set_next
-
-
-tb_set_next = None
-try:
- if platform.python_implementation() == 'CPython':
- tb_set_next = _init_ugly_crap()
-except Exception as exc:
- sys.stderr.write("Failed to initialize cpython support: {!r}".format(exc))
-del _init_ugly_crap
diff --git a/src/tblib/decorators.py b/src/tblib/decorators.py
index 29fdef2..38d0675 100644
--- a/src/tblib/decorators.py
+++ b/src/tblib/decorators.py
@@ -6,7 +6,7 @@ from six import reraise
from . import Traceback
-class Error(object):
+class Error:
def __init__(self, exc_type, exc_value, traceback):
self.exc_type = exc_type
self.exc_value = exc_value
diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py
index cf6e390..d58799f 100644
--- a/src/tblib/pickling_support.py
+++ b/src/tblib/pickling_support.py
@@ -39,8 +39,9 @@ def pickle_exception(obj):
# still be pickled with protocol 5 if pickle.dump() is running with it.
rv = obj.__reduce_ex__(3)
if isinstance(rv, str):
- raise TypeError("str __reduce__ output is not supported")
- assert isinstance(rv, tuple) and len(rv) >= 2
+ raise TypeError('str __reduce__ output is not supported')
+ assert isinstance(rv, tuple)
+ assert len(rv) >= 2
return (unpickle_exception, rv[:2] + (obj.__cause__, obj.__traceback__)) + rv[2:]
@@ -81,7 +82,4 @@ def install(*exc_classes_or_instances):
if len(exc_classes_or_instances) == 1:
return exc
else:
- raise TypeError(
- "Expected subclasses or instances of BaseException, got %s"
- % (type(exc))
- )
+ raise TypeError('Expected subclasses or instances of BaseException, got %s' % (type(exc)))
diff --git a/tests/badmodule.py b/tests/badmodule.py
index 86611f9..4915f00 100644
--- a/tests/badmodule.py
+++ b/tests/badmodule.py
@@ -1,3 +1,3 @@
a = 1
b = 2
-raise Exception("boom!")
+raise Exception('boom!')
diff --git a/tests/examples.py b/tests/examples.py
index d4e64ab..bb61d8c 100644
--- a/tests/examples.py
+++ b/tests/examples.py
@@ -11,14 +11,16 @@ def func_c():
def func_d():
- raise Exception("Guessing time !")
+ raise Exception('Guessing time !')
def bad_syntax():
import badsyntax
- badsyntax
+
+ badsyntax()
def bad_module():
import badmodule
- badmodule
+
+ badmodule()
diff --git a/tests/test_issue30.py b/tests/test_issue30.py
index 270f146..3452597 100644
--- a/tests/test_issue30.py
+++ b/tests/test_issue30.py
@@ -2,9 +2,8 @@ import pickle
import sys
import pytest
-import six
-from tblib import pickling_support # noqa: E402
+from tblib import pickling_support
pytest.importorskip('twisted')
@@ -21,7 +20,8 @@ def test_30():
f = None
try:
- six.reraise(*pickle.loads(s))
+ etype, evalue, etb = pickle.loads(s)
+ raise evalue.with_traceback(etb)
except ValueError:
f = Failure()
diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py
index 18a018c..aa10762 100644
--- a/tests/test_pickle_exception.py
+++ b/tests/test_pickle_exception.py
@@ -18,7 +18,7 @@ has_python3 = sys.version_info.major >= 3
def clear_dispatch_table():
bak = copyreg.dispatch_table.copy()
copyreg.dispatch_table.clear()
- yield
+ yield None
copyreg.dispatch_table.clear()
copyreg.dispatch_table.update(bak)
@@ -27,43 +27,41 @@ class CustomError(Exception):
pass
-@pytest.mark.parametrize(
- "protocol", [None] + list(range(1, pickle.HIGHEST_PROTOCOL + 1))
-)
-@pytest.mark.parametrize("how", ["global", "instance", "class"])
+@pytest.mark.parametrize('protocol', [None, *list(range(1, pickle.HIGHEST_PROTOCOL + 1))])
+@pytest.mark.parametrize('how', ['global', 'instance', 'class'])
def test_install(clear_dispatch_table, how, protocol):
- if how == "global":
+ if how == 'global':
tblib.pickling_support.install()
- elif how == "class":
+ elif how == 'class':
tblib.pickling_support.install(CustomError, ZeroDivisionError)
try:
try:
- 1 / 0
+ 1 / 0 # noqa: B018
except Exception as e:
# Python 3 only syntax
# raise CustomError("foo") from e
- new_e = CustomError("foo")
+ new_e = CustomError('foo')
if has_python3:
new_e.__cause__ = e
raise new_e
except Exception as e:
exc = e
else:
- assert False
+ raise AssertionError
# Populate Exception.__dict__, which is used in some cases
exc.x = 1
if has_python3:
exc.__cause__.x = 2
- if how == "instance":
+ if how == 'instance':
tblib.pickling_support.install(exc)
if protocol:
exc = pickle.loads(pickle.dumps(exc, protocol=protocol))
assert isinstance(exc, CustomError)
- assert exc.args == ("foo",)
+ assert exc.args == ('foo',)
assert exc.x == 1
if has_python3:
assert exc.__traceback__ is not None
@@ -80,19 +78,19 @@ class RegisteredError(Exception):
def test_install_decorator():
with pytest.raises(RegisteredError) as ewrap:
- raise RegisteredError("foo")
+ raise RegisteredError('foo')
exc = ewrap.value
exc.x = 1
exc = pickle.loads(pickle.dumps(exc))
assert isinstance(exc, RegisteredError)
- assert exc.args == ("foo",)
+ assert exc.args == ('foo',)
assert exc.x == 1
if has_python3:
assert exc.__traceback__ is not None
-@pytest.mark.skipif(sys.version_info[0] < 3, reason="No checks done in Python 2")
+@pytest.mark.skipif(sys.version_info[0] < 3, reason='No checks done in Python 2')
def test_install_typeerror():
with pytest.raises(TypeError):
- tblib.pickling_support.install("foo")
+ tblib.pickling_support.install('foo')
diff --git a/tests/test_tblib.py b/tests/test_tblib.py
index bade6d4..d05160b 100644
--- a/tests/test_tblib.py
+++ b/tests/test_tblib.py
@@ -6,7 +6,7 @@ from tblib import pickling_support
pickling_support.install()
-pytest_plugins = 'pytester',
+pytest_plugins = ('pytester',)
def test_parse_traceback():
@@ -31,27 +31,27 @@ KeyboardInterrupt"""
tb2 = Traceback(pytb)
expected_dict = {
- "tb_frame": {
- "f_code": {"co_filename": "file1", "co_name": "<module>"},
- "f_globals": {"__file__": "file1", "__name__": "?"},
- "f_lineno": 123,
+ 'tb_frame': {
+ 'f_code': {'co_filename': 'file1', 'co_name': '<module>'},
+ 'f_globals': {'__file__': 'file1', '__name__': '?'},
+ 'f_lineno': 123,
},
- "tb_lineno": 123,
- "tb_next": {
- "tb_frame": {
- "f_code": {"co_filename": "file2", "co_name": "???"},
- "f_globals": {"__file__": "file2", "__name__": "?"},
- "f_lineno": 234,
+ 'tb_lineno': 123,
+ 'tb_next': {
+ 'tb_frame': {
+ 'f_code': {'co_filename': 'file2', 'co_name': '???'},
+ 'f_globals': {'__file__': 'file2', '__name__': '?'},
+ 'f_lineno': 234,
},
- "tb_lineno": 234,
- "tb_next": {
- "tb_frame": {
- "f_code": {"co_filename": "file3", "co_name": "function3"},
- "f_globals": {"__file__": "file3", "__name__": "?"},
- "f_lineno": 345,
+ 'tb_lineno': 234,
+ 'tb_next': {
+ 'tb_frame': {
+ 'f_code': {'co_filename': 'file3', 'co_name': 'function3'},
+ 'f_globals': {'__file__': 'file3', '__name__': '?'},
+ 'f_lineno': 345,
},
- "tb_lineno": 345,
- "tb_next": None,
+ 'tb_lineno': 345,
+ 'tb_next': None,
},
},
}
@@ -61,9 +61,8 @@ KeyboardInterrupt"""
def test_pytest_integration(testdir):
- test = testdir.makepyfile("""
-import six
-
+ test = testdir.makepyfile(
+ """
from tblib import Traceback
def test_raise():
@@ -77,68 +76,76 @@ Traceback (most recent call last):
File "file4", line 456, in ""
''')
pytb = tb1.as_traceback()
- six.reraise(RuntimeError, RuntimeError(), pytb)
-""")
+ raise RuntimeError().with_traceback(pytb)
+"""
+ )
# mode(auto / long / short / line / native / no).
result = testdir.runpytest_subprocess('--tb=long', '-vv', test)
- result.stdout.fnmatch_lines([
- "_ _ _ _ _ _ _ _ *",
- "",
- "> [?][?][?]",
- "",
- "file1:123:*",
- "_ _ _ _ _ _ _ _ *",
- "",
- "> [?][?][?]",
- "",
- "file2:234:*",
- "_ _ _ _ _ _ _ _ *",
- "",
- "> [?][?][?]",
- "",
- "file3:345:*",
- "_ _ _ _ _ _ _ _ *",
- "",
- "> [?][?][?]",
- "E RuntimeError",
- "",
- "file4:456: RuntimeError",
- "===*=== 1 failed in * ===*===",
- ])
+ result.stdout.fnmatch_lines(
+ [
+ '_ _ _ _ _ _ _ _ *',
+ '',
+ '> [?][?][?]',
+ '',
+ 'file1:123:*',
+ '_ _ _ _ _ _ _ _ *',
+ '',
+ '> [?][?][?]',
+ '',
+ 'file2:234:*',
+ '_ _ _ _ _ _ _ _ *',
+ '',
+ '> [?][?][?]',
+ '',
+ 'file3:345:*',
+ '_ _ _ _ _ _ _ _ *',
+ '',
+ '> [?][?][?]',
+ 'E RuntimeError',
+ '',
+ 'file4:456: RuntimeError',
+ '===*=== 1 failed in * ===*===',
+ ]
+ )
result = testdir.runpytest_subprocess('--tb=short', '-vv', test)
- result.stdout.fnmatch_lines([
- 'test_pytest_integration.py:*: in test_raise',
- ' six.reraise(RuntimeError, RuntimeError(), pytb)',
- 'file1:123: in <module>',
- ' ???',
- 'file2:234: in ???',
- ' ???',
- 'file3:345: in function3',
- ' ???',
- 'file4:456: in ""',
- ' ???',
- 'E RuntimeError',
- ])
+ result.stdout.fnmatch_lines(
+ [
+ 'test_pytest_integration.py:*: in test_raise',
+ ' raise RuntimeError().with_traceback(pytb)',
+ 'file1:123: in <module>',
+ ' ???',
+ 'file2:234: in ???',
+ ' ???',
+ 'file3:345: in function3',
+ ' ???',
+ 'file4:456: in ""',
+ ' ???',
+ 'E RuntimeError',
+ ]
+ )
result = testdir.runpytest_subprocess('--tb=line', '-vv', test)
- result.stdout.fnmatch_lines([
- "===*=== FAILURES ===*===",
- "file4:456: RuntimeError",
- "===*=== 1 failed in * ===*===",
- ])
+ result.stdout.fnmatch_lines(
+ [
+ '===*=== FAILURES ===*===',
+ 'file4:456: RuntimeError',
+ '===*=== 1 failed in * ===*===',
+ ]
+ )
result = testdir.runpytest_subprocess('--tb=native', '-vv', test)
- result.stdout.fnmatch_lines([
- 'Traceback (most recent call last):',
- ' File "*test_pytest_integration.py", line *, in test_raise',
- ' six.reraise(RuntimeError, RuntimeError(), pytb)',
- ' File "file1", line 123, in <module>',
- ' File "file2", line 234, in ???',
- ' File "file3", line 345, in function3',
- ' File "file4", line 456, in ""',
- 'RuntimeError',
-
- ])
+ result.stdout.fnmatch_lines(
+ [
+ 'Traceback (most recent call last):',
+ ' File "*test_pytest_integration.py", line *, in test_raise',
+ ' raise RuntimeError().with_traceback(pytb)',
+ ' File "file1", line 123, in <module>',
+ ' File "file2", line 234, in ???',
+ ' File "file3", line 345, in function3',
+ ' File "file4", line 456, in ""',
+ 'RuntimeError',
+ ]
+ )
diff --git a/tox.ini b/tox.ini
index e052f92..8afc240 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,34 +1,34 @@
[testenv:bootstrap]
deps =
jinja2
- matrix
tox
skip_install = true
commands =
python ci/bootstrap.py --no-env
passenv =
*
-; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist
+; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments
[tox]
envlist =
clean,
check,
docs,
- {py27,py35,py36,py37,py38,pypy,pypy3},
+ {py37,py38,py39,py310,py311,pypy37,pypy38,pypy39},
report
ignore_basepython_conflict = true
[testenv]
basepython =
- pypy: {env:TOXPYTHON:pypy}
- pypy3: {env:TOXPYTHON:pypy3}
- py27: {env:TOXPYTHON:python2.7}
- py35: {env:TOXPYTHON:python3.5}
- py36: {env:TOXPYTHON:python3.6}
+ pypy37: {env:TOXPYTHON:pypy3.7}
+ pypy38: {env:TOXPYTHON:pypy3.8}
+ pypy39: {env:TOXPYTHON:pypy3.9}
py37: {env:TOXPYTHON:python3.7}
py38: {env:TOXPYTHON:python3.8}
- {bootstrap,clean,check,report,codecov,docs}: {env:TOXPYTHON:python3}
+ py39: {env:TOXPYTHON:python3.9}
+ py310: {env:TOXPYTHON:python3.10}
+ py311: {env:TOXPYTHON:python3.11}
+ {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3}
setenv =
PYTHONPATH={toxinidir}/tests
PYTHONUNBUFFERED=yes
@@ -37,28 +37,23 @@ passenv =
usedevelop = false
deps =
pytest
- pytest-travis-fold
pytest-cov
- pytest-clarity
- six
- py{27,35,36,37,38,py,py3}: twisted
commands =
- {posargs:py.test --cov=tblib --cov-report=term-missing -vv tests README.rst}
+ {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests}
[testenv:check]
deps =
docutils
check-manifest
- flake8
+ pre-commit
readme-renderer
pygments
isort
skip_install = true
commands =
python setup.py check --strict --metadata --restructuredtext
- check-manifest {toxinidir}
- flake8 src tests setup.py
- isort --verbose --check-only --diff --recursive src tests setup.py
+ check-manifest .
+ pre-commit run --all-files --show-diff-on-failure
[testenv:docs]
usedevelop = true
@@ -68,15 +63,9 @@ commands =
sphinx-build {posargs:-E} -b html docs dist/docs
sphinx-build -b linkcheck docs dist/docs
-[testenv:codecov]
-deps =
- codecov
-skip_install = true
-commands =
- codecov []
-
[testenv:report]
-deps = coverage
+deps =
+ coverage
skip_install = true
commands =
coverage report
@@ -85,4 +74,5 @@ commands =
[testenv:clean]
commands = coverage erase
skip_install = true
-deps = coverage
+deps =
+ coverage