├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── lint.yml │ ├── release.yml │ └── tests_and_coverage.yml ├── .gitignore ├── .mypy.ini ├── .ruff.toml ├── LICENSE ├── README.md ├── docs └── assets │ ├── logo_1.png │ ├── logo_10.png │ ├── logo_11.png │ ├── logo_12.svg │ ├── logo_13.png │ ├── logo_14.png │ ├── logo_15.png │ ├── logo_16.svg │ ├── logo_2.svg │ ├── logo_3.png │ ├── logo_4.svg │ ├── logo_5.png │ ├── logo_6.svg │ ├── logo_7.svg │ ├── logo_8.svg │ └── logo_9.svg ├── escape ├── __init__.py ├── baked_escaper.py ├── errors.py ├── proxy_module.py ├── py.typed └── wrapper.py ├── pyproject.toml ├── requirements_dev.txt └── tests ├── __init__.py ├── documentation ├── __init__.py └── test_readme.py └── units ├── __init__.py ├── test_baked_escaper.py └── test_proxy_module.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: pomponchik 7 | 8 | --- 9 | 10 | ## Short description 11 | 12 | Replace this text with a short description of the error and the behavior that you expected to see instead. 13 | 14 | 15 | ## Describe the bug in detail 16 | 17 | Please add this test in such a way that it reproduces the bug you found and does not pass: 18 | 19 | ```python 20 | def test_your_bug(): 21 | ... 22 | ``` 23 | 24 | Writing the test, please keep compatibility with the [`pytest`](https://docs.pytest.org/) framework. 25 | 26 | If for some reason you cannot describe the error in the test format, describe here the steps to reproduce it. 27 | 28 | 29 | ## Environment 30 | - OS: ... 31 | - Python version (the output of the `python --version` command): ... 32 | - Version of this package: ... 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation fix 3 | about: Add something to the documentation, delete it, or change it 4 | title: '' 5 | labels: documentation 6 | assignees: pomponchik 7 | --- 8 | 9 | ## It's cool that you're here! 10 | 11 | Documentation is an important part of the project, we strive to make it high-quality and keep it up to date. Please adjust this template by outlining your proposal. 12 | 13 | 14 | ## Type of action 15 | 16 | What do you want to do: remove something, add it, or change it? 17 | 18 | 19 | ## Where? 20 | 21 | Specify which part of the documentation you want to make a change to? For example, the name of an existing documentation section or the line number in a file `README.md`. 22 | 23 | 24 | ## The essence 25 | 26 | Please describe the essence of the proposed change 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: pomponchik 7 | 8 | --- 9 | 10 | ## Short description 11 | 12 | What do you propose and why do you consider it important? 13 | 14 | 15 | ## Some details 16 | 17 | If you can, provide code examples that will show how your proposal will work. Also, if you can, indicate which alternatives to this behavior you have considered. And finally, how do you propose to test the correctness of the implementation of your idea, if at all possible? 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or consultation 3 | about: Ask anything about this project 4 | title: '' 5 | labels: guestion 6 | assignees: pomponchik 7 | 8 | --- 9 | 10 | ## Your question 11 | 12 | Here you can freely describe your question about the project. Please, before doing this, read the documentation provided, and ask the question only if the necessary answer is not there. In addition, please keep in mind that this is a free non-commercial project and user support is optional for its author. The response time is not guaranteed in any way. 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install dependencies 23 | shell: bash 24 | run: pip install -r requirements_dev.txt 25 | 26 | - name: Install the library 27 | shell: bash 28 | run: pip install . 29 | 30 | - name: Run mypy 31 | shell: bash 32 | run: mypy escape --strict 33 | 34 | - name: Run mypy for tests 35 | shell: bash 36 | run: mypy tests 37 | 38 | - name: Run ruff 39 | shell: bash 40 | run: ruff check escape 41 | 42 | - name: Run ruff for tests 43 | shell: bash 44 | run: ruff check tests 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | pypi-publish: 10 | name: upload release to PyPI 11 | runs-on: ubuntu-latest 12 | # Specifying a GitHub environment is optional, but strongly encouraged 13 | environment: release 14 | permissions: 15 | # IMPORTANT: this permission is mandatory for trusted publishing 16 | id-token: write 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | shell: bash 27 | run: pip install -r requirements_dev.txt 28 | 29 | - name: Build the project 30 | shell: bash 31 | run: python -m build . 32 | 33 | - name: Publish package distributions to PyPI 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | -------------------------------------------------------------------------------- /.github/workflows/tests_and_coverage.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [macos-latest, ubuntu-latest, windows-latest] 13 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v3 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install the library 23 | shell: bash 24 | run: pip install . 25 | 26 | - name: Install dependencies 27 | shell: bash 28 | run: pip install -r requirements_dev.txt 29 | 30 | - name: Print all libs 31 | shell: bash 32 | run: pip list 33 | 34 | - name: Run tests and show coverage on the command line 35 | run: coverage run --source=escape --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100 36 | 37 | - name: Upload reports to codecov 38 | env: 39 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 40 | if: runner.os == 'Linux' 41 | run: | 42 | curl -Os https://uploader.codecov.io/latest/linux/codecov 43 | find . -iregex "codecov.*" 44 | chmod +x codecov 45 | ./codecov -t ${CODECOV_TOKEN} 46 | 47 | - name: Run tests and show the branch coverage on the command line 48 | run: coverage run --branch --source=escape --omit="*tests*" -m pytest --cache-clear --assert=plain && coverage report -m --fail-under=100 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | .DS_Store 4 | test.py 5 | *.egg-info 6 | dist 7 | venv 8 | build 9 | .ruff_cache 10 | .mypy_cache 11 | .mutmut-cache 12 | html 13 | .coverage 14 | htmlcov 15 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disable_error_code = operator 3 | -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | ignore = ['E501', 'E712'] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 pomponchik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://raw.githubusercontent.com/pomponchik/escaping/develop/docs/assets/logo_16.svg) 2 | 3 | [![Downloads](https://static.pepy.tech/badge/escaping/month)](https://pepy.tech/project/escaping) 4 | [![Downloads](https://static.pepy.tech/badge/escaping)](https://pepy.tech/project/escaping) 5 | [![codecov](https://codecov.io/gh/pomponchik/escaping/graph/badge.svg?token=q7eAfV5g7q)](https://codecov.io/gh/pomponchik/escaping) 6 | [![Lines of code](https://sloc.xyz/github/pomponchik/escaping/?category=code)](https://github.com/boyter/scc/) 7 | [![Hits-of-Code](https://hitsofcode.com/github/pomponchik/escaping?branch=main)](https://hitsofcode.com/github/pomponchik/escaping/view?branch=main) 8 | [![Test-Package](https://github.com/pomponchik/escaping/actions/workflows/tests_and_coverage.yml/badge.svg)](https://github.com/pomponchik/escaping/actions/workflows/tests_and_coverage.yml) 9 | [![Python versions](https://img.shields.io/pypi/pyversions/escaping.svg)](https://pypi.python.org/pypi/escaping) 10 | [![PyPI version](https://badge.fury.io/py/escaping.svg)](https://badge.fury.io/py/escaping) 11 | [![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) 12 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 13 | 14 | 15 | If you've just confessed and you can't wait to sin again, try this package. It will help you [hide your mistakes](https://en.wikipedia.org/wiki/Error_hiding) and make your life more carefree :) Seriously, the library allows you to solve the problem of exception handling in a more adult way by providing: 16 | 17 | - 🛡️ A universal interface for the decorator and context manager. 18 | - 🛡️ Built-in logging. 19 | - 🛡️ Calling callbacks. 20 | 21 | 22 | ## Table of contents 23 | 24 | - [**Quick start**](#quick-start) 25 | - [**About**](#about) 26 | - [**Decorator mode**](#decorator-mode) 27 | - [**Context manager mode**](#context-manager-mode) 28 | - [**Logging**](#logging) 29 | - [**Callbacks**](#callbacks) 30 | - [**Baking rules**](#baking-rules) 31 | 32 | 33 | ## Quick start 34 | 35 | Install it: 36 | 37 | ```bash 38 | pip install escaping 39 | ``` 40 | 41 | And use: 42 | 43 | ```python 44 | import escape 45 | 46 | @escape 47 | def function(): 48 | raise ValueError 49 | 50 | function() # The exception is suppressed. 51 | ``` 52 | 53 | Read about other library features below. 54 | 55 | 56 | ## About 57 | 58 | This project is dedicated to the most important problem in programming - how do we need to handle errors? Here are some answers to this question that it gives: 59 | 60 | - This should be done in a standardized way. You can decide for yourself how errors will be handled, but with this project, any method you choose can easily become the standard. 61 | 62 | - Mistakes should not be hidden. Even if the exception is suppressed, you should be aware of it. 63 | 64 | An interesting solution that is proposed here is that you are provided with a single interface for error suppression, which can be used as a [context manager](#context-manager-mode) for any block of code, as well as as a [decorator](#decorator-mode) for ordinary, coroutine and generator functions. Wherever you need to suppress an error, you do it the same way, according to the same rules: 65 | 66 | ```python 67 | import escape 68 | 69 | @escape 70 | def function(): 71 | ... 72 | 73 | @escape 74 | async def function(): 75 | ... 76 | 77 | @escape 78 | def function(): 79 | yield something 80 | ... 81 | 82 | with escape: 83 | ... 84 | ``` 85 | 86 | The rules by which you want to suppress errors can be "[baked](#baking-rules)" into a special object so that you don't duplicate it in different parts of the code later. This means that you can come up with error suppression rules once, and then use them everywhere, without duplicating code, which is assumed when using ordinary `try-except` blocks. 87 | 88 | 89 | ## Decorator mode 90 | 91 | The `@escape` decorator suppresses exceptions in a wrapped function (including generator and coroutine ones), which are passed in parentheses. In this way, you can pass any number of exceptions, for example: 92 | 93 | ```python 94 | import asyncio 95 | import escape 96 | 97 | @escape(ValueError, ZeroDivisionError) 98 | def function(): 99 | raise ValueError('oh!') 100 | 101 | @escape(ValueError, ZeroDivisionError) 102 | async def async_function(): 103 | raise ZeroDivisionError('oh!') 104 | 105 | function() # Silence. 106 | asyncio.run(async_function()) # Silence. 107 | ``` 108 | 109 | If you use `@escape` with parentheses but do not pass any exception types, no exceptions will be suppressed: 110 | 111 | ```python 112 | @escape() 113 | def function(): 114 | raise ValueError('oh!') 115 | 116 | function() 117 | #> ValueError: oh! 118 | ``` 119 | 120 | If an exception occurred inside the function wrapped by the decorator, it will return the default value - `None`. You can specify your own default value: 121 | 122 | ```python 123 | @escape(ValueError, default='some value') 124 | def function(): 125 | raise ValueError 126 | 127 | assert function() == 'some value' # It's going to work. 128 | ``` 129 | 130 | Finally, you can use `@escape` as a decorator without parentheses. 131 | 132 | ```python 133 | @escape 134 | def function(): 135 | raise ValueError 136 | 137 | function() # Silence still. 138 | ``` 139 | 140 | In this mode, not all exceptions from the [hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy) are suppressed, but only those that can be expected in the user code. [`Exception`](https://docs.python.org/3/library/exceptions.html#Exception) and all its descendants are suppressed, as well as, starting with `Python 3.11`, [groups of exceptions](https://docs.python.org/3/library/exceptions.html#exception-groups). However, exceptions [`GeneratorExit`](https://docs.python.org/3/library/exceptions.html#GeneratorExit), [`KeyboardInterrupt`](https://docs.python.org/3/library/exceptions.html#KeyboardInterrupt) and [`SystemExit`](https://docs.python.org/3/library/exceptions.html#SystemExit) are not escaped in this mode. This is due to the fact that in most programs none of them is part of the semantics of the program, but is used exclusively for system needs. For example, if `KeyboardInterrupt` was blocked, you would not be able to stop your program using the `Control-C` keyboard shortcut. 141 | 142 | You can also use the same set of exceptions in parenthesis mode as without parentheses. To do this, use the [`Ellipsis`](https://docs.python.org/dev/library/constants.html#Ellipsis) (three dots): 143 | 144 | ```python 145 | @escape(...) 146 | def function_1(): 147 | raise ValueError 148 | 149 | @escape 150 | def function_2(): 151 | raise ValueError 152 | 153 | function_1() # These two functions are completely equivalent. 154 | function_2() # These two functions are completely equivalent. 155 | ``` 156 | 157 | `Ellipsis` can also be used in enumeration, along with other exceptions: 158 | 159 | ```python 160 | @escape(GeneratorExit, ...) 161 | ``` 162 | 163 | 164 | ## Context manager mode 165 | 166 | You can use `escape` as a context manager, which escapes exceptions in the code block wrapped by it. You can call it according to the same rules as the [decorator](#decorator-mode) - pass exceptions or ellipsis there. It also works almost the same way as [`contextlib.suppress`](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) from the standard library, but with a bit more opportunities. Some examples: 167 | 168 | ```python 169 | with escape(ValueError): 170 | raise ValueError 171 | 172 | with escape: 173 | raise ValueError 174 | 175 | with escape(...): 176 | raise ValueError 177 | ``` 178 | 179 | However, as you should understand, the default value cannot be specified in this case. If you try to specify a default value for the context manager, get ready to face an exception: 180 | 181 | ```python 182 | with escape(default='some value'): 183 | ... 184 | 185 | #> escape.errors.SetDefaultReturnValueForContextManagerError: You cannot set a default value for the context manager. This is only possible for the decorator. 186 | ``` 187 | 188 | 189 | ## Logging 190 | 191 | You can pass a logger object to the `escape`. In such case, if an exception is raised inside the context or the function wrapped by the decorator, it will be logged: 192 | 193 | ```python 194 | import logging 195 | import escape 196 | 197 | logging.basicConfig( 198 | level=logging.INFO, 199 | format="%(asctime)s [%(levelname)s] %(message)s", 200 | handlers=[ 201 | logging.StreamHandler(), 202 | ] 203 | ) 204 | 205 | logger = logging.getLogger('logger_name') 206 | 207 | with escape(..., logger=logger): 208 | 1/0 209 | 210 | # You will see a description of the error in the console. 211 | ``` 212 | 213 | It works in any mode: both in the case of the context manager and the decorator. 214 | 215 | By default only exceptions are logged. If the code block or function was executed without errors, the log will not be recorded. Also the log is recorded regardless of whether the exception was suppressed or not. However, depending on this, you will see different log messages to distinguish one situation from another. 216 | 217 | But! You can change the standard logging behavior. 218 | 219 | If you want the log to be recorded for any outcome, including the one where no errors occurred, specify the `success_logging=True` flag (messages will be recorded with the `info` level): 220 | 221 | ```python 222 | with escape(success_logging=True, logger=logger): 223 | pass 224 | #> The code block was executed successfully. 225 | ``` 226 | 227 | In addition, you can change the standard messages that you see in the logs. Keep in mind that this feature narrows down the variety of standard messages, which differ depending on where the error occurred (in a regular function, in a generator or asynchronous function, or perhaps in a block of code wrapped by a context manager), or whether the error was intercepted. You can define your own messages for only two types of situations: when the code was executed without exceptions, and when with an exception. 228 | 229 | Pass your message as `error_log_message` if you want to see it when an error occurred inside the code: 230 | 231 | ```python 232 | with escape(..., error_log_message='Oh my God!', logger=logger): 233 | raise ValueError 234 | #> Oh my God! 235 | ``` 236 | 237 | By analogy, pass `success_log_message` as a message if there are no errors in the code block (but don't forget to set `success_logging=True`!): 238 | 239 | ```python 240 | with escape(success_log_message='Good news, everyone!', success_logging=True, logger=logger): 241 | pass 242 | #> Good news, everyone! 243 | ``` 244 | 245 | You can also be content with the standard log message, but add your own comment to it. For this, use the `doc` argument: 246 | 247 | ```python 248 | with escape(success_logging=True, logger=logger, doc='Nothing is happening here!'): 249 | pass 250 | #> The code block (Nothing is happening here!) was executed successfully. 251 | ``` 252 | 253 | If the exception was suppressed inside the `escape`, the log will be recorded using the `exception` method - this means that the trace will be saved. Otherwise, the `error` method will be used - without saving the traceback, because otherwise, if you catch this exception somewhere else and pledge the traceback, there will be several duplicate tracebacks in your log file. 254 | 255 | 256 | ## Callbacks 257 | 258 | You can pass [callback](https://en.wikipedia.org/wiki/Callback_(computer_programming)) functions to `escape`, which will be automatically called when the wrapped code block or function has completed. 259 | 260 | A callback passed as `success_callback` will be called when the code is executed without errors: 261 | 262 | ```python 263 | with escape(success_callback=lambda: print('The code block ended without errors.')): 264 | ... 265 | ``` 266 | 267 | By analogy, if you pass `error_callback`, this function will be called when an exception is raised inside: 268 | 269 | ```python 270 | with escape(error_callback=lambda: print('Attention!')): 271 | ... 272 | ``` 273 | 274 | If you pass a callback as a `before` parameter, it'll be called before the code block anyway: 275 | 276 | ```python 277 | with escape(before=lambda: print('Something is going to happen now...')): 278 | ... 279 | ``` 280 | 281 | Notice, if an error occurs in this callback that will not be suppressed, the main code will not be executed - an exception will be raised before it starts executing. 282 | 283 | If an error occurs in one of the callbacks, the exception will be suppressed if it would have been suppressed if it had happened in a wrapped code block or function. You can see the corresponding log entry about this if you [pass the logger object](#logging) for registration. If the error inside the callback has been suppressed, it will not affect the logic that was wrapped by `escape` in any way. 284 | 285 | 286 | ## Baking rules 287 | 288 | You can set up an error escaping policy once and then reuse it in different situations. To do this, get a special object through the `bake` method: 289 | 290 | ```python 291 | escaper = escape.bake(ValueError) 292 | ``` 293 | 294 | Creating this object, you can pass all the same arguments as when using `escape` directly as a [decorator](#decorator-mode) or a [context manager](#context-manager-mode): exceptions, [callbacks](#callbacks), or a [logger](#logging). The object "remembers" these arguments until the moment you decide to use it: 295 | 296 | ```python 297 | with escaper: 298 | raise ValueError # It will be suppressed. 299 | ``` 300 | ```python 301 | @escaper 302 | def function(): 303 | raise ValueError # It will be suppressed too. 304 | 305 | function() 306 | ``` 307 | 308 | If necessary, you can combine "baked" arguments and arguments that are passed on demand (executing the sample code requires pre-installation of the [`emptylog`](https://github.com/pomponchik/emptylog?tab=readme-ov-file#printing-logger) library): 309 | 310 | ```python 311 | import escape 312 | from emptylog import PrintingLogger 313 | 314 | escaper = escape.bake(logger=PrintingLogger()) 315 | 316 | @escaper(ValueError) 317 | def function(): 318 | raise ValueError # It will be suppressed too. 319 | 320 | function() 321 | #> 2024-09-06 14:45:19.606267 | EXCEPTION | When executing function "function", the exception "ValueError" was suppressed. 322 | ``` 323 | 324 | In this way, you can add additional exceptions that need to be suppressed - they will be added to the general list of suppressed ones. In addition, you can override some of the named arguments that are "baked" into the object on demand - in this case, the argument that was passed later will be used. 325 | 326 | Arguments baking is an extremely powerful tool, useful for large programs. It allows you to get rid of multiple duplications of code that are often encountered during error handling. In addition, with its help, you can describe the error handling policy centrally, in one place for the entire program, which makes maintaining or changing the program a much easier task. 327 | -------------------------------------------------------------------------------- /docs/assets/logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_1.png -------------------------------------------------------------------------------- /docs/assets/logo_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_10.png -------------------------------------------------------------------------------- /docs/assets/logo_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_11.png -------------------------------------------------------------------------------- /docs/assets/logo_12.svg: -------------------------------------------------------------------------------- 1 | 2 | 91 | -------------------------------------------------------------------------------- /docs/assets/logo_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_13.png -------------------------------------------------------------------------------- /docs/assets/logo_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_14.png -------------------------------------------------------------------------------- /docs/assets/logo_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_15.png -------------------------------------------------------------------------------- /docs/assets/logo_16.svg: -------------------------------------------------------------------------------- 1 | 2 | 91 | -------------------------------------------------------------------------------- /docs/assets/logo_2.svg: -------------------------------------------------------------------------------- 1 | 3 | 111 | 247 | 257 | 301 | -------------------------------------------------------------------------------- /docs/assets/logo_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_3.png -------------------------------------------------------------------------------- /docs/assets/logo_4.svg: -------------------------------------------------------------------------------- 1 | 3 | 111 | 247 | 257 | 301 | -------------------------------------------------------------------------------- /docs/assets/logo_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/docs/assets/logo_5.png -------------------------------------------------------------------------------- /docs/assets/logo_6.svg: -------------------------------------------------------------------------------- 1 | 3 | 102 | 235 | 278 | -------------------------------------------------------------------------------- /docs/assets/logo_7.svg: -------------------------------------------------------------------------------- 1 | 2 | 64 | -------------------------------------------------------------------------------- /docs/assets/logo_8.svg: -------------------------------------------------------------------------------- 1 | 2 | EXCEPTIONESCAPING 101 | -------------------------------------------------------------------------------- /docs/assets/logo_9.svg: -------------------------------------------------------------------------------- 1 | 2 | 97 | -------------------------------------------------------------------------------- /escape/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from escape.proxy_module import ProxyModule as ProxyModule 4 | 5 | 6 | sys.modules[__name__].__class__ = ProxyModule 7 | -------------------------------------------------------------------------------- /escape/baked_escaper.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Type, Union, Callable, Optional, Any 2 | from types import TracebackType 3 | 4 | try: 5 | from types import EllipsisType # type: ignore[attr-defined, unused-ignore] 6 | except ImportError: # pragma: no cover 7 | EllipsisType = type(...) # type: ignore[misc, unused-ignore] # pragma: no cover 8 | 9 | from inspect import isclass 10 | from escape.wrapper import Wrapper 11 | 12 | 13 | class BakedEscaper: 14 | def __init__(self, escaper: 'ProxyModule') -> None: # type: ignore[name-defined] # noqa: F821 15 | self.escaper = escaper 16 | 17 | self.args: List[Union[Callable[..., Any], Type[BaseException], EllipsisType]] = [] 18 | self.kwargs: Dict[str, Any] = {} 19 | 20 | self.wrapper_for_simple_contexts: Wrapper = self.escaper(*(self.args), **(self.kwargs)) 21 | 22 | def __call__(self, *args: Union[Callable[..., Any], Type[BaseException], EllipsisType], **kwargs: Any) -> Union[Callable[..., Any], Callable[[Callable[..., Any]], Callable[..., Any]]]: 23 | copy_args = self.args.copy() 24 | copy_args.extend(args) 25 | copy_kwargs = self.kwargs.copy() 26 | copy_kwargs.update(kwargs) 27 | 28 | if self.escaper.are_it_exceptions(args): 29 | return self.escaper(*(copy_args), **(copy_kwargs)) # type: ignore[no-any-return] 30 | 31 | elif self.escaper.are_it_function(args): 32 | return self.escaper(*(self.args), **(copy_kwargs))(*args) # type: ignore[no-any-return] 33 | 34 | else: 35 | raise ValueError('You are using the escaper incorrectly.') 36 | 37 | def notify_arguments(self, *args: Union[Callable[..., Any], Type[BaseException], EllipsisType], **kwargs: Any) -> None: 38 | for argument in args: 39 | if not (isclass(argument) and issubclass(argument, BaseException)) and not isinstance(argument, EllipsisType): 40 | raise ValueError('You are using the baked escaper object for the wrong purpose.') 41 | self.args.append(argument) 42 | 43 | for name, argument in kwargs.items(): 44 | self.kwargs[name] = argument 45 | 46 | self.wrapper_for_simple_contexts = self.escaper(*(self.args), **(self.kwargs)) 47 | 48 | def __enter__(self) -> 'ProxyModule': # type: ignore[name-defined] # noqa: F821 49 | return self.wrapper_for_simple_contexts.__enter__() 50 | 51 | def __exit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]) -> bool: 52 | return self.wrapper_for_simple_contexts.__exit__(exception_type, exception_value, traceback) 53 | -------------------------------------------------------------------------------- /escape/errors.py: -------------------------------------------------------------------------------- 1 | class SetDefaultReturnValueForContextManagerError(Exception): 2 | pass 3 | 4 | class SetDefaultReturnValueForGeneratorFunctionError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /escape/proxy_module.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Type, Tuple, Callable, Union, Optional, Any 3 | from types import TracebackType, ModuleType 4 | from inspect import isclass 5 | from itertools import chain 6 | 7 | try: 8 | from types import EllipsisType # type: ignore[attr-defined, unused-ignore] 9 | except ImportError: # pragma: no cover 10 | EllipsisType = type(...) # type: ignore[misc, unused-ignore] # pragma: no cover 11 | 12 | from emptylog import LoggerProtocol, EmptyLogger 13 | 14 | from escape.wrapper import Wrapper 15 | from escape.baked_escaper import BakedEscaper 16 | 17 | 18 | if sys.version_info < (3, 11): 19 | muted_by_default_exceptions: Tuple[Type[BaseException], ...] = (Exception,) # pragma: no cover 20 | else: 21 | muted_by_default_exceptions = (Exception, BaseExceptionGroup) # pragma: no cover # noqa: F821 22 | 23 | class ProxyModule(sys.modules[__name__].__class__): # type: ignore[misc] 24 | def __call__(self, *args: Union[Callable[..., Any], Type[BaseException], EllipsisType], default: Any = None, logger: LoggerProtocol = EmptyLogger(), success_callback: Callable[[], Any] = lambda: None, error_callback: Callable[[], Any] = lambda: None, before: Callable[[], Any] = lambda: None, error_log_message: Optional[str] = None, success_log_message: Optional[str] = None, success_logging: bool = False, doc: Optional[str] = None) -> Union[Callable[..., Any], Callable[[Callable[..., Any]], Callable[..., Any]]]: 25 | """ 26 | https://docs.python.org/3/library/exceptions.html#exception-hierarchy 27 | """ 28 | if self.are_it_function(args): 29 | exceptions: Tuple[Type[BaseException], ...] = muted_by_default_exceptions 30 | else: 31 | if self.is_there_ellipsis(args): 32 | exceptions = tuple(chain((x for x in args if x is not Ellipsis), muted_by_default_exceptions)) # type: ignore[misc] 33 | else: 34 | exceptions = args # type: ignore[assignment] 35 | 36 | wrapper_of_wrappers = Wrapper(default, exceptions, logger, success_callback, before, error_log_message, success_logging, success_log_message, error_callback, doc) 37 | 38 | if self.are_it_exceptions(args): 39 | return wrapper_of_wrappers 40 | 41 | elif self.are_it_function(args): 42 | return wrapper_of_wrappers(args[0]) # type: ignore[arg-type, unused-ignore] 43 | 44 | else: 45 | raise ValueError('You are using the decorator for the wrong purpose.') 46 | 47 | def __enter__(self) -> 'ProxyModule': 48 | return self 49 | 50 | def __exit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]) -> bool: 51 | if exception_type is not None: 52 | for muted_exception_type in muted_by_default_exceptions: 53 | if issubclass(exception_type, muted_exception_type): 54 | return True 55 | 56 | return False 57 | 58 | @staticmethod 59 | def is_there_ellipsis(args: Tuple[Union[Type[BaseException], Callable[..., Any], EllipsisType], ...]) -> bool: 60 | return any(x is Ellipsis for x in args) 61 | 62 | @staticmethod 63 | def are_it_exceptions(args: Tuple[Union[Type[BaseException], Callable[..., Any], EllipsisType], ...]) -> bool: 64 | return all((x is Ellipsis) or (isclass(x) and issubclass(x, BaseException)) for x in args) 65 | 66 | @staticmethod 67 | def are_it_function(args: Tuple[Union[Type[BaseException], Callable[..., Any], EllipsisType], ...]) -> bool: 68 | return len(args) == 1 and callable(args[0]) and not (isclass(args[0]) and issubclass(args[0], BaseException)) 69 | 70 | def bake(self, *args: Union[Callable[..., Any], Type[BaseException], EllipsisType], default: Any = None, logger: LoggerProtocol = EmptyLogger(), success_callback: Callable[[], Any] = lambda: None, error_callback: Callable[[], Any] = lambda: None, before: Callable[[], Any] = lambda: None, error_log_message: Optional[str] = None, success_log_message: Optional[str] = None, success_logging: bool = False, doc: Optional[str] = None) -> Callable[..., Union[Callable[..., Any], Callable[[Callable[..., Any]], Callable[..., Any]]]]: 71 | escaper = BakedEscaper(self) 72 | escaper.notify_arguments( 73 | *args, 74 | default=default, 75 | logger=logger, 76 | success_callback=success_callback, 77 | error_callback=error_callback, 78 | before=before, 79 | error_log_message=error_log_message, 80 | success_log_message=success_log_message, 81 | success_logging=success_logging, 82 | doc=doc, 83 | ) 84 | return escaper 85 | 86 | @property 87 | def escape(self) -> ModuleType: 88 | return self 89 | -------------------------------------------------------------------------------- /escape/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/escape/py.typed -------------------------------------------------------------------------------- /escape/wrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Callable, Tuple, Optional, Any 2 | from inspect import iscoroutinefunction, isgeneratorfunction 3 | from functools import wraps 4 | from types import TracebackType 5 | 6 | from emptylog import LoggerProtocol 7 | 8 | from escape.errors import SetDefaultReturnValueForContextManagerError, SetDefaultReturnValueForGeneratorFunctionError 9 | 10 | 11 | class Wrapper: 12 | def __init__(self, default: Any, exceptions: Tuple[Type[BaseException], ...], logger: LoggerProtocol, success_callback: Callable[[], Any], before: Callable[[], Any], error_log_message: Optional[str], success_logging: bool, success_log_message: Optional[str], error_callback: Callable[[], Any], doc: Optional[str] = None) -> None: 13 | self.default: Any = default 14 | self.exceptions: Tuple[Type[BaseException], ...] = exceptions 15 | self.logger: LoggerProtocol = logger 16 | self.success_callback: Callable[[], Any] = success_callback 17 | self.error_callback: Callable[[], Any] = error_callback 18 | self.before: Callable[[], Any] = before 19 | self.error_log_message: Optional[str] = error_log_message 20 | self.success_log_message: Optional[str] = success_log_message 21 | self.success_logging: bool = success_logging 22 | self.doc: Optional[str] = doc 23 | self.wrapped_doc = '' if self.doc is None else f' ({self.doc})' 24 | 25 | def __call__(self, function: Callable[..., Any]) -> Callable[..., Any]: 26 | @wraps(function) 27 | def wrapper(*args: Any, **kwargs: Any) -> Any: 28 | self.run_callback(self.before) 29 | 30 | result = None 31 | success_flag = False 32 | 33 | try: 34 | result = function(*args, **kwargs) 35 | success_flag = True 36 | 37 | except self.exceptions as e: 38 | if self.error_log_message is None: 39 | exception_massage = '' if not str(e) else f' ("{e}")' 40 | self.logger.exception(f'When executing function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was suppressed.') 41 | else: 42 | self.logger.exception(self.error_log_message) 43 | result = self.default 44 | 45 | except BaseException as e: 46 | if self.error_log_message is None: 47 | exception_massage = '' if not str(e) else f' ("{e}")' 48 | self.logger.error(f'When executing function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was not suppressed.') 49 | else: 50 | self.logger.error(self.error_log_message) 51 | self.run_callback(self.error_callback) 52 | raise e 53 | 54 | if success_flag: 55 | if self.success_logging: 56 | if self.success_log_message is None: 57 | self.logger.info(f'The function "{function.__name__}"{self.wrapped_doc} completed successfully.') 58 | else: 59 | self.logger.info(self.success_log_message) 60 | 61 | self.run_callback(self.success_callback) 62 | 63 | else: 64 | self.run_callback(self.error_callback) 65 | 66 | return result 67 | 68 | 69 | @wraps(function) 70 | async def async_wrapper(*args: Any, **kwargs: Any) -> Any: 71 | self.run_callback(self.before) 72 | 73 | result = None 74 | success_flag = False 75 | 76 | try: 77 | result = await function(*args, **kwargs) 78 | success_flag = True 79 | 80 | except self.exceptions as e: 81 | if self.error_log_message is None: 82 | exception_massage = '' if not str(e) else f' ("{e}")' 83 | self.logger.exception(f'When executing coroutine function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was suppressed.') 84 | else: 85 | self.logger.exception(self.error_log_message) 86 | result = self.default 87 | 88 | except BaseException as e: 89 | if self.error_log_message is None: 90 | exception_massage = '' if not str(e) else f' ("{e}")' 91 | self.logger.error(f'When executing coroutine function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was not suppressed.') 92 | else: 93 | self.logger.error(self.error_log_message) 94 | self.run_callback(self.error_callback) 95 | raise e 96 | 97 | if success_flag: 98 | if self.success_logging: 99 | if self.success_log_message is None: 100 | self.logger.info(f'The coroutine function "{function.__name__}"{self.wrapped_doc} completed successfully.') 101 | else: 102 | self.logger.info(self.success_log_message) 103 | 104 | self.run_callback(self.success_callback) 105 | 106 | else: 107 | self.run_callback(self.error_callback) 108 | 109 | return result 110 | 111 | @wraps(function) 112 | def generator_wrapper(*args: Any, **kwargs: Any) -> Any: 113 | self.run_callback(self.before) 114 | 115 | result = None 116 | success_flag = False 117 | 118 | try: 119 | yield from function(*args, **kwargs) 120 | success_flag = True 121 | 122 | except self.exceptions as e: 123 | if self.error_log_message is None: 124 | exception_massage = '' if not str(e) else f' ("{e}")' 125 | self.logger.exception(f'When executing generator function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was suppressed.') 126 | else: 127 | self.logger.exception(self.error_log_message) 128 | result = self.default 129 | 130 | except BaseException as e: 131 | if self.error_log_message is None: 132 | exception_massage = '' if not str(e) else f' ("{e}")' 133 | self.logger.error(f'When executing generator function "{function.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was not suppressed.') 134 | else: 135 | self.logger.error(self.error_log_message) 136 | self.run_callback(self.error_callback) 137 | raise e 138 | 139 | if success_flag: 140 | if self.success_logging: 141 | if self.success_log_message is None: 142 | self.logger.info(f'The generator function "{function.__name__}"{self.wrapped_doc} completed successfully.') 143 | else: 144 | self.logger.info(self.success_log_message) 145 | 146 | self.run_callback(self.success_callback) 147 | 148 | else: 149 | self.run_callback(self.error_callback) 150 | 151 | return result 152 | 153 | 154 | if iscoroutinefunction(function): 155 | return async_wrapper 156 | elif isgeneratorfunction(function): 157 | if self.default is not None: 158 | raise SetDefaultReturnValueForGeneratorFunctionError('You cannot set the default return value for the generator function. This is only possible for normal and coroutine functions.') 159 | return generator_wrapper 160 | return wrapper 161 | 162 | def __enter__(self) -> 'Wrapper': 163 | if self.default is not None: 164 | raise SetDefaultReturnValueForContextManagerError('You cannot set a default value for the context manager. This is only possible for the decorator.') 165 | 166 | self.run_callback(self.before) 167 | 168 | return self 169 | 170 | def __exit__(self, exception_type: Optional[Type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType]) -> bool: 171 | result = False 172 | 173 | if exception_type is not None: 174 | exception_massage = '' if not str(exception_value) else f' ("{exception_value}")' 175 | 176 | for muted_exception_type in self.exceptions: 177 | if issubclass(exception_type, muted_exception_type): 178 | if self.error_log_message is None: 179 | self.logger.exception(f'The "{exception_type.__name__}"{exception_massage} exception was suppressed inside the context{self.wrapped_doc}.') 180 | else: 181 | self.logger.exception(self.error_log_message) 182 | result = True 183 | 184 | if not result: 185 | if self.error_log_message is None: 186 | self.logger.error(f'The "{exception_type.__name__}"{exception_massage} exception was not suppressed inside the context{self.wrapped_doc}.') 187 | else: 188 | self.logger.error(self.error_log_message) 189 | 190 | self.run_callback(self.error_callback) 191 | 192 | else: 193 | if self.success_logging: 194 | if self.success_log_message is None: 195 | self.logger.info(f'The code block{self.wrapped_doc} was executed successfully.') 196 | else: 197 | self.logger.info(self.success_log_message) 198 | 199 | self.run_callback(self.success_callback) 200 | 201 | return result 202 | 203 | def run_callback(self, callback: Callable[[], Any]) -> None: 204 | try: 205 | callback() 206 | 207 | except self.exceptions as e: 208 | exception_massage = '' if not str(e) else f' ("{e}")' 209 | self.logger.exception(f'When executing the callback "{callback.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was suppressed.') 210 | 211 | except BaseException as e: 212 | exception_massage = '' if not str(e) else f' ("{e}")' 213 | self.logger.error(f'When executing the callback "{callback.__name__}"{self.wrapped_doc}, the exception "{type(e).__name__}"{exception_massage} was not suppressed.') 214 | raise e 215 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools==68.0.0'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'escaping' 7 | version = '0.0.15' 8 | authors = [ 9 | { name='Evgeniy Blinov', email='zheni-b@yandex.ru' }, 10 | ] 11 | description = 'Try not to stand out' 12 | readme = 'README.md' 13 | requires-python = '>=3.8' 14 | dependencies = [ 15 | 'emptylog>=0.0.9', 16 | ] 17 | classifiers = [ 18 | 'Operating System :: OS Independent', 19 | 'Operating System :: MacOS :: MacOS X', 20 | 'Operating System :: Microsoft :: Windows', 21 | 'Operating System :: POSIX', 22 | 'Operating System :: POSIX :: Linux', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'Programming Language :: Python :: 3.10', 26 | 'Programming Language :: Python :: 3.11', 27 | 'Programming Language :: Python :: 3.12', 28 | 'Programming Language :: Python :: 3.13', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Intended Audience :: Developers', 31 | 'Topic :: Software Development :: Libraries', 32 | 'Typing :: Typed', 33 | ] 34 | keywords = [ 35 | 'exception handling', 36 | 'exception logging', 37 | 'containerized code', 38 | 'safe code', 39 | 'callbacks', 40 | ] 41 | 42 | [tool.setuptools.package-data] 43 | "escape" = ["py.typed"] 44 | 45 | [tool.mutmut] 46 | paths_to_mutate="escape" 47 | runner="pytest" 48 | 49 | [project.urls] 50 | 'Source' = 'https://github.com/pomponchik/escaping' 51 | 'Tracker' = 'https://github.com/pomponchik/escaping/issues' 52 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest==7.4.3 2 | coverage==7.6.1 3 | build==1.2.2.post1 4 | twine==6.1.0 5 | mypy==1.14.1 6 | ruff==0.9.9 7 | mutmut==3.2.3 8 | full_match==0.0.2 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/tests/__init__.py -------------------------------------------------------------------------------- /tests/documentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/tests/documentation/__init__.py -------------------------------------------------------------------------------- /tests/documentation/test_readme.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import redirect_stdout 3 | from io import StringIO 4 | 5 | import pytest 6 | import full_match 7 | from emptylog import MemoryLogger 8 | 9 | import escape 10 | from escape.baked_escaper import BakedEscaper 11 | 12 | 13 | def test_example_quick_start(): 14 | @escape 15 | def function(): 16 | raise ValueError 17 | 18 | function() # The exception is suppressed. 19 | 20 | 21 | def test_example_decorator_mode_passing_exceptions(): 22 | @escape(ValueError, ZeroDivisionError) 23 | def function(): 24 | raise ValueError('oh!') 25 | 26 | @escape(ValueError, ZeroDivisionError) 27 | async def async_function(): 28 | raise ZeroDivisionError('oh!') 29 | 30 | function() # Silence. 31 | asyncio.run(async_function()) # Silence. 32 | 33 | 34 | def test_example_decorator_mode_not_suppressing_exception(): 35 | @escape() 36 | def function(): 37 | raise ValueError('oh!') 38 | 39 | with pytest.raises(ValueError, match='oh!'): 40 | function() 41 | # > ValueError: oh! 42 | 43 | 44 | def test_example_decorator_mode_default_value(): 45 | @escape(ValueError, default='some value') 46 | def function(): 47 | raise ValueError 48 | 49 | assert function() == 'some value' # It's going to work. 50 | 51 | 52 | def test_example_decorator_mode_with_empty_breackets(): 53 | @escape 54 | def function(): 55 | raise ValueError 56 | 57 | function() # Silence still. 58 | 59 | 60 | def test_example_decorator_more_equivalents(): 61 | @escape(...) 62 | def function_1(): 63 | raise ValueError 64 | 65 | @escape 66 | def function_2(): 67 | raise ValueError 68 | 69 | function_1() # These two functions are completely equivalent. 70 | function_2() # These two functions are completely equivalent. 71 | 72 | 73 | def test_example_decorator_mode_exception_and_ellipsis_separated_by_comma(): 74 | @escape(GeneratorExit, ...) 75 | def function(): 76 | raise GeneratorExit 77 | 78 | function() 79 | 80 | 81 | def test_example_context_manager_basic_examples(): 82 | with escape(ValueError): 83 | raise ValueError 84 | 85 | with escape: 86 | raise ValueError 87 | 88 | with escape(...): 89 | raise ValueError 90 | 91 | 92 | def test_example_context_manager_attempt_to_set_default_value(): 93 | with pytest.raises(escape.errors.SetDefaultReturnValueForContextManagerError, match=full_match('You cannot set a default value for the context manager. This is only possible for the decorator.')): 94 | with escape(default='some value'): 95 | ... 96 | 97 | 98 | def test_example_callbacks_simple_success_callback(): 99 | buffer = StringIO() 100 | 101 | with redirect_stdout(buffer): 102 | with escape(success_callback=lambda: print('The code block ended without errors.')): 103 | pass 104 | 105 | assert buffer.getvalue() == 'The code block ended without errors.\n' 106 | 107 | 108 | def test_example_success_logging_on_in_context_manager(): 109 | logger = MemoryLogger() 110 | 111 | with escape(success_logging=True, logger=logger): 112 | pass 113 | 114 | assert len(logger.data) == 1 115 | assert logger.data.info[0].message == 'The code block was executed successfully.' 116 | 117 | 118 | def test_example_own_message_for_errors(): 119 | logger = MemoryLogger() 120 | 121 | with escape(..., error_log_message='Oh my God!', logger=logger): 122 | raise ValueError 123 | 124 | assert len(logger.data) == 1 125 | assert logger.data.exception[0].message == 'Oh my God!' 126 | 127 | 128 | def test_example_own_message_for_success(): 129 | logger = MemoryLogger() 130 | 131 | with escape(success_log_message='Good news, everyone!', success_logging=True, logger=logger): 132 | pass 133 | 134 | assert len(logger.data) == 1 135 | assert logger.data.info[0].message == 'Good news, everyone!' 136 | 137 | 138 | def test_get_escaper(): 139 | escaper = escape.bake(ValueError) 140 | 141 | assert isinstance(escaper, BakedEscaper) 142 | -------------------------------------------------------------------------------- /tests/units/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomponchik/escaping/1f3376c91f97359c82d233b8d073d0cb37f58d5e/tests/units/__init__.py -------------------------------------------------------------------------------- /tests/units/test_baked_escaper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import full_match 3 | 4 | import escape 5 | from escape.baked_escaper import BakedEscaper 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'wrong_argument', 10 | [ 11 | 1, 12 | None, 13 | 'kek', 14 | ], 15 | ) 16 | def test_call_baked_escaper_with_wrong_positional_argument(wrong_argument): 17 | escaper = BakedEscaper(escape) 18 | with pytest.raises(ValueError, match=full_match('You are using the escaper incorrectly.')): 19 | escaper(wrong_argument) 20 | 21 | 22 | def test_add_exceptions_to_positional_arguments(): 23 | escaper = BakedEscaper(escape) 24 | 25 | assert escaper.args == [] 26 | 27 | escaper.notify_arguments(ValueError) 28 | 29 | assert escaper.args == [ValueError] 30 | 31 | escaper.notify_arguments(ZeroDivisionError) 32 | 33 | assert escaper.args == [ValueError, ZeroDivisionError] 34 | 35 | escaper.notify_arguments(...) 36 | 37 | assert escaper.args == [ValueError, ZeroDivisionError, ...] 38 | 39 | 40 | @pytest.mark.parametrize( 41 | 'wrong_argument', 42 | [ 43 | lambda x: None, 44 | lambda: None, 45 | print, 46 | 1, 47 | None, 48 | 'kek', 49 | ], 50 | ) 51 | def test_call_notify_arguments_with_wrong_positional_argument(wrong_argument): 52 | escaper = BakedEscaper(escape) 53 | 54 | with pytest.raises(ValueError, match=full_match('You are using the baked escaper object for the wrong purpose.')): 55 | escaper.notify_arguments(wrong_argument) 56 | 57 | 58 | def test_empty_baker_contains_empty_collections(): 59 | escaper_1 = BakedEscaper(escape) 60 | escaper_2 = BakedEscaper(escape) 61 | 62 | assert escaper_1.args == [] 63 | assert escaper_2.args == [] 64 | 65 | assert escaper_1.kwargs == {} 66 | assert escaper_2.kwargs == {} 67 | 68 | assert escaper_1.args is not escaper_2.args 69 | assert escaper_1.kwargs is not escaper_2.kwargs 70 | --------------------------------------------------------------------------------