├── tests ├── __init__.py ├── test_utility.py ├── test_error_handling.py ├── test_oo.py └── test_benchmark.py ├── static └── paprika.jpg ├── paprika ├── __init__.py ├── error_handling.py ├── utility.py ├── oo.py └── benchmark.py ├── .pre-commit-config.yaml ├── pyproject.toml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── workflow.yml │ └── unittest.yml ├── LICENSE ├── .gitignore ├── CODE_OF_CONDUCT.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/paprika.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayanht/paprika/HEAD/static/paprika.jpg -------------------------------------------------------------------------------- /paprika/__init__.py: -------------------------------------------------------------------------------- 1 | from paprika.benchmark import * 2 | from paprika.oo import * 3 | from paprika.utility import * 4 | from paprika.error_handling import * 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 20.8b1 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "paprika" 3 | version = "1.3.0" 4 | description = "Paprika is a python library that reduces boilerplate. Heavily inspired by Project Lombok." 5 | authors = ["Rayan Hatout "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/rayanht/paprika" 9 | repository = "https://github.com/rayanht/paprika" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.8" 13 | tabulate = "^0.8.9" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^5.2" 17 | hypothesis = "^6.3.3" 18 | coverage = "^5.4" 19 | pre-commit = "^2.10.1" 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Test and collect coverage 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | env: 7 | OS: ubuntu-latest 8 | PYTHON: '3.9' 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: ‘2’ 13 | 14 | - name: Setup Python 15 | uses: actions/setup-python@master 16 | with: 17 | python-version: 3.7 18 | - name: Generate Report 19 | run: | 20 | pip install tabulate 21 | pip install coverage 22 | pip install hypothesis 23 | coverage run -m unittest 24 | - name: Upload Coverage to Codecov 25 | uses: codecov/codecov-action@v1 26 | with: 27 | verbose: true 28 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | on: [push, pull_request] 3 | jobs: 4 | unittest: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [macos-latest, windows-latest, ubuntu-20.04] 9 | python-version: ["3.8", "3.9"] 10 | steps: 11 | - name: Setup Python 12 | uses: actions/setup-python@master 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - uses: actions/checkout@v2 16 | - name: Setup Poetry 17 | uses: abatilo/actions-poetry@v2.1.0 18 | with: 19 | poetry-version: 1.1.4 20 | - name: Install dependencies 21 | run: poetry install 22 | - name: Run unit tests 23 | run: poetry run python -m unittest 24 | -------------------------------------------------------------------------------- /paprika/error_handling.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import traceback 3 | 4 | 5 | def silent_catch(_func=None, *, exception=None): 6 | return catch(_func=_func, exception=exception, silent=True) 7 | 8 | 9 | def catch(_func=None, *, exception=None, handler=None, silent=False): 10 | if not exception: 11 | exception = Exception 12 | if type(exception) == list: 13 | exception = tuple(exception) 14 | 15 | def decorator_catch(func): 16 | @functools.wraps(func) 17 | def wrapper_catch(*args, **kwargs): 18 | try: 19 | return func(*args, **kwargs) 20 | except exception as e: 21 | if not silent: 22 | if not handler: 23 | traceback.print_exc() 24 | else: 25 | handler(e) 26 | 27 | return wrapper_catch 28 | 29 | if _func is None: 30 | return decorator_catch 31 | else: 32 | return decorator_catch(_func) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /tests/test_utility.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | from paprika import sleep_after, sleep_before, repeat 5 | 6 | 7 | class UtilityTestCases(unittest.TestCase): 8 | # We cannot be too sure that sleeping exceeds the time, so give it 10ms leeway 9 | SLEEP_TOLERANCE = 0.01 10 | 11 | def test_sleep_after(self): 12 | start = time.perf_counter() 13 | 14 | @sleep_after(duration=2) 15 | def f(): 16 | self.assertLess(time.perf_counter() - start, 0.5) 17 | 18 | f() 19 | self.assertGreaterEqual(time.perf_counter() - start, 2.0 - self.SLEEP_TOLERANCE) 20 | 21 | def test_sleep_before(self): 22 | start = time.perf_counter() 23 | 24 | @sleep_before(duration=2) 25 | def f(): 26 | self.assertGreaterEqual(time.perf_counter() - start, 1.5) 27 | 28 | f() 29 | 30 | def test_repeat(self): 31 | cnt = [0] 32 | 33 | @repeat(n=5) 34 | def f(counter): 35 | counter[0] += 1 36 | 37 | f(cnt) 38 | 39 | self.assertEqual(cnt, [5]) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rayan Hatout 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 | -------------------------------------------------------------------------------- /paprika/utility.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import time 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | _DEFAULT_POOL = ThreadPoolExecutor() 6 | 7 | 8 | def threaded(f, executor=None): 9 | @functools.wraps(f) 10 | def wrapper_threaded(*args, **kwargs): 11 | return (executor or _DEFAULT_POOL).submit(f, *args, **kwargs) 12 | 13 | return wrapper_threaded 14 | 15 | 16 | def repeat(n): 17 | def decorator_repeat(func): 18 | @functools.wraps(func) 19 | def wrapper_repeat(*args, **kwargs): 20 | for _ in range(n): 21 | value = func(*args, **kwargs) 22 | return value 23 | 24 | return wrapper_repeat 25 | 26 | return decorator_repeat 27 | 28 | 29 | def sleep_after(duration): 30 | def decorator_sleep(func): 31 | @functools.wraps(func) 32 | def wrapper_sleep(*args, **kwargs): 33 | ret = func(*args, **kwargs) 34 | time.sleep(duration) 35 | return ret 36 | 37 | return wrapper_sleep 38 | 39 | return decorator_sleep 40 | 41 | 42 | def sleep_before(duration): 43 | def decorator_sleep(func): 44 | @functools.wraps(func) 45 | def wrapper_sleep(*args, **kwargs): 46 | time.sleep(duration) 47 | return func(*args, **kwargs) 48 | 49 | return wrapper_sleep 50 | 51 | return decorator_sleep 52 | -------------------------------------------------------------------------------- /tests/test_error_handling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from paprika import catch, silent_catch 4 | 5 | 6 | class ErrorHandlingTestCases(unittest.TestCase): 7 | def test_catch_catches(self): 8 | @catch(exception=BlockingIOError) 9 | def f(): 10 | raise BlockingIOError 11 | 12 | f() 13 | self.assertTrue(True) 14 | 15 | def test_catch_doesnt_catch_unspecified(self): 16 | @catch(exception=BlockingIOError) 17 | def f(): 18 | raise ValueError 19 | 20 | self.assertRaises(ValueError, f) 21 | 22 | def test_catch_uses_handler(self): 23 | def switcheroo_handler(e): 24 | raise ConnectionError from e 25 | 26 | @catch(exception=ValueError, handler=switcheroo_handler) 27 | def f(): 28 | raise ValueError 29 | 30 | self.assertRaises(ConnectionError, f) 31 | 32 | def test_silent_catch_catches(self): 33 | @silent_catch(exception=BlockingIOError) 34 | def f(): 35 | raise BlockingIOError 36 | 37 | f() 38 | self.assertTrue(True) 39 | 40 | def test_catch_catches_multiple(self): 41 | @catch(exception=[FileExistsError, IndexError, IndentationError]) 42 | def f(): 43 | raise IndexError 44 | 45 | f() 46 | self.assertTrue(True) 47 | 48 | def test_silent_catch_catches_multiple(self): 49 | @catch(exception=[FileExistsError, IndexError, IndentationError]) 50 | def f(): 51 | raise IndentationError 52 | 53 | f() 54 | self.assertTrue(True) 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/test_oo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import tempfile 4 | from typing import Any 5 | from hypothesis import given, settings, strategies as st 6 | from paprika import data, NonNull, singleton, pickled 7 | 8 | 9 | class OOTestCases(unittest.TestCase): 10 | @data 11 | class TestClass: 12 | field1: NonNull[int] 13 | field2: str 14 | 15 | @singleton 16 | class TestSingleton: 17 | field1: str 18 | 19 | def test_nonnull(self): 20 | self.assertRaises( 21 | ValueError, self.TestClass, None, {"field1": None, "field2": "test"} 22 | ) 23 | 24 | def test_to_string(self): 25 | self.assertEqual( 26 | str(self.TestClass(field1=42, field2="test")), 27 | "TestClass@[field1=42, field2=test]", 28 | ) 29 | 30 | def test_eq(self): 31 | t1 = self.TestClass(field1=42, field2="test") 32 | t2 = self.TestClass(field1=42, field2="test") 33 | self.assertEqual(t1, t2) 34 | 35 | def test_hash(self): 36 | t1 = self.TestClass(field1=42, field2="test") 37 | t2 = self.TestClass(field1=42, field2="test") 38 | self.assertEqual(hash(t1), hash(t2)) 39 | 40 | def test_singleton(self): 41 | s1 = self.TestSingleton(field1="test") 42 | s2 = self.TestSingleton() 43 | self.assertEqual(s1, s2) 44 | 45 | @data 46 | @pickled 47 | class TestPickledClass: 48 | field1: Any 49 | 50 | @given(st.binary(min_size=100)) 51 | @settings(max_examples=10) 52 | def test_pickled(self, b): 53 | with tempfile.TemporaryDirectory() as tmp_dir: 54 | tmp_file_path = os.path.join(tmp_dir, "data.pickle") 55 | 56 | c = self.TestPickledClass(b) 57 | c.__dump__(tmp_file_path) 58 | unpickled = self.TestPickledClass.__load__(tmp_file_path) 59 | 60 | self.assertEqual(unpickled, c) 61 | 62 | @data 63 | @pickled(protocol=3) 64 | class TestPickledClassProtocol3: 65 | field1: Any 66 | 67 | @given(st.binary(min_size=100)) 68 | @settings(max_examples=10) 69 | def test_pickled_protocol_4(self, b): 70 | with tempfile.TemporaryDirectory() as tmp_dir: 71 | tmp_file_path = os.path.join(tmp_dir, "data.pickle") 72 | 73 | c = self.TestPickledClassProtocol3(b) 74 | c.__dump__(tmp_file_path) 75 | unpickled = self.TestPickledClassProtocol3.__load__(tmp_file_path) 76 | 77 | self.assertEqual(unpickled, c) 78 | -------------------------------------------------------------------------------- /tests/test_benchmark.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | from paprika import access_counter, data, timeit 5 | 6 | 7 | class BenchmarkTestCases(unittest.TestCase): 8 | @data 9 | class Person: 10 | age: int 11 | name: str 12 | 13 | def test_timeit(self): 14 | def test_handler(_, run_time): 15 | self.assertAlmostEqual(run_time, 5, delta=0.5) 16 | 17 | @timeit(handler=test_handler) 18 | def f(): 19 | time.sleep(5) 20 | 21 | f() 22 | 23 | def test_access_counter_mixed_types(self): 24 | def handler(results): 25 | self.assertEqual(results["list"]["nWrites"], 100) 26 | self.assertEqual(results["dict"]["nWrites"], 100) 27 | self.assertEqual(results["person"]["nWrites"], 100) 28 | self.assertEqual(results["tuple"]["nWrites"], 0) 29 | 30 | self.assertEqual(results["list"]["nReads"], 0) 31 | self.assertEqual(results["dict"]["nReads"], 100) 32 | self.assertEqual(results["person"]["nReads"], 100) 33 | self.assertEqual(results["tuple"]["nReads"], 100) 34 | 35 | @access_counter(test_mode=True, test_handler=handler) 36 | def f(list, dict, person, tuple): 37 | for i in range(100): 38 | list[0] = dict["key"] 39 | dict["key"] = person.age 40 | person.age = tuple[0] 41 | 42 | f([1, 2, 3, 4, 5], {"key": 0}, self.Person(name="Rayan", age=19), (0, 0)) 43 | 44 | def test_access_counter_objects(self): 45 | def handler(results): 46 | self.assertEqual(results["person1"]["nWrites"], 100) 47 | self.assertEqual(results["person2"]["nWrites"], 0) 48 | 49 | self.assertEqual(results["person1"]["nReads"], 0) 50 | self.assertEqual(results["person2"]["nReads"], 100) 51 | 52 | @access_counter(test_mode=True, test_handler=handler) 53 | def f(person1, person2): 54 | for i in range(100): 55 | person1.age = person2.age 56 | 57 | f(self.Person(age=19, name="Rayan"), self.Person(age=91, name="nayaR")) 58 | 59 | def test_access_counter_dicts(self): 60 | def handler(results): 61 | self.assertEqual(results["dict1"]["nWrites"], 100) 62 | self.assertEqual(results["dict2"]["nWrites"], 0) 63 | 64 | self.assertEqual(results["dict1"]["nReads"], 0) 65 | self.assertEqual(results["dict2"]["nReads"], 100) 66 | 67 | @access_counter(test_mode=True, test_handler=handler) 68 | def f(dict1, dict2): 69 | for i in range(100): 70 | dict1[0] = dict2[0] 71 | 72 | f({0: 1}, {0: 1}) 73 | 74 | def test_access_counter_lists(self): 75 | def handler(results): 76 | self.assertEqual(results["list1"]["nWrites"], 100) 77 | self.assertEqual(results["list2"]["nWrites"], 0) 78 | 79 | self.assertEqual(results["list1"]["nReads"], 0) 80 | self.assertEqual(results["list2"]["nReads"], 100) 81 | 82 | @access_counter(test_mode=True, test_handler=handler) 83 | def f(list1, list2): 84 | for i in range(100): 85 | list1[0] = list2[0] 86 | 87 | f([0], [0]) 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rayan.hatout@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /paprika/oo.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import pickle 3 | from typing import Type, TypeVar, Generic 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | class NonNull(Generic[T]): 9 | pass 10 | 11 | 12 | def to_string(decorated_class): 13 | def __str__(self): 14 | attributes = [ 15 | attr 16 | for attr in dir(self) 17 | if not attr.startswith("_") 18 | and not ( 19 | hasattr(self.__dict__[attr], "__call__") 20 | if attr in self.__dict__ 21 | else hasattr(decorated_class.__dict__[attr], "__call__") 22 | ) 23 | ] 24 | output_format = [ 25 | f"{attr}={self.__dict__[attr]}" 26 | if attr in self.__dict__ 27 | else f"{attr}={decorated_class.__dict__[attr]}" 28 | for attr in attributes 29 | ] 30 | return f"{decorated_class.__name__}@[{', '.join(output_format)}]" 31 | 32 | decorated_class.__str__ = __str__ 33 | return decorated_class 34 | 35 | 36 | def collect_attributes(decorated_class): 37 | attributes = [name for name in decorated_class.__dict__ if not name.startswith("_")] 38 | if "__annotations__" in decorated_class.__dict__: 39 | for attr_name in decorated_class.__dict__["__annotations__"]: 40 | if attr_name not in attributes: 41 | attributes.append(attr_name) 42 | return attributes 43 | 44 | 45 | def find_required_fields(decorated_class): 46 | return [ 47 | F 48 | for F, T in decorated_class.__dict__["__annotations__"].items() 49 | if hasattr(T, "__dict__") \ 50 | and "__origin__" in T.__dict__ \ 51 | and T.__dict__["__origin__"] == NonNull 52 | ] 53 | 54 | 55 | def bind_fields(inst, fields, attributes, required_fields, kwargs=False): 56 | for attr_name, value in fields: 57 | if attr_name in required_fields and value is None: 58 | raise ValueError(f"Field {attr_name} is marked as non-null") 59 | if not kwargs or (kwargs and attr_name in attributes): 60 | setattr(inst, attr_name, value) 61 | 62 | 63 | def generate_generic_init(attributes, required_fields): 64 | def __init__(self, *args, **kwargs): 65 | bind_fields(self, zip(attributes, args), attributes, required_fields) 66 | bind_fields(self, kwargs.items(), attributes, required_fields, True) 67 | 68 | return __init__ 69 | 70 | 71 | def constructor(decorated_class): 72 | required_fields = find_required_fields(decorated_class) 73 | attributes = collect_attributes(decorated_class) 74 | 75 | decorated_class.__init__ = generate_generic_init(attributes, required_fields) 76 | return decorated_class 77 | 78 | 79 | def equals_and_hashcode(decorated_class): 80 | def __eq__(self, other): 81 | same_class = getattr(self, "__class__") == getattr(other, "__class__") 82 | same_attrs = getattr(self, "__dict__") == getattr(other, "__dict__") 83 | return same_class and same_attrs 84 | 85 | def __hash__(self): 86 | attributes = tuple([getattr(self, "__dict__")[key] for key in 87 | sorted(getattr(self, "__dict__").keys())]) 88 | return hash(attributes) 89 | 90 | decorated_class.__hash__ = __hash__ 91 | decorated_class.__eq__ = __eq__ 92 | return decorated_class 93 | 94 | 95 | def data(decorated_class): 96 | decorated_class = to_string(decorated_class) 97 | decorated_class = constructor(decorated_class) 98 | decorated_class = equals_and_hashcode(decorated_class) 99 | return decorated_class 100 | 101 | 102 | def singleton(cls): 103 | @functools.wraps(cls) 104 | def wrapper_singleton(*args, **kwargs): 105 | if not wrapper_singleton.instance: 106 | try: 107 | wrapper_singleton.instance = cls(*args, **kwargs) 108 | except TypeError: 109 | # TODO test this case, do we really want to make it a dataclass? 110 | wrapper_singleton.instance = data(cls)(*args, **kwargs) 111 | return wrapper_singleton.instance 112 | 113 | wrapper_singleton.instance = None 114 | return wrapper_singleton 115 | 116 | 117 | def pickled(decorated_class=None, protocol=None): 118 | def decorator(decorated_class): 119 | def __dump__(self, file_path): 120 | with open(file_path, "wb") as f: 121 | pickle.dump(self, f, protocol=protocol) 122 | 123 | @staticmethod 124 | def __load__(file_path): 125 | with open(file_path, "rb") as f: 126 | return pickle.load(f) 127 | 128 | decorated_class.__dump__ = __dump__ 129 | decorated_class.__load__ = __load__ 130 | 131 | return decorated_class 132 | 133 | if decorated_class is not None: 134 | return decorator(decorated_class) 135 | 136 | return decorator 137 | -------------------------------------------------------------------------------- /paprika/benchmark.py: -------------------------------------------------------------------------------- 1 | import cProfile 2 | import functools 3 | import inspect 4 | import pstats 5 | import time 6 | from pstats import SortKey 7 | 8 | from tabulate import tabulate 9 | 10 | 11 | class Singleton(object): 12 | _instance = None 13 | 14 | def __new__(cls, *args, **kwargs): 15 | if not isinstance(cls._instance, cls): 16 | cls._instance = object.__new__(cls, *args, **kwargs) 17 | return cls._instance 18 | 19 | 20 | class PerformanceCounter(Singleton): 21 | perf_dict = {} 22 | 23 | 24 | def timeit(_func=None, *, timer=time.perf_counter, handler=None): 25 | def decorator_timeit(func): 26 | @functools.wraps(func) 27 | def wrapper_timeit(*args, **kwargs): 28 | start = timer() 29 | ret = func(*args, **kwargs) 30 | end = timer() 31 | run_time = end - start 32 | if handler: 33 | handler(func.__name__, run_time) 34 | print(f"{func.__name__} executed in {run_time} seconds") 35 | return ret 36 | 37 | return wrapper_timeit 38 | 39 | if _func is None: 40 | return decorator_timeit 41 | else: 42 | return decorator_timeit(_func) 43 | 44 | 45 | def dispatch_access_counter_results(func, new_args, test_mode, test_handler): 46 | print(f"data access summary for function: {func.__name__}") 47 | if test_mode: 48 | test_handler( 49 | { 50 | PerformanceCounter() 51 | .perf_dict[arg]["name"]: PerformanceCounter() 52 | .perf_dict[arg] 53 | for arg in new_args 54 | } 55 | ) 56 | perf_data = [ 57 | [ 58 | PerformanceCounter().perf_dict[arg]["name"], 59 | PerformanceCounter().perf_dict[arg]["nReads"], 60 | PerformanceCounter().perf_dict[arg]["nWrites"], 61 | ] 62 | for arg in new_args 63 | ] 64 | print( 65 | tabulate( 66 | perf_data, 67 | headers=["Arg Name", "nReads", "nWrites"], 68 | tablefmt="grid", 69 | ) 70 | ) 71 | 72 | 73 | class AccessCounter: 74 | def __init__(self, delegate, name): 75 | PerformanceCounter().perf_dict[self] = { 76 | "delegate": delegate, 77 | "name": name, 78 | "nReads": 0, 79 | "nWrites": 0, 80 | } 81 | 82 | def __getitem__(self, item): 83 | PerformanceCounter().perf_dict[self]["nReads"] += 1 84 | return PerformanceCounter().perf_dict[self]["delegate"][item] 85 | 86 | def __setitem__(self, key, value): 87 | PerformanceCounter().perf_dict[self]["nWrites"] += 1 88 | PerformanceCounter().perf_dict[self]["delegate"][key] = value 89 | 90 | def __getattr__(self, item): 91 | PerformanceCounter().perf_dict[self]["nReads"] += 1 92 | return PerformanceCounter().perf_dict[self]["delegate"].__getattribute__(item) 93 | 94 | def __setattr__(self, key, value): 95 | PerformanceCounter().perf_dict[self]["nWrites"] += 1 96 | return PerformanceCounter().perf_dict[self]["delegate"].__setattr__(key, value) 97 | 98 | 99 | def access_counter(_func=None, *, test_mode=False, test_handler=None): 100 | def decorator_access_counter(func): 101 | @functools.wraps(func) 102 | def wrapper_access_counter(*args, **kwargs): 103 | new_args = [] 104 | for arg, arg_name in zip(args, inspect.getfullargspec(func).args): 105 | new_args.append(AccessCounter(delegate=arg, name=arg_name)) 106 | ret = func(*new_args, **kwargs) 107 | if new_args: 108 | dispatch_access_counter_results(func, new_args, test_mode, test_handler) 109 | 110 | return ret 111 | 112 | return wrapper_access_counter 113 | 114 | if _func is None: 115 | return decorator_access_counter 116 | else: 117 | return decorator_access_counter(_func) 118 | 119 | 120 | def hotspots(_func=None, *, n_runs=1, top_n=10): 121 | def decorator_hotspots(func): 122 | @functools.wraps(func) 123 | def wrapper_hotspots(*args, **kwargs): 124 | pr = cProfile.Profile() 125 | pr.enable() 126 | ret = None 127 | for n in range(n_runs): 128 | ret = func(*args, **kwargs) 129 | pr.disable() 130 | pstats.Stats(pr).strip_dirs().sort_stats(SortKey.CUMULATIVE).print_stats( 131 | top_n 132 | ) 133 | return ret 134 | 135 | return wrapper_hotspots 136 | 137 | if _func is None: 138 | return decorator_hotspots 139 | else: 140 | return decorator_hotspots(_func) 141 | 142 | 143 | def profile(decorated_fn=None, *, n_runs=1, top_n=10): 144 | decorated_class = access_counter(decorated_fn) 145 | decorated_class = hotspots(decorated_class, n_runs=n_runs, top_n=top_n) 146 | return decorated_class 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![A plate filled with paprika spice](static/paprika.jpg) 2 | _Image courtesy of Anna Quaglia (Photographer)_ 3 | 4 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/rayanht/paprika/Test%20and%20collect%20coverage) 5 | [![PyPI license](https://img.shields.io/pypi/l/paprika.svg)](https://pypi.python.org/pypi/paprika/) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/eae8b5fab78e30cbab21/maintainability)](https://codeclimate.com/github/rayanht/paprika/maintainability) 7 | [![codecov](https://codecov.io/gh/rayanht/paprika/branch/main/graph/badge.svg?token=21FA3K95AM)](https://codecov.io/gh/rayanht/paprika) 8 | ![PyPI](https://img.shields.io/pypi/v/paprika) 9 | [![Downloads](https://static.pepy.tech/personalized-badge/paprika?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/paprika) 10 | 11 | # Paprika 12 | 13 | Paprika is a python library that reduces boilerplate. It is heavily inspired by 14 | Project Lombok. 15 | 16 | ## Table of Contents 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Features & Examples](#features-and-examples) 20 | - [Object-oriented decorators](#general-utility-decorators) 21 | - [`@to_string`](#to_string) 22 | - [`@equals_and_hashcode`](#equals_and_hashcode) 23 | - [`@data`](#data) 24 | - [On `@data` and NonNull](#on-data-and-nonnull) 25 | - [`@singleton`](#singleton) 26 | - [Important note on combining `@data` and `@singleton`](#important-note-on-combining-data-and-singleton) 27 | - [General utility decorators](#general-utility-decorators) 28 | - [`@threaded`](#threaded) 29 | - [`@repeat`](#repeat) 30 | - [`@pickled`](#pickled) 31 | - [Benchmark decorators](#benchmark-decorators) 32 | - [`@timeit`](#timeit) 33 | - [`@access_counter`](#access_counter) 34 | - [`@hotspots`](#hotspots) 35 | - [`@profile`](#profile) 36 | - [Error-handling decorators](#error-handling-decorators) 37 | - [`@catch`](#catch) 38 | - [`@silent_catch`](#silent_catch) 39 | - [Contributing](#contributing) 40 | - [Authors](#authors) 41 | - [License](#license) 42 | 43 | 44 | ## Installation 45 | 46 | paprika is available on PyPi. 47 | 48 | ```bash 49 | $ pip install paprika 50 | ``` 51 | 52 | ## Usage 53 | 54 | `paprika` is a decorator-only library and all decorators are exposed at the 55 | top-level of the module. If you want to use shorthand notation (i.e. `@data`), 56 | you can import all decorators as follows: 57 | 58 | ```python3 59 | from paprika import * 60 | ``` 61 | 62 | Alternatively, you can opt to use the longhand notation (i.e. `@paprika.data`) 63 | by importing `paprika` as follows: 64 | 65 | ```python3 66 | import paprika 67 | ``` 68 | 69 | ## Features and Examples 70 | 71 | ## Object-oriented decorators 72 | 73 | ### @to_string 74 | 75 | The `@to_string` decorator automatically overrides `__str__` 76 | 77 | #### Python 78 | 79 | ```python3 80 | class Person: 81 | def __init__(self, name: str, age: int): 82 | self.name = name 83 | self.age = age 84 | 85 | def __str__(self): 86 | return f"{self.__name__}@[name={self.name}, age={self.age}]" 87 | ``` 88 | 89 | #### Python with paprika 90 | 91 | ```python3 92 | @to_string 93 | class Person: 94 | def __init__(self, name: str, age: int): 95 | self.name = name 96 | self.age = age 97 | ``` 98 | 99 | ---- 100 | 101 | ### @equals_and_hashcode 102 | 103 | The `@equals_and_hashcode` decorator automatically overrides `__eq__` 104 | and `__hash__` 105 | 106 | #### Python 107 | 108 | ```python3 109 | class Person: 110 | def __init__(self, name: str, age: int): 111 | self.name = name 112 | self.age = age 113 | 114 | def __eq__(self, other): 115 | return (self.__class__ == other.__class__ 116 | and 117 | self.__dict__ == other.__dict__) 118 | 119 | def __hash__(self): 120 | return hash((self.name, self.age)) 121 | ``` 122 | 123 | #### Python with paprika 124 | 125 | ```python3 126 | @equals_and_hashcode 127 | class Person: 128 | def __init__(self, name: str, age: int): 129 | self.name = name 130 | self.age = age 131 | ``` 132 | 133 | --- 134 | 135 | ### @data 136 | 137 | The `@data` decorator creates a dataclass by combining `@to_string` 138 | and `@equals_and_hashcode` and automatically creating a constructor! 139 | 140 | #### Python 141 | 142 | ```python3 143 | class Person: 144 | def __init__(self, name: str, age: int): 145 | self.name = name 146 | self.age = age 147 | 148 | def __str__(self): 149 | return f"{self.__name__}@[name={self.name}, age={self.age}]" 150 | 151 | def __eq__(self, other): 152 | return (self.__class__ == other.__class__ 153 | and 154 | self.__dict__ == other.__dict__) 155 | 156 | def __hash__(self): 157 | return hash((self.name, self.age)) 158 | ``` 159 | 160 | #### Python with paprika 161 | 162 | ```python3 163 | @data 164 | class Person: 165 | name: str 166 | age: int 167 | ``` 168 | 169 | #### On @data and NonNull 170 | 171 | `paprika` exposes a `NonNull` generic type that can be used in conjunction with 172 | the `@data` decorator to enforce that certain arguments passed to the 173 | constructor are not null. The following snippet will raise a `ValueError`: 174 | 175 | ```python3 176 | @data 177 | class Person: 178 | name: NonNull[str] 179 | age: int 180 | 181 | p = Person(name=None, age=42) # ValueError ❌ 182 | ``` 183 | 184 | ---- 185 | 186 | ### @singleton 187 | 188 | The `@singleton` decorator can be used to enforce that a class only gets 189 | instantiated once within the lifetime of a program. Any subsequent instantiation 190 | will return the original instance. 191 | 192 | ```python3 193 | @singleton 194 | class Person: 195 | def __init__(self, name: str, age: int): 196 | self.name = name 197 | self.age = age 198 | 199 | p1 = Person(name="Rayan", age=19) 200 | p2 = Person() 201 | print(p1 == p2 and p1 is p2) # True ✅ 202 | ``` 203 | 204 | `@singleton` can be seamlessly combined with `@data`! 205 | 206 | ```python3 207 | @singleton 208 | @data 209 | class Person: 210 | name: str 211 | age: int 212 | 213 | p1 = Person(name="Rayan", age=19) 214 | p2 = Person() 215 | print(p1 == p2 and p1 is p2) # True ✅ 216 | ``` 217 | 218 | #### Important note on combining @data and @singleton 219 | 220 | When combining `@singleton` with `@data`, `@singleton` should come 221 | before `@data`. Combining them the other way around will work in most cases but 222 | is not thoroughly tested and relies on assumptions that _might_ not hold. 223 | 224 | ## General utility decorators 225 | 226 | ### @threaded 227 | 228 | The `@threaded` decorator will run the decorated function in a thread by 229 | submitting it to a `ThreadPoolExecutor`. When the decorated function is called, 230 | it will immediately return a `Future` object. The result can be extracted by 231 | calling `.result()` on that `Future` 232 | 233 | ```python3 234 | @threaded 235 | def waste_time(sleep_time): 236 | thread_name = threading.current_thread().name 237 | time.sleep(sleep_time) 238 | print(f"{thread_name} woke up after {sleep_time}s!") 239 | return 42 240 | 241 | t1 = waste_time(5) 242 | t2 = waste_time(2) 243 | 244 | print(t1) # 245 | print(t1.result()) # 42 246 | ``` 247 | 248 | ``` 249 | ThreadPoolExecutor-0_1 woke up after 2s! 250 | ThreadPoolExecutor-0_0 woke up after 5s! 251 | ``` 252 | 253 | --- 254 | 255 | ### @repeat 256 | 257 | The `@repeat` decorator will run the decorated function consecutively, as many 258 | times as specified. 259 | 260 | ```python3 261 | @repeat(n=5) 262 | def hello_world(): 263 | print("Hello world!") 264 | 265 | hello_world() 266 | ``` 267 | 268 | ``` 269 | Hello world! 270 | Hello world! 271 | Hello world! 272 | Hello world! 273 | Hello world! 274 | ``` 275 | 276 | ### @pickled 277 | The `@pickled` decorator adds `__dump__` and `__load__` to a class for pickling convenience. 278 | 279 | `__dump__` and `__load__` take in the target and source pickle file paths respectively. 280 | 281 | This decorator takes in an optional `protocol` argument (e.g. `@pickled(protocol=5)`) specifiying the [pickle protocol](https://docs.python.org/3/library/pickle.html#data-stream-format). 282 | 283 | #### Python 284 | 285 | ```python3 286 | class Person: 287 | def __init__(self, name: str): 288 | self.name = name 289 | 290 | def __dump__(self, file_path): 291 | with open(file_path, "wb") as f: 292 | pickle_dump(self, f, protocol=5) 293 | 294 | @staticmethod 295 | def __load__(file_path): 296 | with open(file_path, "rb") as f: 297 | return pickle.load(f) 298 | ``` 299 | 300 | #### Python with paprika 301 | 302 | ```python3 303 | @data 304 | @pickled(protocol=5) 305 | class Person: 306 | name: str 307 | ``` 308 | 309 | 310 | ## Benchmark decorators 311 | 312 | ### timeit 313 | 314 | The `@timeit` decorator times the total execution time of the decorated 315 | function. It uses a `timer::perf_timer` by default but that can be replaced by 316 | any object of type `Callable[None, int]`. 317 | 318 | ```python3 319 | def time_waster1(): 320 | time.sleep(2) 321 | 322 | def time_waster2(): 323 | time.sleep(5) 324 | 325 | @timeit 326 | def test_timeit(): 327 | time_waster1() 328 | time_waster2() 329 | ``` 330 | 331 | ```python 332 | test_timeit executed in 7.002189894999999 seconds 333 | ``` 334 | 335 | Here's how you can replace the default timer: 336 | 337 | ```python 338 | @timeit(timer: lambda: 0) # Or something actually useful like time.time() 339 | def test_timeit(): 340 | time_waster1() 341 | time_waster2() 342 | ``` 343 | 344 | ```python 345 | test_timeit executed in 0 seconds 346 | ``` 347 | 348 | --- 349 | 350 | ### @access_counter 351 | 352 | The `@access_counter` displays a summary of how many times each of the 353 | structures that are passed to the decorated function are accessed 354 | (number of reads and number of writes). 355 | 356 | ```python3 357 | @access_counter 358 | def test_access_counter(list, dict, person, tuple): 359 | for i in range(500): 360 | list[0] = dict["key"] 361 | dict["key"] = person.age 362 | person.age = tuple[0] 363 | 364 | 365 | test_access_counter([1, 2, 3, 4, 5], {"key": 0}, Person(name="Rayan", age=19), 366 | (0, 0)) 367 | ``` 368 | 369 | ``` 370 | data access summary for function: test 371 | +------------+----------+-----------+ 372 | | Arg Name | nReads | nWrites | 373 | +============+==========+===========+ 374 | | list | 0 | 500 | 375 | +------------+----------+-----------+ 376 | | dict | 500 | 500 | 377 | +------------+----------+-----------+ 378 | | person | 500 | 500 | 379 | +------------+----------+-----------+ 380 | | tuple | 500 | 0 | 381 | +------------+----------+-----------+ 382 | ``` 383 | 384 | ___ 385 | 386 | ### @hotspots 387 | 388 | The `@hotspots` automatically runs `cProfiler` on the decorated function and 389 | display the `top_n` (default = 10) most expensive function calls sorted by 390 | cumulative time taken (this metric will be customisable in the future). The 391 | sample error can be reduced by using a higher `n_runs` (default = 1) parameter. 392 | 393 | ```python3 394 | def time_waster1(): 395 | time.sleep(2) 396 | 397 | def time_waster2(): 398 | time.sleep(5) 399 | 400 | @hotspots(top_n=5, n_runs=2) # You can also do just @hotspots 401 | def test_hotspots(): 402 | time_waster1() 403 | time_waster2() 404 | 405 | test_hotspots() 406 | ``` 407 | 408 | ``` 409 | 11 function calls in 14.007 seconds 410 | 411 | Ordered by: cumulative time 412 | 413 | ncalls tottime percall cumtime percall filename:lineno(function) 414 | 2 0.000 0.000 14.007 7.004 main.py:27(test_hot) 415 | 4 14.007 3.502 14.007 3.502 {built-in method time.sleep} 416 | 2 0.000 0.000 10.004 5.002 main.py:23(time_waster2) 417 | 2 0.000 0.000 4.003 2.002 main.py:19(time_waster1) 418 | 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 419 | ``` 420 | 421 | --- 422 | 423 | ### @profile 424 | 425 | The `@profile` decorator is simply syntatic sugar that allows to perform both 426 | hotspot analysis and data access analysis. Under the hood, it simply 427 | uses `@access_counter` followed by `@hotspots`. 428 | 429 | ## Error-handling decorators 430 | 431 | ### @catch 432 | 433 | The `@catch` decorator can be used to wrap a function inside a `try/catch` 434 | block. `@catch` expects to receive in the `exceptions` argument at least one 435 | exception that we want to catch. 436 | 437 | If no exception is provided, `@catch` will by default catch _all_ exceptions ( 438 | excluding `SystemExit`, `KeyboardInterrupt` 439 | and `GeneratorExit` since they do not subclass the generic `Exception` class). 440 | 441 | `@catch` can take a custom exception handler as a parameter. If no handler is 442 | supplied, a stack trace is logged to `stderr` and the program will continue 443 | executing. 444 | 445 | ```python 446 | @catch(exception=ValueError) 447 | def test_catch1(): 448 | raise ValueError 449 | 450 | @catch(exception=[EOFError, KeyError]) 451 | def test_catch2(): 452 | raise ValueError 453 | 454 | test_catch1() 455 | print("Still alive!") # This should get printed since we're catching the ValueError. 456 | 457 | test_catch2() 458 | print("Still alive?") # This will not get printed since we're not catching ValueError in this case. 459 | ``` 460 | 461 | ``` 462 | Traceback (most recent call last): 463 | File "/Users/rayan/Desktop/paprika/paprika/__init__.py", line 292, in wrapper_catch 464 | return func(*args, **kwargs) 465 | File "/Users/rayan/Desktop/paprika/main.py", line 29, in test_exception1 466 | raise ValueError 467 | ValueError 468 | 469 | Still alive! 470 | 471 | Traceback (most recent call last): 472 | File "/Users/rayan/Desktop/paprika/main.py", line 40, in 473 | test_exception2() 474 | File "/Users/rayan/Desktop/paprika/paprika/__init__.py", line 292, in wrapper_catch 475 | return func(*args, **kwargs) 476 | File "/Users/rayan/Desktop/paprika/main.py", line 37, in test_exception2 477 | raise ValueError 478 | ValueError 479 | ``` 480 | 481 | #### Using a custom exception handler 482 | 483 | If provided, a custom exception handler must be of 484 | type `Callable[Exception, Generic[T]]`. In other words, its signature must take 485 | one parameter of type Exception. 486 | 487 | ```python 488 | @catch(exception=ValueError, 489 | handler=lambda x: print(f"Ohno, a {repr(x)} was raised!")) 490 | def test_custom_handler(): 491 | raise ValueError 492 | 493 | test_custom_handler() 494 | ``` 495 | 496 | ``` 497 | Ohno, a ValueError() was raised! 498 | ``` 499 | 500 | --- 501 | 502 | ### @silent_catch 503 | 504 | The `@silent_catch` decorator is very similar to the `@catch` decorator in its 505 | usage. It takes one or more exceptions but then simply catches them silently. 506 | 507 | ```python 508 | @silent_catch(exception=[ValueError, TypeError]) 509 | def test_silent_catch(): 510 | raise TypeError 511 | 512 | test_silent_catch() 513 | print("Still alive!") 514 | ``` 515 | 516 | ``` 517 | Still alive! 518 | ``` 519 | 520 | ## Contributing 521 | 522 | ### Issues 523 | 524 | Encountered a bug? Have an idea for a new feature? This project is open to all 525 | sorts of contribution! Feel free to head to the `Issues` tab and describe your 526 | request! 527 | 528 | ### Development Setup 529 | 530 | This project requires [poetry](https://github.com/python-poetry/poetry). 531 | 532 | #### Recommended Steps 533 | 1. Initialize a virtual environment: `python -m venv .env` 534 | 2. Enter your virtual environment. 535 | 3. Install poetry: `pip install poetry`. 536 | 4. Install dependencies: `poetry install`. 537 | 5. Initialize pre-commit: `pre-commit install`. 538 | 539 | ## Authors 540 | 541 | * **Rayan Hatout** - [GitHub](https://github.com/rayanht) 542 | | [Twitter](https://twitter.com/rayanhtt) 543 | | [LinkedIn](https://www.linkedin.com/in/rayan-hatout/) 544 | 545 | See also the list of [contributors](https://github.com/rayanht/paprika) who 546 | participated in this project. 547 | 548 | ## License 549 | 550 | This project is licensed under the MIT License - see 551 | the [LICENSE](LICENSE) file for details 552 | --------------------------------------------------------------------------------