├── .flake8 ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── README_zh.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── favicon.png │ ├── header_photo_1.png │ ├── logo.png │ └── rpdb.png │ ├── conf.py │ ├── index.rst │ ├── installation.rst │ └── tutorial.rst ├── pdm.lock ├── pydumpling ├── __init__.py ├── __main__.py ├── cli.py ├── debug_dumpling.py ├── fake_types.py ├── helpers.py ├── pydumpling.py └── rpdb.py ├── pyproject.toml ├── requirements.txt ├── static └── rpdb.png ├── tests ├── __init__.py ├── dump │ └── validate_file_name.dump ├── test_debug.py ├── test_dump.py └── test_helpers.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | tests: 11 | name: ${{ matrix.name }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - {name: '3.12', python: '3.12', os: ubuntu-latest} 18 | - {name: '3.11', python: '3.11', os: ubuntu-latest} 19 | - {name: '3.10', python: '3.10', os: ubuntu-latest} 20 | - {name: '3.9', python: '3.9', os: ubuntu-latest} 21 | - {name: '3.8', python: '3.8', os: ubuntu-latest} 22 | - {name: '3.7', python: '3.7', os: ubuntu-latest} 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup PDM 26 | uses: pdm-project/setup-pdm@v3 27 | with: 28 | python-version: ${{ matrix.python }} 29 | cache: true 30 | - name: Install dependencies 31 | run: pdm install -Gtest 32 | - name: Run tests 33 | run: pdm run test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | __pycache__ 3 | *.pyc 4 | /.pytest_cache 5 | *.dump 6 | test.py 7 | /my 8 | .vscode 9 | .idea 10 | .tox 11 | .coverage 12 | 13 | dist/ 14 | .pdm-python 15 | 16 | # Sphinx documentation 17 | docs/build/ 18 | 19 | !tests/dump/* -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | # Set the OS, Python version and other tools you might need 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.11" 9 | 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | sphinx: 15 | configuration: docs/source/conf.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (C) 2022 cocolato 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python post-mortem debugging 2 | 3 | English | [简体中文](README_zh.md) 4 | 5 | It's a fork/optimized version from [elifiner/pydump](https://github.com/elifiner/pydump).The main optimization points are: 6 | * Save the `Python traceback` anywhere, not just when it's an exception. 7 | * Optimize code structure && remove redundant code 8 | * fix bug in python3.10+ 9 | * supported more pdb commnd 10 | * a useful command line tool for debug 11 | * supported remote debug (rpdb) 12 | 13 | 14 | Pydumpling writes the `python current traceback` into a file and 15 | can later load it in a Python debugger. It works with the built-in 16 | pdb and with other popular debuggers (pudb, ipdb and pdbpp). 17 | 18 | ## Why use pydumpling? 19 | 20 | * We usually use `try... except ... ` to catch exceptions that occur in our programs, but do we really know why they occur? 21 | * When your project is running online, you suddenly get an unexpected exception that causes the process to exit. How do you reproduce this problem? 22 | * Not enough information in the logs to help us pinpoint online issues? 23 | * If we were able to save the exception error and then use the debugger to recover the traceback at that time, we could see the entire stack variables along the traceback as if you had caught the exception at the local breakpoint. 24 | 25 | ## Install pydumpling 26 | Python version:>=3.7 27 | 28 | ``` 29 | pip install pydumpling 30 | ``` 31 | 32 | ## How to use pydumpling 33 | 34 | 35 | ### Save the python traceback anywhere. 36 | ```python 37 | from pydumpling import dump_current_traceback 38 | from inspect import currentframe 39 | 40 | 41 | def inner(): 42 | a = 1 43 | b = "2" 44 | dump_current_traceback("test.dump") 45 | c = str(a) + b 46 | 47 | 48 | def outer(): 49 | d = 4 50 | inner() 51 | 52 | ``` 53 | 54 | ### Save the exception traceback. 55 | 56 | In the code, find the place where we need to do the `try ... except ...` and use `save_dumpling()`. When we save the dump file, it will default to `${exception filename}-${error lineno}.dump`. 57 | 58 | ```python 59 | from pydumpling import save_dumping 60 | 61 | def inner(): 62 | a = 1 63 | b = "2" 64 | c = a + b 65 | 66 | 67 | def outer(): 68 | inner() 69 | 70 | 71 | if __name__ == "__main__": 72 | try: 73 | outer() 74 | except Exception: 75 | save_dumping("test.dump") 76 | 77 | ``` 78 | 79 | Now we have the `test.dump` file, which we can use `debub_dumpling` to do pdb debug: 80 | ```python 81 | Python 3.10.6 (main, Aug 1 2022, 20:38:21) [GCC 5.4.0 20160609] on linux 82 | Type "help", "copyright", "credits" or "license" for more information. 83 | >>> from pydumpling import debug_dumpling 84 | >>> debug_dumpling("test.dump") 85 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 86 | -> c = a + b 87 | (Pdb) list 1,17 88 | 1 from pydumpling import save_dumping 89 | 2 90 | 3 def inner(): 91 | 4 >> a = 1 92 | 5 b = "2" 93 | 6 -> c = a + b 94 | 7 95 | 8 96 | 9 def outer(): 97 | 10 inner() 98 | 11 99 | 12 100 | 13 if __name__ == "__main__": 101 | 14 try: 102 | 15 outer() 103 | 16 except Exception: 104 | 17 save_dumping("test.dump") 105 | (Pdb) ll 106 | 3 def inner(): 107 | 4 >> a = 1 108 | 5 b = "2" 109 | 6 -> c = a + b 110 | (Pdb) bt 111 | /home/loyd/vscodeFiles/pydumpling/test.py(15)() 112 | -> outer() 113 | /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 114 | -> inner() 115 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 116 | -> c = a + b 117 | (Pdb) pp a 118 | 1 119 | (Pdb) pp b 120 | '2' 121 | (Pdb) u 122 | > /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 123 | -> inner() 124 | (Pdb) ll 125 | 9 def outer(): 126 | 10 -> inner() 127 | (Pdb) 128 | ``` 129 | 130 | ### Use Command Line 131 | 132 | #### Use command line to print the traceback: 133 | `python -m pydumpling --print test.deump` 134 | 135 | It will print: 136 | ```python 137 | Traceback (most recent call last): 138 | File "/workspaces/pydumpling/tests/test_dump.py", line 20, in test_dumpling 139 | outer() 140 | File "/workspaces/pydumpling/tests/test_dump.py", line 14, in outer 141 | inner() 142 | File "/workspaces/pydumpling/tests/test_dump.py", line 10, in inner 143 | c = a + b # noqa: F841 144 | TypeError: unsupported operand type(s) for +: 'int' and 'str' 145 | ``` 146 | 147 | 148 | #### Use command line to do pdb debug: 149 | `python -m pydumpling --debug test.deump` 150 | 151 | It will open the pdb window: 152 | ```python 153 | -> c = a + b 154 | (Pdb) 155 | ``` 156 | 157 | #### Use command line to do remote pdb debug: 158 | `python -m pydumpling --rdebug test.deump` 159 | It will open the debugger on port 4444, then we can access pdb using telnet、netcat... : 160 | `nc 127.0.0.1 4444` 161 | ![alt text](static/rpdb.png) 162 | 163 | #### Enable global exception catching: 164 | ```python 165 | from pydumpling import catch_any_exception 166 | 167 | catch_any_exception() 168 | 169 | def inner(): 170 | a = 1 171 | b = "2" 172 | c = a + b # noqa: F841 173 | 174 | 175 | def outer(): 176 | inner() 177 | 178 | if __name__ == "__main__": 179 | outer() 180 | 181 | ``` 182 | 183 | ## TODO 184 | - [] 185 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 针对Python的异常调试器 2 | 3 | 这是 [elifiner/pydump](https://github.com/elifiner/pydump) 的fork/优化版本, 主要优化点有: 4 | * 支持在任何地方保存`Python traceback`,而不是只在异常发生的时候 5 | * 优化代码结构, 去除冗余代码 6 | * 修复其在3.10+版本中的bug 7 | * 支持更多的pdb命令 8 | * 提供了一个方便用来调试的命令行工具 9 | * 支持服务器远程调试(remote pdb) 10 | 11 | pydumpling可以在代码的任何位置中,将当前Python程序的traceback写到一个文件中,可以稍后在Python调试器中加载它。目前pydump支持很多兼容PDB api的调试器(pdbpp, udb, ipdb) 12 | 13 | ## 为什么会有Pydump? 14 | 15 | * 我们在日常代码编写中,通常都会用`try ... except ...`去捕获程序中出现的异常,但是我们真的知道这些异常出现的原因吗? 16 | * 当你的项目在线上运行时,突然出现了不符合预期的异常导致进程退出,你应该怎样去复现当时的异常现场? 17 | * 日志中没有足够的信息帮助我们去准确定位线上问题? 18 | * 如果我们能够把线上的异常现场保存下来,然后通过调试器去恢复当时的异常堆栈,我们可以看到这个异常的整条调用链路以及链路上的堆栈变量,就如同你在本地断点捕获到了这个异常一样。 19 | 20 | ## 安装方法 21 | Python版本支持:>=3.7 22 | 23 | ``` 24 | pip install pydumpling 25 | ``` 26 | 27 | ## 使用方法 28 | 29 | ### 在任何地方进行`traceback`的保存 30 | ```python 31 | from pydumpling import dump_current_traceback 32 | from inspect import currentframe 33 | 34 | 35 | def inner(): 36 | a = 1 37 | b = "2" 38 | dump_current_traceback("test.dump") 39 | c = str(a) + b 40 | 41 | 42 | def outer(): 43 | d = 4 44 | inner() 45 | 46 | ``` 47 | 48 | 49 | 50 | ### 在异常发生时进行异常堆栈的保存 51 | 在异常捕获的处理代码中使用`save_dumpling()`. 如果不指定文件名,默认使用:`${exception file}-${line number of the exception}.dump`. 52 | 53 | ```python 54 | from pydumpling import save_dumping 55 | 56 | def inner(): 57 | a = 1 58 | b = "2" 59 | c = a + b 60 | 61 | 62 | def outer(): 63 | inner() 64 | 65 | 66 | if __name__ == "__main__": 67 | try: 68 | outer() 69 | except Exception: 70 | save_dumping("test.dump") 71 | 72 | ``` 73 | 74 | 这样我们就得到了当时异常`traceback`的`dump文件`,通过`debub_dumpling`即可对其进行pdb调试: 75 | 76 | ```python 77 | Python 3.10.6 (main, Aug 1 2022, 20:38:21) [GCC 5.4.0 20160609] on linux 78 | Type "help", "copyright", "credits" or "license" for more information. 79 | >>> from pydumpling import debug_dumpling 80 | >>> debug_dumpling("test.dump") 81 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 82 | -> c = a + b 83 | (Pdb) list 1,17 84 | 1 from pydumpling import save_dumping 85 | 2 86 | 3 def inner(): 87 | 4 >> a = 1 88 | 5 b = "2" 89 | 6 -> c = a + b 90 | 7 91 | 8 92 | 9 def outer(): 93 | 10 inner() 94 | 11 95 | 12 96 | 13 if __name__ == "__main__": 97 | 14 try: 98 | 15 outer() 99 | 16 except Exception: 100 | 17 save_dumping("test.dump") 101 | (Pdb) ll 102 | 3 def inner(): 103 | 4 >> a = 1 104 | 5 b = "2" 105 | 6 -> c = a + b 106 | (Pdb) bt 107 | /home/loyd/vscodeFiles/pydumpling/test.py(15)() 108 | -> outer() 109 | /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 110 | -> inner() 111 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 112 | -> c = a + b 113 | (Pdb) pp a 114 | 1 115 | (Pdb) pp b 116 | '2' 117 | (Pdb) u 118 | > /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 119 | -> inner() 120 | (Pdb) ll 121 | 9 def outer(): 122 | 10 -> inner() 123 | (Pdb) 124 | ``` 125 | 126 | ### 命令行使用 127 | 128 | #### 使用命令行来打印traceback: 129 | `python -m pydumpling --print test.deump` 130 | 131 | 将会输出: 132 | ```python 133 | Traceback (most recent call last): 134 | File "/workspaces/pydumpling/tests/test_dump.py", line 20, in test_dumpling 135 | outer() 136 | File "/workspaces/pydumpling/tests/test_dump.py", line 14, in outer 137 | inner() 138 | File "/workspaces/pydumpling/tests/test_dump.py", line 10, in inner 139 | c = a + b # noqa: F841 140 | TypeError: unsupported operand type(s) for +: 'int' and 'str' 141 | ``` 142 | 143 | 144 | #### 使用命令行来进行pdb调试: 145 | `python -m pydumpling --debug test.deump` 146 | 147 | 将会打开pdb调试会话: 148 | ```python 149 | -> c = a + b 150 | (Pdb) 151 | ``` 152 | 153 | #### 使用命令行来进行remote pdb调试 154 | `python -m pydumpling --rdebug test.deump` 155 | 它会在机器的4444端口上打开pdb调试器,然后我们可以在另外一台机器上使用telnet、netcat来进行远程调试: 156 | `nc 127.0.0.1 4444` 157 | ![alt text](static/rpdb.png) 158 | 159 | #### 开启全局异常捕获: 160 | ```python 161 | from pydumpling import catch_any_exception 162 | 163 | catch_any_exception() 164 | 165 | def inner(): 166 | a = 1 167 | b = "2" 168 | c = a + b # noqa: F841 169 | 170 | 171 | def outer(): 172 | inner() 173 | 174 | if __name__ == "__main__": 175 | outer() 176 | 177 | ``` 178 | 179 | ## TODO 180 | - [] 181 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | alabaster==0.7.13 5 | babel==2.14.0 6 | certifi==2024.2.2 7 | charset-normalizer==3.3.2 8 | colorama==0.4.6; sys_platform == "win32" 9 | dill==0.3.7 10 | docutils==0.17.1 11 | exceptiongroup==1.2.0; python_version < "3.11" 12 | flake8==5.0.4 13 | idna==3.6 14 | imagesize==1.4.1 15 | importlib-metadata==4.2.0; python_version < "3.8" 16 | iniconfig==2.0.0 17 | jinja2==3.1.3 18 | markupsafe==2.1.5 19 | mccabe==0.7.0 20 | packaging==23.2 21 | pluggy==1.2.0 22 | pycodestyle==2.9.1 23 | pyflakes==2.5.0 24 | pygments==2.17.2 25 | pytest==7.4.4 26 | pytest-order==1.2.0 27 | pytz==2024.1; python_version < "3.9" 28 | requests==2.31.0 29 | setuptools==68.0.0 30 | snowballstemmer==2.2.0 31 | sphinx==4.3.2 32 | sphinx-copybutton==0.5.2 33 | sphinx-rtd-theme==1.3.0 34 | sphinx-tabs==3.4.5 35 | sphinxcontrib-applehelp==1.0.2 36 | sphinxcontrib-devhelp==1.0.2 37 | sphinxcontrib-htmlhelp==2.0.0 38 | sphinxcontrib-jquery==4.1 39 | sphinxcontrib-jsmath==1.0.1 40 | sphinxcontrib-qthelp==1.0.3 41 | sphinxcontrib-serializinghtml==1.1.5 42 | tomli==2.0.1; python_version < "3.11" 43 | typing-extensions==4.7.1; python_version < "3.8" 44 | urllib3==2.0.7 45 | zipp==3.15.0; python_version < "3.8" 46 | -------------------------------------------------------------------------------- /docs/source/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/docs/source/_static/favicon.png -------------------------------------------------------------------------------- /docs/source/_static/header_photo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/docs/source/_static/header_photo_1.png -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/_static/rpdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/docs/source/_static/rpdb.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'pydumpling' 10 | copyright = '2024, cocolato' 11 | author = 'cocolato' 12 | release = '0.1.4' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | html_context = { 17 | "display_github": True, # Integrate GitHub 18 | "github_user": "cocolato", # Username 19 | "github_repo": "pydumpling", # Repo name 20 | "github_version": "main", # Version 21 | "conf_py_path": "/source/", # Path in the checkout to the docs root 22 | } 23 | 24 | extensions = [ 25 | "sphinx_rtd_theme", 26 | "sphinx_tabs.tabs", 27 | "sphinx_copybutton" 28 | ] 29 | 30 | templates_path = ['_templates'] 31 | exclude_patterns = [] 32 | 33 | 34 | html_theme = "sphinx_rtd_theme" 35 | 36 | html_static_path = ['_static'] 37 | html_logo = "_static/logo.png" 38 | html_favicon = "_static/favicon.png" 39 | html_title = "Pydumpling Documentation" 40 | html_show_sourcelink = False 41 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Pydumpling 2 | ======================= 3 | 4 | .. image:: https://img.shields.io/pypi/dm/pydumpling 5 | :alt: PyPI - Downloads 6 | :target: https://pypi.org/project/pydumpling/ 7 | 8 | .. image:: https://img.shields.io/pypi/v/pydumpling 9 | :alt: PyPI - Version 10 | :target: https://pypi.org/project/pydumpling/ 11 | 12 | .. image:: https://img.shields.io/github/stars/cocolato/pydumpling 13 | :alt: GitHub Repo stars 14 | :target: https://github.com/cocolato/pydumpling/ 15 | 16 | 17 | 18 | It's a fork/optimized version from `elifiner/pydump`_ .The main optimization points are: 19 | 20 | * Save the ``Python traceback`` anywhere, not just when it's an exception. 21 | * Optimize code structure && remove redundant code 22 | * fix bug in python2.7 && support python3.10+ 23 | * supported more pdb command 24 | * provides command line tools 25 | 26 | .. _elifiner/pydump: https://github.com/elifiner/pydump 27 | 28 | Pydumpling writes the ``python current traceback`` into a file and 29 | can later load it in a Python debugger. It works with the built-in 30 | pdb and with other popular debuggers (`pudb`_, `ipdb`_ and `pdbpp`_). 31 | 32 | .. _pudb: https://github.com/inducer/pudb 33 | .. _ipdb: https://github.com/gotcha/ipdb 34 | .. _pdbpp: https://github.com/pdbpp/pdbpp 35 | 36 | Why use Pydumpling? 37 | ------------------- 38 | 39 | * We usually use ``try... except ... `` to catch exceptions that occur in our programs, but do we really know why they occur? 40 | * When your project is running online, you suddenly get an unexpected exception that causes the process to exit. How do you reproduce this problem? 41 | * Not enough information in the logs to help us pinpoint online issues? 42 | * If we were able to save the exception error and then use the debugger to recover the traceback at that time, we could see the entire stack variables along the traceback as if you had caught the exception at the local breakpoint. 43 | 44 | 45 | User's Guide 46 | ------------ 47 | 48 | Get started with :doc:`installation` 49 | and then get an overview with the :doc:`tutorial` that shows how to use. 50 | 51 | .. toctree:: 52 | :maxdepth: 4 53 | 54 | installation 55 | tutorial -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============= 3 | 4 | Python Version 5 | --------------- 6 | 7 | Python version: >=3.7 8 | 9 | 10 | Install 11 | ------- 12 | 13 | Not published in pypi yet, so use the `.whl` file install pydumpling in the dist path. 14 | 15 | .. tabs:: 16 | 17 | .. group-tab:: Linux/macOS 18 | 19 | .. code-block:: text 20 | 21 | $ pip3 install pydumpling 22 | 23 | .. group-tab:: Windows 24 | 25 | .. code-block:: text 26 | 27 | > pip install pydumpling -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ========= 3 | 4 | Save the python traceback anywhere 5 | ----------------------------------- 6 | 7 | .. code-block:: python 8 | 9 | from pydumpling import dump_current_traceback 10 | 11 | 12 | def inner(): 13 | a = 1 14 | b = "2" 15 | dump_current_traceback("test.dump") 16 | c = str(a) + b 17 | 18 | 19 | def outer(): 20 | d = 4 21 | inner() 22 | 23 | outer() 24 | 25 | 26 | 27 | Save the exception traceback 28 | ---------------------------- 29 | .. code-block:: python 30 | 31 | from pydumpling import save_dumping 32 | 33 | def inner(): 34 | a = 1 35 | b = "2" 36 | c = a + b 37 | 38 | 39 | def outer(): 40 | inner() 41 | 42 | 43 | if __name__ == "__main__": 44 | try: 45 | outer() 46 | except Exception: 47 | save_dumping("test.dump") 48 | 49 | 50 | Use ``debug_dumpling`` to do pdb debug 51 | -------------------------------------- 52 | 53 | .. code-block:: console 54 | 55 | Python 3.10.6 (main, Aug 1 2022, 20:38:21) [GCC 5.4.0 20160609] on linux 56 | Type "help", "copyright", "credits" or "license" for more information. 57 | >>> from pydumpling import debug_dumpling 58 | >>> debug_dumpling("test.dump") 59 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 60 | -> c = a + b 61 | (Pdb) list 1,17 62 | 1 from pydumpling import save_dumping 63 | 2 64 | 3 def inner(): 65 | 4 >> a = 1 66 | 5 b = "2" 67 | 6 -> c = a + b 68 | 7 69 | 8 70 | 9 def outer(): 71 | 10 inner() 72 | 11 73 | 12 74 | 13 if __name__ == "__main__": 75 | 14 try: 76 | 15 outer() 77 | 16 except Exception: 78 | 17 save_dumping("test.dump") 79 | (Pdb) ll 80 | 3 def inner(): 81 | 4 >> a = 1 82 | 5 b = "2" 83 | 6 -> c = a + b 84 | (Pdb) bt 85 | /home/loyd/vscodeFiles/pydumpling/test.py(15)() 86 | -> outer() 87 | /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 88 | -> inner() 89 | > /home/loyd/vscodeFiles/pydumpling/test.py(6)inner() 90 | -> c = a + b 91 | (Pdb) pp a 92 | 1 93 | (Pdb) pp b 94 | '2' 95 | (Pdb) u 96 | > /home/loyd/vscodeFiles/pydumpling/test.py(10)outer() 97 | -> inner() 98 | (Pdb) ll 99 | 9 def outer(): 100 | 10 -> inner() 101 | (Pdb) 102 | 103 | Use command line 104 | ---------------- 105 | 106 | help message 107 | 108 | .. code-block:: console 109 | 110 | python -m pydumpling --help 111 | 112 | .. code-block:: text 113 | 114 | usage: pydumpling [options] filename 115 | 116 | pydumpling cli tools 117 | 118 | positional arguments: 119 | filename the .dump file 120 | 121 | options: 122 | -h, --help show this help message and exit 123 | --print print traceback information 124 | --debug enter pdb debugging interface 125 | --rdebug enter rpdb debugging interface 126 | 127 | Print the traceback 128 | ################### 129 | 130 | Use ``pydumpling --print test.dump`` to print the traceback information. 131 | 132 | It will print the following information: 133 | 134 | .. code-block:: text 135 | 136 | Traceback (most recent call last): 137 | File "/workspaces/pydumpling/tests/test_dump.py", line 20, in test_dumpling 138 | outer() 139 | File "/workspaces/pydumpling/tests/test_dump.py", line 14, in outer 140 | inner() 141 | File "/workspaces/pydumpling/tests/test_dump.py", line 10, in inner 142 | c = a + b # noqa: F841 143 | TypeError: unsupported operand type(s) for +: 'int' and 'str' 144 | 145 | 146 | Do pdb debug with dump file 147 | ############################ 148 | 149 | Use ``pydumpling --debug test.deump`` to do pdb debugging with dump file. 150 | 151 | It will open the pdb window: 152 | 153 | .. code-block:: text 154 | 155 | -> c = a + b 156 | (Pdb) 157 | 158 | 159 | Do remote debug with dump file 160 | ############################## 161 | Use ``python -m pydumpling --rdebug test.dump`` to do remote debug. 162 | It will open the debugger on port 4444, then we can access pdb using `telnet`_, `netcat`_ . 163 | 164 | .. _telnet: https://en.wikipedia.org/wiki/Telnet#Modern_day_uses 165 | .. _netcat: https://netcat.sourceforge.net/ 166 | 167 | 168 | .. image:: _static/rpdb.png 169 | 170 | 171 | Enable global exception catching 172 | ################################ 173 | 174 | .. code-block:: python 175 | 176 | from pydumpling import catch_any_exception 177 | 178 | catch_any_exception() 179 | 180 | def inner(): 181 | a = 1 182 | b = "2" 183 | c = a + b # noqa: F841 184 | 185 | 186 | def outer(): 187 | inner() 188 | 189 | if __name__ == "__main__": 190 | outer() 191 | 192 | -------------------------------------------------------------------------------- /pdm.lock: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # It is not intended for manual editing. 3 | 4 | [metadata] 5 | groups = ["default", "test", "dev"] 6 | strategy = ["cross_platform", "inherit_metadata"] 7 | lock_version = "4.4.1" 8 | content_hash = "sha256:4a929e9b72ae956ecfe5c76ad3eb20057488dca100830712dca0db592978781c" 9 | 10 | [[package]] 11 | name = "colorama" 12 | version = "0.4.6" 13 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 14 | summary = "Cross-platform colored terminal text." 15 | groups = ["dev", "test"] 16 | marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" 17 | files = [ 18 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 19 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 20 | ] 21 | 22 | [[package]] 23 | name = "coverage" 24 | version = "7.2.7" 25 | requires_python = ">=3.7" 26 | summary = "Code coverage measurement for Python" 27 | groups = ["test"] 28 | files = [ 29 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 30 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 31 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 32 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 33 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 34 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 35 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 36 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 37 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 38 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 39 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 40 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 41 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 42 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 43 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 44 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 45 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 46 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 47 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 48 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 49 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 50 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 51 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 52 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 53 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 54 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 55 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 56 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 57 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 58 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 59 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 60 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 61 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 62 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 63 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 64 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 65 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 66 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 67 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 68 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 69 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 70 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 71 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 72 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 73 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 74 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 75 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 76 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 77 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 78 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 79 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 80 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 81 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 82 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 83 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 84 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 85 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 86 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 87 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 88 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 89 | ] 90 | 91 | [[package]] 92 | name = "coverage" 93 | version = "7.2.7" 94 | extras = ["toml"] 95 | requires_python = ">=3.7" 96 | summary = "Code coverage measurement for Python" 97 | groups = ["test"] 98 | dependencies = [ 99 | "coverage==7.2.7", 100 | "tomli; python_full_version <= \"3.11.0a6\"", 101 | ] 102 | files = [ 103 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 104 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 105 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 106 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 107 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 108 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 109 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 110 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 111 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 112 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 113 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 114 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 115 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 116 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 117 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 118 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 119 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 120 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 121 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 122 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 123 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 124 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 125 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 126 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 127 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 128 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 129 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 130 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 131 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 132 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 133 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 134 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 135 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 136 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 137 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 138 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 139 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 140 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 141 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 142 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 143 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 144 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 145 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 146 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 147 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 148 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 149 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 150 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 151 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 152 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 153 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 154 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 155 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 156 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 157 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 158 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 159 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 160 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 161 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 162 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 163 | ] 164 | 165 | [[package]] 166 | name = "dill" 167 | version = "0.3.7" 168 | requires_python = ">=3.7" 169 | summary = "serialize all of Python" 170 | groups = ["default"] 171 | files = [ 172 | {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, 173 | {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, 174 | ] 175 | 176 | [[package]] 177 | name = "distlib" 178 | version = "0.3.8" 179 | summary = "Distribution utilities" 180 | groups = ["dev"] 181 | files = [ 182 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 183 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 184 | ] 185 | 186 | [[package]] 187 | name = "exceptiongroup" 188 | version = "1.2.0" 189 | requires_python = ">=3.7" 190 | summary = "Backport of PEP 654 (exception groups)" 191 | groups = ["test"] 192 | marker = "python_version < \"3.11\"" 193 | files = [ 194 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 195 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 196 | ] 197 | 198 | [[package]] 199 | name = "filelock" 200 | version = "3.12.2" 201 | requires_python = ">=3.7" 202 | summary = "A platform independent file lock." 203 | groups = ["dev"] 204 | files = [ 205 | {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, 206 | {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, 207 | ] 208 | 209 | [[package]] 210 | name = "flake8" 211 | version = "5.0.4" 212 | requires_python = ">=3.6.1" 213 | summary = "the modular source code checker: pep8 pyflakes and co" 214 | groups = ["test"] 215 | dependencies = [ 216 | "importlib-metadata<4.3,>=1.1.0; python_version < \"3.8\"", 217 | "mccabe<0.8.0,>=0.7.0", 218 | "pycodestyle<2.10.0,>=2.9.0", 219 | "pyflakes<2.6.0,>=2.5.0", 220 | ] 221 | files = [ 222 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 223 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 224 | ] 225 | 226 | [[package]] 227 | name = "importlib-metadata" 228 | version = "4.2.0" 229 | requires_python = ">=3.6" 230 | summary = "Read metadata from Python packages" 231 | groups = ["dev", "test"] 232 | marker = "python_version < \"3.8\"" 233 | dependencies = [ 234 | "typing-extensions>=3.6.4; python_version < \"3.8\"", 235 | "zipp>=0.5", 236 | ] 237 | files = [ 238 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 239 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 240 | ] 241 | 242 | [[package]] 243 | name = "iniconfig" 244 | version = "2.0.0" 245 | requires_python = ">=3.7" 246 | summary = "brain-dead simple config-ini parsing" 247 | groups = ["test"] 248 | files = [ 249 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 250 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 251 | ] 252 | 253 | [[package]] 254 | name = "mccabe" 255 | version = "0.7.0" 256 | requires_python = ">=3.6" 257 | summary = "McCabe checker, plugin for flake8" 258 | groups = ["test"] 259 | files = [ 260 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 261 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 262 | ] 263 | 264 | [[package]] 265 | name = "packaging" 266 | version = "24.0" 267 | requires_python = ">=3.7" 268 | summary = "Core utilities for Python packages" 269 | groups = ["default", "dev", "test"] 270 | files = [ 271 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 272 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 273 | ] 274 | 275 | [[package]] 276 | name = "platformdirs" 277 | version = "2.6.2" 278 | requires_python = ">=3.7" 279 | summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 280 | groups = ["dev"] 281 | dependencies = [ 282 | "typing-extensions>=4.4; python_version < \"3.8\"", 283 | ] 284 | files = [ 285 | {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, 286 | {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, 287 | ] 288 | 289 | [[package]] 290 | name = "pluggy" 291 | version = "1.2.0" 292 | requires_python = ">=3.7" 293 | summary = "plugin and hook calling mechanisms for python" 294 | groups = ["dev", "test"] 295 | dependencies = [ 296 | "importlib-metadata>=0.12; python_version < \"3.8\"", 297 | ] 298 | files = [ 299 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 300 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 301 | ] 302 | 303 | [[package]] 304 | name = "py" 305 | version = "1.11.0" 306 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 307 | summary = "library with cross-python path, ini-parsing, io, code, log facilities" 308 | groups = ["dev"] 309 | files = [ 310 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 311 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 312 | ] 313 | 314 | [[package]] 315 | name = "pycodestyle" 316 | version = "2.9.1" 317 | requires_python = ">=3.6" 318 | summary = "Python style guide checker" 319 | groups = ["test"] 320 | files = [ 321 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 322 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 323 | ] 324 | 325 | [[package]] 326 | name = "pyflakes" 327 | version = "2.5.0" 328 | requires_python = ">=3.6" 329 | summary = "passive checker of Python programs" 330 | groups = ["test"] 331 | files = [ 332 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 333 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 334 | ] 335 | 336 | [[package]] 337 | name = "pytest" 338 | version = "7.4.4" 339 | requires_python = ">=3.7" 340 | summary = "pytest: simple powerful testing with Python" 341 | groups = ["test"] 342 | dependencies = [ 343 | "colorama; sys_platform == \"win32\"", 344 | "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", 345 | "importlib-metadata>=0.12; python_version < \"3.8\"", 346 | "iniconfig", 347 | "packaging", 348 | "pluggy<2.0,>=0.12", 349 | "tomli>=1.0.0; python_version < \"3.11\"", 350 | ] 351 | files = [ 352 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 353 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 354 | ] 355 | 356 | [[package]] 357 | name = "pytest-cov" 358 | version = "4.1.0" 359 | requires_python = ">=3.7" 360 | summary = "Pytest plugin for measuring coverage." 361 | groups = ["test"] 362 | dependencies = [ 363 | "coverage[toml]>=5.2.1", 364 | "pytest>=4.6", 365 | ] 366 | files = [ 367 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 368 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 369 | ] 370 | 371 | [[package]] 372 | name = "pytest-order" 373 | version = "1.2.1" 374 | requires_python = ">=3.6" 375 | summary = "pytest plugin to run your tests in a specific order" 376 | groups = ["test"] 377 | dependencies = [ 378 | "pytest>=5.0; python_version < \"3.10\"", 379 | "pytest>=6.2.4; python_version >= \"3.10\"", 380 | ] 381 | files = [ 382 | {file = "pytest-order-1.2.1.tar.gz", hash = "sha256:4451bd8821ba4fa2109455a2fcc882af60ef8e53e09d244d67674be08f56eac3"}, 383 | {file = "pytest_order-1.2.1-py3-none-any.whl", hash = "sha256:c3082fc73f9ddcf13e4a22dda9bbcc2f39865bf537438a1d50fa241e028dd743"}, 384 | ] 385 | 386 | [[package]] 387 | name = "six" 388 | version = "1.16.0" 389 | requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 390 | summary = "Python 2 and 3 compatibility utilities" 391 | groups = ["dev"] 392 | files = [ 393 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 394 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 395 | ] 396 | 397 | [[package]] 398 | name = "tomli" 399 | version = "2.0.1" 400 | requires_python = ">=3.7" 401 | summary = "A lil' TOML parser" 402 | groups = ["dev", "test"] 403 | marker = "python_version < \"3.11\"" 404 | files = [ 405 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 406 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 407 | ] 408 | 409 | [[package]] 410 | name = "tox" 411 | version = "3.28.0" 412 | requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 413 | summary = "tox is a generic virtualenv management and test command line tool" 414 | groups = ["dev"] 415 | dependencies = [ 416 | "colorama>=0.4.1; platform_system == \"Windows\"", 417 | "filelock>=3.0.0", 418 | "importlib-metadata>=0.12; python_version < \"3.8\"", 419 | "packaging>=14", 420 | "pluggy>=0.12.0", 421 | "py>=1.4.17", 422 | "six>=1.14.0", 423 | "tomli>=2.0.1; python_version >= \"3.7\" and python_version < \"3.11\"", 424 | "virtualenv!=20.0.0,!=20.0.1,!=20.0.2,!=20.0.3,!=20.0.4,!=20.0.5,!=20.0.6,!=20.0.7,>=16.0.0", 425 | ] 426 | files = [ 427 | {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, 428 | {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, 429 | ] 430 | 431 | [[package]] 432 | name = "tox-pdm" 433 | version = "0.6.1" 434 | requires_python = ">=3.7" 435 | summary = "A plugin for tox that utilizes PDM as the package manager and installer" 436 | groups = ["dev"] 437 | dependencies = [ 438 | "tomli; python_version < \"3.11\"", 439 | "tox>=3.18.0", 440 | ] 441 | files = [ 442 | {file = "tox-pdm-0.6.1.tar.gz", hash = "sha256:952ea67f2ec891f11eb00749f63fc0f980384435ca782c448d154390f9f42f5e"}, 443 | {file = "tox_pdm-0.6.1-py3-none-any.whl", hash = "sha256:9e3cf83b7b55c3e33aaee0e65cf341739581ff4604a4178f0ef7dbab73a0bb35"}, 444 | ] 445 | 446 | [[package]] 447 | name = "typing-extensions" 448 | version = "4.7.1" 449 | requires_python = ">=3.7" 450 | summary = "Backported and Experimental Type Hints for Python 3.7+" 451 | groups = ["dev", "test"] 452 | marker = "python_version < \"3.8\"" 453 | files = [ 454 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 455 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 456 | ] 457 | 458 | [[package]] 459 | name = "virtualenv" 460 | version = "20.16.2" 461 | requires_python = ">=3.6" 462 | summary = "Virtual Python Environment builder" 463 | groups = ["dev"] 464 | dependencies = [ 465 | "distlib<1,>=0.3.1", 466 | "filelock<4,>=3.2", 467 | "importlib-metadata>=0.12; python_version < \"3.8\"", 468 | "platformdirs<3,>=2", 469 | ] 470 | files = [ 471 | {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, 472 | {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, 473 | ] 474 | 475 | [[package]] 476 | name = "zipp" 477 | version = "3.15.0" 478 | requires_python = ">=3.7" 479 | summary = "Backport of pathlib-compatible object wrapper for zip files" 480 | groups = ["dev", "test"] 481 | marker = "python_version < \"3.8\"" 482 | files = [ 483 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 484 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 485 | ] 486 | -------------------------------------------------------------------------------- /pydumpling/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | from .debug_dumpling import debug_dumpling, load_dumpling 5 | from .helpers import catch_any_exception 6 | from .pydumpling import __version__, dump_current_traceback, save_dumping 7 | from .rpdb import r_post_mortem 8 | 9 | __version__ == __version__ 10 | __all__ = ["debug_dumpling", "load_dumpling", "save_dumping", 11 | "dump_current_traceback", "r_post_mortem", "catch_any_exception"] 12 | -------------------------------------------------------------------------------- /pydumpling/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /pydumpling/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from .debug_dumpling import debug_dumpling, load_dumpling 4 | from .helpers import print_traceback_and_except, validate_file_name 5 | from .rpdb import r_post_mortem 6 | 7 | 8 | parser = argparse.ArgumentParser( 9 | description="pydumpling cli tools", 10 | prog="pydumpling", 11 | usage="%(prog)s [options] filename" 12 | ) 13 | 14 | # print or debug 15 | pydumpling_cli_action_group = parser.add_mutually_exclusive_group(required=True) 16 | 17 | pydumpling_cli_action_group.add_argument( 18 | "--print", 19 | action="store_true", 20 | help="print traceback information" 21 | ) 22 | 23 | pydumpling_cli_action_group.add_argument( 24 | "--debug", 25 | action="store_true", 26 | help="enter pdb debugging interface" 27 | ) 28 | 29 | pydumpling_cli_action_group.add_argument( 30 | "--rdebug", 31 | action="store_true", 32 | help="enter rpdb debugging interface" 33 | ) 34 | 35 | parser.add_argument( 36 | "filename", 37 | type=validate_file_name, 38 | help="the .dump file" 39 | ) 40 | 41 | 42 | def main() -> None: 43 | args = parser.parse_args() 44 | file_name = args.filename 45 | if args.print: 46 | dumpling_ = load_dumpling(file_name) 47 | print_traceback_and_except(dumpling_) 48 | elif args.debug: 49 | debug_dumpling(file_name) 50 | elif args.rdebug: 51 | r_post_mortem(file_name) 52 | -------------------------------------------------------------------------------- /pydumpling/debug_dumpling.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | import gzip 5 | import inspect 6 | import pdb 7 | import pickle 8 | import types 9 | 10 | import dill 11 | from packaging.version import parse 12 | 13 | from .fake_types import FakeCode, FakeFrame, FakeTraceback 14 | 15 | 16 | def load_dumpling(filename): 17 | with gzip.open(filename, "rb") as f: 18 | try: 19 | return dill.load(f) 20 | except Exception: 21 | return pickle.load(f) 22 | 23 | 24 | def mock_inspect(): 25 | inspect.isframe = lambda obj: isinstance( 26 | obj, types.FrameType) or obj.__class__ == FakeFrame 27 | inspect.istraceback = lambda obj: isinstance( 28 | obj, types.TracebackType) or obj.__class__ == FakeTraceback 29 | inspect.iscode = lambda obj: isinstance( 30 | obj, types.CodeType) or obj.__class__ == FakeCode 31 | 32 | 33 | def debug_dumpling(dump_file, pdb=pdb): 34 | mock_inspect() 35 | dumpling = load_dumpling(dump_file) 36 | if not parse("0.0.1") <= parse(dumpling["version"]) < parse("1.0.0"): 37 | raise ValueError("Unsupported dumpling version: %s" % 38 | dumpling["version"]) 39 | tb = dumpling["traceback"] 40 | pdb.post_mortem(tb) 41 | -------------------------------------------------------------------------------- /pydumpling/fake_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | import os 5 | import sys 6 | 7 | import dill 8 | 9 | 10 | class FakeType(object): 11 | @classmethod 12 | def _safe_repr(cls, v): 13 | try: 14 | return repr(v) 15 | except Exception as e: 16 | return "repr error: %s" % str(e) 17 | 18 | @classmethod 19 | def _convert_dict(cls, v): 20 | return {cls._convert(k): cls._convert(i) for k, i in v.items()} 21 | 22 | @classmethod 23 | def _convert_obj(cls, obj): 24 | try: 25 | return FakeClass(cls._safe_repr(obj), cls._convert_dict(obj.__dict__)) 26 | except Exception: 27 | return cls._convert(obj) 28 | 29 | @classmethod 30 | def _convert_seq(cls, v): 31 | return map(cls._convert, v) 32 | 33 | @classmethod 34 | def _convert(cls, v): 35 | 36 | if v is None: 37 | return v 38 | 39 | if dill is not None: 40 | try: 41 | dill.dumps(v) 42 | return v 43 | except Exception: 44 | return cls._safe_repr(v) 45 | else: 46 | from datetime import date, datetime, time, timedelta 47 | 48 | BUILTIN = (str, unicode, int, long, float, date, time, datetime, timedelta) if sys.version_info.major == 2 \ 49 | else (str, int, float, date, time, datetime, timedelta) # noqa: F821 50 | 51 | if type(v) in BUILTIN: 52 | return v 53 | 54 | if isinstance(v, (tuple, list, set)): 55 | return type(v)(cls._convert_seq(v)) 56 | 57 | if isinstance(v, dict): 58 | return cls._convert_dict(v) 59 | 60 | return cls._safe_repr(v) 61 | 62 | 63 | class FakeTraceback(FakeType): 64 | 65 | def __init__(self, traceback=None): 66 | self.tb_frame = FakeFrame( 67 | traceback.tb_frame) if traceback and traceback.tb_frame else None 68 | self.tb_lineno = traceback.tb_lineno if traceback else None 69 | self.tb_next = FakeTraceback( 70 | traceback.tb_next) if traceback and traceback.tb_next else None 71 | self.tb_lasti = traceback.tb_lasti if traceback else 0 72 | 73 | 74 | class FakeFrame(FakeType): 75 | def __init__(self, frame): 76 | self.f_code = FakeCode(frame.f_code) 77 | self.f_locals = self._convert_dict(frame.f_locals) 78 | if "self" in frame.f_locals: 79 | self.f_locals["self"] = self._convert_obj(frame.f_locals["self"]) 80 | self.f_globals = self._convert_dict(frame.f_globals) 81 | self.f_lineno = frame.f_lineno 82 | self.f_back = FakeFrame(frame.f_back) if frame.f_back else None 83 | self.f_lasti = frame.f_lasti 84 | self.f_builtins = frame.f_builtins 85 | 86 | 87 | class FakeClass(FakeType): 88 | 89 | def __init__(self, repr, vals): 90 | self.__repr = repr 91 | self.__dict__.update(vars) 92 | 93 | def __repr__(self): 94 | return self.__repr 95 | 96 | 97 | class FakeCode(FakeType): 98 | 99 | def __init__(self, code): 100 | self.co_filename = os.path.abspath(code.co_filename) 101 | self.co_name = code.co_name 102 | self.co_argcount = code.co_argcount 103 | self.co_consts = tuple(FakeCode(c) if hasattr( 104 | c, "co_filename") else c for c in code.co_consts) 105 | self.co_firstlineno = code.co_firstlineno 106 | self.co_lnotab = code.co_lnotab 107 | self.co_varnames = code.co_varnames 108 | self.co_flags = code.co_flags 109 | self.co_code = code.co_code 110 | self._co_lines = list(code.co_lines()) if hasattr( 111 | code, "co_lines") else [] 112 | if hasattr(code, "co_kwonlyargcount"): 113 | self.co_kwonlyargcount = code.co_kwonlyargcount 114 | if hasattr(code, "co_positions"): 115 | self.co_positions = code.co_positions 116 | 117 | def co_lines(self): 118 | return iter(self._co_lines) 119 | -------------------------------------------------------------------------------- /pydumpling/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import argparse 4 | from traceback import print_exception, print_tb 5 | 6 | from .pydumpling import save_dumping 7 | 8 | 9 | DUMP_FILE_EXTENSION: str = ".dump" 10 | 11 | 12 | def print_traceback_and_except(dumpling_result): 13 | exc_tb = dumpling_result["traceback"] 14 | except_extra = dumpling_result.get("exc_extra") 15 | exc_type = except_extra["exc_type"] if except_extra else None 16 | exc_value = except_extra["exc_value"] if except_extra else None 17 | if exc_type and exc_value: 18 | print_exception(exc_type, exc_value, exc_tb) 19 | else: 20 | print_tb(exc_tb) 21 | 22 | 23 | def catch_any_exception(): 24 | original_hook = sys.excepthook 25 | 26 | def _hook(exc_type, exc_value, exc_tb): 27 | save_dumping(exc_info=(exc_type, exc_value, exc_tb)) 28 | original_hook(exc_type, exc_value, exc_tb) # call sys original hook 29 | 30 | sys.excepthook = _hook 31 | 32 | 33 | def validate_file_name(file_name: str) -> str: 34 | """check file extension name and exists""" 35 | if not file_name.endswith(DUMP_FILE_EXTENSION): 36 | raise argparse.ArgumentTypeError("File must be .dump file") 37 | if not os.path.exists(file_name): 38 | raise argparse.ArgumentTypeError(f"File {file_name} not found") 39 | return file_name 40 | -------------------------------------------------------------------------------- /pydumpling/pydumpling.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | import gzip 5 | import inspect 6 | import pickle 7 | import sys 8 | import warnings 9 | 10 | import dill 11 | 12 | from .fake_types import FakeFrame, FakeTraceback 13 | 14 | __version__ = "0.1.6" 15 | 16 | 17 | def save_dumping(filename=None, exc_info=None): 18 | try: 19 | if exc_info is None: 20 | exc_type, exc_value, exc_tb = sys.exc_info() 21 | else: 22 | exc_type, exc_value, exc_tb = exc_info 23 | 24 | if filename is None: 25 | filename = "%s-%d.dump" % ( 26 | exc_tb.tb_frame.f_code.co_filename, exc_tb.tb_frame.f_lineno) 27 | 28 | fake_tb = FakeTraceback(exc_tb) 29 | dumpling = { 30 | "traceback": fake_tb, 31 | "version": __version__, 32 | "exc_extra": { 33 | "exc_type": exc_type, 34 | "exc_value": exc_value, 35 | }, 36 | "dump_type": "DILL" 37 | } 38 | with gzip.open(filename, "wb") as f: 39 | try: 40 | dill.dump(dumpling, f, protocol=dill.HIGHEST_PROTOCOL) 41 | except Exception: 42 | dumpling["dump_type"] = "PICKLE" 43 | pickle.dump(dumpling, f, protocol=dill.HIGHEST_PROTOCOL) 44 | except Exception as e: 45 | err_msg = "Unexpected error: %s when dumping traceback" % str(e) 46 | warnings.warn(err_msg, RuntimeWarning) 47 | 48 | 49 | def dump_current_traceback(filename=None): 50 | try: 51 | fake_tb = gen_tb_from_frame(inspect.currentframe()) 52 | dumpling = { 53 | "traceback": fake_tb, 54 | "version": __version__, 55 | "dump_type": "DILL" 56 | } 57 | if filename is None: 58 | filename = "%s:%d.dump" % ( 59 | fake_tb.tb_frame.f_code.co_filename, fake_tb.tb_frame.f_lineno) 60 | with gzip.open(filename, "wb") as f: 61 | try: 62 | dill.dump(dumpling, f, protocol=dill.HIGHEST_PROTOCOL) 63 | except Exception: 64 | dumpling["dump_type"] = "PICKLE" 65 | pickle.dump(dumpling, f, protocol=dill.HIGHEST_PROTOCOL) 66 | except Exception as e: 67 | err_msg = "Unexpected error: %s when dumping traceback" % str(e) 68 | warnings.warn(err_msg, RuntimeWarning) 69 | 70 | 71 | def gen_tb_from_frame(f): 72 | tb = FakeTraceback() 73 | tb.tb_frame = FakeFrame(f) 74 | tb.tb_lasti = f.f_lasti 75 | tb.tb_lineno = f.f_lineno 76 | queue = [] 77 | f = f.f_back 78 | if f is None: 79 | return tb 80 | while f: 81 | tb = FakeTraceback() 82 | tb.tb_frame = FakeFrame(f) 83 | tb.tb_lasti = f.f_lasti 84 | tb.tb_lineno = f.f_lineno 85 | queue.append(tb) 86 | f = f.f_back 87 | 88 | for i in range(len(queue)-1, 0, -1): 89 | queue[i].tb_next = queue[i-1] 90 | queue[0].tb_next = None 91 | return queue[-1] 92 | -------------------------------------------------------------------------------- /pydumpling/rpdb.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | import socket 3 | import sys 4 | import threading 5 | 6 | from .debug_dumpling import load_dumpling, mock_inspect 7 | 8 | DEFAULT_ADDR = "127.0.0.1" 9 | DEFAULT_PORT = 4444 10 | 11 | 12 | class FileObjectWrapper(object): 13 | def __init__(self, fileobject, stdio): 14 | self._obj = fileobject 15 | self._io = stdio 16 | 17 | def __getattr__(self, attr): 18 | if hasattr(self._obj, attr): 19 | attr = getattr(self._obj, attr) 20 | elif hasattr(self._io, attr): 21 | attr = getattr(self._io, attr) 22 | else: 23 | raise AttributeError("Attribute %s is not found" % attr) 24 | return attr 25 | 26 | 27 | class Rpdb(pdb.Pdb): 28 | 29 | def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): 30 | """Initialize the socket and initialize pdb.""" 31 | 32 | # Backup stdin and stdout before replacing them by the socket handle 33 | self.old_stdout = sys.stdout 34 | self.old_stdin = sys.stdin 35 | self.port = port 36 | 37 | # Open a 'reusable' socket to let the webapp reload on the same port 38 | self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 40 | self.skt.bind((addr, port)) 41 | self.skt.listen(1) 42 | 43 | # Writes to stdout are forbidden in mod_wsgi environments 44 | try: 45 | sys.stderr.write("pdb is running on %s:%d\n" 46 | % self.skt.getsockname()) 47 | except IOError: 48 | pass 49 | 50 | (clientsocket, address) = self.skt.accept() 51 | handle = clientsocket.makefile('rw') 52 | pdb.Pdb.__init__(self, completekey='tab', 53 | stdin=FileObjectWrapper(handle, self.old_stdin), 54 | stdout=FileObjectWrapper(handle, self.old_stdin)) 55 | sys.stdout = sys.stdin = handle 56 | self.handle = handle 57 | OCCUPIED.claim(port, sys.stdout) 58 | 59 | def shutdown(self): 60 | """Revert stdin and stdout, close the socket.""" 61 | sys.stdout = self.old_stdout 62 | sys.stdin = self.old_stdin 63 | self.handle.close() 64 | OCCUPIED.unclaim(self.port) 65 | self.skt.shutdown(socket.SHUT_RDWR) 66 | self.skt.close() 67 | 68 | def do_continue(self, arg): 69 | """Clean-up and do underlying continue.""" 70 | try: 71 | return pdb.Pdb.do_continue(self, arg) 72 | finally: 73 | self.shutdown() 74 | 75 | do_c = do_cont = do_continue 76 | 77 | def do_quit(self, arg): 78 | """Clean-up and do underlying quit.""" 79 | try: 80 | return pdb.Pdb.do_quit(self, arg) 81 | finally: 82 | self.shutdown() 83 | 84 | do_q = do_exit = do_quit 85 | 86 | def do_EOF(self, arg): 87 | """Clean-up and do underlying EOF.""" 88 | try: 89 | return pdb.Pdb.do_EOF(self, arg) 90 | finally: 91 | self.shutdown() 92 | 93 | 94 | class OccupiedPorts(object): 95 | """Maintain rpdb port versus stdin/out file handles. 96 | 97 | Provides the means to determine whether or not a collision binding to a 98 | particular port is with an already operating rpdb session. 99 | 100 | Determination is according to whether a file handle is equal to what is 101 | registered against the specified port. 102 | """ 103 | 104 | def __init__(self): 105 | self.lock = threading.RLock() 106 | self.claims = {} 107 | 108 | def claim(self, port, handle): 109 | self.lock.acquire(True) 110 | self.claims[port] = id(handle) 111 | self.lock.release() 112 | 113 | def is_claimed(self, port, handle): 114 | self.lock.acquire(True) 115 | got = (self.claims.get(port) == id(handle)) 116 | self.lock.release() 117 | return got 118 | 119 | def unclaim(self, port): 120 | self.lock.acquire(True) 121 | del self.claims[port] 122 | self.lock.release() 123 | 124 | 125 | # {port: sys.stdout} pairs to track recursive rpdb invocation on same port. 126 | # This scheme doesn't interfere with recursive invocations on separate ports - 127 | # useful, eg, for concurrently debugging separate threads. 128 | OCCUPIED = OccupiedPorts() 129 | 130 | 131 | def r_post_mortem(dump_file, addr=DEFAULT_ADDR, port=DEFAULT_PORT): 132 | mock_inspect() 133 | dumpling = load_dumpling(dump_file) 134 | tb = dumpling["traceback"] 135 | debugger = Rpdb(addr=addr, port=port) 136 | debugger.reset() 137 | debugger.interaction(None, tb) 138 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pdm] 2 | distribution = true 3 | 4 | [tool.pdm.scripts] 5 | test = {composite = ["pdm install", "flake8 ./pydumpling ./tests", "pytest --cov=pydumpling --cov-report=term-missing tests/"]} 6 | docs = {shell = "cd docs && make html"} # build sphinx docs 7 | docs_export = { shell = "pdm export -G doc -o docs/requirements.txt --without-hashes" } # export requirements for docs 8 | docs_preview = {shell = 'python -m http.server -d docs\build\html'} 9 | 10 | [tool.pdm.build] 11 | includes = ["pydumpling/*.py"] 12 | 13 | [tool.pdm.dev-dependencies] 14 | test = [ 15 | "pytest-order>=1.2.0", 16 | "flake8>=5.0.4", 17 | "pytest-cov>=4.1.0", 18 | ] 19 | dev = [ 20 | "tox-pdm>=0.6.1", 21 | ] 22 | [build-system] 23 | requires = ["pdm-backend"] 24 | build-backend = "pdm.backend" 25 | 26 | 27 | [project] 28 | name = "pydumpling" 29 | version = "0.1.6" 30 | description = "Python post-mortem debugger" 31 | authors = [ 32 | {name = "cocolato", email = "haiizhu@outlook.com"}, 33 | ] 34 | dependencies = [ 35 | "dill<1.0.0,>=0.3.2", 36 | "packaging>=24.0", 37 | ] 38 | requires-python = ">=3.7" 39 | readme = "README.md" 40 | license = {text = "MIT"} 41 | 42 | 43 | [project.urls] 44 | homepage = "https://github.com/cocolato/pydumpling" 45 | 46 | [project.optional-dependencies] 47 | doc = [ 48 | "sphinx>=4.3.2", 49 | "sphinx-rtd-theme>=1.3.0", 50 | "sphinx-tabs>=3.4.5", 51 | "sphinx-copybutton>=0.5.2", 52 | ] 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is @generated by PDM. 2 | # Please do not edit it manually. 3 | 4 | colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" 5 | dill==0.3.7 6 | distlib==0.3.8 7 | exceptiongroup==1.2.0; python_version < "3.11" 8 | filelock==3.12.2 9 | flake8==5.0.4 10 | importlib-metadata==4.2.0; python_version < "3.8" 11 | iniconfig==2.0.0 12 | mccabe==0.7.0 13 | packaging==24.0 14 | platformdirs==2.6.2 15 | pluggy==1.2.0 16 | py==1.11.0 17 | pycodestyle==2.9.1 18 | pyflakes==2.5.0 19 | pytest==7.4.4 20 | pytest-order==1.2.0 21 | six==1.16.0 22 | tomli==2.0.1; python_version < "3.11" 23 | tox==3.28.0 24 | tox-pdm==0.6.1 25 | typing-extensions==4.7.1; python_version < "3.8" 26 | virtualenv==20.16.2 27 | zipp==3.15.0; python_version < "3.8" 28 | -------------------------------------------------------------------------------- /static/rpdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/static/rpdb.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/tests/__init__.py -------------------------------------------------------------------------------- /tests/dump/validate_file_name.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocolato/pydumpling/05768ada82ee4a3f652c160e280e44858855aeef/tests/dump/validate_file_name.dump -------------------------------------------------------------------------------- /tests/test_debug.py: -------------------------------------------------------------------------------- 1 | from pydumpling import load_dumpling, __version__ 2 | from pydumpling.fake_types import FakeTraceback 3 | import pytest 4 | 5 | 6 | @pytest.mark.order(2) 7 | def test_debug(): 8 | dumpling = load_dumpling("test.dump") 9 | assert isinstance(dumpling["traceback"], FakeTraceback) 10 | assert dumpling["version"] == __version__ 11 | 12 | dumpling2 = load_dumpling("current_tb.dump") 13 | assert isinstance(dumpling2["traceback"], FakeTraceback) 14 | assert dumpling2["version"] == __version__ 15 | -------------------------------------------------------------------------------- /tests/test_dump.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pydumpling import save_dumping, dump_current_traceback 3 | import pytest 4 | 5 | 6 | def inner(): 7 | a = 1 8 | b = "2" 9 | dump_current_traceback("current_tb.dump") 10 | c = a + b # noqa: F841 11 | 12 | 13 | def outer(): 14 | inner() 15 | 16 | 17 | @pytest.mark.order(1) 18 | def test_dumpling(): 19 | try: 20 | outer() 21 | except Exception as e: 22 | print(e) 23 | save_dumping(filename="test.dump") 24 | 25 | assert os.path.exists("test.dump") 26 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from argparse import ArgumentTypeError 3 | 4 | from pydumpling.helpers import validate_file_name 5 | 6 | 7 | def test_validate_file_name(): 8 | dump_file = "./tests/dump/validate_file_name.dump" 9 | assert validate_file_name(dump_file) == dump_file 10 | 11 | with pytest.raises(ArgumentTypeError, match="File must be .dump file"): 12 | validate_file_name("test.txt") 13 | 14 | with pytest.raises(ArgumentTypeError, match="File missing.dump not found"): 15 | validate_file_name("missing.dump") 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4.0 3 | envlist = py3{12,11,10,9,8,7} 4 | 5 | [testenv] 6 | groups = test 7 | allowlist_externals = pytest 8 | commands = pytest -v 9 | 10 | [testenv:lint] 11 | groups = lint 12 | commands = 13 | flake8 pydumpling/ tests/ 14 | --------------------------------------------------------------------------------