From 70411ecb7d9d096fb1f8c51db1b7d7a3768b5e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 11 May 2022 13:02:38 +0300 Subject: [PATCH 01/57] Upgrade skel. Switch to github actions. --- .appveyor.yml | 78 ------- .bumpversion.cfg | 11 +- .cookiecutterrc | 85 ++++---- .editorconfig | 9 +- .github/workflows/github-actions.yml | 203 ++++++++++++++++++ .gitignore | 5 +- .pre-commit-config.yaml | 24 +++ .travis.yml | 56 ----- CONTRIBUTING.rst | 11 +- LICENSE | 2 +- MANIFEST.in | 9 +- README.rst | 19 +- ci/appveyor-with-compiler.cmd | 23 -- ci/bootstrap.py | 18 +- ci/requirements.txt | 3 +- ci/templates/.appveyor.yml | 49 ----- .../.github/workflows/github-actions.yml | 65 ++++++ ci/templates/.travis.yml | 46 ---- docs/conf.py | 11 +- pyproject.toml | 10 + pytest.ini | 30 +++ setup.cfg | 29 +-- setup.py | 14 +- tox.ini | 27 +-- 24 files changed, 454 insertions(+), 383 deletions(-) delete mode 100644 .appveyor.yml create mode 100644 .github/workflows/github-actions.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 .travis.yml delete mode 100644 ci/appveyor-with-compiler.cmd delete mode 100644 ci/templates/.appveyor.yml create mode 100644 ci/templates/.github/workflows/github-actions.yml delete mode 100644 ci/templates/.travis.yml create mode 100644 pyproject.toml create mode 100644 pytest.ini mode change 100644 => 100755 setup.py diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 71d0450..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,78 +0,0 @@ -version: '{branch}-{build}' -build: off -environment: - matrix: - - TOXENV: check - TOXPYTHON: C:\Python36\python.exe - PYTHON_HOME: C:\Python36 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '32' - - TOXENV: py27,codecov - TOXPYTHON: C:\Python27\python.exe - PYTHON_HOME: C:\Python27 - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '32' - - TOXENV: py27,codecov - TOXPYTHON: C:\Python27-x64\python.exe - PYTHON_HOME: C:\Python27-x64 - PYTHON_VERSION: '2.7' - PYTHON_ARCH: '64' - WINDOWS_SDK_VERSION: v7.0 - - TOXENV: py35,codecov - TOXPYTHON: C:\Python35\python.exe - PYTHON_HOME: C:\Python35 - PYTHON_VERSION: '3.5' - PYTHON_ARCH: '32' - - TOXENV: py35,codecov - TOXPYTHON: C:\Python35-x64\python.exe - PYTHON_HOME: C:\Python35-x64 - PYTHON_VERSION: '3.5' - PYTHON_ARCH: '64' - - TOXENV: py36,codecov - TOXPYTHON: C:\Python36\python.exe - PYTHON_HOME: C:\Python36 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '32' - - TOXENV: py36,codecov - TOXPYTHON: C:\Python36-x64\python.exe - PYTHON_HOME: C:\Python36-x64 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '64' - - TOXENV: py37,codecov - TOXPYTHON: C:\Python37\python.exe - PYTHON_HOME: C:\Python37 - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '32' - - TOXENV: py37,codecov - TOXPYTHON: C:\Python37-x64\python.exe - PYTHON_HOME: C:\Python37-x64 - PYTHON_VERSION: '3.7' - PYTHON_ARCH: '64' - - TOXENV: py38,codecov - TOXPYTHON: C:\Python38\python.exe - PYTHON_HOME: C:\Python38 - PYTHON_VERSION: '3.8' - PYTHON_ARCH: '32' - - TOXENV: py38,codecov - TOXPYTHON: C:\Python38-x64\python.exe - PYTHON_HOME: C:\Python38-x64 - PYTHON_VERSION: '3.8' - PYTHON_ARCH: '64' -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' - - '%PYTHON_HOME%\Scripts\virtualenv --version' - - '%PYTHON_HOME%\Scripts\easy_install --version' - - '%PYTHON_HOME%\Scripts\pip --version' - - '%PYTHON_HOME%\Scripts\tox --version' -test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox -on_failure: - - ps: dir "env:" - - ps: get-content .tox\*\log\* - -### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): -# on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1d8622c..edca607 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -7,9 +7,13 @@ tag = True search = version='{current_version}' replace = version='{new_version}' -[bumpversion:file:README.rst] -search = v{current_version}. -replace = v{new_version}. +[bumpversion:file (badge):README.rst] +search = /v{current_version}.svg +replace = /v{new_version}.svg + +[bumpversion:file (link):README.rst] +search = /v{current_version}...master +replace = /v{new_version}...master [bumpversion:file:docs/conf.py] search = version = release = '{current_version}' @@ -18,4 +22,3 @@ replace = version = release = '{new_version}' [bumpversion:file:src/tblib/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' - diff --git a/.cookiecutterrc b/.cookiecutterrc index 1836a3d..500f6e5 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,52 +1,57 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) -cookiecutter: - full_name: Ionel Cristian Mărieș - email: contact@ionelmc.ro - website: https://blog.ionelmc.ro/ - project_name: tblib - repo_name: python-tblib - repo_hosting: github.com - repo_hosting_domain: github.com - repo_username: ionelmc - package_name: tblib - distribution_name: tblib - project_short_description: Traceback serialization library. - release_date: '2020-03-07' - year_from: '2013' - year_to: '2' - version: 1.6.0 - license: BSD 2-Clause License - c_extension_support: no - c_extension_optional: no - c_extension_module: '-' +default_context: + allow_tests_inside_package: no + appveyor: no c_extension_function: '-' + c_extension_module: '-' + c_extension_optional: no + c_extension_support: no c_extension_test_pypi: no c_extension_test_pypi_username: '-' - test_matrix_configurator: no - test_matrix_separate_coverage: no - test_runner: pytest - setup_py_uses_test_runner: no - setup_py_uses_setuptools_scm: no - pypi_badge: yes - pypi_disable_upload: no - allow_tests_inside_package: no - linter: flake8 + codacy: no + codacy_projectid: '-' + codeclimate: no + codecov: yes command_line_interface: no command_line_interface_bin_name: '-' coveralls: no - coveralls_token: '-' - codecov: yes - landscape: no + distribution_name: tblib + email: contact@ionelmc.ro + full_name: Ionel Cristian Mărieș + github_actions: yes + github_actions_osx: yes + github_actions_windows: yes + legacy_python: yes + license: BSD 2-Clause License + linter: flake8 + package_name: tblib + pre_commit: yes + pre_commit_formatter: black + 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 + requiresio: yes scrutinizer: no - codacy: no - codacy_projectid: '-' - codeclimate: no + setup_py_uses_pytest_runner: no + setup_py_uses_setuptools_scm: no sphinx_docs: yes - sphinx_theme: sphinx-py3doc-enhanced-theme - sphinx_doctest: no sphinx_docs_hosting: https://python-tblib.readthedocs.io/ - travis: yes + sphinx_doctest: no + sphinx_theme: sphinx-py3doc-enhanced-theme + test_matrix_configurator: no + test_matrix_separate_coverage: no + travis: no travis_osx: no - appveyor: yes - requiresio: yes + version: 1.7.0 + version_manager: bump2version + website: https://blog.ionelmc.ro/ + year_from: '2013' + year_to: '2022' diff --git a/.editorconfig b/.editorconfig index 6eb7567..586c736 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,12 +2,19 @@ root = true [*] +# Use Unix-style newlines for most files (except Windows files, see below). end_of_line = lf trim_trailing_whitespace = true -insert_final_newline = true indent_style = space +insert_final_newline = true indent_size = 4 charset = utf-8 [*.{bat,cmd,ps1}] end_of_line = crlf + +[*.{yml,yaml}] +indent_size = 2 + +[*.tsv] +indent_style = tab diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..ce429a2 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,203 @@ +name: build +on: [push, pull_request] +jobs: + test: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: 'check' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'docs' + os: 'ubuntu-latest' + - name: 'py27 (ubuntu)' + python: '2.7' + toxpython: 'python2.7' + python_arch: 'x64' + tox_env: 'py27' + os: 'ubuntu-latest' + - name: 'py27 (windows)' + python: '2.7' + toxpython: 'python2.7' + python_arch: 'x64' + tox_env: 'py27' + os: 'windows-latest' + - name: 'py27 (macos)' + python: '2.7' + toxpython: 'python2.7' + python_arch: 'x64' + tox_env: 'py27' + os: 'macos-latest' + - name: 'py36 (ubuntu)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36' + os: 'ubuntu-latest' + - name: 'py36 (windows)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36' + os: 'windows-latest' + - name: 'py36 (macos)' + python: '3.6' + toxpython: 'python3.6' + python_arch: 'x64' + tox_env: 'py36' + os: 'macos-latest' + - name: 'py37 (ubuntu)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37' + os: 'ubuntu-latest' + - name: 'py37 (windows)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37' + os: 'windows-latest' + - name: 'py37 (macos)' + python: '3.7' + toxpython: 'python3.7' + python_arch: 'x64' + tox_env: 'py37' + os: 'macos-latest' + - name: 'py38 (ubuntu)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38' + os: 'ubuntu-latest' + - name: 'py38 (windows)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38' + os: 'windows-latest' + - name: 'py38 (macos)' + python: '3.8' + toxpython: 'python3.8' + python_arch: 'x64' + tox_env: 'py38' + os: 'macos-latest' + - name: 'py39 (ubuntu)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39' + os: 'ubuntu-latest' + - name: 'py39 (windows)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39' + os: 'windows-latest' + - name: 'py39 (macos)' + python: '3.9' + toxpython: 'python3.9' + python_arch: 'x64' + tox_env: 'py39' + os: 'macos-latest' + - name: 'py310 (ubuntu)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310' + os: 'ubuntu-latest' + - name: 'py310 (windows)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310' + os: 'windows-latest' + - name: 'py310 (macos)' + python: '3.10' + toxpython: 'python3.10' + python_arch: 'x64' + tox_env: 'py310' + os: 'macos-latest' + - name: 'pypy (ubuntu)' + python: 'pypy-.' + toxpython: 'pypy.' + python_arch: 'x64' + tox_env: 'pypy' + os: 'ubuntu-latest' + - name: 'pypy (windows)' + python: 'pypy-.' + toxpython: 'pypy.' + python_arch: 'x64' + tox_env: 'pypy' + os: 'windows-latest' + - name: 'pypy (macos)' + python: 'pypy-.' + toxpython: 'pypy.' + python_arch: 'x64' + tox_env: 'pypy' + os: 'macos-latest' + - name: 'pypy37 (ubuntu)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37' + os: 'ubuntu-latest' + - name: 'pypy37 (windows)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37' + os: 'windows-latest' + - name: 'pypy37 (macos)' + python: 'pypy-3.7' + toxpython: 'pypy3.7' + python_arch: 'x64' + tox_env: 'pypy37' + os: 'macos-latest' + - name: 'pypy38 (ubuntu)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38' + os: 'ubuntu-latest' + - name: 'pypy38 (windows)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38' + os: 'windows-latest' + - name: 'pypy38 (macos)' + python: 'pypy-3.8' + toxpython: 'pypy3.8' + python_arch: 'x64' + tox_env: 'pypy38' + os: 'macos-latest' + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.python_arch }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '${{ matrix.toxpython }}' + run: > + tox -e ${{ matrix.tox_env }} -v diff --git a/.gitignore b/.gitignore index dfe5838..83a43fd 100644 --- a/.gitignore +++ b/.gitignore @@ -39,11 +39,14 @@ htmlcov # Translations *.mo -# Mr Developer +# Buildout .mr.developer.cfg + +# IDE project files .project .pydevproject .idea +.vscode *.iml *.komodoproject diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8b21f59 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +# To install the git pre-commit hook run: +# pre-commit install +# To update the pre-commit hooks run: +# pre-commit install-hooks +exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: master + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements + - repo: https://github.com/timothycrosley/isort + rev: master + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: main + hooks: + - id: black + - repo: https://gitlab.com/pycqa/flake8 + rev: master + hooks: + - id: flake8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0e947cb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -language: python -dist: xenial -cache: false -env: - global: - - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so - - SEGFAULT_SIGNALS=all -matrix: - include: - - python: '3.6' - env: - - TOXENV=check - - python: '3.6' - env: - - TOXENV=docs - - env: - - TOXENV=py27,codecov - python: '2.7' - - env: - - TOXENV=py35,codecov - python: '3.5' - - env: - - TOXENV=py36,codecov - python: '3.6' - - env: - - TOXENV=py37,codecov - python: '3.7' - - env: - - TOXENV=py38,codecov - python: '3.8' - - env: - - TOXENV=pypy,codecov - python: 'pypy' - - env: - - TOXENV=pypy3,codecov - - TOXPYTHON=pypy3 - python: 'pypy3' -before_install: - - python --version - - uname -a - - lsb_release -a || true -install: - - python -mpip install --progress-bar=off tox -rci/requirements.txt - - virtualenv --version - - easy_install --version - - pip --version - - tox --version -script: - - tox -v -after_failure: - - more .tox/log/* | cat - - more .tox/*/log/* | cat -notifications: - email: - on_success: never - on_failure: always diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1d6bfc5..a89ed3a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -41,7 +41,7 @@ To set up `python-tblib` for local development: (look for the "Fork" button). 2. Clone your fork locally:: - git clone git@github.com:ionelmc/python-tblib.git + git clone git@github.com:YOURGITHUBNAME/python-tblib.git 3. Create a branch for local development:: @@ -68,15 +68,12 @@ If you need some code review or feedback while you're developing the code just m For merging, you should: -1. Include passing tests (run ``tox``) [1]_. +1. Include passing tests (run ``tox``). 2. Update documentation when there's new API, functionality etc. 3. Add a note to ``CHANGELOG.rst`` about the changes. 4. Add yourself to ``AUTHORS.rst``. -.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will - `run the tests `_ for each change you add in the pull request. - It will be slower though ... Tips ---- @@ -85,6 +82,6 @@ To run a subset of tests:: tox -e envname -- pytest -k test_myfeature -To run all the test environments in *parallel* (you need to ``pip install detox``):: +To run all the test environments in *parallel*:: - detox + tox -p auto diff --git a/LICENSE b/LICENSE index 7dac254..151345b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2013-2020, Ionel Cristian Mărieș. All rights reserved. +Copyright (c) 2013-2022, Ionel Cristian Mărieș. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in index 8b9e93d..d0dac9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,9 +4,14 @@ graft ci graft tests include .bumpversion.cfg -include .coveragerc include .cookiecutterrc +include .coveragerc include .editorconfig +include .github/workflows/github-actions.yml +include .pre-commit-config.yaml +include .readthedocs.yml +include pytest.ini +include tox.ini include AUTHORS.rst include CHANGELOG.rst @@ -14,6 +19,4 @@ include CONTRIBUTING.rst include LICENSE include README.rst -include tox.ini .travis.yml .appveyor.yml .readthedocs.yml - global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.rst b/README.rst index 3d003da..832a987 100644 --- a/README.rst +++ b/README.rst @@ -10,29 +10,24 @@ Overview * - docs - |docs| * - tests - - | |travis| |appveyor| |requires| + - | |github-actions| |requires| | |codecov| * - package - | |version| |wheel| |supported-versions| |supported-implementations| | |commits-since| - -.. |docs| image:: https://codecov.io/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master - :target: https://readthedocs.org/projects/python-tblib +.. |docs| image:: https://readthedocs.org/projects/python-tblib/badge/?style=flat + :target: https://python-tblib.readthedocs.io/ :alt: Documentation Status -.. |travis| image:: https://api.travis-ci.org/ionelmc/python-tblib.svg?branch=master - :alt: Travis-CI Build Status - :target: https://travis-ci.org/ionelmc/python-tblib - -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/ionelmc/python-tblib?branch=master&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/ionelmc/python-tblib +.. |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 .. |requires| image:: https://requires.io/github/ionelmc/python-tblib/requirements.svg?branch=master :alt: Requirements Status :target: https://requires.io/github/ionelmc/python-tblib/requirements/?branch=master -.. |codecov| image:: https://codecov.io/github/ionelmc/python-tblib/coverage.svg?branch=master +.. |codecov| image:: https://codecov.io/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master :alt: Coverage Status :target: https://codecov.io/github/ionelmc/python-tblib diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd deleted file mode 100644 index 289585f..0000000 --- a/ci/appveyor-with-compiler.cmd +++ /dev/null @@ -1,23 +0,0 @@ -:: Very simple setup: -:: - if WINDOWS_SDK_VERSION is set then activate the SDK. -:: - disable the WDK if it's around. - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows -SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" -ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% - -IF EXIST %WIN_WDK% ( - REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ - REN %WIN_WDK% 0wdf -) -IF "%WINDOWS_SDK_VERSION%"=="" GOTO main - -SET DISTUTILS_USE_SDK=1 -SET MSSdk=1 -"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% -CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - -:main -ECHO Executing: %COMMAND_TO_RUN% -CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 2597983..3ca06b7 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -11,8 +11,10 @@ from os.path import dirname from os.path import exists from os.path import join +from os.path import relpath base_path = dirname(dirname(abspath(__file__))) +templates_path = join(base_path, "ci", "templates") def check_call(args): @@ -47,16 +49,17 @@ def exec_in_env(): print("+ exec", python_executable, __file__, "--no-env") os.execv(python_executable, [python_executable, __file__, "--no-env"]) + def main(): import jinja2 print("Project path: {0}".format(base_path)) jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + loader=jinja2.FileSystemLoader(templates_path), trim_blocks=True, lstrip_blocks=True, - keep_trailing_newline=True + keep_trailing_newline=True, ) tox_environments = [ @@ -70,10 +73,12 @@ def main(): ] tox_environments = [line for line in tox_environments if line.startswith('py')] - for name in os.listdir(join("ci", "templates")): - with open(join(base_path, name), "w") as fh: - fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) - print("Wrote {}".format(name)) + for root, _, files in os.walk(templates_path): + for name in files: + relative = relpath(root, templates_path) + with open(join(base_path, relative, name), "w") as fh: + fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) + print("Wrote {}".format(name)) print("DONE.") @@ -86,4 +91,3 @@ def main(): else: print("Unexpected arguments {0}".format(args), file=sys.stderr) sys.exit(1) - diff --git a/ci/requirements.txt b/ci/requirements.txt index b2a21e5..a0ef106 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,4 +1,5 @@ virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 -six>=1.12.0 +six>=1.14.0 +tox diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml deleted file mode 100644 index bb4a055..0000000 --- a/ci/templates/.appveyor.yml +++ /dev/null @@ -1,49 +0,0 @@ -version: '{branch}-{build}' -build: off -environment: - matrix: - - TOXENV: check - TOXPYTHON: C:\Python36\python.exe - PYTHON_HOME: C:\Python36 - PYTHON_VERSION: '3.6' - PYTHON_ARCH: '32' -{% for env in tox_environments %} -{% if env.startswith(('py2', 'py3')) %} - - TOXENV: {{ env }},codecov{{ "" }} - TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe - PYTHON_HOME: C:\Python{{ env[2:4] }} - PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' - PYTHON_ARCH: '32' -{% if 'nocov' in env %} - WHEEL_PATH: .tox/dist -{% endif %} - - TOXENV: {{ env }},codecov{{ "" }} - TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe - PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 - PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' - PYTHON_ARCH: '64' -{% if 'nocov' in env %} - WHEEL_PATH: .tox/dist -{% endif %} -{% if env.startswith('py2') %} - WINDOWS_SDK_VERSION: v7.0 -{% endif %} -{% endif %}{% endfor %} -init: - - ps: echo $env:TOXENV - - ps: ls C:\Python* -install: - - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' - - '%PYTHON_HOME%\Scripts\virtualenv --version' - - '%PYTHON_HOME%\Scripts\easy_install --version' - - '%PYTHON_HOME%\Scripts\pip --version' - - '%PYTHON_HOME%\Scripts\tox --version' -test_script: - - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox -on_failure: - - ps: dir "env:" - - ps: get-content .tox\*\log\* - -### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): -# on_finish: -# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml new file mode 100644 index 0000000..7ee6426 --- /dev/null +++ b/ci/templates/.github/workflows/github-actions.yml @@ -0,0 +1,65 @@ +name: build +on: [push, pull_request] +jobs: + test: + name: {{ '${{ matrix.name }}' }} + runs-on: {{ '${{ matrix.os }}' }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: 'check' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'check' + os: 'ubuntu-latest' + - name: 'docs' + python: '3.9' + toxpython: 'python3.9' + tox_env: 'docs' + os: 'ubuntu-latest' +{% for env in tox_environments %} +{% set prefix = env.split('-')[0] -%} +{% if prefix.startswith('pypy') %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set cpython %}pp{{ prefix[4:5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% else %} +{% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% set cpython %}cp{{ prefix[2:] }}{% endset %} +{% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} +{% endif %} +{% for os, python_arch in [ + ['ubuntu', 'x64'], + ['windows', 'x64'], + ['macos', 'x64'], +] %} + - name: '{{ env }} ({{ os }})' + python: '{{ python }}' + toxpython: '{{ toxpython }}' + python_arch: '{{ python_arch }}' + tox_env: '{{ env }}{% if 'cover' in env %},codecov{% endif %}' + os: '{{ os }}-latest' +{% endfor %} +{% endfor %} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + with: + python-version: {{ '${{ matrix.python }}' }} + architecture: {{ '${{ matrix.python_arch }}' }} + - name: install dependencies + run: | + python -mpip install --progress-bar=off -r ci/requirements.txt + virtualenv --version + pip --version + tox --version + pip list --format=freeze + - name: test + env: + TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' + run: > + tox -e {{ '${{ matrix.tox_env }}' }} -v diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml deleted file mode 100644 index 03110ef..0000000 --- a/ci/templates/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: python -dist: xenial -cache: false -env: - global: - - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so - - SEGFAULT_SIGNALS=all -matrix: - include: - - python: '3.6' - env: - - TOXENV=check - - python: '3.6' - env: - - TOXENV=docs -{%- for env in tox_environments %}{{ '' }} - - env: - - TOXENV={{ env }},codecov -{%- if env.startswith('pypy3') %}{{ '' }} - - TOXPYTHON=pypy3 - python: 'pypy3' -{%- elif env.startswith('pypy') %}{{ '' }} - python: 'pypy' -{%- else %}{{ '' }} - python: '{{ '{0[2]}.{0[3]}'.format(env) }}' -{%- endif %} -{%- endfor %}{{ '' }} -before_install: - - python --version - - uname -a - - lsb_release -a || true -install: - - python -mpip install --progress-bar=off tox -rci/requirements.txt - - virtualenv --version - - easy_install --version - - pip --version - - tox --version -script: - - tox -v -after_failure: - - more .tox/log/* | cat - - more .tox/*/log/* | cat -notifications: - email: - on_success: never - on_failure: always diff --git a/docs/conf.py b/docs/conf.py index af0256c..6ea2ca0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import os +import sphinx_py3doc_enhanced_theme extensions = [ 'autoapi.extension', @@ -19,7 +19,7 @@ source_suffix = '.rst' master_doc = 'index' project = 'tblib' -year = '2013-2020' +year = '2013-2022' author = 'Ionel Cristian Mărieș' copyright = '{0}, {1}'.format(year, author) version = release = '1.7.0' @@ -30,18 +30,17 @@ 'issue': ('https://github.com/ionelmc/python-tblib/issues/%s', '#'), 'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #'), } -import sphinx_py3doc_enhanced_theme -html_theme = "sphinx_py3doc_enhanced_theme" +html_theme = 'sphinx_py3doc_enhanced_theme' html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] html_theme_options = { - 'githuburl': 'https://github.com/ionelmc/python-tblib/' + 'githuburl': 'https://github.com/ionelmc/python-tblib/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], + '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], } html_short_title = '%s-%s' % (project, version) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2b774bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = [ + "setuptools>=30.3.0", + "wheel", +] + +[tool.black] +line-length = 140 +target-version = ['py27'] +skip-string-normalization = true diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5f7ccc6 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,30 @@ +[pytest] +# If a pytest section is found in one of the possible config files +# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, +# so if you add a pytest config section elsewhere, +# you will need to delete this section from setup.cfg. +norecursedirs = + migrations + +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict-markers + --ignore=tests/badmodule.py + --ignore=tests/badsyntax.py + --doctest-modules + --doctest-glob=\*.rst + --tb=short +testpaths = + tests + +# Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors +filterwarnings = + error +# You can add exclusions, some examples: +# ignore:'tblib' defines default_app_config:PendingDeprecationWarning:: +# ignore:The {{% if::: +# ignore:Coverage disabled via --no-cov switch! diff --git a/setup.cfg b/setup.cfg index 1348d5f..40baa66 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,31 +3,7 @@ universal = 1 [flake8] max-line-length = 140 -exclude = */migrations/* - -[tool: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 - --ignore=tests/badmodule.py - --ignore=tests/badsyntax.py - --doctest-modules - --doctest-continue-on-failure - --doctest-glob=\*.rst - --tb=short -testpaths = - tests +exclude = .tox,.eggs,ci/templates,build,dist [tool:isort] force_single_line = True @@ -35,5 +11,4 @@ line_length = 120 known_first_party = tblib default_section = THIRDPARTY forced_separate = test_tblib -not_skip = __init__.py -skip = migrations +skip = .tox,.eggs,ci/templates,build,dist diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 9193b68..7648137 --- a/setup.py +++ b/setup.py @@ -16,10 +16,7 @@ def read(*names, **kwargs): - with io.open( - join(dirname(__file__), *names), - encoding=kwargs.get('encoding', 'utf8') - ) as fh: + with io.open(join(dirname(__file__), *names), encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() @@ -28,9 +25,9 @@ def read(*names, **kwargs): version='1.7.0', license='BSD-2-Clause', description='Traceback serialization library.', - long_description='%s\n%s' % ( + long_description='{}\n{}'.format( re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), - re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')), ), author='Ionel Cristian Mărieș', author_email='contact@ionelmc.ro', @@ -51,10 +48,11 @@ def read(*names, **kwargs): 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', # uncomment if you test on these interpreters: @@ -71,7 +69,7 @@ def read(*names, **kwargs): keywords=[ 'traceback', 'debugging', 'exceptions', ], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', install_requires=[ # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/tox.ini b/tox.ini index e052f92..2b8942f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [testenv:bootstrap] deps = jinja2 - matrix tox skip_install = true commands = @@ -15,20 +14,22 @@ envlist = clean, check, docs, - {py27,py35,py36,py37,py38,pypy,pypy3}, + {py27,py36,py37,py38,py39,py310,pypy,pypy37,pypy38}, report ignore_basepython_conflict = true [testenv] basepython = pypy: {env:TOXPYTHON:pypy} - pypy3: {env:TOXPYTHON:pypy3} + pypy37: {env:TOXPYTHON:pypy3.7} + pypy38: {env:TOXPYTHON:pypy3.8} py27: {env:TOXPYTHON:python2.7} - py35: {env:TOXPYTHON:python3.5} py36: {env:TOXPYTHON:python3.6} py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} - {bootstrap,clean,check,report,codecov,docs}: {env:TOXPYTHON:python3} + py39: {env:TOXPYTHON:python3.9} + py310: {env:TOXPYTHON:python3.10} + {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -37,13 +38,11 @@ passenv = usedevelop = false deps = pytest - pytest-travis-fold pytest-cov - pytest-clarity six - py{27,35,36,37,38,py,py3}: twisted + twisted commands = - {posargs:py.test --cov=tblib --cov-report=term-missing -vv tests README.rst} + {posargs:pytest --cov --cov-report=term-missing -vv tests} [testenv:check] deps = @@ -57,8 +56,8 @@ skip_install = true commands = python setup.py check --strict --metadata --restructuredtext check-manifest {toxinidir} - flake8 src tests setup.py - isort --verbose --check-only --diff --recursive src tests setup.py + flake8 + isort --verbose --check-only --diff --filter-files . [testenv:docs] usedevelop = true @@ -76,7 +75,8 @@ commands = codecov [] [testenv:report] -deps = coverage +deps = + coverage skip_install = true commands = coverage report @@ -85,4 +85,5 @@ commands = [testenv:clean] commands = coverage erase skip_install = true -deps = coverage +deps = + coverage From 9e0a0f13238cde8f379dd4234db8d29b551c5b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 11 May 2022 13:29:43 +0300 Subject: [PATCH 02/57] Drop legacy python support. Also remove use of pypy tproxy use (it's deprecated anyway) since it's not needed for pypy3. --- .cookiecutterrc | 2 +- .github/workflows/github-actions.yml | 36 ------------- pyproject.toml | 2 +- setup.cfg | 3 -- setup.py | 6 +-- src/tblib/__init__.py | 68 +---------------------- src/tblib/cpython.py | 80 ---------------------------- tox.ini | 4 +- 8 files changed, 7 insertions(+), 194 deletions(-) delete mode 100644 src/tblib/cpython.py diff --git a/.cookiecutterrc b/.cookiecutterrc index 500f6e5..922bdfa 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -22,7 +22,7 @@ default_context: github_actions: yes github_actions_osx: yes github_actions_windows: yes - legacy_python: yes + legacy_python: no license: BSD 2-Clause License linter: flake8 package_name: tblib diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index ce429a2..88ad0fc 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -19,24 +19,6 @@ jobs: toxpython: 'python3.9' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py27 (ubuntu)' - python: '2.7' - toxpython: 'python2.7' - python_arch: 'x64' - tox_env: 'py27' - os: 'ubuntu-latest' - - name: 'py27 (windows)' - python: '2.7' - toxpython: 'python2.7' - python_arch: 'x64' - tox_env: 'py27' - os: 'windows-latest' - - name: 'py27 (macos)' - python: '2.7' - toxpython: 'python2.7' - python_arch: 'x64' - tox_env: 'py27' - os: 'macos-latest' - name: 'py36 (ubuntu)' python: '3.6' toxpython: 'python3.6' @@ -127,24 +109,6 @@ jobs: python_arch: 'x64' tox_env: 'py310' os: 'macos-latest' - - name: 'pypy (ubuntu)' - python: 'pypy-.' - toxpython: 'pypy.' - python_arch: 'x64' - tox_env: 'pypy' - os: 'ubuntu-latest' - - name: 'pypy (windows)' - python: 'pypy-.' - toxpython: 'pypy.' - python_arch: 'x64' - tox_env: 'pypy' - os: 'windows-latest' - - name: 'pypy (macos)' - python: 'pypy-.' - toxpython: 'pypy.' - python_arch: 'x64' - tox_env: 'pypy' - os: 'macos-latest' - name: 'pypy37 (ubuntu)' python: 'pypy-3.7' toxpython: 'pypy3.7' diff --git a/pyproject.toml b/pyproject.toml index 2b774bc..a51acf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,5 @@ requires = [ [tool.black] line-length = 140 -target-version = ['py27'] +target-version = ['py36'] skip-string-normalization = true diff --git a/setup.cfg b/setup.cfg index 40baa66..1f37e56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [flake8] max-line-length = 140 exclude = .tox,.eggs,ci/templates,build,dist diff --git a/setup.py b/setup.py index 7648137..d6e955f 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python # -*- encoding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function import io import re @@ -46,8 +44,8 @@ def read(*names, **kwargs): 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', @@ -69,7 +67,7 @@ def read(*names, **kwargs): keywords=[ 'traceback', 'debugging', 'exceptions', ], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', + python_requires='>=3.6', install_requires=[ # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 7e717b4..1e88a0f 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -4,22 +4,9 @@ from types import FrameType from types import TracebackType -try: - from __pypy__ import tproxy -except ImportError: - tproxy = None -try: - from .cpython import tb_set_next -except ImportError: - tb_set_next = None - -if not tb_set_next and not tproxy: - raise ImportError("Cannot use tblib. Runtime not supported.") - __version__ = '1.7.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' -PY3 = sys.version_info[0] == 3 FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') @@ -59,16 +46,6 @@ def __init__(self, code): self.co_flags = 64 self.co_firstlineno = 0 - # noinspection SpellCheckingInspection - def __tproxy__(self, operation, *args, **kwargs): - """ - Necessary for PyPy's tproxy. - """ - if operation in ('__getattribute__', '__getattr__'): - return getattr(self, args[0]) - else: - return getattr(self, operation)(*args, **kwargs) - class Frame(object): """ @@ -92,19 +69,6 @@ def clear(self): in turn is called by unittest.TestCase.assertRaises """ - # noinspection SpellCheckingInspection - def __tproxy__(self, operation, *args, **kwargs): - """ - Necessary for PyPy's tproxy. - """ - if operation in ('__getattribute__', '__getattr__'): - if args[0] == 'f_code': - return tproxy(CodeType, self.f_code.__tproxy__) - else: - return getattr(self, args[0]) - else: - return getattr(self, operation)(*args, **kwargs) - class Traceback(object): """ @@ -133,11 +97,6 @@ def as_traceback(self): """ Convert to a builtin Traceback object that is usable for raising or rendering a stacktrace. """ - if tproxy: - return tproxy(TracebackType, self.__tproxy__) - if not tb_set_next: - raise RuntimeError("Unsupported Python interpreter!") - current = self top_tb = None tb = None @@ -149,7 +108,7 @@ def as_traceback(self): code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=()) - elif PY3: + else: code = CodeType( 0, code.co_kwonlyargcount, code.co_nlocals, code.co_stacksize, code.co_flags, @@ -157,14 +116,6 @@ def as_traceback(self): f_code.co_filename, f_code.co_name, code.co_firstlineno, code.co_lnotab, (), () ) - else: - code = CodeType( - 0, - code.co_nlocals, code.co_stacksize, code.co_flags, - code.co_code, code.co_consts, code.co_names, code.co_varnames, - f_code.co_filename.encode(), f_code.co_name.encode(), - code.co_firstlineno, code.co_lnotab, (), () - ) # noinspection PyBroadException try: @@ -174,7 +125,7 @@ def as_traceback(self): if top_tb is None: top_tb = next_tb if tb is not None: - tb_set_next(tb, next_tb) + tb.tb_next = next_tb tb = next_tb del next_tb @@ -186,21 +137,6 @@ def as_traceback(self): del tb to_traceback = as_traceback - # noinspection SpellCheckingInspection - def __tproxy__(self, operation, *args, **kwargs): - """ - Necessary for PyPy's tproxy. - """ - if operation in ('__getattribute__', '__getattr__'): - if args[0] == 'tb_next': - return self.tb_next and self.tb_next.as_traceback() - elif args[0] == 'tb_frame': - return tproxy(FrameType, self.tb_frame.__tproxy__) - else: - return getattr(self, args[0]) - else: - return getattr(self, operation)(*args, **kwargs) - def as_dict(self): """ Converts to a dictionary representation. You can serialize the result to JSON as it only has diff --git a/src/tblib/cpython.py b/src/tblib/cpython.py deleted file mode 100644 index 5c4bf20..0000000 --- a/src/tblib/cpython.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Taken verbatim from Jinja2. - -https://github.com/mitsuhiko/jinja2/blob/master/jinja2/debug.py#L267 -""" -import platform -import sys - - -def _init_ugly_crap(): - """This function implements a few ugly things so that we can patch the - traceback objects. The function returned allows resetting `tb_next` on - any python traceback object. Do not attempt to use this on non cpython - interpreters - """ - import ctypes - from types import TracebackType - - # figure out side of _Py_ssize_t - if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): - _Py_ssize_t = ctypes.c_int64 - else: - _Py_ssize_t = ctypes.c_int - - # regular python - class _PyObject(ctypes.Structure): - pass - - _PyObject._fields_ = [ - ('ob_refcnt', _Py_ssize_t), - ('ob_type', ctypes.POINTER(_PyObject)) - ] - - # python with trace - if hasattr(sys, 'getobjects'): - class _PyObject(ctypes.Structure): - pass - - _PyObject._fields_ = [ - ('_ob_next', ctypes.POINTER(_PyObject)), - ('_ob_prev', ctypes.POINTER(_PyObject)), - ('ob_refcnt', _Py_ssize_t), - ('ob_type', ctypes.POINTER(_PyObject)) - ] - - class _Traceback(_PyObject): - pass - - _Traceback._fields_ = [ - ('tb_next', ctypes.POINTER(_Traceback)), - ('tb_frame', ctypes.POINTER(_PyObject)), - ('tb_lasti', ctypes.c_int), - ('tb_lineno', ctypes.c_int) - ] - - def tb_set_next(tb, next): - """Set the tb_next attribute of a traceback object.""" - if not (isinstance(tb, TracebackType) and (next is None or isinstance(next, TracebackType))): - raise TypeError('tb_set_next arguments must be traceback objects') - obj = _Traceback.from_address(id(tb)) - if tb.tb_next is not None: - old = _Traceback.from_address(id(tb.tb_next)) - old.ob_refcnt -= 1 - if next is None: - obj.tb_next = ctypes.POINTER(_Traceback)() - else: - next = _Traceback.from_address(id(next)) - next.ob_refcnt += 1 - obj.tb_next = ctypes.pointer(next) - - return tb_set_next - - -tb_set_next = None -try: - if platform.python_implementation() == 'CPython': - tb_set_next = _init_ugly_crap() -except Exception as exc: - sys.stderr.write("Failed to initialize cpython support: {!r}".format(exc)) -del _init_ugly_crap diff --git a/tox.ini b/tox.ini index 2b8942f..6d3fe06 100644 --- a/tox.ini +++ b/tox.ini @@ -14,16 +14,14 @@ envlist = clean, check, docs, - {py27,py36,py37,py38,py39,py310,pypy,pypy37,pypy38}, + {py36,py37,py38,py39,py310,pypy37,pypy38}, report ignore_basepython_conflict = true [testenv] basepython = - pypy: {env:TOXPYTHON:pypy} pypy37: {env:TOXPYTHON:pypy3.7} pypy38: {env:TOXPYTHON:pypy3.8} - py27: {env:TOXPYTHON:python2.7} py36: {env:TOXPYTHON:python3.6} py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} From 4ddf1f07329d0583b35cbcf49c01dc39461283b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 11 May 2022 14:01:33 +0300 Subject: [PATCH 03/57] Also drop python 3.6 since tb_next is readonly and it's not worth maintaining that hack. --- .github/workflows/github-actions.yml | 18 ------------------ setup.py | 3 +-- tox.ini | 3 +-- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 88ad0fc..fa53eb5 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -19,24 +19,6 @@ jobs: toxpython: 'python3.9' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py36 (ubuntu)' - python: '3.6' - toxpython: 'python3.6' - python_arch: 'x64' - tox_env: 'py36' - os: 'ubuntu-latest' - - name: 'py36 (windows)' - python: '3.6' - toxpython: 'python3.6' - python_arch: 'x64' - tox_env: 'py36' - os: 'windows-latest' - - name: 'py36 (macos)' - python: '3.6' - toxpython: 'python3.6' - python_arch: 'x64' - tox_env: 'py36' - os: 'macos-latest' - name: 'py37 (ubuntu)' python: '3.7' toxpython: 'python3.7' diff --git a/setup.py b/setup.py index d6e955f..86a732b 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,6 @@ def read(*names, **kwargs): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -67,7 +66,7 @@ def read(*names, **kwargs): keywords=[ 'traceback', 'debugging', 'exceptions', ], - python_requires='>=3.6', + python_requires='>=3.7', install_requires=[ # eg: 'aspectlib==1.1.1', 'six>=1.7', ], diff --git a/tox.ini b/tox.ini index 6d3fe06..4c9405e 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = clean, check, docs, - {py36,py37,py38,py39,py310,pypy37,pypy38}, + {py37,py38,py39,py310,pypy37,pypy38}, report ignore_basepython_conflict = true @@ -22,7 +22,6 @@ ignore_basepython_conflict = true basepython = pypy37: {env:TOXPYTHON:pypy3.7} pypy38: {env:TOXPYTHON:pypy3.8} - py36: {env:TOXPYTHON:python3.6} py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} From dd926c1e5dc5bbe5e1fc494443bbac8970c7d3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 11 May 2022 14:03:26 +0300 Subject: [PATCH 04/57] Apply black, fix some docs issues and more cleanup. --- .pre-commit-config.yaml | 8 +-- CHANGELOG.rst | 2 +- ci/appveyor-download.py | 109 ----------------------------- docs/index.rst | 2 +- setup.py | 4 +- src/tblib/__init__.py | 37 ++++++---- src/tblib/pickling_support.py | 5 +- tests/examples.py | 2 + tests/test_pickle_exception.py | 4 +- tests/test_tblib.py | 121 ++++++++++++++++++--------------- 10 files changed, 100 insertions(+), 194 deletions(-) delete mode 100755 ci/appveyor-download.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b21f59..d4132ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,20 +5,20 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: master + rev: v4.2.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements - repo: https://github.com/timothycrosley/isort - rev: master + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/psf/black - rev: main + rev: 22.3.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: master + rev: 3.9.2 hooks: - id: flake8 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c3c5aeb..89cd975 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,7 +36,7 @@ Changelog * Add support for PyPy3.5-5.7.1-beta. Previously ``AttributeError: 'Frame' object has no attribute 'clear'`` could be raised. See PyPy - issue `#2532 `_. + issue `#2532 `_. 1.3.1 (2017-03-27) ~~~~~~~~~~~~~~~~~~ diff --git a/ci/appveyor-download.py b/ci/appveyor-download.py deleted file mode 100755 index 8373863..0000000 --- a/ci/appveyor-download.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python -""" -Use the AppVeyor API to download Windows artifacts. - -Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py -# Licensed under the Apache License: https://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -""" -from __future__ import unicode_literals - -import argparse -import os -import zipfile - -import requests - - -def make_auth_headers(): - """Make the authentication headers needed to use the Appveyor API.""" - path = os.path.expanduser("~/.appveyor.token") - if not os.path.exists(path): - raise RuntimeError( - "Please create a file named `.appveyor.token` in your home directory. " - "You can get the token from https://ci.appveyor.com/api-token" - ) - with open(path) as f: - token = f.read().strip() - - headers = { - 'Authorization': 'Bearer {}'.format(token), - } - return headers - - -def download_latest_artifacts(account_project, build_id): - """Download all the artifacts from the latest build.""" - if build_id is None: - url = "https://ci.appveyor.com/api/projects/{}".format(account_project) - else: - url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id) - build = requests.get(url, headers=make_auth_headers()).json() - jobs = build['build']['jobs'] - print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) - - for job in jobs: - name = job['name'] - print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) - - url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId']) - response = requests.get(url, headers=make_auth_headers()) - artifacts = response.json() - - for artifact in artifacts: - is_zip = artifact['type'] == "Zip" - filename = artifact['fileName'] - print(u" {0}, {1} bytes".format(filename, artifact['size'])) - - url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename) - download_url(url, filename, make_auth_headers()) - - if is_zip: - unpack_zipfile(filename) - os.remove(filename) - - -def ensure_dirs(filename): - """Make sure the directories exist for `filename`.""" - dirname = os.path.dirname(filename) - if dirname and not os.path.exists(dirname): - os.makedirs(dirname) - - -def download_url(url, filename, headers): - """Download a file from `url` to `filename`.""" - ensure_dirs(filename) - response = requests.get(url, headers=headers, stream=True) - if response.status_code == 200: - with open(filename, 'wb') as f: - for chunk in response.iter_content(16 * 1024): - f.write(chunk) - else: - print(u" Error downloading {}: {}".format(url, response)) - - -def unpack_zipfile(filename): - """Unpack a zipfile, using the names in the zip.""" - with open(filename, 'rb') as fzip: - z = zipfile.ZipFile(fzip) - for name in z.namelist(): - print(u" extracting {}".format(name)) - ensure_dirs(name) - z.extract(name) - - -parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.') -parser.add_argument('--id', - metavar='PROJECT_ID', - default='ionelmc/python-tblib', - help='Project ID in AppVeyor.') -parser.add_argument('build', - nargs='?', - metavar='BUILD_ID', - help='Build ID in AppVeyor. Eg: master-123') - -if __name__ == "__main__": - # import logging - # logging.basicConfig(level="DEBUG") - args = parser.parse_args() - download_latest_artifacts(args.id, args.build) diff --git a/docs/index.rst b/docs/index.rst index e55d633..040ea20 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Contents installation usage contributing + autoapi/index authors changelog @@ -18,4 +19,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/setup.py b/setup.py index 86a732b..58f3cd8 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,9 @@ def read(*names, **kwargs): 'Issue Tracker': 'https://github.com/ionelmc/python-tblib/issues', }, keywords=[ - 'traceback', 'debugging', 'exceptions', + 'traceback', + 'debugging', + 'exceptions', ], python_requires='>=3.7', install_requires=[ diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 1e88a0f..c319184 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -1,8 +1,6 @@ import re import sys from types import CodeType -from types import FrameType -from types import TracebackType __version__ = '1.7.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' @@ -33,6 +31,7 @@ class Code(object): """ Class that replicates just enough of the builtin Code object to enable serialization and traceback rendering. """ + co_code = None def __init__(self, code): @@ -51,13 +50,10 @@ class Frame(object): """ Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering. """ + def __init__(self, frame): self.f_locals = {} - self.f_globals = { - k: v - for k, v in frame.f_globals.items() - if k in ("__file__", "__name__") - } + self.f_globals = {k: v for k, v in frame.f_globals.items() if k in ("__file__", "__name__")} self.f_code = Code(frame.f_code) self.f_lineno = frame.f_lineno @@ -74,6 +70,7 @@ class Traceback(object): """ Class that wraps builtin Traceback objects. """ + tb_next = None def __init__(self, tb): @@ -105,16 +102,24 @@ def as_traceback(self): code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec') if hasattr(code, "replace"): # Python 3.8 and newer - code = code.replace(co_argcount=0, - co_filename=f_code.co_filename, co_name=f_code.co_name, - co_freevars=(), co_cellvars=()) + code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=()) else: code = CodeType( - 0, code.co_kwonlyargcount, - code.co_nlocals, code.co_stacksize, code.co_flags, - code.co_code, code.co_consts, code.co_names, code.co_varnames, - f_code.co_filename, f_code.co_name, - code.co_firstlineno, code.co_lnotab, (), () + 0, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + f_code.co_filename, + f_code.co_name, + code.co_firstlineno, + code.co_lnotab, + (), + (), ) # noinspection PyBroadException @@ -135,6 +140,7 @@ def as_traceback(self): finally: del top_tb del tb + to_traceback = as_traceback def as_dict(self): @@ -161,6 +167,7 @@ def as_dict(self): 'tb_lineno': self.tb_lineno, 'tb_next': tb_next, } + to_dict = as_dict @classmethod diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index cf6e390..5ec6e36 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -81,7 +81,4 @@ def install(*exc_classes_or_instances): if len(exc_classes_or_instances) == 1: return exc else: - raise TypeError( - "Expected subclasses or instances of BaseException, got %s" - % (type(exc)) - ) + raise TypeError("Expected subclasses or instances of BaseException, got %s" % (type(exc))) diff --git a/tests/examples.py b/tests/examples.py index d4e64ab..441491b 100644 --- a/tests/examples.py +++ b/tests/examples.py @@ -16,9 +16,11 @@ def func_d(): def bad_syntax(): import badsyntax + badsyntax def bad_module(): import badmodule + badmodule diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 18a018c..c5cd6c8 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -27,9 +27,7 @@ class CustomError(Exception): pass -@pytest.mark.parametrize( - "protocol", [None] + list(range(1, pickle.HIGHEST_PROTOCOL + 1)) -) +@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": diff --git a/tests/test_tblib.py b/tests/test_tblib.py index bade6d4..058d53a 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -6,7 +6,7 @@ pickling_support.install() -pytest_plugins = 'pytester', +pytest_plugins = ('pytester',) def test_parse_traceback(): @@ -61,7 +61,8 @@ def test_parse_traceback(): def test_pytest_integration(testdir): - test = testdir.makepyfile(""" + test = testdir.makepyfile( + """ import six from tblib import Traceback @@ -78,67 +79,75 @@ def test_raise(): ''') pytb = tb1.as_traceback() six.reraise(RuntimeError, RuntimeError(), pytb) -""") +""" + ) # mode(auto / long / short / line / native / no). result = testdir.runpytest_subprocess('--tb=long', '-vv', test) - result.stdout.fnmatch_lines([ - "_ _ _ _ _ _ _ _ *", - "", - "> [?][?][?]", - "", - "file1:123:*", - "_ _ _ _ _ _ _ _ *", - "", - "> [?][?][?]", - "", - "file2:234:*", - "_ _ _ _ _ _ _ _ *", - "", - "> [?][?][?]", - "", - "file3:345:*", - "_ _ _ _ _ _ _ _ *", - "", - "> [?][?][?]", - "E RuntimeError", - "", - "file4:456: RuntimeError", - "===*=== 1 failed in * ===*===", - ]) + result.stdout.fnmatch_lines( + [ + "_ _ _ _ _ _ _ _ *", + "", + "> [?][?][?]", + "", + "file1:123:*", + "_ _ _ _ _ _ _ _ *", + "", + "> [?][?][?]", + "", + "file2:234:*", + "_ _ _ _ _ _ _ _ *", + "", + "> [?][?][?]", + "", + "file3:345:*", + "_ _ _ _ _ _ _ _ *", + "", + "> [?][?][?]", + "E RuntimeError", + "", + "file4:456: RuntimeError", + "===*=== 1 failed in * ===*===", + ] + ) result = testdir.runpytest_subprocess('--tb=short', '-vv', test) - result.stdout.fnmatch_lines([ - 'test_pytest_integration.py:*: in test_raise', - ' six.reraise(RuntimeError, RuntimeError(), pytb)', - 'file1:123: in ', - ' ???', - 'file2:234: in ???', - ' ???', - 'file3:345: in function3', - ' ???', - 'file4:456: in ""', - ' ???', - 'E RuntimeError', - ]) + result.stdout.fnmatch_lines( + [ + 'test_pytest_integration.py:*: in test_raise', + ' six.reraise(RuntimeError, RuntimeError(), 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.stdout.fnmatch_lines( + [ + "===*=== FAILURES ===*===", + "file4:456: RuntimeError", + "===*=== 1 failed in * ===*===", + ] + ) result = testdir.runpytest_subprocess('--tb=native', '-vv', test) - result.stdout.fnmatch_lines([ - 'Traceback (most recent call last):', - ' File "*test_pytest_integration.py", line *, in test_raise', - ' six.reraise(RuntimeError, RuntimeError(), pytb)', - ' File "file1", line 123, in ', - ' File "file2", line 234, in ???', - ' File "file3", line 345, in function3', - ' File "file4", line 456, in ""', - 'RuntimeError', - - ]) + result.stdout.fnmatch_lines( + [ + 'Traceback (most recent call last):', + ' File "*test_pytest_integration.py", line *, in test_raise', + ' six.reraise(RuntimeError, RuntimeError(), pytb)', + ' File "file1", line 123, in ', + ' File "file2", line 234, in ???', + ' File "file3", line 345, in function3', + ' File "file4", line 456, in ""', + 'RuntimeError', + ] + ) From 645799658e0b52b58b71a89821116284583f8667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 10:57:06 +0300 Subject: [PATCH 05/57] Update project skel and ass Python 3.11. --- .bumpversion.cfg | 4 + .cookiecutterrc | 50 +++++------- .github/workflows/github-actions.yml | 48 ++++++++++-- .gitignore | 58 +++++++------- .pre-commit-config.yaml | 32 ++++---- README.rst | 6 +- ci/bootstrap.py | 78 ++++++++----------- ci/requirements.txt | 1 + .../.github/workflows/github-actions.yml | 14 ++-- docs/conf.py | 7 +- pyproject.toml | 49 +++++++++++- setup.cfg | 11 --- setup.py | 14 +--- tox.ini | 24 ++---- 14 files changed, 215 insertions(+), 181 deletions(-) delete mode 100644 setup.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg index edca607..6c794f5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -22,3 +22,7 @@ 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} diff --git a/.cookiecutterrc b/.cookiecutterrc index 922bdfa..b86e39e 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,55 +1,45 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: - allow_tests_inside_package: no - appveyor: no + allow_tests_inside_package: 'no' c_extension_function: '-' c_extension_module: '-' - c_extension_optional: no - c_extension_support: no - c_extension_test_pypi: no - c_extension_test_pypi_username: '-' - codacy: no + c_extension_optional: 'no' + c_extension_support: 'no' + codacy: 'no' codacy_projectid: '-' - codeclimate: no - codecov: yes - command_line_interface: no + codeclimate: 'no' + codecov: 'yes' + command_line_interface: 'no' command_line_interface_bin_name: '-' - coveralls: no + coveralls: 'no' distribution_name: tblib email: contact@ionelmc.ro + formatter_quote_style: single full_name: Ionel Cristian Mărieș - github_actions: yes - github_actions_osx: yes - github_actions_windows: yes - legacy_python: no + github_actions: 'yes' + github_actions_osx: 'yes' + github_actions_windows: 'yes' license: BSD 2-Clause License - linter: flake8 package_name: tblib - pre_commit: yes - pre_commit_formatter: black + pre_commit: 'yes' project_name: tblib project_short_description: Traceback serialization library. - pypi_badge: yes - pypi_disable_upload: no + 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 - requiresio: yes - scrutinizer: no - setup_py_uses_pytest_runner: no - setup_py_uses_setuptools_scm: no - sphinx_docs: yes + scrutinizer: 'no' + setup_py_uses_setuptools_scm: 'no' + sphinx_docs: 'yes' sphinx_docs_hosting: https://python-tblib.readthedocs.io/ - sphinx_doctest: no + sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme - test_matrix_configurator: no - test_matrix_separate_coverage: no - travis: no - travis_osx: no + test_matrix_separate_coverage: 'no' version: 1.7.0 version_manager: bump2version website: https://blog.ionelmc.ro/ diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index fa53eb5..51985bc 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -10,13 +10,13 @@ jobs: matrix: include: - name: 'check' - python: '3.9' - toxpython: 'python3.9' + python: '3.11' + toxpython: 'python3.11' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' - python: '3.9' - toxpython: 'python3.9' + python: '3.11' + toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - name: 'py37 (ubuntu)' @@ -91,6 +91,24 @@ jobs: python_arch: 'x64' tox_env: 'py310' os: 'macos-latest' + - name: 'py311 (ubuntu)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311' + os: 'ubuntu-latest' + - name: 'py311 (windows)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311' + os: 'windows-latest' + - name: 'py311 (macos)' + python: '3.11' + toxpython: 'python3.11' + python_arch: 'x64' + tox_env: 'py311' + os: 'macos-latest' - name: 'pypy37 (ubuntu)' python: 'pypy-3.7' toxpython: 'pypy3.7' @@ -127,11 +145,29 @@ jobs: python_arch: 'x64' tox_env: 'pypy38' os: 'macos-latest' + - name: 'pypy39 (ubuntu)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39' + os: 'ubuntu-latest' + - name: 'pypy39 (windows)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39' + os: 'windows-latest' + - name: 'pypy39 (macos)' + python: 'pypy-3.9' + toxpython: 'pypy3.9' + python_arch: 'x64' + tox_env: 'pypy39' + os: 'macos-latest' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} diff --git a/.gitignore b/.gitignore index 83a43fd..77973dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,40 +1,53 @@ *.py[cod] __pycache__ +# Temp files +.*.sw[po] +*~ +*.bak +.DS_Store + # C extensions *.so -# Packages +# Build and package files *.egg *.egg-info -dist -build -eggs +.bootstrap +.build +.cache .eggs -parts +.env +.installed.cfg +.ve bin -var -sdist -wheelhouse +build develop-eggs -.installed.cfg +dist +eggs lib lib64 -venv*/ -pyvenv*/ +parts pip-wheel-metadata/ +pyvenv*/ +sdist +var +venv*/ +wheelhouse # Installer logs pip-log.txt # Unit test / coverage reports +.benchmarks .coverage -.tox .coverage.* +.pytest .pytest_cache/ -nosetests.xml +.tox coverage.xml htmlcov +nosetests.xml # Translations *.mo @@ -43,12 +56,12 @@ htmlcov .mr.developer.cfg # IDE project files +*.iml +*.komodoproject +.idea .project .pydevproject -.idea .vscode -*.iml -*.komodoproject # Complexity output/*.html @@ -57,18 +70,5 @@ output/*/index.html # Sphinx docs/_build -.DS_Store -*~ -.*.sw[po] -.build -.ve -.env -.cache -.pytest -.benchmarks -.bootstrap -.appveyor.token -*.bak - # Mypy Cache .mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d4132ab..d37ca47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,22 @@ -# To install the git pre-commit hook run: -# pre-commit install -# To update the pre-commit hooks run: -# pre-commit install-hooks +# 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)(/|$)' +# Note the order is intentional to avoid multiple passes of the hooks repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 - hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: debug-statements - - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.274 hooks: - - id: isort + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.3.0 hooks: - id: black - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 hooks: - - id: flake8 + - id: trailing-whitespace + - id: end-of-file-fixer + - id: debug-statements diff --git a/README.rst b/README.rst index 832a987..df6142f 100644 --- a/README.rst +++ b/README.rst @@ -23,13 +23,9 @@ Overview :alt: GitHub Actions Build Status :target: https://github.com/ionelmc/python-tblib/actions -.. |requires| image:: https://requires.io/github/ionelmc/python-tblib/requirements.svg?branch=master - :alt: Requirements Status - :target: https://requires.io/github/ionelmc/python-tblib/requirements/?branch=master - .. |codecov| image:: https://codecov.io/gh/ionelmc/python-tblib/branch/master/graphs/badge.svg?branch=master :alt: Coverage Status - :target: https://codecov.io/github/ionelmc/python-tblib + :target: https://app.codecov.io/github/ionelmc/python-tblib .. |version| image:: https://img.shields.io/pypi/v/tblib.svg :alt: PyPI Package latest release diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 3ca06b7..f3c9a7e 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -1,67 +1,57 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - import os +import pathlib import subprocess import sys -from os.path import abspath -from os.path import dirname -from os.path import exists -from os.path import join -from os.path import relpath -base_path = dirname(dirname(abspath(__file__))) -templates_path = join(base_path, "ci", "templates") +base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent +templates_path = base_path / 'ci' / 'templates' def check_call(args): - print("+", *args) + print('+', *args) subprocess.check_call(args) def exec_in_env(): - env_path = join(base_path, ".tox", "bootstrap") - if sys.platform == "win32": - bin_path = join(env_path, "Scripts") + env_path = base_path / '.tox' / 'bootstrap' + if sys.platform == 'win32': + bin_path = env_path / 'Scripts' else: - bin_path = join(env_path, "bin") - if not exists(env_path): + bin_path = env_path / 'bin' + if not env_path.exists(): import subprocess - print("Making bootstrap env in: {0} ...".format(env_path)) + print(f'Making bootstrap env in: {env_path} ...') try: - check_call([sys.executable, "-m", "venv", env_path]) + check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: - check_call([sys.executable, "-m", "virtualenv", env_path]) + check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: - check_call(["virtualenv", env_path]) - print("Installing `jinja2` into bootstrap environment...") - check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) - python_executable = join(bin_path, "python") - if not os.path.exists(python_executable): - python_executable += '.exe' + check_call(['virtualenv', env_path]) + print('Installing `jinja2` into bootstrap environment...') + check_call([bin_path / 'pip', 'install', 'jinja2', 'tox']) + python_executable = bin_path / 'python' + if not python_executable.exists(): + python_executable = python_executable.with_suffix('.exe') - print("Re-executing with: {0}".format(python_executable)) - print("+ exec", python_executable, __file__, "--no-env") - os.execv(python_executable, [python_executable, __file__, "--no-env"]) + 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("Project path: {0}".format(base_path)) + print(f'Project path: {base_path}') jinja = jinja2.Environment( - loader=jinja2.FileSystemLoader(templates_path), + 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 @@ -72,22 +62,22 @@ def main(): for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] - - for root, _, files in os.walk(templates_path): - for name in files: - relative = relpath(root, templates_path) - with open(join(base_path, relative, name), "w") as fh: - fh.write(jinja.get_template(join(relative, name)).render(tox_environments=tox_environments)) - print("Wrote {}".format(name)) - print("DONE.") + 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__": +if __name__ == '__main__': args = sys.argv[1:] - if args == ["--no-env"]: + if args == ['--no-env']: main() elif not args: exec_in_env() else: - print("Unexpected arguments {0}".format(args), file=sys.stderr) + print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) diff --git a/ci/requirements.txt b/ci/requirements.txt index a0ef106..a1708f4 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -3,3 +3,4 @@ pip>=19.1.1 setuptools>=18.0.1 six>=1.14.0 tox +twine diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index 7ee6426..b8e2655 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -10,13 +10,13 @@ jobs: matrix: include: - name: 'check' - python: '3.9' - toxpython: 'python3.9' + python: '3.11' + toxpython: 'python3.11' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' - python: '3.9' - toxpython: 'python3.9' + python: '3.11' + toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' {% for env in tox_environments %} @@ -39,15 +39,15 @@ jobs: python: '{{ python }}' toxpython: '{{ toxpython }}' python_arch: '{{ python_arch }}' - tox_env: '{{ env }}{% if 'cover' in env %},codecov{% endif %}' + tox_env: '{{ env }}' os: '{{ os }}-latest' {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} diff --git a/docs/conf.py b/docs/conf.py index 6ea2ca0..e32ed65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import sphinx_py3doc_enhanced_theme extensions = [ @@ -21,7 +18,7 @@ project = 'tblib' year = '2013-2022' author = 'Ionel Cristian Mărieș' -copyright = '{0}, {1}'.format(year, author) +copyright = f'{year}, {author}' version = release = '1.7.0' pygments_style = 'trac' @@ -42,7 +39,7 @@ html_sidebars = { '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], } -html_short_title = '%s-%s' % (project, version) +html_short_title = f'{project}-{version}' napoleon_use_ivar = True napoleon_use_rtype = False diff --git a/pyproject.toml b/pyproject.toml index a51acf9..d5ad57e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,54 @@ requires = [ "wheel", ] +[tool.ruff.per-file-ignores] +"ci/*" = ["S"] + +[tool.ruff] +extend-exclude = ["static", "ci/templates"] +ignore = [ + "RUF001", # ruff-specific rules ambiguous-unicode-character-string + "S101", # flake8-bandit assert + "S308", # flake8-bandit suspicious-mark-safe-usage + "E501", # pycodestyle line-too-long +] +line-length = 140 +select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "E", # pycodestyle errors + "EXE", # flake8-executable + "F", # pyflakes + "I", # isort + "INT", # flake8-gettext + "PIE", # flake8-pie + "PLC", # pylint convention + "PLE", # pylint errors + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RSE", # flake8-raise + "RUF", # ruff-specific rules + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings +] +src = ["src", "tests"] +target-version = "py37" + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.isort] +forced-separate = ["conftest"] +force-single-line = true + [tool.black] line-length = 140 -target-version = ['py36'] +target-version = ["py37"] skip-string-normalization = true + +[tool.ruff.flake8-quotes] +inline-quotes = "single" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1f37e56..0000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -max-line-length = 140 -exclude = .tox,.eggs,ci/templates,build,dist - -[tool:isort] -force_single_line = True -line_length = 120 -known_first_party = tblib -default_section = THIRDPARTY -forced_separate = test_tblib -skip = .tox,.eggs,ci/templates,build,dist diff --git a/setup.py b/setup.py index 58f3cd8..fa266dc 100755 --- a/setup.py +++ b/setup.py @@ -1,20 +1,13 @@ #!/usr/bin/env python -# -*- encoding: utf-8 -*- - -import io import re -from glob import glob -from os.path import basename -from os.path import dirname -from os.path import join -from os.path import splitext +from pathlib import Path from setuptools import find_packages from setuptools import setup def read(*names, **kwargs): - with io.open(join(dirname(__file__), *names), encoding=kwargs.get('encoding', 'utf8')) as fh: + with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() @@ -32,7 +25,7 @@ def read(*names, **kwargs): url='https://github.com/ionelmc/python-tblib', packages=find_packages('src'), package_dir={'': 'src'}, - py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, zip_safe=False, classifiers=[ @@ -50,6 +43,7 @@ def read(*names, **kwargs): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', # uncomment if you test on these interpreters: diff --git a/tox.ini b/tox.ini index 4c9405e..8afc240 100644 --- a/tox.ini +++ b/tox.ini @@ -7,14 +7,14 @@ commands = python ci/bootstrap.py --no-env passenv = * -; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist +; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] envlist = clean, check, docs, - {py37,py38,py39,py310,pypy37,pypy38}, + {py37,py38,py39,py310,py311,pypy37,pypy38,pypy39}, report ignore_basepython_conflict = true @@ -22,10 +22,12 @@ ignore_basepython_conflict = true basepython = pypy37: {env:TOXPYTHON:pypy3.7} pypy38: {env:TOXPYTHON:pypy3.8} + pypy39: {env:TOXPYTHON:pypy3.9} py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} + py311: {env:TOXPYTHON:python3.11} {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests @@ -36,25 +38,22 @@ usedevelop = false deps = pytest pytest-cov - six - twisted commands = - {posargs:pytest --cov --cov-report=term-missing -vv tests} + {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} [testenv:check] deps = docutils check-manifest - flake8 + pre-commit readme-renderer pygments isort skip_install = true commands = python setup.py check --strict --metadata --restructuredtext - check-manifest {toxinidir} - flake8 - isort --verbose --check-only --diff --filter-files . + check-manifest . + pre-commit run --all-files --show-diff-on-failure [testenv:docs] usedevelop = true @@ -64,13 +63,6 @@ commands = sphinx-build {posargs:-E} -b html docs dist/docs sphinx-build -b linkcheck docs dist/docs -[testenv:codecov] -deps = - codecov -skip_install = true -commands = - codecov [] - [testenv:report] deps = coverage From ba3a4014c54e13379618696df5a2b74a1b43fbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 10:58:16 +0300 Subject: [PATCH 06/57] Apply some ruff refactors and formatting. --- src/tblib/__init__.py | 12 ++--- src/tblib/decorators.py | 2 +- src/tblib/pickling_support.py | 7 +-- tests/badmodule.py | 2 +- tests/examples.py | 2 +- tests/test_issue30.py | 2 +- tests/test_pickle_exception.py | 24 +++++----- tests/test_tblib.py | 86 +++++++++++++++++----------------- 8 files changed, 69 insertions(+), 68 deletions(-) diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index c319184..094d9ca 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -27,7 +27,7 @@ class TracebackParseError(Exception): pass -class Code(object): +class Code: """ Class that replicates just enough of the builtin Code object to enable serialization and traceback rendering. """ @@ -46,14 +46,14 @@ def __init__(self, code): self.co_firstlineno = 0 -class Frame(object): +class Frame: """ Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering. """ def __init__(self, frame): self.f_locals = {} - self.f_globals = {k: v for k, v in frame.f_globals.items() if k in ("__file__", "__name__")} + self.f_globals = {k: v for k, v in frame.f_globals.items() if k in ('__file__', '__name__')} self.f_code = Code(frame.f_code) self.f_lineno = frame.f_lineno @@ -66,7 +66,7 @@ def clear(self): """ -class Traceback(object): +class Traceback: """ Class that wraps builtin Traceback objects. """ @@ -100,7 +100,7 @@ def as_traceback(self): while current: f_code = current.tb_frame.f_code code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec') - if hasattr(code, "replace"): + if hasattr(code, 'replace'): # Python 3.8 and newer code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=()) else: @@ -237,4 +237,4 @@ def from_string(cls, string, strict=True): ) return cls(previous) else: - raise TracebackParseError("Could not find any frames in %r." % string) + raise TracebackParseError('Could not find any frames in %r.' % string) diff --git a/src/tblib/decorators.py b/src/tblib/decorators.py index 29fdef2..38d0675 100644 --- a/src/tblib/decorators.py +++ b/src/tblib/decorators.py @@ -6,7 +6,7 @@ from . import Traceback -class Error(object): +class Error: def __init__(self, exc_type, exc_value, traceback): self.exc_type = exc_type self.exc_value = exc_value diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 5ec6e36..d58799f 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -39,8 +39,9 @@ def pickle_exception(obj): # still be pickled with protocol 5 if pickle.dump() is running with it. rv = obj.__reduce_ex__(3) if isinstance(rv, str): - raise TypeError("str __reduce__ output is not supported") - assert isinstance(rv, tuple) and len(rv) >= 2 + raise TypeError('str __reduce__ output is not supported') + assert isinstance(rv, tuple) + assert len(rv) >= 2 return (unpickle_exception, rv[:2] + (obj.__cause__, obj.__traceback__)) + rv[2:] @@ -81,4 +82,4 @@ def install(*exc_classes_or_instances): if len(exc_classes_or_instances) == 1: return exc else: - raise TypeError("Expected subclasses or instances of BaseException, got %s" % (type(exc))) + raise TypeError('Expected subclasses or instances of BaseException, got %s' % (type(exc))) diff --git a/tests/badmodule.py b/tests/badmodule.py index 86611f9..4915f00 100644 --- a/tests/badmodule.py +++ b/tests/badmodule.py @@ -1,3 +1,3 @@ a = 1 b = 2 -raise Exception("boom!") +raise Exception('boom!') diff --git a/tests/examples.py b/tests/examples.py index 441491b..756bf81 100644 --- a/tests/examples.py +++ b/tests/examples.py @@ -11,7 +11,7 @@ def func_c(): def func_d(): - raise Exception("Guessing time !") + raise Exception('Guessing time !') def bad_syntax(): diff --git a/tests/test_issue30.py b/tests/test_issue30.py index 270f146..18ac380 100644 --- a/tests/test_issue30.py +++ b/tests/test_issue30.py @@ -4,7 +4,7 @@ import pytest import six -from tblib import pickling_support # noqa: E402 +from tblib import pickling_support pytest.importorskip('twisted') diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index c5cd6c8..3cd32bd 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -27,12 +27,12 @@ class CustomError(Exception): pass -@pytest.mark.parametrize("protocol", [None] + list(range(1, pickle.HIGHEST_PROTOCOL + 1))) -@pytest.mark.parametrize("how", ["global", "instance", "class"]) +@pytest.mark.parametrize('protocol', [None, *list(range(1, pickle.HIGHEST_PROTOCOL + 1))]) +@pytest.mark.parametrize('how', ['global', 'instance', 'class']) def test_install(clear_dispatch_table, how, protocol): - if how == "global": + if how == 'global': tblib.pickling_support.install() - elif how == "class": + elif how == 'class': tblib.pickling_support.install(CustomError, ZeroDivisionError) try: @@ -41,27 +41,27 @@ def test_install(clear_dispatch_table, how, protocol): except Exception as e: # Python 3 only syntax # raise CustomError("foo") from e - new_e = CustomError("foo") + new_e = CustomError('foo') if has_python3: new_e.__cause__ = e raise new_e except Exception as e: exc = e else: - assert False + raise AssertionError # Populate Exception.__dict__, which is used in some cases exc.x = 1 if has_python3: exc.__cause__.x = 2 - if how == "instance": + if how == 'instance': tblib.pickling_support.install(exc) if protocol: exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) assert isinstance(exc, CustomError) - assert exc.args == ("foo",) + assert exc.args == ('foo',) assert exc.x == 1 if has_python3: assert exc.__traceback__ is not None @@ -78,19 +78,19 @@ class RegisteredError(Exception): def test_install_decorator(): with pytest.raises(RegisteredError) as ewrap: - raise RegisteredError("foo") + raise RegisteredError('foo') exc = ewrap.value exc.x = 1 exc = pickle.loads(pickle.dumps(exc)) assert isinstance(exc, RegisteredError) - assert exc.args == ("foo",) + assert exc.args == ('foo',) assert exc.x == 1 if has_python3: assert exc.__traceback__ is not None -@pytest.mark.skipif(sys.version_info[0] < 3, reason="No checks done in Python 2") +@pytest.mark.skipif(sys.version_info[0] < 3, reason='No checks done in Python 2') def test_install_typeerror(): with pytest.raises(TypeError): - tblib.pickling_support.install("foo") + tblib.pickling_support.install('foo') diff --git a/tests/test_tblib.py b/tests/test_tblib.py index 058d53a..f435aa2 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -31,27 +31,27 @@ def test_parse_traceback(): tb2 = Traceback(pytb) expected_dict = { - "tb_frame": { - "f_code": {"co_filename": "file1", "co_name": ""}, - "f_globals": {"__file__": "file1", "__name__": "?"}, - "f_lineno": 123, + 'tb_frame': { + 'f_code': {'co_filename': 'file1', 'co_name': ''}, + 'f_globals': {'__file__': 'file1', '__name__': '?'}, + 'f_lineno': 123, }, - "tb_lineno": 123, - "tb_next": { - "tb_frame": { - "f_code": {"co_filename": "file2", "co_name": "???"}, - "f_globals": {"__file__": "file2", "__name__": "?"}, - "f_lineno": 234, + 'tb_lineno': 123, + 'tb_next': { + 'tb_frame': { + 'f_code': {'co_filename': 'file2', 'co_name': '???'}, + 'f_globals': {'__file__': 'file2', '__name__': '?'}, + 'f_lineno': 234, }, - "tb_lineno": 234, - "tb_next": { - "tb_frame": { - "f_code": {"co_filename": "file3", "co_name": "function3"}, - "f_globals": {"__file__": "file3", "__name__": "?"}, - "f_lineno": 345, + 'tb_lineno': 234, + 'tb_next': { + 'tb_frame': { + 'f_code': {'co_filename': 'file3', 'co_name': 'function3'}, + 'f_globals': {'__file__': 'file3', '__name__': '?'}, + 'f_lineno': 345, }, - "tb_lineno": 345, - "tb_next": None, + 'tb_lineno': 345, + 'tb_next': None, }, }, } @@ -87,28 +87,28 @@ def test_raise(): 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 * ===*===", + '_ _ _ _ _ _ _ _ *', + '', + '> [?][?][?]', + '', + 'file1:123:*', + '_ _ _ _ _ _ _ _ *', + '', + '> [?][?][?]', + '', + 'file2:234:*', + '_ _ _ _ _ _ _ _ *', + '', + '> [?][?][?]', + '', + 'file3:345:*', + '_ _ _ _ _ _ _ _ *', + '', + '> [?][?][?]', + 'E RuntimeError', + '', + 'file4:456: RuntimeError', + '===*=== 1 failed in * ===*===', ] ) @@ -132,9 +132,9 @@ def test_raise(): result = testdir.runpytest_subprocess('--tb=line', '-vv', test) result.stdout.fnmatch_lines( [ - "===*=== FAILURES ===*===", - "file4:456: RuntimeError", - "===*=== 1 failed in * ===*===", + '===*=== FAILURES ===*===', + 'file4:456: RuntimeError', + '===*=== 1 failed in * ===*===', ] ) From 8b6899b4c878e276f81a336ddf5226d3a83570e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 11:04:18 +0300 Subject: [PATCH 07/57] Remove six as a test dep. --- tests/test_issue30.py | 4 ++-- tests/test_tblib.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_issue30.py b/tests/test_issue30.py index 18ac380..3452597 100644 --- a/tests/test_issue30.py +++ b/tests/test_issue30.py @@ -2,7 +2,6 @@ import sys import pytest -import six from tblib import pickling_support @@ -21,7 +20,8 @@ def test_30(): f = None try: - six.reraise(*pickle.loads(s)) + etype, evalue, etb = pickle.loads(s) + raise evalue.with_traceback(etb) except ValueError: f = Failure() diff --git a/tests/test_tblib.py b/tests/test_tblib.py index f435aa2..d05160b 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -63,8 +63,6 @@ def test_parse_traceback(): def test_pytest_integration(testdir): test = testdir.makepyfile( """ -import six - from tblib import Traceback def test_raise(): @@ -78,7 +76,7 @@ def test_raise(): File "file4", line 456, in "" ''') pytb = tb1.as_traceback() - six.reraise(RuntimeError, RuntimeError(), pytb) + raise RuntimeError().with_traceback(pytb) """ ) @@ -116,7 +114,7 @@ def test_raise(): result.stdout.fnmatch_lines( [ 'test_pytest_integration.py:*: in test_raise', - ' six.reraise(RuntimeError, RuntimeError(), pytb)', + ' raise RuntimeError().with_traceback(pytb)', 'file1:123: in ', ' ???', 'file2:234: in ???', @@ -143,7 +141,7 @@ def test_raise(): [ 'Traceback (most recent call last):', ' File "*test_pytest_integration.py", line *, in test_raise', - ' six.reraise(RuntimeError, RuntimeError(), pytb)', + ' raise RuntimeError().with_traceback(pytb)', ' File "file1", line 123, in ', ' File "file2", line 234, in ???', ' File "file3", line 345, in function3', From c808f05456b6522c0b52ec65ccbbd1f64cb4c083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 11:10:05 +0300 Subject: [PATCH 08/57] Fix some linting issues. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + src/tblib/__init__.py | 4 ++-- tests/examples.py | 4 ++-- tests/test_pickle_exception.py | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d37ca47..1831efd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # pre-commit install --install-hooks # To update the versions: # pre-commit autoupdate -exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' +exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|tests/bad.*.py)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/pyproject.toml b/pyproject.toml index d5ad57e..c9dd47b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ extend-exclude = ["static", "ci/templates"] ignore = [ "RUF001", # ruff-specific rules ambiguous-unicode-character-string "S101", # flake8-bandit assert + "S301", # flake8-bandit pickle "S308", # flake8-bandit suspicious-mark-safe-usage "E501", # pycodestyle line-too-long ] diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 094d9ca..f41bf30 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -15,7 +15,7 @@ def __getattr__(self, name): try: return self[name] except KeyError: - raise AttributeError(name) + raise AttributeError(name) from None # noinspection PyPep8Naming @@ -124,7 +124,7 @@ def as_traceback(self): # noinspection PyBroadException try: - exec(code, dict(current.tb_frame.f_globals), {}) + exec(code, dict(current.tb_frame.f_globals), {}) # noqa: S102 except Exception: next_tb = sys.exc_info()[2].tb_next if top_tb is None: diff --git a/tests/examples.py b/tests/examples.py index 756bf81..bb61d8c 100644 --- a/tests/examples.py +++ b/tests/examples.py @@ -17,10 +17,10 @@ def func_d(): def bad_syntax(): import badsyntax - badsyntax + badsyntax() def bad_module(): import badmodule - badmodule + badmodule() diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 3cd32bd..aa10762 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -18,7 +18,7 @@ def clear_dispatch_table(): bak = copyreg.dispatch_table.copy() copyreg.dispatch_table.clear() - yield + yield None copyreg.dispatch_table.clear() copyreg.dispatch_table.update(bak) @@ -37,7 +37,7 @@ def test_install(clear_dispatch_table, how, protocol): try: try: - 1 / 0 + 1 / 0 # noqa: B018 except Exception as e: # Python 3 only syntax # raise CustomError("foo") from e From 979adab8014b2cef98af1f638d1b04bc9c065a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 11:14:27 +0300 Subject: [PATCH 09/57] Update changelog. --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89cd975..9a34ad5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +2.0.0 (2023-06-22) +~~~~~~~~~~~~~~~~~~ + +* Removed support for legacy Pythons (2.7 and 3.6) and added Python 3.11 in the test grid. +* Some cleanups and refactors (mostly from ruff). + 1.7.0 (2020-07-24) ~~~~~~~~~~~~~~~~~~ From ae82d7fc4b928c610acabd41bfc057b93faff902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 22 Jun 2023 11:14:31 +0300 Subject: [PATCH 10/57] =?UTF-8?q?Bump=20version:=201.7.0=20=E2=86=92=202.0?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/tblib/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6c794f5..09d7051 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.7.0 +current_version = 2.0.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index b86e39e..5fea57e 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 1.7.0 + version: 2.0.0 version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' diff --git a/README.rst b/README.rst index df6142f..1a296cb 100644 --- a/README.rst +++ b/README.rst @@ -43,9 +43,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/tblib -.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v1.7.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v2.0.0.svg :alt: Commits since latest release - :target: https://github.com/ionelmc/python-tblib/compare/v1.7.0...master + :target: https://github.com/ionelmc/python-tblib/compare/v2.0.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index e32ed65..2169b97 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ year = '2013-2022' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' -version = release = '1.7.0' +version = release = '2.0.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/setup.py b/setup.py index fa266dc..f47d250 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='tblib', - version='1.7.0', + version='2.0.0', license='BSD-2-Clause', description='Traceback serialization library.', long_description='{}\n{}'.format( diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index f41bf30..a9b1084 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -2,7 +2,7 @@ import sys from types import CodeType -__version__ = '1.7.0' +__version__ = '2.0.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') From 12c678f751f547a094762e0484bc8fbbfe8c6a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Fri, 23 Jun 2023 07:45:15 +0200 Subject: [PATCH 11/57] Remove redundant wheel dep from pyproject.toml Remove the redundant `wheel` dependency, as it is added by the backend automatically. Listing it explicitly in the documentation was a historical mistake and has been fixed since, see: https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c9dd47b..52e351c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools>=30.3.0", - "wheel", ] [tool.ruff.per-file-ignores] From 166bc55d1485544c259a246b24abc1b463381e6c Mon Sep 17 00:00:00 2001 From: Tim Maxwell Date: Thu, 5 Oct 2023 16:01:09 -0700 Subject: [PATCH 12/57] Add support for various Python 3.11 things --- src/tblib/pickling_support.py | 82 +++++++++++++++++++++++++++++++--- tests/test_pickle_exception.py | 40 +++++++++++++++-- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index d58799f..451374b 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -22,14 +22,20 @@ def pickle_traceback(tb): return unpickle_traceback, (Frame(tb.tb_frame), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next)) -def unpickle_exception(func, args, cause, tb): +# unpickle_exception_3_11() / pickle_exception_3_11() are used in Python 3.11 and newer + +def unpickle_exception_3_11(func, args, cause, context, notes, suppress_context, tb): inst = func(*args) inst.__cause__ = cause + inst.__context__ = context + if notes is not None: + inst.__notes__ = notes + inst.__suppress_context__ = suppress_context inst.__traceback__ = tb return inst -def pickle_exception(obj): +def pickle_exception_3_11(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 @@ -43,9 +49,49 @@ def pickle_exception(obj): assert isinstance(rv, tuple) assert len(rv) >= 2 + return ( + unpickle_exception_3_11, + rv[:2] + ( + obj.__cause__, + obj.__context__, + getattr(obj, "__notes__", None), + obj.__suppress_context__, + obj.__traceback__, + ), + ) + rv[2:] + + +# unpickle_exception() / pickle_exception() are used on Python 3.10 and older; or when deserializing +# old Pickle archives created by Python 3.10 and older. + +def unpickle_exception(func, args, cause, tb): + inst = func(*args) + inst.__cause__ = cause + inst.__traceback__ = tb + return inst + + +def pickle_exception(obj): + 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 + + # NOTE: The __context__ and __suppress_context__ attributes actually existed prior to Python + # 3.11, so in theory we should support them here. But existing Pickle archives might refer to + # unpickle_exception(), so we need to keep it around anyway; and it's not worth the trouble + # introducing a third pair of pickling/unpickling functions. + return (unpickle_exception, rv[:2] + (obj.__cause__, obj.__traceback__)) + rv[2:] +if sys.version_info >= (3, 11): + pickle_exception_latest = pickle_exception_3_11 +else: + pickle_exception_latest = pickle_exception + + def _get_subclasses(cls): # Depth-first traversal of all direct and indirect subclasses of cls to_visit = [cls] @@ -68,18 +114,40 @@ def install(*exc_classes_or_instances): if not exc_classes_or_instances: for exception_cls in _get_subclasses(BaseException): - copyreg.pickle(exception_cls, pickle_exception) + copyreg.pickle(exception_cls, pickle_exception_latest) return for exc in exc_classes_or_instances: if isinstance(exc, BaseException): - while exc is not None: - copyreg.pickle(type(exc), pickle_exception) - exc = exc.__cause__ + _install_for_instance(exc, set()) elif isinstance(exc, type) and issubclass(exc, BaseException): - copyreg.pickle(exc, pickle_exception) + copyreg.pickle(exc, pickle_exception_latest) # Allow using @install as a decorator for Exception classes if len(exc_classes_or_instances) == 1: return exc else: raise TypeError('Expected subclasses or instances of BaseException, got %s' % (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_latest) + + 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) \ No newline at end of file diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index aa10762..c997c75 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -12,6 +12,7 @@ import tblib.pickling_support has_python3 = sys.version_info.major >= 3 +has_python311 = sys.version_info >= (3, 11) @pytest.fixture @@ -33,17 +34,24 @@ def test_install(clear_dispatch_table, how, protocol): if how == 'global': tblib.pickling_support.install() elif how == 'class': - tblib.pickling_support.install(CustomError, ZeroDivisionError) + tblib.pickling_support.install(CustomError, ValueError, ZeroDivisionError) try: try: - 1 / 0 # noqa: B018 + try: + 1 / 0 # noqa: B018 + finally: + # In Python 3, 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') if has_python3: new_e.__cause__ = e + if has_python311: + new_e.add_note("note 1") + new_e.add_note("note 2") raise new_e except Exception as e: exc = e @@ -54,6 +62,7 @@ def test_install(clear_dispatch_table, how, protocol): exc.x = 1 if has_python3: exc.__cause__.x = 2 + exc.__cause__.__context__.x = 3 if how == 'instance': tblib.pickling_support.install(exc) @@ -65,10 +74,17 @@ def test_install(clear_dispatch_table, how, protocol): assert exc.x == 1 if has_python3: assert exc.__traceback__ is not None - assert isinstance(exc.__cause__, ZeroDivisionError) + assert isinstance(exc.__cause__, ValueError) assert exc.__cause__.__traceback__ is not None assert exc.__cause__.x == 2 assert exc.__cause__.__cause__ is None + if has_python311: + assert exc.__notes__ == ["note 1", "note 2"] + 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 + @tblib.pickling_support.install @@ -90,6 +106,24 @@ def test_install_decorator(): assert exc.__traceback__ is not None +@pytest.mark.skipif(sys.version_info < (3,11), reason="no BaseExceptionGroup before Python 3.11") +def test_install_instance_recursively(clear_dispatch_table): + exc = BaseExceptionGroup( + "test", + [ + ValueError("foo"), + CustomError("bar") + ] + ) + exc.exceptions[0].__cause__ = ZeroDivisionError("baz") + exc.exceptions[0].__cause__.__context__ = AttributeError("quux") + + tblib.pickling_support.install(exc) + + installed = set(c for c in copyreg.dispatch_table if issubclass(c, BaseException)) + assert installed == {ExceptionGroup, ValueError, CustomError, ZeroDivisionError, AttributeError} + + @pytest.mark.skipif(sys.version_info[0] < 3, reason='No checks done in Python 2') def test_install_typeerror(): with pytest.raises(TypeError): From 66cac232f852374c3d264748a876294f7d13e445 Mon Sep 17 00:00:00 2001 From: Tim Maxwell Date: Thu, 5 Oct 2023 17:56:55 -0700 Subject: [PATCH 13/57] Simpler design that merges the pickle/unpickle functions --- src/tblib/pickling_support.py | 58 +++++++++-------------------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 451374b..8ad250b 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -22,20 +22,20 @@ def pickle_traceback(tb): return unpickle_traceback, (Frame(tb.tb_frame), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next)) -# unpickle_exception_3_11() / pickle_exception_3_11() are used in Python 3.11 and newer - -def unpickle_exception_3_11(func, args, cause, context, notes, suppress_context, tb): +# 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 - inst.__suppress_context__ = suppress_context - inst.__traceback__ = tb return inst -def pickle_exception_3_11(obj): +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 @@ -50,48 +50,18 @@ def pickle_exception_3_11(obj): assert len(rv) >= 2 return ( - unpickle_exception_3_11, + unpickle_exception, rv[:2] + ( obj.__cause__, + obj.__traceback__, obj.__context__, - getattr(obj, "__notes__", None), obj.__suppress_context__, - obj.__traceback__, + # __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:] -# unpickle_exception() / pickle_exception() are used on Python 3.10 and older; or when deserializing -# old Pickle archives created by Python 3.10 and older. - -def unpickle_exception(func, args, cause, tb): - inst = func(*args) - inst.__cause__ = cause - inst.__traceback__ = tb - return inst - - -def pickle_exception(obj): - 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 - - # NOTE: The __context__ and __suppress_context__ attributes actually existed prior to Python - # 3.11, so in theory we should support them here. But existing Pickle archives might refer to - # unpickle_exception(), so we need to keep it around anyway; and it's not worth the trouble - # introducing a third pair of pickling/unpickling functions. - - return (unpickle_exception, rv[:2] + (obj.__cause__, obj.__traceback__)) + rv[2:] - - -if sys.version_info >= (3, 11): - pickle_exception_latest = pickle_exception_3_11 -else: - pickle_exception_latest = pickle_exception - - def _get_subclasses(cls): # Depth-first traversal of all direct and indirect subclasses of cls to_visit = [cls] @@ -114,14 +84,14 @@ def install(*exc_classes_or_instances): if not exc_classes_or_instances: for exception_cls in _get_subclasses(BaseException): - copyreg.pickle(exception_cls, pickle_exception_latest) + 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_latest) + copyreg.pickle(exc, pickle_exception) # Allow using @install as a decorator for Exception classes if len(exc_classes_or_instances) == 1: return exc @@ -138,7 +108,7 @@ def _install_for_instance(exc, seen): return seen.add(id(exc)) - copyreg.pickle(type(exc), pickle_exception_latest) + copyreg.pickle(type(exc), pickle_exception) if exc.__cause__ is not None: _install_for_instance(exc.__cause__, seen) @@ -150,4 +120,4 @@ def _install_for_instance(exc, seen): if hasattr(exc, "exceptions") and isinstance(exc.exceptions, (tuple, list)): for subexc in exc.exceptions: if isinstance(subexc, BaseException): - _install_for_instance(subexc, seen) \ No newline at end of file + _install_for_instance(subexc, seen) From b425df07a397145a972a47c4e566c9a9c64ad59d Mon Sep 17 00:00:00 2001 From: Tim Maxwell Date: Thu, 5 Oct 2023 18:07:56 -0700 Subject: [PATCH 14/57] bump version; test more stuff in pre-3.11 --- setup.py | 2 +- tests/test_pickle_exception.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index f47d250..6f4f8fc 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='tblib', - version='2.0.0', + version='2.1.0', license='BSD-2-Clause', description='Traceback serialization library.', long_description='{}\n{}'.format( diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index c997c75..dbf19da 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -41,9 +41,10 @@ def test_install(clear_dispatch_table, how, protocol): try: 1 / 0 # noqa: B018 finally: - # In Python 3, the ValueError's __context__ will be the ZeroDivisionError + # The ValueError's __context__ will be the ZeroDivisionError raise ValueError("blah") except Exception as e: + assert isinstance(e.__context__, ZeroDivisionError) # Python 3 only syntax # raise CustomError("foo") from e new_e = CustomError('foo') @@ -74,16 +75,20 @@ def test_install(clear_dispatch_table, how, protocol): assert exc.x == 1 if has_python3: 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 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 + @@ -106,7 +111,7 @@ def test_install_decorator(): assert exc.__traceback__ is not None -@pytest.mark.skipif(sys.version_info < (3,11), reason="no BaseExceptionGroup before Python 3.11") +@pytest.mark.skipif(not has_python311, reason="no BaseExceptionGroup before Python 3.11") def test_install_instance_recursively(clear_dispatch_table): exc = BaseExceptionGroup( "test", From d35e55e9c57177e1d9537607a63d9f3ef4eb9ce5 Mon Sep 17 00:00:00 2001 From: Tim Maxwell Date: Fri, 6 Oct 2023 12:27:24 -0700 Subject: [PATCH 15/57] Run linter --- src/tblib/pickling_support.py | 8 +++++--- tests/test_pickle_exception.py | 26 +++++++++----------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 8ad250b..ecfdbac 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -51,13 +51,14 @@ def pickle_exception(obj): return ( unpickle_exception, - rv[:2] + ( + 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), + getattr(obj, '__notes__', None), ), ) + rv[2:] @@ -98,6 +99,7 @@ def install(*exc_classes_or_instances): else: raise TypeError('Expected subclasses or instances of BaseException, got %s' % (type(exc))) + def _install_for_instance(exc, seen): assert isinstance(exc, BaseException) @@ -117,7 +119,7 @@ def _install_for_instance(exc, 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)): + if hasattr(exc, 'exceptions') and isinstance(exc.exceptions, (tuple, list)): for subexc in exc.exceptions: if isinstance(subexc, BaseException): _install_for_instance(subexc, seen) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index dbf19da..12952fb 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -42,7 +42,7 @@ def test_install(clear_dispatch_table, how, protocol): 1 / 0 # noqa: B018 finally: # The ValueError's __context__ will be the ZeroDivisionError - raise ValueError("blah") + raise ValueError('blah') except Exception as e: assert isinstance(e.__context__, ZeroDivisionError) # Python 3 only syntax @@ -51,8 +51,8 @@ def test_install(clear_dispatch_table, how, protocol): if has_python3: new_e.__cause__ = e if has_python311: - new_e.add_note("note 1") - new_e.add_note("note 2") + new_e.add_note('note 1') + new_e.add_note('note 2') raise new_e except Exception as e: exc = e @@ -87,9 +87,7 @@ def test_install(clear_dispatch_table, how, protocol): assert exc.__cause__.__context__.__context__ is None if has_python311: - assert exc.__notes__ == ["note 1", "note 2"] - - + assert exc.__notes__ == ['note 1', 'note 2'] @tblib.pickling_support.install @@ -111,21 +109,15 @@ def test_install_decorator(): assert exc.__traceback__ is not None -@pytest.mark.skipif(not has_python311, reason="no BaseExceptionGroup before Python 3.11") +@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") - ] - ) - exc.exceptions[0].__cause__ = ZeroDivisionError("baz") - exc.exceptions[0].__cause__.__context__ = AttributeError("quux") + exc = BaseExceptionGroup('test', [ValueError('foo'), CustomError('bar')]) + exc.exceptions[0].__cause__ = ZeroDivisionError('baz') + exc.exceptions[0].__cause__.__context__ = AttributeError('quux') tblib.pickling_support.install(exc) - installed = set(c for c in copyreg.dispatch_table if issubclass(c, BaseException)) + installed = {c for c in copyreg.dispatch_table if issubclass(c, BaseException)} assert installed == {ExceptionGroup, ValueError, CustomError, ZeroDivisionError, AttributeError} From dcb16f003c454e9e0eeb7cc8260cf4c33e914dab Mon Sep 17 00:00:00 2001 From: Tim Maxwell Date: Fri, 6 Oct 2023 12:28:53 -0700 Subject: [PATCH 16/57] Fix remaining error manually --- tests/test_pickle_exception.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 12952fb..d28fb14 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -44,7 +44,6 @@ def test_install(clear_dispatch_table, how, protocol): # The ValueError's __context__ will be the ZeroDivisionError raise ValueError('blah') except Exception as e: - assert isinstance(e.__context__, ZeroDivisionError) # Python 3 only syntax # raise CustomError("foo") from e new_e = CustomError('foo') From 8985b952cd8af594b3a9c822f793f980ee79aeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 21 Oct 2023 17:14:19 +0300 Subject: [PATCH 17/57] Update skel. Drop EOL python 3.7 and add 3.12 in the test grid. --- .cookiecutterrc | 2 +- .github/workflows/github-actions.yml | 60 ++++++++++++++-------------- .pre-commit-config.yaml | 6 +-- CONTRIBUTING.rst | 4 +- LICENSE | 2 +- README.rst | 2 +- pyproject.toml | 4 +- setup.py | 18 ++++----- tox.ini | 6 +-- 9 files changed, 51 insertions(+), 53 deletions(-) diff --git a/.cookiecutterrc b/.cookiecutterrc index 5fea57e..6f91f74 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -44,4 +44,4 @@ default_context: version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' - year_to: '2022' + year_to: '2023' diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 51985bc..0d2c740 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -19,24 +19,6 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py37 (ubuntu)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37' - os: 'ubuntu-latest' - - name: 'py37 (windows)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37' - os: 'windows-latest' - - name: 'py37 (macos)' - python: '3.7' - toxpython: 'python3.7' - python_arch: 'x64' - tox_env: 'py37' - os: 'macos-latest' - name: 'py38 (ubuntu)' python: '3.8' toxpython: 'python3.8' @@ -109,23 +91,23 @@ jobs: python_arch: 'x64' tox_env: 'py311' os: 'macos-latest' - - name: 'pypy37 (ubuntu)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312 (ubuntu)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37' + tox_env: 'py312' os: 'ubuntu-latest' - - name: 'pypy37 (windows)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312 (windows)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37' + tox_env: 'py312' os: 'windows-latest' - - name: 'pypy37 (macos)' - python: 'pypy-3.7' - toxpython: 'pypy3.7' + - name: 'py312 (macos)' + python: '3.12' + toxpython: 'python3.12' python_arch: 'x64' - tox_env: 'pypy37' + tox_env: 'py312' os: 'macos-latest' - name: 'pypy38 (ubuntu)' python: 'pypy-3.8' @@ -163,6 +145,24 @@ jobs: python_arch: 'x64' tox_env: 'pypy39' os: 'macos-latest' + - name: 'pypy310 (ubuntu)' + python: 'pypy-3.1' + toxpython: 'pypy3.1' + python_arch: 'x64' + tox_env: 'pypy310' + os: 'ubuntu-latest' + - name: 'pypy310 (windows)' + python: 'pypy-3.1' + toxpython: 'pypy3.1' + python_arch: 'x64' + tox_env: 'pypy310' + os: 'windows-latest' + - name: 'pypy310 (macos)' + python: 'pypy-3.1' + toxpython: 'pypy3.1' + python_arch: 'x64' + tox_env: 'pypy310' + os: 'macos-latest' steps: - uses: actions/checkout@v3 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1831efd..e96cf93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,16 +6,16 @@ exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|tests/bad.*.py)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.274 + rev: v0.1.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a89ed3a..bba6ab4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -49,7 +49,7 @@ To set up `python-tblib` for local development: Now you can make your changes locally. -4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: +4. When you're done making changes run all the checks and docs builder with one command:: tox @@ -73,8 +73,6 @@ For merging, you should: 3. Add a note to ``CHANGELOG.rst`` about the changes. 4. Add yourself to ``AUTHORS.rst``. - - Tips ---- diff --git a/LICENSE b/LICENSE index 151345b..9bb39bf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2013-2022, Ionel Cristian Mărieș. All rights reserved. +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: diff --git a/README.rst b/README.rst index 1a296cb..cefd5a5 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ Overview * - docs - |docs| * - tests - - | |github-actions| |requires| + - | |github-actions| | |codecov| * - package - | |version| |wheel| |supported-versions| |supported-implementations| diff --git a/pyproject.toml b/pyproject.toml index 52e351c..af75660 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ select = [ "W", # pycodestyle warnings ] src = ["src", "tests"] -target-version = "py37" +target-version = "py38" [tool.ruff.flake8-pytest-style] fixture-parentheses = false @@ -50,7 +50,7 @@ force-single-line = true [tool.black] line-length = 140 -target-version = ["py37"] +target-version = ["py38"] skip-string-normalization = true [tool.ruff.flake8-quotes] diff --git a/setup.py b/setup.py index 6f4f8fc..4226f9e 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='tblib', - version='2.1.0', + version='2.0.0', license='BSD-2-Clause', description='Traceback serialization library.', long_description='{}\n{}'.format( @@ -39,17 +39,17 @@ def read(*names, **kwargs): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', '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', + # "Programming Language :: Python :: Implementation :: IronPython", + # "Programming Language :: Python :: Implementation :: Jython", + # "Programming Language :: Python :: Implementation :: Stackless", 'Topic :: Utilities', ], project_urls={ @@ -62,13 +62,13 @@ def read(*names, **kwargs): 'debugging', 'exceptions', ], - python_requires='>=3.7', + python_requires='>=3.8', install_requires=[ - # eg: 'aspectlib==1.1.1', 'six>=1.7', + # eg: "aspectlib==1.1.1", "six>=1.7", ], extras_require={ # eg: - # 'rst': ['docutils>=0.11'], - # ':python_version=="2.6"': ['argparse'], + # "rst": ["docutils>=0.11"], + # ":python_version=="2.6"": ["argparse"], }, ) diff --git a/tox.ini b/tox.ini index 8afc240..7d6a38e 100644 --- a/tox.ini +++ b/tox.ini @@ -14,20 +14,20 @@ envlist = clean, check, docs, - {py37,py38,py39,py310,py311,pypy37,pypy38,pypy39}, + {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}, report ignore_basepython_conflict = true [testenv] basepython = - pypy37: {env:TOXPYTHON:pypy3.7} pypy38: {env:TOXPYTHON:pypy3.8} pypy39: {env:TOXPYTHON:pypy3.9} - py37: {env:TOXPYTHON:python3.7} + pypy310: {env:TOXPYTHON:pypy3.10} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} + py312: {env:TOXPYTHON:python3.12} {bootstrap,clean,check,report,docs,codecov}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests From ba23e5f664adcf8fc7f4fd245263f54ce1d68b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 21 Oct 2023 17:44:08 +0300 Subject: [PATCH 18/57] Add some extra assertions to make sure the roundtrip produces the same traceback text. --- tests/test_pickle_exception.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index d28fb14..f0d8ba6 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -1,3 +1,5 @@ +from traceback import format_exception + try: import copyreg except ImportError: @@ -52,12 +54,14 @@ def test_install(clear_dispatch_table, how, protocol): if has_python311: new_e.add_note('note 1') new_e.add_note('note 2') - raise new_e + raise new_e from e except Exception as e: exc = e else: raise AssertionError + expected_format_exception = ''.join(format_exception(type(exc), exc, exc.__traceback__)) + print(expected_format_exception) # Populate Exception.__dict__, which is used in some cases exc.x = 1 if has_python3: @@ -88,6 +92,8 @@ def test_install(clear_dispatch_table, how, protocol): if has_python311: assert exc.__notes__ == ['note 1', 'note 2'] + assert expected_format_exception == ''.join(format_exception(type(exc), exc, exc.__traceback__)) + @tblib.pickling_support.install class RegisteredError(Exception): From 3bbe381cdddf63c7f55f5359397ae92ae39e8523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 21 Oct 2023 17:45:35 +0300 Subject: [PATCH 19/57] Disable granular error positions (pep657) since those don't survive a tblib roundtrip anyway. In other words tblib doesn't support fine grained error locations, for now... --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 7d6a38e..10215f8 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ basepython = setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes + PYTHONNODEBUGRANGES=yes passenv = * usedevelop = false From 7d4370889f037387c23fb8378a665605b35b4183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 02:36:11 +0300 Subject: [PATCH 20/57] Add get_locals kw-only argument to pickle_traceback, Frame and Traceback to allow customization of locals extraction. Fixes #41. Closes #66. Closes #67. --- src/tblib/__init__.py | 24 +++++++---- src/tblib/pickling_support.py | 28 +++++------- tests/test_pickle_exception.py | 78 +++++++++++++++++++++++----------- tests/test_tblib.py | 51 ++++++++++++++++++++++ 4 files changed, 130 insertions(+), 51 deletions(-) diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index a9b1084..6e2e1ea 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -51,8 +51,8 @@ class Frame: Class that replicates just enough of the builtin Frame object to enable serialization and traceback rendering. """ - def __init__(self, frame): - self.f_locals = {} + 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 @@ -73,9 +73,8 @@ class Traceback: tb_next = None - def __init__(self, tb): - self.tb_frame = Frame(tb.tb_frame) - # noinspection SpellCheckingInspection + 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 @@ -84,7 +83,7 @@ def __init__(self, tb): cls = type(self) while tb is not None: traceback = object.__new__(cls) - traceback.tb_frame = Frame(tb.tb_frame) + 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 @@ -124,7 +123,7 @@ def as_traceback(self): # noinspection PyBroadException try: - exec(code, dict(current.tb_frame.f_globals), {}) # noqa: S102 + 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: @@ -151,7 +150,7 @@ def as_dict(self): if self.tb_next is None: tb_next = None else: - tb_next = self.tb_next.to_dict() + tb_next = self.tb_next.as_dict() code = { 'co_filename': self.tb_frame.f_code.co_filename, @@ -159,6 +158,7 @@ def as_dict(self): } 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, } @@ -186,6 +186,7 @@ def from_dict(cls, dct): ) 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'], ) @@ -194,7 +195,7 @@ def from_dict(cls, dct): tb_lineno=dct['tb_lineno'], tb_next=tb_next, ) - return cls(tb) + return cls(tb, get_locals=get_all_locals) @classmethod def from_string(cls, string, strict=True): @@ -230,6 +231,7 @@ def from_string(cls, string, strict=True): __file__=frame['co_filename'], __name__='?', ), + f_locals={}, f_code=_AttrDict(frame), f_lineno=int(frame['tb_lineno']), ), @@ -238,3 +240,7 @@ def from_string(cls, string, strict=True): return cls(previous) else: raise TracebackParseError('Could not find any frames in %r.' % string) + + +def get_all_locals(frame): + return dict(frame.f_locals) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index ecfdbac..bb67e76 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -1,14 +1,10 @@ -import sys +import copyreg +from functools import partial from types import TracebackType from . import Frame from . import Traceback -if sys.version_info.major >= 3: - import copyreg -else: - import copy_reg as copyreg - def unpickle_traceback(tb_frame, tb_lineno, tb_next): ret = object.__new__(Traceback) @@ -18,8 +14,12 @@ def unpickle_traceback(tb_frame, tb_lineno, tb_next): return ret.as_traceback() -def pickle_traceback(tb): - return unpickle_traceback, (Frame(tb.tb_frame), tb.tb_lineno, tb.tb_next and Traceback(tb.tb_next)) +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 @@ -72,16 +72,8 @@ def _get_subclasses(cls): to_visit += list(this.__subclasses__()) -def install(*exc_classes_or_instances): - copyreg.pickle(TracebackType, pickle_traceback) - - if sys.version_info.major < 3: - # Dummy decorator? - if len(exc_classes_or_instances) == 1: - exc = exc_classes_or_instances[0] - if isinstance(exc, type) and issubclass(exc, BaseException): - return exc - return +def install(*exc_classes_or_instances, get_locals=None): + copyreg.pickle(TracebackType, partial(pickle_traceback, get_locals=get_locals)) if not exc_classes_or_instances: for exception_cls in _get_subclasses(BaseException): diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index f0d8ba6..5ff4679 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -13,7 +13,6 @@ import tblib.pickling_support -has_python3 = sys.version_info.major >= 3 has_python311 = sys.version_info >= (3, 11) @@ -49,11 +48,10 @@ def test_install(clear_dispatch_table, how, protocol): # Python 3 only syntax # raise CustomError("foo") from e new_e = CustomError('foo') - if has_python3: - new_e.__cause__ = e - if has_python311: - new_e.add_note('note 1') - new_e.add_note('note 2') + 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 @@ -64,9 +62,8 @@ def test_install(clear_dispatch_table, how, protocol): print(expected_format_exception) # Populate Exception.__dict__, which is used in some cases exc.x = 1 - if has_python3: - exc.__cause__.x = 2 - exc.__cause__.__context__.x = 3 + exc.__cause__.x = 2 + exc.__cause__.__context__.x = 3 if how == 'instance': tblib.pickling_support.install(exc) @@ -76,21 +73,20 @@ def test_install(clear_dispatch_table, how, protocol): assert isinstance(exc, CustomError) assert exc.args == ('foo',) assert exc.x == 1 - if has_python3: - assert exc.__traceback__ is not None + 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__, 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 + 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'] + if has_python311: + assert exc.__notes__ == ['note 1', 'note 2'] assert expected_format_exception == ''.join(format_exception(type(exc), exc, exc.__traceback__)) @@ -110,8 +106,7 @@ def test_install_decorator(): assert isinstance(exc, RegisteredError) assert exc.args == ('foo',) assert exc.x == 1 - if has_python3: - assert exc.__traceback__ is not None + assert exc.__traceback__ is not None @pytest.mark.skipif(not has_python311, reason='no BaseExceptionGroup before Python 3.11') @@ -126,7 +121,42 @@ def test_install_instance_recursively(clear_dispatch_table): assert installed == {ExceptionGroup, ValueError, CustomError, ZeroDivisionError, AttributeError} -@pytest.mark.skipif(sys.version_info[0] < 3, reason='No checks done in Python 2') def test_install_typeerror(): with pytest.raises(TypeError): tblib.pickling_support.install('foo') + + +@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)) + assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1} diff --git a/tests/test_tblib.py b/tests/test_tblib.py index d05160b..2f04cd9 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -9,6 +9,54 @@ 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( """ @@ -34,6 +82,7 @@ def test_parse_traceback(): 'tb_frame': { 'f_code': {'co_filename': 'file1', 'co_name': ''}, 'f_globals': {'__file__': 'file1', '__name__': '?'}, + 'f_locals': {}, 'f_lineno': 123, }, 'tb_lineno': 123, @@ -41,6 +90,7 @@ def test_parse_traceback(): 'tb_frame': { 'f_code': {'co_filename': 'file2', 'co_name': '???'}, 'f_globals': {'__file__': 'file2', '__name__': '?'}, + 'f_locals': {}, 'f_lineno': 234, }, 'tb_lineno': 234, @@ -48,6 +98,7 @@ def test_parse_traceback(): 'tb_frame': { 'f_code': {'co_filename': 'file3', 'co_name': 'function3'}, 'f_globals': {'__file__': 'file3', '__name__': '?'}, + 'f_locals': {}, 'f_lineno': 345, }, 'tb_lineno': 345, From 8d8bf6e64620b990109fc3fcfaa56e4c88b8b544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 02:42:38 +0300 Subject: [PATCH 21/57] Fix link and update. --- AUTHORS.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5f6547d..5d3dcd3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,6 @@ Authors * Elliott Sales de Andrade - https://github.com/QuLogic * Victor Stinner - https://github.com/vstinner * Guido Imperiale - https://github.com/crusaderky -* Ivanq - https://github.com/imachug +* Alisa Sireneva - https://github.com/purplesyringa +* Michał Górny - https://github.com/mgorny +* Tim Maxwell - https://github.com/tmaxwell-anthropic From 20957ca318190857f141dc75ac90cba7cb55286f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 02:56:59 +0300 Subject: [PATCH 22/57] Add some docstrings. --- src/tblib/__init__.py | 21 +++++++++++++++++++++ src/tblib/pickling_support.py | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 6e2e1ea..0ad19ed 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -49,6 +49,12 @@ def __init__(self, code): 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): @@ -69,6 +75,21 @@ def clear(self): 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 diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index bb67e76..0085140 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -73,6 +73,11 @@ def _get_subclasses(cls): 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: From db64b01d66867c0d1c195fd91b5a513dc085f2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 03:01:28 +0300 Subject: [PATCH 23/57] Update changelog. --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a34ad5..e315ea4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ========= +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) ~~~~~~~~~~~~~~~~~~ From 64a45bd9ba1589617528bacc62c9ad8247cf46fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 03:02:35 +0300 Subject: [PATCH 24/57] Fix test grid for pypy. --- .github/workflows/github-actions.yml | 12 ++++++------ ci/templates/.github/workflows/github-actions.yml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 0d2c740..7ea058e 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -146,20 +146,20 @@ jobs: tox_env: 'pypy39' os: 'macos-latest' - name: 'pypy310 (ubuntu)' - python: 'pypy-3.1' - toxpython: 'pypy3.1' + python: 'pypy-3.10' + toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'ubuntu-latest' - name: 'pypy310 (windows)' - python: 'pypy-3.1' - toxpython: 'pypy3.1' + python: 'pypy-3.10' + toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'windows-latest' - name: 'pypy310 (macos)' - python: 'pypy-3.1' - toxpython: 'pypy3.1' + python: 'pypy-3.10' + toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'macos-latest' diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index b8e2655..a99ff59 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -22,9 +22,9 @@ jobs: {% for env in tox_environments %} {% set prefix = env.split('-')[0] -%} {% if prefix.startswith('pypy') %} -{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set python %}pypy-{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% set cpython %}pp{{ prefix[4:5] }}{% endset %} -{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5] }}{% endset %} +{% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% else %} {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} {% set cpython %}cp{{ prefix[2:] }}{% endset %} From 6cd4b055336056320608ae042484fc7ba970f930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 03:14:06 +0300 Subject: [PATCH 25/57] Add missing os field. --- .readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 59ff5c0..561d565 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,6 +3,8 @@ version: 2 sphinx: configuration: docs/conf.py formats: all +build: + os: ubuntu-22.04 python: install: - requirements: docs/requirements.txt From 79866a66be0b6ffdfcfcd277389beaa52edf9b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 03:32:49 +0300 Subject: [PATCH 26/57] Manually specify a python. --- .readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 561d565..009a913 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,6 +5,8 @@ sphinx: formats: all build: os: ubuntu-22.04 + tools: + python: "3" python: install: - requirements: docs/requirements.txt From 1e5a2e6bf1ec012fdaa7883a5e7843c968682a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 22 Oct 2023 03:34:50 +0300 Subject: [PATCH 27/57] =?UTF-8?q?Bump=20version:=202.0.0=20=E2=86=92=203.0?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/tblib/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 09d7051..2658a5d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.0 +current_version = 3.0.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 6f91f74..6b22c96 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' - version: 2.0.0 + version: 3.0.0 version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' diff --git a/README.rst b/README.rst index cefd5a5..49604e5 100644 --- a/README.rst +++ b/README.rst @@ -43,9 +43,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/tblib -.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v2.0.0.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v3.0.0.svg :alt: Commits since latest release - :target: https://github.com/ionelmc/python-tblib/compare/v2.0.0...master + :target: https://github.com/ionelmc/python-tblib/compare/v3.0.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index 2169b97..c9f498a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ year = '2013-2022' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' -version = release = '2.0.0' +version = release = '3.0.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/setup.py b/setup.py index 4226f9e..c5364a1 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='tblib', - version='2.0.0', + version='3.0.0', license='BSD-2-Clause', description='Traceback serialization library.', long_description='{}\n{}'.format( diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 0ad19ed..70af7ff 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -2,7 +2,7 @@ import sys from types import CodeType -__version__ = '2.0.0' +__version__ = '3.0.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') From 04dc5426812c508a10774122450acf386e546dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 13 Dec 2023 17:57:26 +0200 Subject: [PATCH 28/57] Loosen up test. Ref #74. --- tests/test_pickle_exception.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 5ff4679..995e53a 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -58,7 +58,11 @@ def test_install(clear_dispatch_table, how, protocol): else: raise AssertionError - expected_format_exception = ''.join(format_exception(type(exc), exc, exc.__traceback__)) + expected_format_exception = ''.join(format_exception(type(exc), exc, exc.__traceback__)).replace( + '~~^~~', + '^^^^^^^^^^^^^^^^^', + ) + print(expected_format_exception) # Populate Exception.__dict__, which is used in some cases exc.x = 1 From 66e14cdd101e4214cca6acca8f48b9abe4f75729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Fri, 15 Dec 2023 16:52:07 +0200 Subject: [PATCH 29/57] More aggressive location stripping. Ref #74. --- tests/test_pickle_exception.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 995e53a..53a9dce 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -29,6 +29,10 @@ 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): @@ -58,12 +62,8 @@ def test_install(clear_dispatch_table, how, protocol): else: raise AssertionError - expected_format_exception = ''.join(format_exception(type(exc), exc, exc.__traceback__)).replace( - '~~^~~', - '^^^^^^^^^^^^^^^^^', - ) + expected_format_exception = strip_locations(''.join(format_exception(type(exc), exc, exc.__traceback__))) - print(expected_format_exception) # Populate Exception.__dict__, which is used in some cases exc.x = 1 exc.__cause__.x = 2 @@ -92,7 +92,7 @@ def test_install(clear_dispatch_table, how, protocol): if has_python311: assert exc.__notes__ == ['note 1', 'note 2'] - assert expected_format_exception == ''.join(format_exception(type(exc), exc, exc.__traceback__)) + assert expected_format_exception == strip_locations(''.join(format_exception(type(exc), exc, exc.__traceback__))) @tblib.pickling_support.install From 9f6f864f7de6ce6640bab6d962f00b956da75361 Mon Sep 17 00:00:00 2001 From: Alexandre Detiste Date: Wed, 1 May 2024 13:43:53 +0200 Subject: [PATCH 30/57] vendore reraise() from six --- ci/requirements.txt | 1 - setup.py | 2 +- src/tblib/decorators.py | 14 ++++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ci/requirements.txt b/ci/requirements.txt index a1708f4..b4f1852 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,6 +1,5 @@ virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 -six>=1.14.0 tox twine diff --git a/setup.py b/setup.py index c5364a1..c09d4c3 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ def read(*names, **kwargs): ], python_requires='>=3.8', install_requires=[ - # eg: "aspectlib==1.1.1", "six>=1.7", + # eg: "aspectlib==1.1.1", ], extras_require={ # eg: diff --git a/src/tblib/decorators.py b/src/tblib/decorators.py index 38d0675..a1ace45 100644 --- a/src/tblib/decorators.py +++ b/src/tblib/decorators.py @@ -1,11 +1,21 @@ import sys from functools import wraps -from six import reraise - 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 From e47172da6c268812f65135ab1e224d8a443c914a Mon Sep 17 00:00:00 2001 From: Haoyu Weng Date: Tue, 25 Mar 2025 16:44:17 +0000 Subject: [PATCH 31/57] Avoid repeated code compilation --- src/tblib/__init__.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 70af7ff..ba3260e 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -117,27 +117,38 @@ def as_traceback(self): current = self top_tb = None tb = None + stub = compile( + 'raise __traceback_maker', + '', + 'exec', + ) while current: f_code = current.tb_frame.f_code - code = compile('\n' * (current.tb_lineno - 1) + 'raise __traceback_maker', current.tb_frame.f_code.co_filename, 'exec') - if hasattr(code, 'replace'): + if hasattr(stub, 'replace'): # Python 3.8 and newer - code = code.replace(co_argcount=0, co_filename=f_code.co_filename, co_name=f_code.co_name, co_freevars=(), co_cellvars=()) + 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=(), + ) else: code = CodeType( 0, - code.co_kwonlyargcount, - code.co_nlocals, - code.co_stacksize, - code.co_flags, - code.co_code, - code.co_consts, - code.co_names, - code.co_varnames, + stub.co_kwonlyargcount, + stub.co_nlocals, + stub.co_stacksize, + stub.co_flags, + stub.co_code, + stub.co_consts, + stub.co_names, + stub.co_varnames, f_code.co_filename, f_code.co_name, - code.co_firstlineno, - code.co_lnotab, + current.tb_lineno, + stub.co_lnotab, (), (), ) From 92df3b2257d283795292c2c50a21fc0f70dff2c1 Mon Sep 17 00:00:00 2001 From: Haoyu Weng Date: Tue, 25 Mar 2025 17:55:17 +0000 Subject: [PATCH 32/57] Add tests and author information --- AUTHORS.rst | 1 + tests/test_tblib.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5d3dcd3..156e0c3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,3 +13,4 @@ Authors * 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 diff --git a/tests/test_tblib.py b/tests/test_tblib.py index 2f04cd9..6d01ebe 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -111,6 +111,18 @@ def test_parse_traceback(): 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 10413a2c8bf00cc9256ffe0677194d1ae375a929 Mon Sep 17 00:00:00 2001 From: Haoyu Weng Date: Tue, 25 Mar 2025 23:24:27 +0000 Subject: [PATCH 33/57] Remove support for Python 3.7 --- src/tblib/__init__.py | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index ba3260e..a83d9bf 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -1,6 +1,5 @@ import re import sys -from types import CodeType __version__ = '3.0.0' __all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' @@ -124,34 +123,14 @@ def as_traceback(self): ) while current: f_code = current.tb_frame.f_code - if hasattr(stub, 'replace'): - # Python 3.8 and newer - 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=(), - ) - else: - code = CodeType( - 0, - stub.co_kwonlyargcount, - stub.co_nlocals, - stub.co_stacksize, - stub.co_flags, - stub.co_code, - stub.co_consts, - stub.co_names, - stub.co_varnames, - f_code.co_filename, - f_code.co_name, - current.tb_lineno, - stub.co_lnotab, - (), - (), - ) + 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: From 67bbe3ffe5403307c8b57b613ed4c7e287695120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 15:06:58 +0300 Subject: [PATCH 34/57] Add some perf tests. --- pytest.ini | 1 + tests/test_perf.py | 29 +++++++++++++++++++++++++++++ tox.ini | 1 + 3 files changed, 31 insertions(+) create mode 100644 tests/test_perf.py diff --git a/pytest.ini b/pytest.ini index 5f7ccc6..d4de49c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -18,6 +18,7 @@ addopts = --doctest-modules --doctest-glob=\*.rst --tb=short + --benchmark-disable testpaths = tests diff --git a/tests/test_perf.py b/tests/test_perf.py new file mode 100644 index 0000000..2cfedff --- /dev/null +++ b/tests/test_perf.py @@ -0,0 +1,29 @@ +from 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() diff --git a/tox.ini b/tox.ini index 10215f8..c826299 100644 --- a/tox.ini +++ b/tox.ini @@ -39,6 +39,7 @@ usedevelop = false deps = pytest pytest-cov + pytest-benchmark commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} From 79fea5dbd7dca0581c73dc813f789d0ef3e04fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 15:16:37 +0300 Subject: [PATCH 35/57] Drop support for 3.8 and apply some formatting/skel updates. --- .cookiecutterrc | 6 +- .github/workflows/github-actions.yml | 70 ++++++++----------- .pre-commit-config.yaml | 13 ++-- README.rst | 8 +-- .../.github/workflows/github-actions.yml | 16 +++-- docs/conf.py | 19 ++--- pyproject.toml | 33 ++++----- setup.py | 9 ++- src/tblib/__init__.py | 4 +- src/tblib/pickling_support.py | 2 +- tests/test_issue30.py | 2 +- tests/test_pickle_exception.py | 10 +-- tests/test_tblib.py | 2 +- tox.ini | 10 +-- 14 files changed, 94 insertions(+), 110 deletions(-) diff --git a/.cookiecutterrc b/.cookiecutterrc index 6b22c96..833f8b2 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,9 +1,6 @@ # Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: - allow_tests_inside_package: 'no' - c_extension_function: '-' - c_extension_module: '-' c_extension_optional: 'no' c_extension_support: 'no' codacy: 'no' @@ -17,10 +14,12 @@ default_context: 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 @@ -40,6 +39,7 @@ default_context: sphinx_doctest: 'no' sphinx_theme: sphinx-py3doc-enhanced-theme test_matrix_separate_coverage: 'no' + tests_inside_package: 'no' version: 3.0.0 version_manager: bump2version website: https://blog.ionelmc.ro/ diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 7ea058e..4bb0ee7 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -1,5 +1,5 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: test: name: ${{ matrix.name }} @@ -19,24 +19,6 @@ jobs: toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py38 (ubuntu)' - python: '3.8' - toxpython: 'python3.8' - python_arch: 'x64' - tox_env: 'py38' - os: 'ubuntu-latest' - - name: 'py38 (windows)' - python: '3.8' - toxpython: 'python3.8' - python_arch: 'x64' - tox_env: 'py38' - os: 'windows-latest' - - name: 'py38 (macos)' - python: '3.8' - toxpython: 'python3.8' - python_arch: 'x64' - tox_env: 'py38' - os: 'macos-latest' - name: 'py39 (ubuntu)' python: '3.9' toxpython: 'python3.9' @@ -52,7 +34,7 @@ jobs: - name: 'py39 (macos)' python: '3.9' toxpython: 'python3.9' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'py39' os: 'macos-latest' - name: 'py310 (ubuntu)' @@ -70,7 +52,7 @@ jobs: - name: 'py310 (macos)' python: '3.10' toxpython: 'python3.10' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'py310' os: 'macos-latest' - name: 'py311 (ubuntu)' @@ -88,7 +70,7 @@ jobs: - name: 'py311 (macos)' python: '3.11' toxpython: 'python3.11' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'py311' os: 'macos-latest' - name: 'py312 (ubuntu)' @@ -106,26 +88,26 @@ jobs: - name: 'py312 (macos)' python: '3.12' toxpython: 'python3.12' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'py312' os: 'macos-latest' - - name: 'pypy38 (ubuntu)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'py313 (ubuntu)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy38' + tox_env: 'py313' os: 'ubuntu-latest' - - name: 'pypy38 (windows)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' + - name: 'py313 (windows)' + python: '3.13' + toxpython: 'python3.13' python_arch: 'x64' - tox_env: 'pypy38' + tox_env: 'py313' os: 'windows-latest' - - name: 'pypy38 (macos)' - python: 'pypy-3.8' - toxpython: 'pypy3.8' - python_arch: 'x64' - tox_env: 'pypy38' + - 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' @@ -142,7 +124,7 @@ jobs: - name: 'pypy39 (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'pypy39' os: 'macos-latest' - name: 'pypy310 (ubuntu)' @@ -160,14 +142,14 @@ jobs: - name: 'pypy310 (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' - python_arch: 'x64' + python_arch: 'arm64' tox_env: 'pypy310' os: 'macos-latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} @@ -183,3 +165,11 @@ jobs: 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 }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e96cf93..76749ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,20 +2,17 @@ # pre-commit install --install-hooks # To update the versions: # pre-commit autoupdate -exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|tests/bad.*.py)(/|$)' +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.1.1 + rev: v0.11.2 hooks: - id: ruff - args: [--fix, --exit-non-zero-on-fix, --show-fixes] - - repo: https://github.com/psf/black - rev: 23.10.0 - hooks: - - id: black + args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/README.rst b/README.rst index 49604e5..832f5ba 100644 --- a/README.rst +++ b/README.rst @@ -10,13 +10,11 @@ Overview * - docs - |docs| * - tests - - | |github-actions| - | |codecov| + - |github-actions| |codecov| * - package - - | |version| |wheel| |supported-versions| |supported-implementations| - | |commits-since| + - |version| |wheel| |supported-versions| |supported-implementations| |commits-since| .. |docs| image:: https://readthedocs.org/projects/python-tblib/badge/?style=flat - :target: https://python-tblib.readthedocs.io/ + :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 diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index a99ff59..0c3d40b 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -1,5 +1,5 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: test: name: {{ '${{ matrix.name }}' }} @@ -33,7 +33,7 @@ jobs: {% for os, python_arch in [ ['ubuntu', 'x64'], ['windows', 'x64'], - ['macos', 'x64'], + ['macos', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' @@ -44,10 +44,10 @@ jobs: {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} @@ -63,3 +63,11 @@ jobs: 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 %} diff --git a/docs/conf.py b/docs/conf.py index c9f498a..9508050 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,6 @@ -import sphinx_py3doc_enhanced_theme - extensions = [ - 'autoapi.extension', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', 'sphinx.ext.coverage', 'sphinx.ext.doctest', 'sphinx.ext.extlinks', @@ -10,13 +9,10 @@ 'sphinx.ext.todo', 'sphinx.ext.viewcode', ] -autoapi_type = 'python' -autoapi_dirs = ['../src'] - source_suffix = '.rst' master_doc = 'index' project = 'tblib' -year = '2013-2022' +year = '2013-2023' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' version = release = '3.0.0' @@ -24,11 +20,11 @@ pygments_style = 'trac' templates_path = ['.'] extlinks = { - 'issue': ('https://github.com/ionelmc/python-tblib/issues/%s', '#'), - 'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #'), + 'issue': ('https://github.com/ionelmc/python-tblib/issues/%s', '#%s'), + 'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #%s'), } + html_theme = 'sphinx_py3doc_enhanced_theme' -html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] html_theme_options = { 'githuburl': 'https://github.com/ionelmc/python-tblib/', } @@ -36,9 +32,6 @@ html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False -html_sidebars = { - '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], -} html_short_title = f'{project}-{version}' napoleon_use_ivar = True diff --git a/pyproject.toml b/pyproject.toml index af75660..3accd6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,26 @@ [build-system] requires = [ - "setuptools>=30.3.0", + "setuptools>=40.1.0", ] -[tool.ruff.per-file-ignores] -"ci/*" = ["S"] - [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 - "S301", # flake8-bandit pickle "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 ] -line-length = 140 select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions @@ -30,28 +35,20 @@ select = [ "PLE", # pylint errors "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib - "Q", # flake8-quotes "RSE", # flake8-raise "RUF", # ruff-specific rules "S", # flake8-bandit "UP", # pyupgrade "W", # pycodestyle warnings ] -src = ["src", "tests"] -target-version = "py38" -[tool.ruff.flake8-pytest-style] +[tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false -[tool.ruff.isort] +[tool.ruff.lint.isort] forced-separate = ["conftest"] force-single-line = true -[tool.black] -line-length = 140 -target-version = ["py38"] -skip-string-normalization = true - -[tool.ruff.flake8-quotes] -inline-quotes = "single" +[tool.ruff.format] +quote-style = "single" diff --git a/setup.py b/setup.py index c09d4c3..00f5342 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import re from pathlib import Path -from setuptools import find_packages +from setuptools import find_namespace_packages from setuptools import setup @@ -23,7 +23,7 @@ def read(*names, **kwargs): author='Ionel Cristian Mărieș', author_email='contact@ionelmc.ro', url='https://github.com/ionelmc/python-tblib', - packages=find_packages('src'), + packages=find_namespace_packages('src'), package_dir={'': 'src'}, py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, @@ -32,18 +32,17 @@ def read(*names, **kwargs): # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', '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.8', '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: @@ -62,7 +61,7 @@ def read(*names, **kwargs): 'debugging', 'exceptions', ], - python_requires='>=3.8', + python_requires='>=3.9', install_requires=[ # eg: "aspectlib==1.1.1", ], diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index a83d9bf..cdc179d 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -2,7 +2,7 @@ import sys __version__ = '3.0.0' -__all__ = 'Traceback', 'TracebackParseError', 'Frame', 'Code' +__all__ = 'Code', 'Frame', 'Traceback', 'TracebackParseError' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') @@ -250,7 +250,7 @@ def from_string(cls, string, strict=True): ) return cls(previous) else: - raise TracebackParseError('Could not find any frames in %r.' % string) + raise TracebackParseError(f'Could not find any frames in {string!r}.') def get_all_locals(frame): diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 0085140..6fdd30f 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -94,7 +94,7 @@ def install(*exc_classes_or_instances, get_locals=None): if len(exc_classes_or_instances) == 1: return exc else: - raise TypeError('Expected subclasses or instances of BaseException, got %s' % (type(exc))) + raise TypeError(f'Expected subclasses or instances of BaseException, got {type(exc)}') def _install_for_instance(exc, seen): diff --git a/tests/test_issue30.py b/tests/test_issue30.py index 3452597..cb66bd0 100644 --- a/tests/test_issue30.py +++ b/tests/test_issue30.py @@ -20,7 +20,7 @@ def test_30(): f = None try: - etype, evalue, etb = pickle.loads(s) + etype, evalue, etb = pickle.loads(s) # noqa: S301 raise evalue.with_traceback(etb) except ValueError: f = Failure() diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 53a9dce..a16c3a1 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -72,7 +72,7 @@ def test_install(clear_dispatch_table, how, protocol): if how == 'instance': tblib.pickling_support.install(exc) if protocol: - exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) + exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 assert isinstance(exc, CustomError) assert exc.args == ('foo',) @@ -105,7 +105,7 @@ def test_install_decorator(): raise RegisteredError('foo') exc = ewrap.value exc.x = 1 - exc = pickle.loads(pickle.dumps(exc)) + exc = pickle.loads(pickle.dumps(exc)) # noqa: S301 assert isinstance(exc, RegisteredError) assert exc.args == ('foo',) @@ -115,14 +115,14 @@ def test_install_decorator(): @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')]) + 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} + assert installed == {ExceptionGroup, ValueError, CustomError, ZeroDivisionError, AttributeError} # noqa: F821 def test_install_typeerror(): @@ -162,5 +162,5 @@ def func(my_arg='2'): if how == 'instance': tblib.pickling_support.install(exc, get_locals=get_locals) - exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) + exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1} diff --git a/tests/test_tblib.py b/tests/test_tblib.py index 6d01ebe..bac06e1 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -107,7 +107,7 @@ def test_parse_traceback(): }, } tb3 = Traceback.from_dict(expected_dict) - tb4 = pickle.loads(pickle.dumps(tb3)) + tb4 = pickle.loads(pickle.dumps(tb3)) # noqa: S301 assert tb4.as_dict() == tb3.as_dict() == tb2.as_dict() == tb1.as_dict() == expected_dict diff --git a/tox.ini b/tox.ini index c826299..92154c0 100644 --- a/tox.ini +++ b/tox.ini @@ -14,20 +14,19 @@ envlist = clean, check, docs, - {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}, + {py39,py310,py311,py312,py313,pypy39,pypy310}, report ignore_basepython_conflict = true [testenv] basepython = - pypy38: {env:TOXPYTHON:pypy3.8} pypy39: {env:TOXPYTHON:pypy3.9} pypy310: {env:TOXPYTHON:pypy3.10} - py38: {env:TOXPYTHON:python3.8} 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 @@ -74,7 +73,10 @@ commands = coverage html [testenv:clean] -commands = coverage erase +commands = + python setup.py clean + coverage erase skip_install = true deps = + setuptools coverage From be707e9b08e7e1824be2d5882ae26996c5bf360f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 15:54:32 +0300 Subject: [PATCH 36/57] Switch to furo theme and fix link. --- .cookiecutterrc | 2 +- CHANGELOG.rst | 2 +- docs/conf.py | 2 +- docs/requirements.txt | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.cookiecutterrc b/.cookiecutterrc index 833f8b2..594c8de 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -37,7 +37,7 @@ default_context: sphinx_docs: 'yes' sphinx_docs_hosting: https://python-tblib.readthedocs.io/ sphinx_doctest: 'no' - sphinx_theme: sphinx-py3doc-enhanced-theme + sphinx_theme: furo test_matrix_separate_coverage: 'no' tests_inside_package: 'no' version: 3.0.0 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e315ea4..06dce67 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -51,7 +51,7 @@ Changelog * Add support for PyPy3.5-5.7.1-beta. Previously ``AttributeError: 'Frame' object has no attribute 'clear'`` could be raised. See PyPy - issue `#2532 `_. + issue `#2532 `_. 1.3.1 (2017-03-27) ~~~~~~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index 9508050..56436f0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ 'pr': ('https://github.com/ionelmc/python-tblib/pull/%s', 'PR #%s'), } -html_theme = 'sphinx_py3doc_enhanced_theme' +html_theme = 'furo' html_theme_options = { 'githuburl': 'https://github.com/ionelmc/python-tblib/', } diff --git a/docs/requirements.txt b/docs/requirements.txt index deb6219..c03e307 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,2 @@ sphinx>=1.3 -sphinx-py3doc-enhanced-theme -sphinx-autoapi +furo From 5dddd6ad96db61ba7ba538da3322beddddefd81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 15:57:32 +0300 Subject: [PATCH 37/57] Update changelog. --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 06dce67..e3a5db9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ 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) ~~~~~~~~~~~~~~~~~~ From 8b1e1a2cc66ef41d39021ff8ce1e21f826a152ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 15:57:52 +0300 Subject: [PATCH 38/57] =?UTF-8?q?Bump=20version:=203.0.0=20=E2=86=92=203.1?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- setup.py | 2 +- src/tblib/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2658a5d..da3332b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.0.0 +current_version = 3.1.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 594c8de..6822d5b 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_theme: furo test_matrix_separate_coverage: 'no' tests_inside_package: 'no' - version: 3.0.0 + version: 3.1.0 version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' diff --git a/README.rst b/README.rst index 832f5ba..1ed2db9 100644 --- a/README.rst +++ b/README.rst @@ -41,9 +41,9 @@ Overview :alt: Supported implementations :target: https://pypi.org/project/tblib -.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v3.0.0.svg +.. |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.0.0...master + :target: https://github.com/ionelmc/python-tblib/compare/v3.1.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index 56436f0..34a8cf7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ year = '2013-2023' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' -version = release = '3.0.0' +version = release = '3.1.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/setup.py b/setup.py index 00f5342..14c6267 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='tblib', - version='3.0.0', + version='3.1.0', license='BSD-2-Clause', description='Traceback serialization library.', long_description='{}\n{}'.format( diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index cdc179d..772f7ec 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -1,7 +1,7 @@ import re import sys -__version__ = '3.0.0' +__version__ = '3.1.0' __all__ = 'Code', 'Frame', 'Traceback', 'TracebackParseError' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$') From 4287a2e51b6c4fdd55630d8315a958e53ae6636d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 31 Mar 2025 16:25:03 +0300 Subject: [PATCH 39/57] Forgot to add. --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e3a5db9..6414793 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,7 @@ Changelog * Improved performance of ``as_traceback`` by a large factor. Contributed by Haoyu Weng in `#81 `_. +* Dropped support for now-EOL Python 3.8 and added 3.13 in the test grid. 3.0.0 (2023-10-22) ~~~~~~~~~~~~~~~~~~ From a9491833474e3ac7b9f22d84c4512cca171ef682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 11 Sep 2025 21:37:00 +0300 Subject: [PATCH 40/57] Create SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..da9c516 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. From 1da47d4e15302eab76a7948f47aceab450db780b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 18 Oct 2025 02:55:18 +0300 Subject: [PATCH 41/57] Up project skel and fix some lint issues. --- .bumpversion.cfg | 6 +- .cookiecutterrc | 2 +- .coveragerc | 2 +- .github/workflows/github-actions.yml | 151 ++++++++++++++---- .gitignore | 1 + .pre-commit-config.yaml | 11 +- .readthedocs.yml | 2 +- .taplo.toml | 3 + LICENSE | 2 +- MANIFEST.in | 2 + ci/bootstrap.py | 4 +- ci/requirements.txt | 10 +- .../.github/workflows/github-actions.yml | 28 ++-- docs/conf.py | 12 +- pyproject.toml | 90 +++++++++-- setup.py | 50 +----- src/tblib/pickling_support.py | 7 +- tests/examples.py | 4 +- tests/test_issue30.py | 4 +- tox.ini | 26 ++- 20 files changed, 268 insertions(+), 149 deletions(-) create mode 100644 .taplo.toml diff --git a/.bumpversion.cfg b/.bumpversion.cfg index da3332b..2693d65 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,9 +3,9 @@ current_version = 3.1.0 commit = True tag = True -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" [bumpversion:file (badge):README.rst] search = /v{current_version}.svg diff --git a/.cookiecutterrc b/.cookiecutterrc index 6822d5b..f069a1d 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -44,4 +44,4 @@ default_context: version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' - year_to: '2023' + year_to: '2025' diff --git a/.coveragerc b/.coveragerc index 2f29a3a..1fc951c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,7 +8,7 @@ branch = true source = tblib tests -parallel = true +patch = subprocess [report] show_missing = true diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 4bb0ee7..8f18b6c 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -10,143 +10,231 @@ jobs: matrix: include: - name: 'check' - python: '3.11' - toxpython: 'python3.11' + python: '3.13' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' - python: '3.11' - toxpython: 'python3.11' + python: '3.13' tox_env: 'docs' os: 'ubuntu-latest' - name: 'py39 (ubuntu)' python: '3.9' - toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39' os: 'ubuntu-latest' + cover: true - name: 'py39 (windows)' python: '3.9' - toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39' os: 'windows-latest' + cover: true + - name: 'py39 (windows)' + python: '3.9' + python_arch: 'x86' + tox_env: 'py39' + os: 'windows-latest' + cover: true - name: 'py39 (macos)' python: '3.9' - toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39' os: 'macos-latest' + cover: true - name: 'py310 (ubuntu)' python: '3.10' - toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310' os: 'ubuntu-latest' + cover: true - name: 'py310 (windows)' python: '3.10' - toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310' os: 'windows-latest' + cover: true + - name: 'py310 (windows)' + python: '3.10' + python_arch: 'x86' + tox_env: 'py310' + os: 'windows-latest' + cover: true - name: 'py310 (macos)' python: '3.10' - toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310' os: 'macos-latest' + cover: true - name: 'py311 (ubuntu)' python: '3.11' - toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311' os: 'ubuntu-latest' + cover: true - name: 'py311 (windows)' python: '3.11' - toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311' os: 'windows-latest' + cover: true + - name: 'py311 (windows)' + python: '3.11' + python_arch: 'x86' + tox_env: 'py311' + os: 'windows-latest' + cover: true - name: 'py311 (macos)' python: '3.11' - toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311' os: 'macos-latest' + cover: true - name: 'py312 (ubuntu)' python: '3.12' - toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' os: 'ubuntu-latest' + cover: true - name: 'py312 (windows)' python: '3.12' - toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312' os: 'windows-latest' + cover: true + - name: 'py312 (windows)' + python: '3.12' + python_arch: 'x86' + tox_env: 'py312' + os: 'windows-latest' + cover: true - name: 'py312 (macos)' python: '3.12' - toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312' os: 'macos-latest' + cover: true - name: 'py313 (ubuntu)' python: '3.13' - toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313' os: 'ubuntu-latest' + cover: true - name: 'py313 (windows)' python: '3.13' - toxpython: 'python3.13' python_arch: 'x64' tox_env: 'py313' os: 'windows-latest' + cover: true + - name: 'py313 (windows)' + python: '3.13' + python_arch: 'x86' + tox_env: 'py313' + os: 'windows-latest' + cover: true - name: 'py313 (macos)' python: '3.13' - toxpython: 'python3.13' python_arch: 'arm64' tox_env: 'py313' os: 'macos-latest' + cover: true + - name: 'py314 (ubuntu)' + python: '3.14' + python_arch: 'x64' + tox_env: 'py314' + os: 'ubuntu-latest' + cover: true + - name: 'py314 (windows)' + python: '3.14' + python_arch: 'x64' + tox_env: 'py314' + os: 'windows-latest' + cover: true + - name: 'py314 (windows)' + python: '3.14' + python_arch: 'x86' + tox_env: 'py314' + os: 'windows-latest' + cover: true + - name: 'py314 (macos)' + python: '3.14' + python_arch: 'arm64' + tox_env: 'py314' + os: 'macos-latest' + cover: true - name: 'pypy39 (ubuntu)' python: 'pypy-3.9' - toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39' os: 'ubuntu-latest' + cover: true - name: 'pypy39 (windows)' python: 'pypy-3.9' - toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39' os: 'windows-latest' + cover: true + - name: 'pypy39 (windows)' + python: 'pypy-3.9' + python_arch: 'x86' + tox_env: 'pypy39' + os: 'windows-latest' + cover: true - name: 'pypy39 (macos)' python: 'pypy-3.9' - toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39' os: 'macos-latest' + cover: true - name: 'pypy310 (ubuntu)' python: 'pypy-3.10' - toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'ubuntu-latest' + cover: true - name: 'pypy310 (windows)' python: 'pypy-3.10' - toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'windows-latest' + cover: true + - name: 'pypy310 (windows)' + python: 'pypy-3.10' + python_arch: 'x86' + tox_env: 'pypy310' + os: 'windows-latest' + cover: true - name: 'pypy310 (macos)' python: 'pypy-3.10' - toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310' os: 'macos-latest' + cover: true + - name: 'pypy311 (ubuntu)' + python: 'pypy-3.11' + python_arch: 'x64' + tox_env: 'pypy311' + os: 'ubuntu-latest' + cover: true + - name: 'pypy311 (windows)' + python: 'pypy-3.11' + python_arch: 'x64' + tox_env: 'pypy311' + os: 'windows-latest' + cover: true + - name: 'pypy311 (windows)' + python: 'pypy-3.11' + python_arch: 'x86' + tox_env: 'pypy311' + os: 'windows-latest' + cover: true + - name: 'pypy311 (macos)' + python: 'pypy-3.11' + python_arch: 'arm64' + tox_env: 'pypy311' + os: 'macos-latest' + cover: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-python@v5 @@ -161,15 +249,16 @@ jobs: tox --version pip list --format=freeze - name: test - env: - TOXPYTHON: '${{ matrix.toxpython }}' run: > tox -e ${{ matrix.tox_env }} -v + - uses: codecov/codecov-action@v5 + if: matrix.cover + with: + flags: ${{ matrix.name }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true finish: needs: test if: ${{ always() }} runs-on: ubuntu-latest steps: - - uses: codecov/codecov-action@v3 - with: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 77973dd..49e39a8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__ *~ *.bak .DS_Store +Thumbs.db # C extensions *.so diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 76749ba..cb42dd0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,14 +6,21 @@ 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 + rev: v0.14.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format + - id: taplo-lint - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] - id: debug-statements diff --git a/.readthedocs.yml b/.readthedocs.yml index 009a913..a2d480b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ sphinx: configuration: docs/conf.py formats: all build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3" python: diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..8f8054d --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,3 @@ +[formatting] +array_auto_collapse = false +indent_string = " " diff --git a/LICENSE b/LICENSE index 9bb39bf..844749a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2013-2023, Ionel Cristian Mărieș. All rights reserved. +Copyright (c) 2013-2025, Ionel Cristian Mărieș. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in index d0dac9c..4bac1f0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,8 @@ include .editorconfig include .github/workflows/github-actions.yml include .pre-commit-config.yaml include .readthedocs.yml +include .taplo.toml +include pyproject.toml include pytest.ini include tox.ini diff --git a/ci/bootstrap.py b/ci/bootstrap.py index f3c9a7e..d069fb5 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -20,8 +20,6 @@ def exec_in_env(): 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]) @@ -42,7 +40,7 @@ def exec_in_env(): def main(): - import jinja2 + import jinja2 # noqa: PLC0415 print(f'Project path: {base_path}') diff --git a/ci/requirements.txt b/ci/requirements.txt index b4f1852..c1a2807 100644 --- a/ci/requirements.txt +++ b/ci/requirements.txt @@ -1,5 +1,5 @@ -virtualenv>=16.6.0 -pip>=19.1.1 -setuptools>=18.0.1 -tox -twine +pip>=25 +setuptools>=80 +setuptools_scm>=8 +tox>=4 +virtualenv>=20.34 diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index 0c3d40b..ac17262 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -10,41 +10,39 @@ jobs: matrix: include: - name: 'check' - python: '3.11' - toxpython: 'python3.11' + python: '3.13' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' - python: '3.11' - toxpython: 'python3.11' + python: '3.13' tox_env: 'docs' os: 'ubuntu-latest' {% for env in tox_environments %} {% set prefix = env.split('-')[0] -%} +{% set freethreaded = prefix.endswith('t') %} {% 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 python %}{{ prefix[2] }}.{{ prefix[3:].rstrip('t') }}{% 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'], + ['windows', 'x86'], ['macos', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' - toxpython: '{{ toxpython }}' python_arch: '{{ python_arch }}' tox_env: '{{ env }}' os: '{{ os }}-latest' + cover: true {% endfor %} {% endfor %} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: actions/setup-python@v5 @@ -59,15 +57,17 @@ jobs: tox --version pip list --format=freeze - name: test - env: - TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' run: > tox -e {{ '${{ matrix.tox_env }}' }} -v + - uses: codecov/codecov-action@v5 + if: matrix.cover + with: + flags: {{ '${{ matrix.name }}' }} + token: {{ '${{ secrets.CODECOV_TOKEN }}' }} + verbose: true finish: needs: test if: {{ '${{ always() }}' }} runs-on: ubuntu-latest steps: - - uses: codecov/codecov-action@v3 - with: - CODECOV_TOKEN: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} +{{ '' }} diff --git a/docs/conf.py b/docs/conf.py index 34a8cf7..ff25663 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ source_suffix = '.rst' master_doc = 'index' project = 'tblib' -year = '2013-2023' +year = '2013-2025' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' version = release = '3.1.0' @@ -26,7 +26,15 @@ html_theme = 'furo' html_theme_options = { - 'githuburl': 'https://github.com/ionelmc/python-tblib/', + 'source_repository': 'https://github.com/ionelmc/python-tblib/', + 'source_branch': 'master', + 'source_directory': 'docs/', + 'footer_icons': [ + { + 'url': 'https://github.com/ionelmc/python-tblib/', + 'html': 'github.com/ionelmc/python-tblib', + }, + ], } html_use_smartypants = True diff --git a/pyproject.toml b/pyproject.toml index 3accd6d..1e99e81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,67 @@ [build-system] requires = [ - "setuptools>=40.1.0", + "setuptools>=80", ] +build-backend = "setuptools.build_meta" + +[project] +dynamic = [ + "readme", +] +name = "tblib" +version = "3.1.0" +license = "BSD-2-Clause" +license-files = ["LICENSE"] +description = "Traceback serialization library." +authors = [ + { name = "Ionel Cristian Mărieș", email = "contact@ionelmc.ro" }, +] +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 :: 3.14", + "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", +] +keywords = [ + "traceback", + "debugging", + "exceptions", +] +requires-python = ">=3.9" +dependencies = [ +] + +[project.optional-dependencies] +# rst = ["docutils>=0.11"] + +[dependency-groups] +test = [ + "pytest", +] + +[project.urls] +"Sources" = "https://github.com/ionelmc/python-tblib" +"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" [tool.ruff] extend-exclude = ["static", "ci/templates"] @@ -15,31 +75,31 @@ target-version = "py39" [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 + "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 + "B", # flake8-bugbear + "C4", # flake8-comprehensions "DTZ", # flake8-datetimez - "E", # pycodestyle errors + "E", # pycodestyle errors "EXE", # flake8-executable - "F", # pyflakes - "I", # isort + "F", # pyflakes + "I", # isort "INT", # flake8-gettext "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint errors - "PT", # flake8-pytest-style + "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "RSE", # flake8-raise "RUF", # ruff-specific rules - "S", # flake8-bandit - "UP", # pyupgrade - "W", # pycodestyle warnings + "S", # flake8-bandit + "UP", # pyupgrade + "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] diff --git a/setup.py b/setup.py index 14c6267..4472314 100755 --- a/setup.py +++ b/setup.py @@ -12,62 +12,14 @@ def read(*names, **kwargs): 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', + long_description_content_type='text/x-rst', 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"], - }, ) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 6fdd30f..48670ba 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -51,8 +51,8 @@ def pickle_exception(obj): return ( unpickle_exception, - rv[:2] - + ( + ( + *rv[:2], obj.__cause__, obj.__traceback__, obj.__context__, @@ -60,7 +60,8 @@ def pickle_exception(obj): # __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:] + *rv[2:], + ) def _get_subclasses(cls): diff --git a/tests/examples.py b/tests/examples.py index bb61d8c..dfff3e9 100644 --- a/tests/examples.py +++ b/tests/examples.py @@ -15,12 +15,12 @@ def func_d(): def bad_syntax(): - import badsyntax + import badsyntax # noqa: PLC0415 badsyntax() def bad_module(): - import badmodule + import badmodule # noqa: PLC0415 badmodule() diff --git a/tests/test_issue30.py b/tests/test_issue30.py index cb66bd0..09d3069 100644 --- a/tests/test_issue30.py +++ b/tests/test_issue30.py @@ -9,7 +9,7 @@ def test_30(): - from twisted.python.failure import Failure + from twisted.python.failure import Failure # noqa: PLC0415 pickling_support.install() @@ -20,7 +20,7 @@ def test_30(): f = None try: - etype, evalue, etb = pickle.loads(s) # noqa: S301 + _etype, evalue, etb = pickle.loads(s) # noqa: S301 raise evalue.with_traceback(etb) except ValueError: f = Failure() diff --git a/tox.ini b/tox.ini index 92154c0..ed14c7c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,31 +14,25 @@ envlist = clean, check, docs, - {py39,py310,py311,py312,py313,pypy39,pypy310}, + {py39,py310,py311,py312,py313,py314,pypy39,pypy310,pypy311}, 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 = * +package = wheel usedevelop = false +dependency_groups = + test deps = - pytest pytest-cov pytest-benchmark + setuptools>=80 commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} @@ -49,10 +43,12 @@ deps = pre-commit readme-renderer pygments - isort + twine + uv skip_install = true commands = - python setup.py check --strict --metadata --restructuredtext + uv build --sdist --out-dir build + twine check build/*.tar.gz check-manifest . pre-commit run --all-files --show-diff-on-failure @@ -68,6 +64,8 @@ commands = deps = coverage skip_install = true +setenv = + PYTHONPATH={toxinidir}/src commands = coverage report coverage html @@ -78,5 +76,5 @@ commands = coverage erase skip_install = true deps = - setuptools + setuptools>=80 coverage From 0f1078599ca512a1a88bc3769c73e89473c32476 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Mon, 8 Jul 2024 23:11:42 +0100 Subject: [PATCH 42/57] Fix doctests The doctests weren't being run and had bitrotted. Make them work again. --- README.rst | 32 ++++++++++++++++++++------------ tox.ini | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 1ed2db9..278a575 100644 --- a/README.rst +++ b/README.rst @@ -144,7 +144,7 @@ Raising :: - >>> from six import reraise + >>> from tblib.decorators import reraise >>> reraise(*pickle.loads(s1)) Traceback (most recent call last): ... @@ -431,22 +431,26 @@ json.JSONDecoder:: {'tb_frame': {'f_code': {'co_filename': '', 'co_name': ''}, 'f_globals': {'__name__': '__main__'}, - 'f_lineno': 5}, + 'f_lineno': 5, + 'f_locals': {}}, 'tb_lineno': 2, 'tb_next': {'tb_frame': {'f_code': {'co_filename': ..., 'co_name': 'inner_2'}, 'f_globals': {'__name__': '__main__'}, - 'f_lineno': 2}, + 'f_lineno': 2, + 'f_locals': {}}, 'tb_lineno': 2, 'tb_next': {'tb_frame': {'f_code': {'co_filename': ..., 'co_name': 'inner_1'}, 'f_globals': {'__name__': '__main__'}, - 'f_lineno': 2}, + 'f_lineno': 2, + 'f_locals': {}}, 'tb_lineno': 2, 'tb_next': {'tb_frame': {'f_code': {'co_filename': ..., 'co_name': 'inner_0'}, 'f_globals': {'__name__': '__main__'}, - 'f_lineno': 2}, + 'f_lineno': 2, + 'f_locals': {}}, 'tb_lineno': 2, 'tb_next': None}}}} @@ -501,7 +505,7 @@ tblib.Traceback.from_string File "...examples.py", line 10, in func_c func_d() File "...examples.py", line 14, in func_d - raise Exception("Guessing time !") + raise Exception('Guessing time !') Exception: fail @@ -532,7 +536,7 @@ If you use the ``strict=False`` option then parsing is a bit more lax:: File "...examples.py", line 10, in func_c func_d() File "...examples.py", line 14, in func_d - raise Exception("Guessing time !") + raise Exception('Guessing time !') Exception: fail tblib.decorators.return_error @@ -605,6 +609,8 @@ Not very useful is it? Let's sort this out:: i.reraise() File "...tblib...decorators.py", line ..., in reraise reraise(self.exc_type, self.exc_value, self.traceback) + File "...tblib...decorators.py", line ..., in reraise + raise value.with_traceback(tb) File "...tblib...decorators.py", line ..., in return_exceptions_wrapper return func(*args, **kwargs) File "...tblib...decorators.py", line ..., in apply_with_return_error @@ -616,7 +622,7 @@ Not very useful is it? Let's sort this out:: File "...examples.py", line 10, in func_c func_d() File "...examples.py", line 14, in func_d - raise Exception("Guessing time !") + raise Exception('Guessing time !') Exception: Guessing time ! >>> pool.terminate() @@ -658,11 +664,13 @@ What if we have a local call stack ? local_0() File "", line 6, in local_0 i.reraise() - File "...tblib...decorators.py", line 20, in reraise + File "...tblib...decorators.py", line ..., in reraise reraise(self.exc_type, self.exc_value, self.traceback) - File "...tblib...decorators.py", line 27, in return_exceptions_wrapper + File "...tblib...decorators.py", line ..., in reraise + raise value.with_traceback(tb) + File "...tblib...decorators.py", line ..., in return_exceptions_wrapper return func(*args, **kwargs) - File "...tblib...decorators.py", line 47, in apply_with_return_error + File "...tblib...decorators.py", line ..., in apply_with_return_error return args[0](*args[1:]) File "...tests...examples.py", line 2, in func_a func_b() @@ -671,7 +679,7 @@ What if we have a local call stack ? File "...tests...examples.py", line 10, in func_c func_d() File "...tests...examples.py", line 14, in func_d - raise Exception("Guessing time !") + raise Exception('Guessing time !') Exception: Guessing time ! diff --git a/tox.ini b/tox.ini index ed14c7c..b0ca017 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ deps = pytest-benchmark setuptools>=80 commands = - {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests} + {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests README.rst} [testenv:check] deps = From ce6413dbf1175aee8cfca56a2f41dc0470fc0ab5 Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Fri, 2 Aug 2024 13:58:22 +0100 Subject: [PATCH 43/57] Ignore tests/bad*.py in coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes `CoverageWarning: Couldn't parse Python file '.../tests/badsyntax.py' (couldnt-parse)`. Based on a change by Oldřich Jedlička. --- .coveragerc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 1fc951c..ca3d005 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,4 +13,6 @@ patch = subprocess [report] show_missing = true precision = 2 -omit = *migrations* +omit = + *migrations* + tests/bad*.py From caedb094e646eb4dea91c2e54139362a374b59e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Old=C5=99ich=20Jedli=C4=8Dka?= Date: Fri, 3 Nov 2023 11:25:31 +0100 Subject: [PATCH 44/57] Deserialize by using __new__ whenever possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using of the class constructor and passing it the args values is not always correct, the custom parameters could be different to the args actually recorded by the built-in Exception class. Try to use the __new__ to create class instance and initialize the args attribute afterwards to prevent calling constructor with wrong arguments. Other instance attributes are initialized in a standard way by pickle module, so this way the reconstruction of the exception is complete. Subclasses of OSError are known exceptions to this, because they initialize read-only attributes from the constructor argument values, so usage of the __new__ factory method leads to lost values of those read-only attributes. Fixes: #65 Signed-off-by: Oldřich Jedlička --- src/tblib/pickling_support.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index 48670ba..b035582 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -22,6 +22,19 @@ def pickle_traceback(tb, *, get_locals=None): ) +def unpickle_exception_with_new(func, args, cause, tb, context, suppress_context, notes): + inst = func.__new__(func) + if args is not None: + inst.args = args + inst.__cause__ = cause + inst.__traceback__ = tb + inst.__context__ = context + inst.__suppress_context__ = suppress_context + if notes is not None: + inst.__notes__ = notes + return inst + + # Note: Older versions of tblib will generate pickle archives that call unpickle_exception() with # fewer arguments. We assign default values to some of the arguments to support this. def unpickle_exception(func, args, cause, tb, context=None, suppress_context=False, notes=None): @@ -49,8 +62,18 @@ def pickle_exception(obj): assert isinstance(rv, tuple) assert len(rv) >= 2 + # Use __new__ whenever there is no customization by __reduce__ and + # __reduce_ex__. Note that OSError and descendants are known to require + # using a constructor, otherwise they do not set the errno, strerror and other + # attributes. + use_new = ( + obj.__class__.__reduce__ is BaseException.__reduce__ + and obj.__class__.__reduce_ex__ is BaseException.__reduce_ex__ + and not isinstance(obj, OSError) + ) + return ( - unpickle_exception, + unpickle_exception_with_new if use_new else unpickle_exception, ( *rv[:2], obj.__cause__, From d66df68760d6c0c31fe5f2bf6b943fc315c19c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Old=C5=99ich=20Jedli=C4=8Dka?= Date: Tue, 7 Nov 2023 22:13:39 +0100 Subject: [PATCH 45/57] Tests for custom __reduce__, __reduce_ex__ and specifically issue #65 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oldřich Jedlička --- tests/test_issue65.py | 27 ++++++++++ tests/test_pickle_exception.py | 92 ++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tests/test_issue65.py diff --git a/tests/test_issue65.py b/tests/test_issue65.py new file mode 100644 index 0000000..9da4a9f --- /dev/null +++ b/tests/test_issue65.py @@ -0,0 +1,27 @@ +import pickle + +from tblib import pickling_support + + +class HTTPrettyError(Exception): + pass + + +class UnmockedError(HTTPrettyError): + def __init__(self): + super().__init__('No mocking was registered, and real connections are not allowed (httpretty.allow_net_connect = False).') + + +def test_65(): + pickling_support.install() + + try: + raise UnmockedError + except Exception as e: + exc = e + + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, UnmockedError) + assert exc.args == ('No mocking was registered, and real connections are not allowed (httpretty.allow_net_connect = False).',) + assert exc.__traceback__ is not None diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index a16c3a1..c401622 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -164,3 +164,95 @@ def func(my_arg='2'): exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1} + + +class CustomWithAttributesException(Exception): + def __init__(self, message, arg1, arg2, arg3): + super().__init__(message) + self.values12 = (arg1, arg2) + self.value3 = arg3 + + +def test_custom_with_attributes(): + try: + raise CustomWithAttributesException('bar', 1, 2, 3) + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, CustomWithAttributesException) + assert exc.args == ('bar',) + assert exc.values12 == (1, 2) + assert exc.value3 == 3 + assert exc.__traceback__ is not None + + +class CustomReduceException(Exception): + def __init__(self, message, arg1, arg2, arg3): + super().__init__(message) + self.values12 = (arg1, arg2) + self.value3 = arg3 + + def __reduce__(self): + return self.__class__, self.args + self.values12 + (self.value3,) + + +def test_custom_reduce(): + try: + raise CustomReduceException('foo', 1, 2, 3) + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, CustomReduceException) + assert exc.args == ('foo',) + assert exc.values12 == (1, 2) + assert exc.value3 == 3 + assert exc.__traceback__ is not None + + +class CustomReduceExException(Exception): + def __init__(self, message, arg1, arg2, protocol): + super().__init__(message) + self.values12 = (arg1, arg2) + self.value3 = protocol + + def __reduce_ex__(self, protocol): + return self.__class__, self.args + self.values12 + (self.value3,) + + +@pytest.mark.parametrize('protocol', [None, *list(range(1, pickle.HIGHEST_PROTOCOL + 1))]) +def test_custom_reduce_ex(protocol): + try: + raise CustomReduceExException('foo', 1, 2, 3) + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) + + assert isinstance(exc, CustomReduceExException) + assert exc.args == ('foo',) + assert exc.values12 == (1, 2) + assert exc.value3 == 3 + assert exc.__traceback__ is not None + + +def test_oserror(): + try: + raise OSError(13, 'Permission denied') + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, OSError) + assert exc.args == (13, 'Permission denied') + assert exc.errno == 13 + assert exc.strerror == 'Permission denied' + assert exc.__traceback__ is not None From 0b67609280c349546336fa177cb5f1049a434e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 18 Oct 2025 08:21:14 +0300 Subject: [PATCH 46/57] Extend a bit the new unpickler to cover a wider range of busted exception subclasses. --- pyproject.toml | 1 + src/tblib/pickling_support.py | 99 ++++++++++++++++++++-------------- tests/test_issue30.py | 2 +- tests/test_pickle_exception.py | 68 +++++++++++++++++++++-- tests/test_tblib.py | 2 +- 5 files changed, 126 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1e99e81..2ce5445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ ignore = [ "S603", # flake8-bandit subprocess-without-shell-equals-true "S607", # flake8-bandit start-process-with-partial-path "E501", # pycodestyle line-too-long + "S301", ] select = [ "B", # flake8-bugbear diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index b035582..a561aa4 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -22,10 +22,10 @@ def pickle_traceback(tb, *, get_locals=None): ) -def unpickle_exception_with_new(func, args, cause, tb, context, suppress_context, notes): +def unpickle_exception_with_attrs(func, attrs, cause, tb, context, suppress_context, notes): inst = func.__new__(func) - if args is not None: - inst.args = args + for key, value in attrs.items(): + setattr(inst, key, value) inst.__cause__ = cause inst.__traceback__ = tb inst.__context__ = context @@ -48,43 +48,62 @@ def unpickle_exception(func, args, cause, tb, context=None, suppress_context=Fal 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 - - # Use __new__ whenever there is no customization by __reduce__ and - # __reduce_ex__. Note that OSError and descendants are known to require - # using a constructor, otherwise they do not set the errno, strerror and other - # attributes. - use_new = ( - obj.__class__.__reduce__ is BaseException.__reduce__ - and obj.__class__.__reduce_ex__ is BaseException.__reduce_ex__ - and not isinstance(obj, OSError) - ) - - return ( - unpickle_exception_with_new if use_new else 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 pickle_exception( + obj, builtin_reducers=(OSError.__reduce__, BaseException.__reduce__), builtin_inits=(OSError.__init__, BaseException.__init__) +): + reduced_value = obj.__reduce__() + if isinstance(reduced_value, str): + raise TypeError('Did not expect {repr(obj)}.__reduce__() to return a string!') + + func = type(obj) + # Detect busted objects: they have a custom __init__ but no __reduce__. + # This also means the resulting exceptions may be a bit "dulled" down - the args from __reduce__ are discarded. + if func.__reduce__ in builtin_reducers and func.__init__ not in builtin_inits: + _, args, *optionals = reduced_value + attrs = { + '__dict__': obj.__dict__, + 'args': obj.args, + } + if isinstance(obj, OSError): + attrs.update( + errno=obj.errno, + strerror=obj.strerror, + winerror=getattr(obj, 'winerror', None), + filename=obj.filename, + filename2=obj.filename2, + ) + + return ( + unpickle_exception_with_attrs, + ( + func, + attrs, + 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), + ), + *optionals, + ) + else: + func, args, *optionals = reduced_value + + return ( + unpickle_exception, + ( + func, + args, + 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), + ), + *optionals, + ) def _get_subclasses(cls): diff --git a/tests/test_issue30.py b/tests/test_issue30.py index 09d3069..a446914 100644 --- a/tests/test_issue30.py +++ b/tests/test_issue30.py @@ -20,7 +20,7 @@ def test_30(): f = None try: - _etype, evalue, etb = pickle.loads(s) # noqa: S301 + _etype, evalue, etb = pickle.loads(s) raise evalue.with_traceback(etb) except ValueError: f = Failure() diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index c401622..476a47d 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -72,7 +72,7 @@ def test_install(clear_dispatch_table, how, protocol): if how == 'instance': tblib.pickling_support.install(exc) if protocol: - exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 + exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) assert isinstance(exc, CustomError) assert exc.args == ('foo',) @@ -105,7 +105,7 @@ def test_install_decorator(): raise RegisteredError('foo') exc = ewrap.value exc.x = 1 - exc = pickle.loads(pickle.dumps(exc)) # noqa: S301 + exc = pickle.loads(pickle.dumps(exc)) assert isinstance(exc, RegisteredError) assert exc.args == ('foo',) @@ -162,7 +162,7 @@ def func(my_arg='2'): if how == 'instance': tblib.pickling_support.install(exc, get_locals=get_locals) - exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) # noqa: S301 + exc = pickle.loads(pickle.dumps(exc, protocol=protocol)) assert exc.__traceback__.tb_next.tb_frame.f_locals == {'my_variable': 1} @@ -189,6 +189,66 @@ def test_custom_with_attributes(): assert exc.__traceback__ is not None +class CustomOSError(OSError): + def __init__(self, message, errno, strerror: str, filename, none: None, filename2): + super().__init__(errno, strerror, filename, none, filename2) + self.message = message + + +def test_custom_oserror(): + try: + raise CustomOSError('bar', 2, 'err', 3, None, 5) + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, CustomOSError) + assert exc.message == 'bar' + assert exc.errno == 2 + assert exc.strerror == 'err' + assert exc.filename == 3 + assert exc.filename2 == 5 + assert exc.__traceback__ is not None + + +def test_oserror(): + try: + raise OSError(2, 'err', 3, None, 5) + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, OSError) + assert exc.errno == 2 + assert exc.strerror == 'err' + assert exc.filename == 3 + assert exc.filename2 == 5 + assert exc.__traceback__ is not None + + +class BadError(Exception): + def __init__(self): + super().__init__('Bad Bad Bad!') + + +def test_baderror(): + try: + raise BadError + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, BadError) + assert exc.args == ('Bad Bad Bad!',) + assert exc.__traceback__ is not None + + class CustomReduceException(Exception): def __init__(self, message, arg1, arg2, arg3): super().__init__(message) @@ -242,7 +302,7 @@ def test_custom_reduce_ex(protocol): assert exc.__traceback__ is not None -def test_oserror(): +def test_oserror_simple(): try: raise OSError(13, 'Permission denied') except Exception as e: diff --git a/tests/test_tblib.py b/tests/test_tblib.py index bac06e1..6d01ebe 100644 --- a/tests/test_tblib.py +++ b/tests/test_tblib.py @@ -107,7 +107,7 @@ def test_parse_traceback(): }, } tb3 = Traceback.from_dict(expected_dict) - tb4 = pickle.loads(pickle.dumps(tb3)) # noqa: S301 + tb4 = pickle.loads(pickle.dumps(tb3)) assert tb4.as_dict() == tb3.as_dict() == tb2.as_dict() == tb1.as_dict() == expected_dict From d6504876e412e872994b06f55485dfde0f1867b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 18 Oct 2025 08:27:41 +0300 Subject: [PATCH 47/57] Enable coveralls. --- .github/workflows/github-actions.yml | 9 +++++++++ ci/templates/.github/workflows/github-actions.yml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 8f18b6c..a76bf38 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -251,6 +251,12 @@ jobs: - name: test run: > tox -e ${{ matrix.tox_env }} -v + - uses: coverallsapp/github-action@v2 + if: matrix.cover + continue-on-error: true + with: + flag-name: ${{ matrix.name }} + parallel: true - uses: codecov/codecov-action@v5 if: matrix.cover with: @@ -262,3 +268,6 @@ jobs: if: ${{ always() }} runs-on: ubuntu-latest steps: + - uses: coverallsapp/github-action@v2 + with: + parallel-finished: true diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index ac17262..ff7b53f 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -59,6 +59,12 @@ jobs: - name: test run: > tox -e {{ '${{ matrix.tox_env }}' }} -v + - uses: coverallsapp/github-action@v2 + if: matrix.cover + continue-on-error: true + with: + flag-name: {{ '${{ matrix.name }}' }} + parallel: true - uses: codecov/codecov-action@v5 if: matrix.cover with: @@ -70,4 +76,7 @@ jobs: if: {{ '${{ always() }}' }} runs-on: ubuntu-latest steps: + - uses: coverallsapp/github-action@v2 + with: + parallel-finished: true {{ '' }} From 7e8dba94a0fcc91d6837b65a928722dd45bda4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 09:21:47 +0300 Subject: [PATCH 48/57] Some packaging fixes. --- MANIFEST.in | 1 + pyproject.toml | 2 ++ tox.ini | 1 - 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4bac1f0..4ad6232 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,5 +20,6 @@ include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE include README.rst +include SECURITY.md global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/pyproject.toml b/pyproject.toml index 2ce5445..54f2108 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,8 @@ dependencies = [ [dependency-groups] test = [ "pytest", + "twisted", + "pytest-benchmark", ] [project.urls] diff --git a/tox.ini b/tox.ini index b0ca017..542cbf8 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,6 @@ dependency_groups = test deps = pytest-cov - pytest-benchmark setuptools>=80 commands = {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv tests README.rst} From ac82d41f1f5039d977d1c6dad1304ee06ace42bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 09:22:01 +0300 Subject: [PATCH 49/57] Skip these, they are broken on PyPy. --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 278a575..bed352a 100644 --- a/README.rst +++ b/README.rst @@ -595,7 +595,7 @@ 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: + >>> try: # doctest: +SKIP ... for i in pool.map(apply_with_return_error, zip(repeat(func_a), range(5))): ... if isinstance(i, Error): ... i.reraise() @@ -651,10 +651,11 @@ What if we have a local call stack ? >>> def local_2(): ... local_1() ... - >>> try: + >>> try: # doctest: +SKIP ... local_2() ... except: ... print(traceback.format_exc()) + ... Traceback (most recent call last): File "", line 2, in local_2() From 13802fdec8353ead4a7ebfddddf854a856880bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 15:23:26 +0300 Subject: [PATCH 50/57] Do not set winerror or filenames if they are None. --- .pre-commit-config.yaml | 2 +- src/tblib/pickling_support.py | 14 +++++++------- tests/test_pickle_exception.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb42dd0..90fdbc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,5 +22,5 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: mixed-line-ending - args: [--fix=lf] + args: [--fix] - id: debug-statements diff --git a/src/tblib/pickling_support.py b/src/tblib/pickling_support.py index a561aa4..d0bdae8 100644 --- a/src/tblib/pickling_support.py +++ b/src/tblib/pickling_support.py @@ -65,13 +65,13 @@ def pickle_exception( 'args': obj.args, } if isinstance(obj, OSError): - attrs.update( - errno=obj.errno, - strerror=obj.strerror, - winerror=getattr(obj, 'winerror', None), - filename=obj.filename, - filename2=obj.filename2, - ) + attrs.update(errno=obj.errno, strerror=obj.strerror) + if (winerror := getattr(obj, 'winerror', None)) is not None: + attrs['winerror'] = winerror + if obj.filename is not None: + attrs['filename'] = obj.filename + if obj.filename2 is not None: + attrs['filename2'] = obj.filename2 return ( unpickle_exception_with_attrs, diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 476a47d..0e1f35e 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -1,3 +1,4 @@ +import traceback from traceback import format_exception try: @@ -230,6 +231,35 @@ def test_oserror(): assert exc.__traceback__ is not None +class OpenError(Exception): + pass + + +def bad_open(): + try: + raise PermissionError(13, 'Booboo', 'filename', None, None) + except Exception as e: + raise OpenError(e) from e + + +def test_permissionerror(): + try: + bad_open() + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + print(''.join(traceback.format_exception(exc))) + assert isinstance(exc, OpenError) + assert exc.__traceback__ is not None + assert repr(exc) == "OpenError(PermissionError(13, 'Booboo'))" + assert str(exc) == "[Errno 13] Booboo: 'filename'" + assert exc.args[0].errno == 13 + assert exc.args[0].strerror == 'Booboo' + assert exc.args[0].filename == 'filename' + + class BadError(Exception): def __init__(self): super().__init__('Bad Bad Bad!') From 615cdb38daafc77ae9a04b7f39b6d8bb2a2396a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 15:29:07 +0300 Subject: [PATCH 51/57] Remove debug print. --- tests/test_pickle_exception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 0e1f35e..014fc1c 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -250,7 +250,7 @@ def test_permissionerror(): tblib.pickling_support.install(exc) exc = pickle.loads(pickle.dumps(exc)) - print(''.join(traceback.format_exception(exc))) + assert isinstance(exc, OpenError) assert exc.__traceback__ is not None assert repr(exc) == "OpenError(PermissionError(13, 'Booboo'))" From 97a6aef884a3bfc3acf3e1662287f6325e0fe0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 15:38:15 +0300 Subject: [PATCH 52/57] Lint fix. --- .pre-commit-config.yaml | 1 - tests/test_pickle_exception.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90fdbc4..ca6432e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,5 +22,4 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: mixed-line-ending - args: [--fix] - id: debug-statements diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index 014fc1c..dc52405 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -1,4 +1,3 @@ -import traceback from traceback import format_exception try: From 4a09ea1ad1cb14c139337f814ec71ac24ff6bbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 15:54:18 +0300 Subject: [PATCH 53/57] Add coveralls badge. --- .cookiecutterrc | 2 +- README.rst | 12 ++++-------- ci/templates/.github/workflows/github-actions.yml | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.cookiecutterrc b/.cookiecutterrc index f069a1d..f360b98 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -9,7 +9,7 @@ default_context: codecov: 'yes' command_line_interface: 'no' command_line_interface_bin_name: '-' - coveralls: 'no' + coveralls: 'yes' distribution_name: tblib email: contact@ionelmc.ro formatter_quote_style: single diff --git a/README.rst b/README.rst index bed352a..fb3bfcf 100644 --- a/README.rst +++ b/README.rst @@ -10,37 +10,33 @@ Overview * - docs - |docs| * - tests - - |github-actions| |codecov| + - |github-actions| |coveralls| |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 - +.. |coveralls| image:: https://coveralls.io/repos/github/ionelmc/python-tblib/badge.svg?branch=master + :alt: Coverage Status + :target: https://coveralls.io/github/ionelmc/python-tblib?branch=master .. |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 diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index ff7b53f..469538d 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -79,4 +79,4 @@ jobs: - uses: coverallsapp/github-action@v2 with: parallel-finished: true -{{ '' }} +{{- '' }} From 5bd4af8dc93351e26fc1d306f21fb2502de5ac0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 15:56:29 +0300 Subject: [PATCH 54/57] Dont include 32bit envs anymore. --- .github/workflows/github-actions.yml | 108 +++++------------- .../.github/workflows/github-actions.yml | 3 +- 2 files changed, 28 insertions(+), 83 deletions(-) diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a76bf38..3c737eb 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -17,217 +17,163 @@ jobs: python: '3.13' tox_env: 'docs' os: 'ubuntu-latest' - - name: 'py39 (ubuntu)' + - name: 'py39 (ubuntu/x64)' python: '3.9' python_arch: 'x64' tox_env: 'py39' os: 'ubuntu-latest' cover: true - - name: 'py39 (windows)' + - name: 'py39 (windows/x64)' python: '3.9' python_arch: 'x64' tox_env: 'py39' os: 'windows-latest' cover: true - - name: 'py39 (windows)' - python: '3.9' - python_arch: 'x86' - tox_env: 'py39' - os: 'windows-latest' - cover: true - - name: 'py39 (macos)' + - name: 'py39 (macos/arm64)' python: '3.9' python_arch: 'arm64' tox_env: 'py39' os: 'macos-latest' cover: true - - name: 'py310 (ubuntu)' + - name: 'py310 (ubuntu/x64)' python: '3.10' python_arch: 'x64' tox_env: 'py310' os: 'ubuntu-latest' cover: true - - name: 'py310 (windows)' + - name: 'py310 (windows/x64)' python: '3.10' python_arch: 'x64' tox_env: 'py310' os: 'windows-latest' cover: true - - name: 'py310 (windows)' - python: '3.10' - python_arch: 'x86' - tox_env: 'py310' - os: 'windows-latest' - cover: true - - name: 'py310 (macos)' + - name: 'py310 (macos/arm64)' python: '3.10' python_arch: 'arm64' tox_env: 'py310' os: 'macos-latest' cover: true - - name: 'py311 (ubuntu)' + - name: 'py311 (ubuntu/x64)' python: '3.11' python_arch: 'x64' tox_env: 'py311' os: 'ubuntu-latest' cover: true - - name: 'py311 (windows)' + - name: 'py311 (windows/x64)' python: '3.11' python_arch: 'x64' tox_env: 'py311' os: 'windows-latest' cover: true - - name: 'py311 (windows)' - python: '3.11' - python_arch: 'x86' - tox_env: 'py311' - os: 'windows-latest' - cover: true - - name: 'py311 (macos)' + - name: 'py311 (macos/arm64)' python: '3.11' python_arch: 'arm64' tox_env: 'py311' os: 'macos-latest' cover: true - - name: 'py312 (ubuntu)' + - name: 'py312 (ubuntu/x64)' python: '3.12' python_arch: 'x64' tox_env: 'py312' os: 'ubuntu-latest' cover: true - - name: 'py312 (windows)' + - name: 'py312 (windows/x64)' python: '3.12' python_arch: 'x64' tox_env: 'py312' os: 'windows-latest' cover: true - - name: 'py312 (windows)' - python: '3.12' - python_arch: 'x86' - tox_env: 'py312' - os: 'windows-latest' - cover: true - - name: 'py312 (macos)' + - name: 'py312 (macos/arm64)' python: '3.12' python_arch: 'arm64' tox_env: 'py312' os: 'macos-latest' cover: true - - name: 'py313 (ubuntu)' + - name: 'py313 (ubuntu/x64)' python: '3.13' python_arch: 'x64' tox_env: 'py313' os: 'ubuntu-latest' cover: true - - name: 'py313 (windows)' + - name: 'py313 (windows/x64)' python: '3.13' python_arch: 'x64' tox_env: 'py313' os: 'windows-latest' cover: true - - name: 'py313 (windows)' - python: '3.13' - python_arch: 'x86' - tox_env: 'py313' - os: 'windows-latest' - cover: true - - name: 'py313 (macos)' + - name: 'py313 (macos/arm64)' python: '3.13' python_arch: 'arm64' tox_env: 'py313' os: 'macos-latest' cover: true - - name: 'py314 (ubuntu)' + - name: 'py314 (ubuntu/x64)' python: '3.14' python_arch: 'x64' tox_env: 'py314' os: 'ubuntu-latest' cover: true - - name: 'py314 (windows)' + - name: 'py314 (windows/x64)' python: '3.14' python_arch: 'x64' tox_env: 'py314' os: 'windows-latest' cover: true - - name: 'py314 (windows)' - python: '3.14' - python_arch: 'x86' - tox_env: 'py314' - os: 'windows-latest' - cover: true - - name: 'py314 (macos)' + - name: 'py314 (macos/arm64)' python: '3.14' python_arch: 'arm64' tox_env: 'py314' os: 'macos-latest' cover: true - - name: 'pypy39 (ubuntu)' + - name: 'pypy39 (ubuntu/x64)' python: 'pypy-3.9' python_arch: 'x64' tox_env: 'pypy39' os: 'ubuntu-latest' cover: true - - name: 'pypy39 (windows)' + - name: 'pypy39 (windows/x64)' python: 'pypy-3.9' python_arch: 'x64' tox_env: 'pypy39' os: 'windows-latest' cover: true - - name: 'pypy39 (windows)' - python: 'pypy-3.9' - python_arch: 'x86' - tox_env: 'pypy39' - os: 'windows-latest' - cover: true - - name: 'pypy39 (macos)' + - name: 'pypy39 (macos/arm64)' python: 'pypy-3.9' python_arch: 'arm64' tox_env: 'pypy39' os: 'macos-latest' cover: true - - name: 'pypy310 (ubuntu)' + - name: 'pypy310 (ubuntu/x64)' python: 'pypy-3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'ubuntu-latest' cover: true - - name: 'pypy310 (windows)' + - name: 'pypy310 (windows/x64)' python: 'pypy-3.10' python_arch: 'x64' tox_env: 'pypy310' os: 'windows-latest' cover: true - - name: 'pypy310 (windows)' - python: 'pypy-3.10' - python_arch: 'x86' - tox_env: 'pypy310' - os: 'windows-latest' - cover: true - - name: 'pypy310 (macos)' + - name: 'pypy310 (macos/arm64)' python: 'pypy-3.10' python_arch: 'arm64' tox_env: 'pypy310' os: 'macos-latest' cover: true - - name: 'pypy311 (ubuntu)' + - name: 'pypy311 (ubuntu/x64)' python: 'pypy-3.11' python_arch: 'x64' tox_env: 'pypy311' os: 'ubuntu-latest' cover: true - - name: 'pypy311 (windows)' + - name: 'pypy311 (windows/x64)' python: 'pypy-3.11' python_arch: 'x64' tox_env: 'pypy311' os: 'windows-latest' cover: true - - name: 'pypy311 (windows)' - python: 'pypy-3.11' - python_arch: 'x86' - tox_env: 'pypy311' - os: 'windows-latest' - cover: true - - name: 'pypy311 (macos)' + - name: 'pypy311 (macos/arm64)' python: 'pypy-3.11' python_arch: 'arm64' tox_env: 'pypy311' diff --git a/ci/templates/.github/workflows/github-actions.yml b/ci/templates/.github/workflows/github-actions.yml index 469538d..05c828e 100644 --- a/ci/templates/.github/workflows/github-actions.yml +++ b/ci/templates/.github/workflows/github-actions.yml @@ -30,10 +30,9 @@ jobs: {% for os, python_arch in [ ['ubuntu', 'x64'], ['windows', 'x64'], - ['windows', 'x86'], ['macos', 'arm64'], ] %} - - name: '{{ env }} ({{ os }})' + - name: '{{ env }} ({{ os }}/{{ python_arch }})' python: '{{ python }}' python_arch: '{{ python_arch }}' tox_env: '{{ env }}' From 12c87705a19f454c0c25f32345fb77e1de628f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 20 Oct 2025 19:43:58 +0300 Subject: [PATCH 55/57] Add a test with native oserror. --- tests/test_pickle_exception.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index dc52405..ebe93ee 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -1,3 +1,4 @@ +import os from traceback import format_exception try: @@ -345,3 +346,20 @@ def test_oserror_simple(): assert exc.errno == 13 assert exc.strerror == 'Permission denied' assert exc.__traceback__ is not None + + +def test_real_oserror(): + try: + os.open('non-existing-file', os.O_RDONLY) + except Exception as e: + exc = e + else: + pytest.fail('os.open should have raised an OSError') + + str_output = str(exc) + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, OSError) + assert exc.errno == 2 + assert str_output == str(exc) From abec37d4526fb86f95c7567644f1fd6c2cec4215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 21 Oct 2025 11:22:00 +0300 Subject: [PATCH 56/57] Update changelog and add one more test. --- CHANGELOG.rst | 20 ++++++++++++++++++++ tests/test_pickle_exception.py | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6414793..aa6cc7f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,26 @@ Changelog ========= +3.2.0 (2025-10-21) +~~~~~~~~~~~~~~~~~~ + +* Changed ``tblib.pickling_support.install`` to support exceptions with ``__init__`` that does match the default + ``BaseException.__reduce__`` (as it expects the positional arguments to ``__init__`` to match the ``args`` attribute). + + Special handling for OSError (and subclasses) is also included. The errno, strerror, winerror, filename and filename2 attributes will be added in the reduce structure (if set). + + This will support exception subclasses that do this without defining a custom ``__reduce__``: + + .. code-block:: python + + def __init__(self): + super().__init__('mistery argument') + + def __init__(self, mistery_argument): + super().__init__() + self.mistery_argument = mistery_argument + + 3.1.0 (2025-03-31) ~~~~~~~~~~~~~~~~~~ diff --git a/tests/test_pickle_exception.py b/tests/test_pickle_exception.py index ebe93ee..2696c09 100644 --- a/tests/test_pickle_exception.py +++ b/tests/test_pickle_exception.py @@ -279,6 +279,27 @@ def test_baderror(): assert exc.__traceback__ is not None +class BadError2(Exception): + def __init__(self, stuff): + super().__init__() + self.stuff = stuff + + +def test_baderror2(): + try: + raise BadError2('123') + except Exception as e: + exc = e + + tblib.pickling_support.install(exc) + exc = pickle.loads(pickle.dumps(exc)) + + assert isinstance(exc, BadError2) + assert exc.args == () + assert exc.stuff == '123' + assert exc.__traceback__ is not None + + class CustomReduceException(Exception): def __init__(self, message, arg1, arg2, arg3): super().__init__(message) From ebc83e93b6342950cb93cf296991fb99ce4aa13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 21 Oct 2025 11:22:05 +0300 Subject: [PATCH 57/57] =?UTF-8?q?Bump=20version:=203.1.0=20=E2=86=92=203.2?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- .cookiecutterrc | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- pyproject.toml | 2 +- src/tblib/__init__.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2693d65..160e3ce 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.1.0 +current_version = 3.2.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index f360b98..1f92549 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -40,7 +40,7 @@ default_context: sphinx_theme: furo test_matrix_separate_coverage: 'no' tests_inside_package: 'no' - version: 3.1.0 + version: 3.2.0 version_manager: bump2version website: https://blog.ionelmc.ro/ year_from: '2013' diff --git a/README.rst b/README.rst index fb3bfcf..8e0b583 100644 --- a/README.rst +++ b/README.rst @@ -37,9 +37,9 @@ Overview .. |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 +.. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-tblib/v3.2.0.svg :alt: Commits since latest release - :target: https://github.com/ionelmc/python-tblib/compare/v3.1.0...master + :target: https://github.com/ionelmc/python-tblib/compare/v3.2.0...master .. end-badges diff --git a/docs/conf.py b/docs/conf.py index ff25663..c6ff32a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ year = '2013-2025' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' -version = release = '3.1.0' +version = release = '3.2.0' pygments_style = 'trac' templates_path = ['.'] diff --git a/pyproject.toml b/pyproject.toml index 54f2108..8737205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dynamic = [ "readme", ] name = "tblib" -version = "3.1.0" +version = "3.2.0" license = "BSD-2-Clause" license-files = ["LICENSE"] description = "Traceback serialization library." diff --git a/src/tblib/__init__.py b/src/tblib/__init__.py index 772f7ec..6565aef 100644 --- a/src/tblib/__init__.py +++ b/src/tblib/__init__.py @@ -1,7 +1,7 @@ import re import sys -__version__ = '3.1.0' +__version__ = '3.2.0' __all__ = 'Code', 'Frame', 'Traceback', 'TracebackParseError' FRAME_RE = re.compile(r'^\s*File "(?P.+)", line (?P\d+)(, in (?P.+))?$')