././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1743425901.883515 tblib-3.1.0/0000755000175000017500000000000014772510556011661 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/.bumpversion.cfg0000664000175000017500000000133614772510520014765 0ustar00ionelionel[bumpversion] current_version = 3.1.0 commit = True tag = True [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{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}' replace = version = release = '{new_version}' [bumpversion:file:src/tblib/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' [bumpversion:file:.cookiecutterrc] search = version: {current_version} replace = version: {new_version} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/.cookiecutterrc0000644000175000017500000000261114772510520014676 0ustar00ionelionel# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: 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 formatter_quote_style: single full_name: Ionel Cristian Mărieș function_name: compute github_actions: 'yes' github_actions_osx: 'yes' github_actions_windows: 'yes' license: BSD 2-Clause License module_name: core package_name: tblib pre_commit: 'yes' project_name: 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 scrutinizer: 'no' setup_py_uses_setuptools_scm: 'no' sphinx_docs: 'yes' sphinx_docs_hosting: https://python-tblib.readthedocs.io/ sphinx_doctest: 'no' sphinx_theme: furo test_matrix_separate_coverage: 'no' tests_inside_package: 'no' version: 3.1.0 version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' year_to: '2023' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/.coveragerc0000664000175000017500000000025514772504455014007 0ustar00ionelionel[paths] source = src */site-packages [run] branch = true source = tblib tests parallel = true [report] show_missing = true precision = 2 omit = *migrations* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/.editorconfig0000664000175000017500000000054114772504455014341 0ustar00ionelionel# see https://editorconfig.org/ root = true [*] # Use Unix-style newlines for most files (except Windows files, see below). end_of_line = lf trim_trailing_whitespace = 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8775403 tblib-3.1.0/.github/0000755000175000017500000000000014772510556013221 5ustar00ionelionel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8804646 tblib-3.1.0/.github/workflows/0000755000175000017500000000000014772510556015256 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/.github/workflows/github-actions.yml0000644000175000017500000001226314772504501020716 0ustar00ionelionelname: build on: [push, pull_request, workflow_dispatch] 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: '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: 'arm64' 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: 'arm64' 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: 'arm64' tox_env: 'py311' os: 'macos-latest' - name: 'py312 (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' os: 'ubuntu-latest' - name: 'py312 (windows)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' os: 'windows-latest' - name: 'py312 (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312' os: 'macos-latest' - name: 'py313 (ubuntu)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313' os: 'ubuntu-latest' - name: 'py313 (windows)' python: '3.13' toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313' os: 'windows-latest' - name: 'py313 (macos)' python: '3.13' toxpython: 'python3.13' python_arch: 'arm64' tox_env: 'py313' 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: 'arm64' tox_env: 'pypy39' os: 'macos-latest' - name: 'pypy310 (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'ubuntu-latest' - name: 'pypy310 (windows)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'windows-latest' - name: 'pypy310 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310' os: 'macos-latest' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 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 finish: needs: test if: ${{ always() }} runs-on: ubuntu-latest steps: - uses: codecov/codecov-action@v3 with: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/.pre-commit-config.yaml0000644000175000017500000000122614772504501016134 0ustar00ionelionel# 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/badsyntax.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.11.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/.readthedocs.yml0000664000175000017500000000043214772504455014751 0ustar00ionelionel# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py formats: all build: os: ubuntu-22.04 tools: python: "3" python: install: - requirements: docs/requirements.txt - method: pip path: . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/AUTHORS.rst0000644000175000017500000000116414772504501013533 0ustar00ionelionelAuthors ======= * Ionel Cristian Mărieș - https://blog.ionelmc.ro * Arcadiy Ivanov - https://github.com/arcivanov * Beckjake - https://github.com/beckjake * DRayX - https://github.com/DRayX * Jason Madden - https://github.com/jamadden * Jon Dufresne - https://github.com/jdufresne * Elliott Sales de Andrade - https://github.com/QuLogic * Victor Stinner - https://github.com/vstinner * Guido Imperiale - https://github.com/crusaderky * Alisa Sireneva - https://github.com/purplesyringa * Michał Górny - https://github.com/mgorny * Tim Maxwell - https://github.com/tmaxwell-anthropic * Haoyu Weng - https://github.com/wengh ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425844.0 tblib-3.1.0/CHANGELOG.rst0000644000175000017500000000573114772510464013706 0ustar00ionelionel Changelog ========= 3.1.0 (2025-03-31) ~~~~~~~~~~~~~~~~~~ * Improved performance of ``as_traceback`` by a large factor. Contributed by Haoyu Weng in `#81 `_. 3.0.0 (2023-10-22) ~~~~~~~~~~~~~~~~~~ * Added support for ``__context__``, ``__suppress_context__`` and ``__notes__``. Contributed by Tim Maxwell in `#72 `_. * Added the ``get_locals`` argument to ``tblib.pickling_support.install()``, ``tblib.Traceback`` and ``tblib.Frame``. Fixes `#41 `_. * Dropped support for now-EOL Python 3.7 and added 3.12 in the test grid. 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 `_. 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 `_. 1.5.0 (2019-10-23) ~~~~~~~~~~~~~~~~~~ * Added support for Python 3.8. Contributed by Victor Stinner in `#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 `_. * 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 `_. 1.3.1 (2017-03-27) ~~~~~~~~~~~~~~~~~~ * Fixed handling for tracebacks due to exceeding the recursion limit. Fixes `#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 `_ and `#11 `_. 1.1.0 (2015-07-27) ~~~~~~~~~~~~~~~~~~ * Added support for Python 2.6. Contributed by Arcadiy Ivanov in `#8 `_. 1.0.0 (2015-03-30) ~~~~~~~~~~~~~~~~~~ * Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks. Contributed by beckjake in `#5 `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/CONTRIBUTING.rst0000644000175000017500000000443514772504455014331 0ustar00ionelionel============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. Bug reports =========== When `reporting a bug `_ please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Documentation improvements ========================== tblib could always use more documentation, whether as part of the official tblib docs, in docstrings, or even on the web in blog posts, articles, and such. Feature requests and feedback ============================= The best way to send feedback is to file an issue at https://github.com/ionelmc/python-tblib/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that code contributions are welcome :) Development =========== To set up `python-tblib` for local development: 1. Fork `python-tblib `_ (look for the "Fork" button). 2. Clone your fork locally:: git clone git@github.com:YOURGITHUBNAME/python-tblib.git 3. Create a branch for local development:: git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 4. When you're done making changes run all the checks and docs builder with one command:: tox 5. Commit your changes and push your branch to GitHub:: git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature 6. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- If you need some code review or feedback while you're developing the code just make the pull request. For merging, you should: 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``. Tips ---- To run a subset of tests:: tox -e envname -- pytest -k test_myfeature To run all the test environments in *parallel*:: tox -p auto ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/LICENSE0000644000175000017500000000246214772504501012663 0ustar00ionelionelBSD 2-Clause License Copyright (c) 2013-2023, 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: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/MANIFEST.in0000644000175000017500000000065314772504455013424 0ustar00ionelionelgraft docs graft src graft ci graft tests include .bumpversion.cfg 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 include CONTRIBUTING.rst include LICENSE include README.rst global-exclude *.py[cod] __pycache__/* *.so *.dylib ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8834891 tblib-3.1.0/PKG-INFO0000644000175000017500000006243014772510556012763 0ustar00ionelionelMetadata-Version: 2.4 Name: tblib Version: 3.1.0 Summary: Traceback serialization library. Home-page: https://github.com/ionelmc/python-tblib Author: Ionel Cristian Mărieș Author-email: contact@ionelmc.ro 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 Keywords: traceback,debugging,exceptions Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities Requires-Python: >=3.9 License-File: LICENSE License-File: AUTHORS.rst Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: project-url Dynamic: requires-python Dynamic: summary ======== Overview ======== Serialization library for Exceptions and Tracebacks. * Free software: BSD license It allows you to: * `Pickle `_ 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'...), ) >>> pickle.loads(s2) (<...Exception'>, Exception('fail'...), ) >>> pickle.loads(s3) (<...Exception'>, Exception('fail'...), ) Raising ~~~~~~~ :: >>> from six import reraise >>> reraise(*pickle.loads(s1)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s2)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s3)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 3, in 1 / 0 ZeroDivisionError: division by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File "", line 1, in raise pickle.loads(s) File "", line 5, in 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], ...) 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 `_, `Dask.distributed `_, 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 "", line 1, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", line 2, in local_0 reraise(*exc_info) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in bad_module() File "...tests...examples.py", line 23, in bad_module import badmodule File "...tests...badmodule.py", line 3, in 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 "", line 1, in f def f(): f() File "", line 1, in f def f(): f() File "", 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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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': '', 'co_name': ''}, '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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 6, in 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 "", line 6, in 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 >>> e.reraise() Traceback (most recent call last): ... File "", line 1, in 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 "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 2, in 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 ! >>> 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 "", line 4, in 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 ! >>> 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 "", line 2, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", 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 ! 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 `_ for figuring a way to create traceback objects. Changelog ========= 3.1.0 (2025-03-31) ~~~~~~~~~~~~~~~~~~ * Improved performance of ``as_traceback`` by a large factor. Contributed by Haoyu Weng in `#81 `_. 3.0.0 (2023-10-22) ~~~~~~~~~~~~~~~~~~ * Added support for ``__context__``, ``__suppress_context__`` and ``__notes__``. Contributed by Tim Maxwell in `#72 `_. * Added the ``get_locals`` argument to ``tblib.pickling_support.install()``, ``tblib.Traceback`` and ``tblib.Frame``. Fixes `#41 `_. * Dropped support for now-EOL Python 3.7 and added 3.12 in the test grid. 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 `_. 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 `_. 1.5.0 (2019-10-23) ~~~~~~~~~~~~~~~~~~ * Added support for Python 3.8. Contributed by Victor Stinner in `#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 `_. * 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 `_. 1.3.1 (2017-03-27) ~~~~~~~~~~~~~~~~~~ * Fixed handling for tracebacks due to exceeding the recursion limit. Fixes `#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 `_ and `#11 `_. 1.1.0 (2015-07-27) ~~~~~~~~~~~~~~~~~~ * Added support for Python 2.6. Contributed by Arcadiy Ivanov in `#8 `_. 1.0.0 (2015-03-30) ~~~~~~~~~~~~~~~~~~ * Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks. Contributed by beckjake in `#5 `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/README.rst0000644000175000017500000005461214772510520013347 0ustar00ionelionel======== Overview ======== .. start-badges .. list-table:: :stub-columns: 1 * - docs - |docs| * - tests - |github-actions| |codecov| * - package - |version| |wheel| |supported-versions| |supported-implementations| |commits-since| .. |docs| image:: https://readthedocs.org/projects/python-tblib/badge/?style=flat :target: https://readthedocs.org/projects/python-tblib/ :alt: Documentation Status .. |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/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master :alt: Coverage Status :target: https://app.codecov.io/github/ionelmc/python-tblib .. |version| image:: https://img.shields.io/pypi/v/tblib.svg :alt: PyPI Package latest release :target: https://pypi.org/project/tblib .. |wheel| image:: https://img.shields.io/pypi/wheel/tblib.svg :alt: PyPI Wheel :target: https://pypi.org/project/tblib .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/tblib.svg :alt: Supported versions :target: https://pypi.org/project/tblib .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/tblib.svg :alt: Supported implementations :target: https://pypi.org/project/tblib .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v3.1.0.svg :alt: Commits since latest release :target: https://github.com/ionelmc/python-tblib/compare/v3.1.0...master .. end-badges Serialization library for Exceptions and Tracebacks. * Free software: BSD license It allows you to: * `Pickle `_ 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'...), ) >>> pickle.loads(s2) (<...Exception'>, Exception('fail'...), ) >>> pickle.loads(s3) (<...Exception'>, Exception('fail'...), ) Raising ~~~~~~~ :: >>> from six import reraise >>> reraise(*pickle.loads(s1)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s2)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s3)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 3, in 1 / 0 ZeroDivisionError: division by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File "", line 1, in raise pickle.loads(s) File "", line 5, in 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], ...) 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 `_, `Dask.distributed `_, 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 "", line 1, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", line 2, in local_0 reraise(*exc_info) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in bad_module() File "...tests...examples.py", line 23, in bad_module import badmodule File "...tests...badmodule.py", line 3, in 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 "", line 1, in f def f(): f() File "", line 1, in f def f(): f() File "", 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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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': '', 'co_name': ''}, '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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 6, in 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 "", line 6, in 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 >>> e.reraise() Traceback (most recent call last): ... File "", line 1, in 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 "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 2, in 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 ! >>> 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 "", line 4, in 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 ! >>> 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 "", line 2, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", 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 ! 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 `_ for figuring a way to create traceback objects. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8806763 tblib-3.1.0/ci/0000755000175000017500000000000014772510556012254 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/ci/bootstrap.py0000755000175000017500000000546314772504501014647 0ustar00ionelionel#!/usr/bin/env python import os import pathlib import subprocess import sys base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent templates_path = base_path / 'ci' / 'templates' def check_call(args): print('+', *args) subprocess.check_call(args) def exec_in_env(): env_path = base_path / '.tox' / 'bootstrap' if sys.platform == 'win32': bin_path = env_path / 'Scripts' else: bin_path = env_path / 'bin' if not env_path.exists(): import subprocess print(f'Making bootstrap env in: {env_path} ...') try: check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: 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']) def main(): import jinja2 print(f'Project path: {base_path}') jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, ) tox_environments = [ line.strip() # 'tox' need not be installed globally, but must be importable # by the Python that is running this script. # This uses sys.executable the same way that the call in # cookiecutter-pylibrary/hooks/post_gen_project.py # invokes this bootstrap.py itself. 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.') if __name__ == '__main__': args = sys.argv[1:] if args == ['--no-env']: main() elif not args: exec_in_env() else: print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/ci/requirements.txt0000644000175000017500000000007414772504455015542 0ustar00ionelionelvirtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 tox twine ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8775403 tblib-3.1.0/ci/templates/0000755000175000017500000000000014772510556014252 5ustar00ionelionel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8775403 tblib-3.1.0/ci/templates/.github/0000755000175000017500000000000014772510556015612 5ustar00ionelionel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8807716 tblib-3.1.0/ci/templates/.github/workflows/0000755000175000017500000000000014772510556017647 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/ci/templates/.github/workflows/github-actions.yml0000644000175000017500000000423514772504455023317 0ustar00ionelionelname: build on: [push, pull_request, workflow_dispatch] 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', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' toxpython: '{{ toxpython }}' python_arch: '{{ python_arch }}' tox_env: '{{ env }}' os: '{{ os }}-latest' {% endfor %} {% endfor %} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 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 finish: needs: test if: {{ '${{ always() }}' }} runs-on: ubuntu-latest steps: - uses: codecov/codecov-action@v3 with: CODECOV_TOKEN: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8817234 tblib-3.1.0/docs/0000755000175000017500000000000014772510556012611 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/authors.rst0000664000175000017500000000003414772504455015030 0ustar00ionelionel.. include:: ../AUTHORS.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/changelog.rst0000664000175000017500000000003614772504455015274 0ustar00ionelionel.. include:: ../CHANGELOG.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/docs/conf.py0000644000175000017500000000170314772510520014100 0ustar00ionelionelextensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage', 'sphinx.ext.doctest', 'sphinx.ext.extlinks', 'sphinx.ext.ifconfig', 'sphinx.ext.napoleon', 'sphinx.ext.todo', 'sphinx.ext.viewcode', ] source_suffix = '.rst' master_doc = 'index' project = 'tblib' year = '2013-2023' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' version = release = '3.1.0' pygments_style = 'trac' templates_path = ['.'] extlinks = { 'issue': ('https://github.com/ionelmc/python-tblib/issues/%s', '#%s'), 'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #%s'), } html_theme = 'furo' html_theme_options = { 'githuburl': 'https://github.com/ionelmc/python-tblib/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False html_short_title = f'{project}-{version}' napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/contributing.rst0000664000175000017500000000004114772504455016050 0ustar00ionelionel.. include:: ../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/docs/index.rst0000644000175000017500000000036214772504501014444 0ustar00ionelionel======== Contents ======== .. toctree:: :maxdepth: 2 readme installation usage contributing autoapi/index authors changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/installation.rst0000664000175000017500000000012514772504455016045 0ustar00ionelionel============ Installation ============ At the command line:: pip install tblib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/readme.rst0000664000175000017500000000003314772504455014577 0ustar00ionelionel.. include:: ../README.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425579.0 tblib-3.1.0/docs/requirements.txt0000644000175000017500000000002114772510053016056 0ustar00ionelionelsphinx>=1.3 furo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/docs/spelling_wordlist.txt0000664000175000017500000000015514772504455017122 0ustar00ionelionelbuiltin builtins classmethod staticmethod classmethods staticmethods args kwargs callstack Changelog Indices ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/docs/usage.rst0000644000175000017500000000007614772504501014443 0ustar00ionelionel===== Usage ===== To use tblib in a project:: import tblib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423789.0 tblib-3.1.0/pyproject.toml0000644000175000017500000000244014772504455014576 0ustar00ionelionel[build-system] requires = [ "setuptools>=40.1.0", ] [tool.ruff] extend-exclude = ["static", "ci/templates"] line-length = 140 src = ["src", "tests"] target-version = "py39" [tool.ruff.lint.per-file-ignores] "ci/*" = ["S"] [tool.ruff.lint] ignore = [ "RUF001", # ruff-specific rules ambiguous-unicode-character-string "S101", # flake8-bandit assert "S308", # flake8-bandit suspicious-mark-safe-usage "S603", # flake8-bandit subprocess-without-shell-equals-true "S607", # flake8-bandit start-process-with-partial-path "E501", # pycodestyle line-too-long ] 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 "RSE", # flake8-raise "RUF", # ruff-specific rules "S", # flake8-bandit "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [tool.ruff.lint.isort] forced-separate = ["conftest"] force-single-line = true [tool.ruff.format] quote-style = "single" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/pytest.ini0000644000175000017500000000153214772504501013704 0ustar00ionelionel[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 --benchmark-disable 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! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8837378 tblib-3.1.0/setup.cfg0000644000175000017500000000004614772510556013502 0ustar00ionelionel[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/setup.py0000755000175000017500000000520514772510520013367 0ustar00ionelionel#!/usr/bin/env python import re from pathlib import Path from setuptools import find_namespace_packages from setuptools import setup def read(*names, **kwargs): with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() setup( name='tblib', version='3.1.0', license='BSD-2-Clause', description='Traceback serialization library.', 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')), ), author='Ionel Cristian Mărieș', author_email='contact@ionelmc.ro', url='https://github.com/ionelmc/python-tblib', packages=find_namespace_packages('src'), package_dir={'': 'src'}, py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, zip_safe=False, classifiers=[ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Operating System :: Unix', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', # uncomment if you test on these interpreters: # "Programming Language :: Python :: Implementation :: IronPython", # "Programming Language :: Python :: Implementation :: Jython", # "Programming Language :: Python :: Implementation :: Stackless", 'Topic :: Utilities', ], project_urls={ 'Documentation': 'https://python-tblib.readthedocs.io/', 'Changelog': 'https://python-tblib.readthedocs.io/en/latest/changelog.html', 'Issue Tracker': 'https://github.com/ionelmc/python-tblib/issues', }, keywords=[ 'traceback', 'debugging', 'exceptions', ], python_requires='>=3.9', install_requires=[ # eg: "aspectlib==1.1.1", ], extras_require={ # eg: # "rst": ["docutils>=0.11"], # ":python_version=="2.6"": ["argparse"], }, ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1743425901.878181 tblib-3.1.0/src/0000755000175000017500000000000014772510556012450 5ustar00ionelionel././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8819993 tblib-3.1.0/src/tblib/0000755000175000017500000000000014772510556013544 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425872.0 tblib-3.1.0/src/tblib/__init__.py0000644000175000017500000001675614772510520015663 0ustar00ionelionelimport re import sys __version__ = '3.1.0' __all__ = 'Code', 'Frame', 'Traceback', 'TracebackParseError' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') class _AttrDict(dict): __slots__ = () def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError(name) from None # noinspection PyPep8Naming class __traceback_maker(Exception): pass class TracebackParseError(Exception): pass 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): self.co_filename = code.co_filename self.co_name = code.co_name self.co_argcount = 0 self.co_kwonlyargcount = 0 self.co_varnames = () self.co_nlocals = 0 self.co_stacksize = 0 self.co_flags = 64 self.co_firstlineno = 0 class Frame: """ Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering. Args: get_locals (callable): A function that take a frame argument and returns a dict. See :class:`Traceback` class for example. """ def __init__(self, frame, *, get_locals=None): self.f_locals = {} if get_locals is None else get_locals(frame) 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 def clear(self): """ For compatibility with PyPy 3.5; clear() was added to frame in Python 3.4 and is called by traceback.clear_frames(), which in turn is called by unittest.TestCase.assertRaises """ class Traceback: """ Class that wraps builtin Traceback objects. Args: get_locals (callable): A function that take a frame argument and returns a dict. Ideally you will only return exactly what you need, and only with simple types that can be json serializable. Example: .. code:: python def get_locals(frame): if frame.f_locals.get("__tracebackhide__"): return {"__tracebackhide__": True} else: return {} """ tb_next = None def __init__(self, tb, *, get_locals=None): self.tb_frame = Frame(tb.tb_frame, get_locals=get_locals) self.tb_lineno = int(tb.tb_lineno) # Build in place to avoid exceeding the recursion limit tb = tb.tb_next prev_traceback = self cls = type(self) while tb is not None: traceback = object.__new__(cls) traceback.tb_frame = Frame(tb.tb_frame, get_locals=get_locals) traceback.tb_lineno = int(tb.tb_lineno) prev_traceback.tb_next = traceback prev_traceback = traceback tb = tb.tb_next def as_traceback(self): """ Convert to a builtin Traceback object that is usable for raising or rendering a stacktrace. """ current = self top_tb = None tb = None stub = compile( 'raise __traceback_maker', '', 'exec', ) while current: f_code = current.tb_frame.f_code code = stub.replace( co_firstlineno=current.tb_lineno, co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=(), ) # noinspection PyBroadException try: exec(code, dict(current.tb_frame.f_globals), dict(current.tb_frame.f_locals)) # 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.tb_next = next_tb tb = next_tb del next_tb current = current.tb_next try: return top_tb finally: del top_tb del tb to_traceback = as_traceback def as_dict(self): """ Converts to a dictionary representation. You can serialize the result to JSON as it only has builtin objects like dicts, lists, ints or strings. """ if self.tb_next is None: tb_next = None else: tb_next = self.tb_next.as_dict() code = { 'co_filename': self.tb_frame.f_code.co_filename, 'co_name': self.tb_frame.f_code.co_name, } frame = { 'f_globals': self.tb_frame.f_globals, 'f_locals': self.tb_frame.f_locals, 'f_code': code, 'f_lineno': self.tb_frame.f_lineno, } return { 'tb_frame': frame, 'tb_lineno': self.tb_lineno, 'tb_next': tb_next, } to_dict = as_dict @classmethod def from_dict(cls, dct): """ Creates an instance from a dictionary with the same structure as ``.as_dict()`` returns. """ if dct['tb_next']: tb_next = cls.from_dict(dct['tb_next']) else: tb_next = None code = _AttrDict( co_filename=dct['tb_frame']['f_code']['co_filename'], co_name=dct['tb_frame']['f_code']['co_name'], ) frame = _AttrDict( f_globals=dct['tb_frame']['f_globals'], f_locals=dct['tb_frame'].get('f_locals', {}), f_code=code, f_lineno=dct['tb_frame']['f_lineno'], ) tb = _AttrDict( tb_frame=frame, tb_lineno=dct['tb_lineno'], tb_next=tb_next, ) return cls(tb, get_locals=get_all_locals) @classmethod def from_string(cls, string, strict=True): """ Creates an instance by parsing a stacktrace. Strict means that parsing stops when lines are not indented by at least two spaces anymore. """ frames = [] header = strict for line in string.splitlines(): line = line.rstrip() if header: if line == 'Traceback (most recent call last):': header = False continue frame_match = FRAME_RE.match(line) if frame_match: frames.append(frame_match.groupdict()) elif line.startswith(' '): pass elif strict: break # traceback ended if frames: previous = None for frame in reversed(frames): previous = _AttrDict( frame, tb_frame=_AttrDict( frame, f_globals=_AttrDict( __file__=frame['co_filename'], __name__='?', ), f_locals={}, f_code=_AttrDict(frame), f_lineno=int(frame['tb_lineno']), ), tb_next=previous, ) return cls(previous) else: raise TracebackParseError(f'Could not find any frames in {string!r}.') def get_all_locals(frame): return dict(frame.f_locals) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743417670.0 tblib-3.1.0/src/tblib/decorators.py0000644000175000017500000000241714772470506016267 0ustar00ionelionelimport sys from functools import wraps from . import Traceback def reraise(tp, value, tb=None): try: if value is None: value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value finally: value = None tb = None class Error: def __init__(self, exc_type, exc_value, traceback): self.exc_type = exc_type self.exc_value = exc_value self.__traceback = Traceback(traceback) @property def traceback(self): return self.__traceback.as_traceback() def reraise(self): reraise(self.exc_type, self.exc_value, self.traceback) def return_error(func, exc_type=Exception): @wraps(func) def return_exceptions_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except exc_type: return Error(*sys.exc_info()) return return_exceptions_wrapper returns_error = return_errors = returns_errors = return_error # cause I make too many typos @return_error def apply_with_return_error(args): """ args is a tuple where the first argument is a callable. eg:: apply_with_return_error((func, 1, 2, 3)) - this will call func(1, 2, 3) """ return args[0](*args[1:]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423166.0 tblib-3.1.0/src/tblib/pickling_support.py0000644000175000017500000001050014772503276017507 0ustar00ionelionelimport copyreg from functools import partial from types import TracebackType from . import Frame from . import Traceback def unpickle_traceback(tb_frame, tb_lineno, tb_next): ret = object.__new__(Traceback) ret.tb_frame = tb_frame ret.tb_lineno = tb_lineno ret.tb_next = tb_next return ret.as_traceback() def pickle_traceback(tb, *, get_locals=None): return unpickle_traceback, ( Frame(tb.tb_frame, get_locals=get_locals), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next, get_locals=get_locals), ) # Note: Older versions of tblib will generate pickle archives that call unpickle_exception() with # fewer arguments. We assign default values to some of the arguments to support this. def unpickle_exception(func, args, cause, tb, context=None, suppress_context=False, notes=None): inst = func(*args) inst.__cause__ = cause inst.__traceback__ = tb inst.__context__ = context inst.__suppress_context__ = suppress_context if notes is not None: inst.__notes__ = notes return inst def pickle_exception(obj): # All exceptions, unlike generic Python objects, define __reduce_ex__ # __reduce_ex__(4) should be no different from __reduce_ex__(3). # __reduce_ex__(5) could bring benefits in the unlikely case the exception # directly contains buffers, but PickleBuffer objects will cause a crash when # running on protocol=4, and there's no clean way to figure out the current # protocol from here. Note that any object returned by __reduce_ex__(3) will # 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) assert len(rv) >= 2 return ( unpickle_exception, rv[:2] + ( obj.__cause__, obj.__traceback__, obj.__context__, obj.__suppress_context__, # __notes__ doesn't exist prior to Python 3.11; and even on Python 3.11 it may be absent getattr(obj, '__notes__', None), ), ) + rv[2:] def _get_subclasses(cls): # Depth-first traversal of all direct and indirect subclasses of cls to_visit = [cls] while to_visit: this = to_visit.pop() yield this to_visit += list(this.__subclasses__()) def install(*exc_classes_or_instances, get_locals=None): """ Args: get_locals (callable): A function that take a frame argument and returns a dict. See :class:`tblib.Traceback` class for example. """ copyreg.pickle(TracebackType, partial(pickle_traceback, get_locals=get_locals)) if not exc_classes_or_instances: for exception_cls in _get_subclasses(BaseException): copyreg.pickle(exception_cls, pickle_exception) return for exc in exc_classes_or_instances: if isinstance(exc, BaseException): _install_for_instance(exc, set()) elif isinstance(exc, type) and issubclass(exc, BaseException): copyreg.pickle(exc, pickle_exception) # Allow using @install as a decorator for Exception classes if len(exc_classes_or_instances) == 1: return exc else: raise TypeError(f'Expected subclasses or instances of BaseException, got {type(exc)}') def _install_for_instance(exc, seen): assert isinstance(exc, BaseException) # Prevent infinite recursion if we somehow get a self-referential exception. (Self-referential # exceptions should never normally happen, but if it did somehow happen, we want to pickle the # exception faithfully so the developer can troubleshoot why it happened.) if id(exc) in seen: return seen.add(id(exc)) copyreg.pickle(type(exc), pickle_exception) if exc.__cause__ is not None: _install_for_instance(exc.__cause__, seen) if exc.__context__ is not None: _install_for_instance(exc.__context__, seen) # This case is meant to cover BaseExceptionGroup on Python 3.11 as well as backports like the # exceptiongroup module if hasattr(exc, 'exceptions') and isinstance(exc.exceptions, (tuple, list)): for subexc in exc.exceptions: if isinstance(subexc, BaseException): _install_for_instance(subexc, seen) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8832479 tblib-3.1.0/src/tblib.egg-info/0000755000175000017500000000000014772510556015236 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425901.0 tblib-3.1.0/src/tblib.egg-info/PKG-INFO0000644000175000017500000006243014772510555016337 0ustar00ionelionelMetadata-Version: 2.4 Name: tblib Version: 3.1.0 Summary: Traceback serialization library. Home-page: https://github.com/ionelmc/python-tblib Author: Ionel Cristian Mărieș Author-email: contact@ionelmc.ro 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 Keywords: traceback,debugging,exceptions Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities Requires-Python: >=3.9 License-File: LICENSE License-File: AUTHORS.rst Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: project-url Dynamic: requires-python Dynamic: summary ======== Overview ======== Serialization library for Exceptions and Tracebacks. * Free software: BSD license It allows you to: * `Pickle `_ 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'...), ) >>> pickle.loads(s2) (<...Exception'>, Exception('fail'...), ) >>> pickle.loads(s3) (<...Exception'>, Exception('fail'...), ) Raising ~~~~~~~ :: >>> from six import reraise >>> reraise(*pickle.loads(s1)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s2)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", line 2, in inner_0 raise Exception('fail') Exception: fail >>> reraise(*pickle.loads(s3)) Traceback (most recent call last): ... File "", line 1, in reraise(*pickle.loads(s2)) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 3, in 1 / 0 ZeroDivisionError: division by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File "", line 1, in raise pickle.loads(s) File "", line 5, in 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], ...) 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 `_, `Dask.distributed `_, 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 "", line 1, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", line 2, in local_0 reraise(*exc_info) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in 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 "", line 1, in reraise(et, ev, tb.as_traceback()) File "", line 2, in bad_module() File "...tests...examples.py", line 23, in bad_module import badmodule File "...tests...badmodule.py", line 3, in 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 "", line 1, in f def f(): f() File "", line 1, in f def f(): f() File "", 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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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': '', 'co_name': ''}, '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 "", line 6, in reraise(et, ev, tb.as_traceback()) File "", line 2, in inner_2() File "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 6, in 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 "", line 6, in 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 >>> e.reraise() Traceback (most recent call last): ... File "", line 1, in 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 "", line 2, in inner_2 inner_1() File "", line 2, in inner_1 inner_0() File "", 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 "", line 2, in 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 ! >>> 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 "", line 4, in 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 ! >>> 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 "", line 2, in local_2() File "", line 2, in local_2 local_1() File "", line 2, in local_1 local_0() File "", 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 ! 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 `_ for figuring a way to create traceback objects. Changelog ========= 3.1.0 (2025-03-31) ~~~~~~~~~~~~~~~~~~ * Improved performance of ``as_traceback`` by a large factor. Contributed by Haoyu Weng in `#81 `_. 3.0.0 (2023-10-22) ~~~~~~~~~~~~~~~~~~ * Added support for ``__context__``, ``__suppress_context__`` and ``__notes__``. Contributed by Tim Maxwell in `#72 `_. * Added the ``get_locals`` argument to ``tblib.pickling_support.install()``, ``tblib.Traceback`` and ``tblib.Frame``. Fixes `#41 `_. * Dropped support for now-EOL Python 3.7 and added 3.12 in the test grid. 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 `_. 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 `_. 1.5.0 (2019-10-23) ~~~~~~~~~~~~~~~~~~ * Added support for Python 3.8. Contributed by Victor Stinner in `#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 `_. * 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 `_. 1.3.1 (2017-03-27) ~~~~~~~~~~~~~~~~~~ * Fixed handling for tracebacks due to exceeding the recursion limit. Fixes `#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 `_ and `#11 `_. 1.1.0 (2015-07-27) ~~~~~~~~~~~~~~~~~~ * Added support for Python 2.6. Contributed by Arcadiy Ivanov in `#8 `_. 1.0.0 (2015-03-30) ~~~~~~~~~~~~~~~~~~ * Added ``to_dict`` method and ``from_dict`` classmethod on Tracebacks. Contributed by beckjake in `#5 `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425901.0 tblib-3.1.0/src/tblib.egg-info/SOURCES.txt0000644000175000017500000000162314772510555017123 0ustar00ionelionel.bumpversion.cfg .cookiecutterrc .coveragerc .editorconfig .pre-commit-config.yaml .readthedocs.yml AUTHORS.rst CHANGELOG.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst pyproject.toml pytest.ini setup.py tox.ini .github/workflows/github-actions.yml ci/bootstrap.py ci/requirements.txt ci/templates/.github/workflows/github-actions.yml docs/authors.rst docs/changelog.rst docs/conf.py docs/contributing.rst docs/index.rst docs/installation.rst docs/readme.rst docs/requirements.txt docs/spelling_wordlist.txt docs/usage.rst src/tblib/__init__.py src/tblib/decorators.py src/tblib/pickling_support.py src/tblib.egg-info/PKG-INFO src/tblib.egg-info/SOURCES.txt src/tblib.egg-info/dependency_links.txt src/tblib.egg-info/not-zip-safe src/tblib.egg-info/top_level.txt tests/badmodule.py tests/badsyntax.py tests/examples.py tests/test_issue30.py tests/test_perf.py tests/test_pickle_exception.py tests/test_tblib.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425901.0 tblib-3.1.0/src/tblib.egg-info/dependency_links.txt0000644000175000017500000000000114772510555021303 0ustar00ionelionel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425900.0 tblib-3.1.0/src/tblib.egg-info/not-zip-safe0000644000175000017500000000000114772510554017462 0ustar00ionelionel ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743425901.0 tblib-3.1.0/src/tblib.egg-info/top_level.txt0000644000175000017500000000000614772510555017763 0ustar00ionelioneltblib ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1743425901.8831418 tblib-3.1.0/tests/0000755000175000017500000000000014772510556013023 5ustar00ionelionel././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423103.0 tblib-3.1.0/tests/badmodule.py0000644000175000017500000000004514772503177015331 0ustar00ionelionela = 1 b = 2 raise Exception('boom!') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1556796189.0 tblib-3.1.0/tests/badsyntax.py0000664000175000017500000000005713462551435015372 0ustar00ionelionel# flake8: noqa """ bad bad bad """ is very bad ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423103.0 tblib-3.1.0/tests/examples.py0000644000175000017500000000040114772503177015207 0ustar00ionelioneldef func_a(_): func_b() def func_b(): func_c() def func_c(): func_d() def func_d(): raise Exception('Guessing time !') def bad_syntax(): import badsyntax badsyntax() def bad_module(): import badmodule badmodule() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423313.0 tblib-3.1.0/tests/test_issue30.py0000644000175000017500000000076114772503521015725 0ustar00ionelionelimport pickle import sys import pytest from tblib import pickling_support pytest.importorskip('twisted') def test_30(): from twisted.python.failure import Failure pickling_support.install() try: raise ValueError except ValueError: s = pickle.dumps(sys.exc_info()) f = None try: etype, evalue, etb = pickle.loads(s) # noqa: S301 raise evalue.with_traceback(etb) except ValueError: f = Failure() assert f is not None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743422852.0 tblib-3.1.0/tests/test_perf.py0000644000175000017500000000117314772502604015365 0ustar00ionelionelfrom tblib import Traceback EXAMPLE = """ Traceback (most recent call last): File "file1", line 9999, in code1 File "file2", line 9999, in code2 File "file3", line 9999, in code3 File "file4", line 9999, in code4 File "file5", line 9999, in code5 File "file6", line 9999, in code6 File "file7", line 9999, in code7 File "file8", line 9999, in code8 File "file9", line 9999, in code9 """ def test_perf(benchmark): @benchmark def run(): Traceback.from_string(EXAMPLE).as_traceback() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423367.0 tblib-3.1.0/tests/test_pickle_exception.py0000644000175000017500000001200314772503607017754 0ustar00ionelionelfrom traceback import format_exception try: import copyreg except ImportError: # Python 2 import copy_reg as copyreg import pickle import sys import pytest import tblib.pickling_support has_python311 = sys.version_info >= (3, 11) @pytest.fixture def clear_dispatch_table(): bak = copyreg.dispatch_table.copy() copyreg.dispatch_table.clear() yield None copyreg.dispatch_table.clear() copyreg.dispatch_table.update(bak) class CustomError(Exception): pass def strip_locations(tb_text): return tb_text.replace(' ~~^~~\n', '').replace(' ^^^^^^^^^^^^^^^^^\n', '') @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': tblib.pickling_support.install() elif how == 'class': tblib.pickling_support.install(CustomError, ValueError, ZeroDivisionError) try: try: try: 1 / 0 # noqa: B018 finally: # The ValueError's __context__ will be the ZeroDivisionError raise ValueError('blah') except Exception as e: # Python 3 only syntax # raise CustomError("foo") from e new_e = CustomError('foo') new_e.__cause__ = e if has_python311: new_e.add_note('note 1') new_e.add_note('note 2') raise new_e from e except Exception as e: exc = e else: raise AssertionError expected_format_exception = strip_locations(''.join(format_exception(type(exc), exc, exc.__traceback__))) # Populate Exception.__dict__, which is used in some cases exc.x = 1 exc.__cause__.x = 2 exc.__cause__.__context__.x = 3 if how == 'instance': tblib.pickling_support.install(exc) if protocol: exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 assert isinstance(exc, CustomError) assert exc.args == ('foo',) assert exc.x == 1 assert exc.__traceback__ is not None assert isinstance(exc.__cause__, ValueError) assert exc.__cause__.__traceback__ is not None assert exc.__cause__.x == 2 assert exc.__cause__.__cause__ is None assert isinstance(exc.__cause__.__context__, ZeroDivisionError) assert exc.__cause__.__context__.x == 3 assert exc.__cause__.__context__.__cause__ is None assert exc.__cause__.__context__.__context__ is None if has_python311: assert exc.__notes__ == ['note 1', 'note 2'] assert expected_format_exception == strip_locations(''.join(format_exception(type(exc), exc, exc.__traceback__))) @tblib.pickling_support.install class RegisteredError(Exception): pass def test_install_decorator(): with pytest.raises(RegisteredError) as ewrap: raise RegisteredError('foo') exc = ewrap.value exc.x = 1 exc = pickle.loads(pickle.dumps(exc)) # noqa: S301 assert isinstance(exc, RegisteredError) assert exc.args == ('foo',) assert exc.x == 1 assert exc.__traceback__ is not None @pytest.mark.skipif(not has_python311, reason='no BaseExceptionGroup before Python 3.11') def test_install_instance_recursively(clear_dispatch_table): exc = BaseExceptionGroup('test', [ValueError('foo'), CustomError('bar')]) # noqa: F821 exc.exceptions[0].__cause__ = ZeroDivisionError('baz') exc.exceptions[0].__cause__.__context__ = AttributeError('quux') tblib.pickling_support.install(exc) installed = {c for c in copyreg.dispatch_table if issubclass(c, BaseException)} assert installed == {ExceptionGroup, ValueError, CustomError, ZeroDivisionError, AttributeError} # noqa: F821 def test_install_typeerror(): with pytest.raises(TypeError): tblib.pickling_support.install('foo') @pytest.mark.parametrize('protocol', [None, *list(range(1, pickle.HIGHEST_PROTOCOL + 1))]) @pytest.mark.parametrize('how', ['global', 'instance', 'class']) def test_get_locals(clear_dispatch_table, how, protocol): def get_locals(frame): if 'my_variable' in frame.f_locals: return {'my_variable': int(frame.f_locals['my_variable'])} else: return {} if how == 'global': tblib.pickling_support.install(get_locals=get_locals) elif how == 'class': tblib.pickling_support.install(CustomError, ValueError, ZeroDivisionError, get_locals=get_locals) def func(my_arg='2'): my_variable = '1' raise ValueError(my_variable) try: func() except Exception as e: exc = e else: raise AssertionError f_locals = exc.__traceback__.tb_next.tb_frame.f_locals assert 'my_variable' in f_locals assert f_locals['my_variable'] == '1' if how == 'instance': tblib.pickling_support.install(exc, get_locals=get_locals) exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423242.0 tblib-3.1.0/tests/test_tblib.py0000644000175000017500000001376414772503412015534 0ustar00ionelionelimport pickle import traceback from tblib import Traceback from tblib import pickling_support pickling_support.install() pytest_plugins = ('pytester',) def test_get_locals(): def get_locals(frame): print(frame, frame.f_locals) if 'my_variable' in frame.f_locals: return {'my_variable': int(frame.f_locals['my_variable'])} else: return {} def func(my_arg='2'): my_variable = '1' raise ValueError(my_variable) try: func() except Exception as e: exc = e else: raise AssertionError f_locals = exc.__traceback__.tb_next.tb_frame.f_locals assert 'my_variable' in f_locals assert f_locals['my_variable'] == '1' value = Traceback(exc.__traceback__, get_locals=get_locals).as_dict() lineno = exc.__traceback__.tb_lineno assert value == { 'tb_frame': { 'f_globals': {'__name__': 'test_tblib', '__file__': __file__}, 'f_locals': {}, 'f_code': {'co_filename': __file__, 'co_name': 'test_get_locals'}, 'f_lineno': lineno + 10, }, 'tb_lineno': lineno, 'tb_next': { 'tb_frame': { 'f_globals': {'__name__': 'test_tblib', '__file__': __file__}, 'f_locals': {'my_variable': 1}, 'f_code': {'co_filename': __file__, 'co_name': 'func'}, 'f_lineno': lineno - 3, }, 'tb_lineno': lineno - 3, 'tb_next': None, }, } assert Traceback.from_dict(value).tb_next.tb_frame.f_locals == {'my_variable': 1} def test_parse_traceback(): tb1 = Traceback.from_string( """ Traceback (most recent call last): File "file1", line 123, in code1 File "file2", line 234, in ??? code2 File "file3", line 345, in function3 File "file4", line 456, in code4 KeyboardInterrupt""" ) pytb = tb1.as_traceback() assert traceback.format_tb(pytb) == [ ' File "file1", line 123, in \n', ' File "file2", line 234, in ???\n', ' File "file3", line 345, in function3\n', ] tb2 = Traceback(pytb) expected_dict = { 'tb_frame': { 'f_code': {'co_filename': 'file1', 'co_name': ''}, 'f_globals': {'__file__': 'file1', '__name__': '?'}, 'f_locals': {}, 'f_lineno': 123, }, 'tb_lineno': 123, 'tb_next': { 'tb_frame': { 'f_code': {'co_filename': 'file2', 'co_name': '???'}, 'f_globals': {'__file__': 'file2', '__name__': '?'}, 'f_locals': {}, 'f_lineno': 234, }, 'tb_lineno': 234, 'tb_next': { 'tb_frame': { 'f_code': {'co_filename': 'file3', 'co_name': 'function3'}, 'f_globals': {'__file__': 'file3', '__name__': '?'}, 'f_locals': {}, 'f_lineno': 345, }, 'tb_lineno': 345, 'tb_next': None, }, }, } tb3 = Traceback.from_dict(expected_dict) tb4 = pickle.loads(pickle.dumps(tb3)) # noqa: S301 assert tb4.as_dict() == tb3.as_dict() == tb2.as_dict() == tb1.as_dict() == expected_dict def test_large_line_number(): line_number = 2**31 - 1 tb1 = Traceback.from_string( f""" Traceback (most recent call last): File "file1", line {line_number}, in code1 """ ).as_traceback() assert tb1.tb_lineno == line_number def test_pytest_integration(testdir): test = testdir.makepyfile( """ from tblib import Traceback def test_raise(): tb1 = Traceback.from_string(''' Traceback (most recent call last): File "file1", line 123, in code1 File "file2", line 234, in ??? code2 File "file3", line 345, in function3 File "file4", line 456, in "" ''') pytb = tb1.as_traceback() 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 = testdir.runpytest_subprocess('--tb=short', '-vv', test) result.stdout.fnmatch_lines( [ 'test_pytest_integration.py:*: in test_raise', ' raise RuntimeError().with_traceback(pytb)', 'file1:123: in ', ' ???', '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 = 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', ' raise RuntimeError().with_traceback(pytb)', ' File "file1", line 123, in ', ' File "file2", line 234, in ???', ' File "file3", line 345, in function3', ' File "file4", line 456, in ""', 'RuntimeError', ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1743423809.0 tblib-3.1.0/tox.ini0000644000175000017500000000327414772504501013173 0ustar00ionelionel[testenv:bootstrap] deps = jinja2 tox skip_install = true commands = python ci/bootstrap.py --no-env passenv = * ; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] envlist = clean, check, docs, {py39,py310,py311,py312,py313,pypy39,pypy310}, report ignore_basepython_conflict = true [testenv] basepython = pypy39: {env:TOXPYTHON:pypy3.9} pypy310: {env:TOXPYTHON:pypy3.10} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} py313: {env:TOXPYTHON:python3.13} {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes PYTHONNODEBUGRANGES=yes passenv = * usedevelop = false deps = pytest pytest-cov pytest-benchmark commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} [testenv:check] deps = docutils check-manifest pre-commit readme-renderer pygments isort skip_install = true commands = python setup.py check --strict --metadata --restructuredtext check-manifest . pre-commit run --all-files --show-diff-on-failure [testenv:docs] usedevelop = true deps = -r{toxinidir}/docs/requirements.txt commands = sphinx-build {posargs:-E} -b html docs dist/docs sphinx-build -b linkcheck docs dist/docs [testenv:report] deps = coverage skip_install = true commands = coverage report coverage html [testenv:clean] commands = python setup.py clean coverage erase skip_install = true deps = setuptools coverage