├── .coveragerc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build_wheel.bat ├── create_pdoc.bat ├── create_pdoc.sh ├── create_requirements.bat ├── docs └── pedantic │ ├── constants.html │ ├── decorators │ ├── class_decorators.html │ ├── cls_deco_frozen_dataclass.html │ ├── fn_deco_context_manager.html │ ├── fn_deco_count_calls.html │ ├── fn_deco_deprecated.html │ ├── fn_deco_does_same_as_function.html │ ├── fn_deco_in_subprocess.html │ ├── fn_deco_mock.html │ ├── fn_deco_overrides.html │ ├── fn_deco_pedantic.html │ ├── fn_deco_rename_kwargs.html │ ├── fn_deco_require_kwargs.html │ ├── fn_deco_retry.html │ ├── fn_deco_timer.html │ ├── fn_deco_trace.html │ ├── fn_deco_trace_if_returns.html │ ├── fn_deco_unimplemented.html │ ├── fn_deco_validate │ │ ├── convert_value.html │ │ ├── exceptions.html │ │ ├── fn_deco_validate.html │ │ ├── index.html │ │ ├── parameters │ │ │ ├── abstract_external_parameter.html │ │ │ ├── abstract_parameter.html │ │ │ ├── deserializable.html │ │ │ ├── environment_variable_parameter.html │ │ │ ├── flask_parameters.html │ │ │ └── index.html │ │ └── validators │ │ │ ├── abstract_validator.html │ │ │ ├── composite_validator.html │ │ │ ├── datetime_isoformat.html │ │ │ ├── datetime_unix_timestamp.html │ │ │ ├── email.html │ │ │ ├── enum.html │ │ │ ├── for_each.html │ │ │ ├── index.html │ │ │ ├── is_uuid.html │ │ │ ├── match_pattern.html │ │ │ ├── max.html │ │ │ ├── max_length.html │ │ │ ├── min.html │ │ │ ├── min_length.html │ │ │ └── not_empty.html │ └── index.html │ ├── env_var_logic.html │ ├── examples │ ├── index.html │ └── validate.html │ ├── exceptions.html │ ├── get_context.html │ ├── helper_methods.html │ ├── index.html │ ├── mixins │ ├── generic_mixin.html │ ├── index.html │ └── with_decorated_methods.html │ ├── models │ ├── decorated_function.html │ ├── function_call.html │ ├── generator_wrapper.html │ └── index.html │ ├── tests │ ├── index.html │ ├── test_assert_value_matches_type.html │ ├── test_async_context_manager.html │ ├── test_context_manager.html │ ├── test_frozen_dataclass.html │ ├── test_generator_wrapper.html │ ├── test_generic_mixin.html │ ├── test_in_subprocess.html │ ├── test_rename_kwargs.html │ ├── test_resolve_forward_ref.html │ ├── test_retry.html │ ├── test_with_decorated_methods.html │ ├── tests_class_decorators.html │ ├── tests_combination_of_decorators.html │ ├── tests_decorated_function.html │ ├── tests_docstring.html │ ├── tests_doctests.html │ ├── tests_environment_variables.html │ ├── tests_generator.html │ ├── tests_generic_classes.html │ ├── tests_main.html │ ├── tests_mock.html │ ├── tests_pedantic.html │ ├── tests_pedantic_async.html │ ├── tests_pedantic_class.html │ ├── tests_pedantic_class_docstring.html │ ├── tests_pedantic_python_311.html │ ├── tests_require_kwargs.html │ ├── tests_small_method_decorators.html │ └── validate │ │ ├── index.html │ │ ├── test_convert_value.html │ │ ├── test_datetime_isoformat.html │ │ ├── test_flask_parameters.html │ │ ├── test_parameter_environment_variable.html │ │ ├── test_validate.html │ │ ├── test_validator_composite.html │ │ ├── test_validator_datetime_unix_timestamp.html │ │ ├── test_validator_email.html │ │ ├── test_validator_for_each.html │ │ ├── test_validator_is_enum.html │ │ ├── test_validator_is_uuid.html │ │ ├── test_validator_match_pattern.html │ │ ├── test_validator_max.html │ │ ├── test_validator_max_length.html │ │ ├── test_validator_min.html │ │ ├── test_validator_min_length.html │ │ └── test_validator_not_empty.html │ └── type_checking_logic │ ├── check_docstring.html │ ├── check_generic_classes.html │ ├── check_types.html │ ├── index.html │ └── resolve_forward_ref.html ├── get_changelog.sh ├── get_coverage.bat ├── pedantic ├── __init__.py ├── constants.py ├── decorators │ ├── __init__.py │ ├── class_decorators.py │ ├── cls_deco_frozen_dataclass.py │ ├── fn_deco_context_manager.py │ ├── fn_deco_count_calls.py │ ├── fn_deco_deprecated.py │ ├── fn_deco_does_same_as_function.py │ ├── fn_deco_in_subprocess.py │ ├── fn_deco_mock.py │ ├── fn_deco_overrides.py │ ├── fn_deco_pedantic.py │ ├── fn_deco_rename_kwargs.py │ ├── fn_deco_require_kwargs.py │ ├── fn_deco_retry.py │ ├── fn_deco_timer.py │ ├── fn_deco_trace.py │ ├── fn_deco_trace_if_returns.py │ ├── fn_deco_unimplemented.py │ └── fn_deco_validate │ │ ├── __init__.py │ │ ├── convert_value.py │ │ ├── exceptions.py │ │ ├── fn_deco_validate.py │ │ ├── parameters │ │ ├── __init__.py │ │ ├── abstract_external_parameter.py │ │ ├── abstract_parameter.py │ │ ├── deserializable.py │ │ ├── environment_variable_parameter.py │ │ └── flask_parameters.py │ │ └── validators │ │ ├── __init__.py │ │ ├── abstract_validator.py │ │ ├── composite_validator.py │ │ ├── datetime_isoformat.py │ │ ├── datetime_unix_timestamp.py │ │ ├── email.py │ │ ├── enum.py │ │ ├── for_each.py │ │ ├── is_uuid.py │ │ ├── match_pattern.py │ │ ├── max.py │ │ ├── max_length.py │ │ ├── min.py │ │ ├── min_length.py │ │ └── not_empty.py ├── env_var_logic.py ├── examples │ ├── __init__.py │ ├── config.csv │ └── validate.py ├── exceptions.py ├── get_context.py ├── helper_methods.py ├── mixins │ ├── __init__.py │ ├── generic_mixin.py │ └── with_decorated_methods.py ├── models │ ├── __init__.py │ ├── decorated_function.py │ ├── function_call.py │ └── generator_wrapper.py ├── tests │ ├── __init__.py │ ├── test_assert_value_matches_type.py │ ├── test_async_context_manager.py │ ├── test_context_manager.py │ ├── test_frozen_dataclass.py │ ├── test_generator_wrapper.py │ ├── test_generic_mixin.py │ ├── test_in_subprocess.py │ ├── test_rename_kwargs.py │ ├── test_resolve_forward_ref.py │ ├── test_retry.py │ ├── test_with_decorated_methods.py │ ├── tests_class_decorators.py │ ├── tests_combination_of_decorators.py │ ├── tests_decorated_function.py │ ├── tests_docstring.py │ ├── tests_doctests.py │ ├── tests_environment_variables.py │ ├── tests_generator.py │ ├── tests_generic_classes.py │ ├── tests_main.py │ ├── tests_mock.py │ ├── tests_pedantic.py │ ├── tests_pedantic_async.py │ ├── tests_pedantic_class.py │ ├── tests_pedantic_class_docstring.py │ ├── tests_pedantic_python_311.py │ ├── tests_require_kwargs.py │ ├── tests_small_method_decorators.py │ └── validate │ │ ├── __init__.py │ │ ├── test_convert_value.py │ │ ├── test_datetime_isoformat.py │ │ ├── test_flask_parameters.py │ │ ├── test_parameter_environment_variable.py │ │ ├── test_validate.py │ │ ├── test_validator_composite.py │ │ ├── test_validator_datetime_unix_timestamp.py │ │ ├── test_validator_email.py │ │ ├── test_validator_for_each.py │ │ ├── test_validator_is_enum.py │ │ ├── test_validator_is_uuid.py │ │ ├── test_validator_match_pattern.py │ │ ├── test_validator_max.py │ │ ├── test_validator_max_length.py │ │ ├── test_validator_min.py │ │ ├── test_validator_min_length.py │ │ └── test_validator_not_empty.py └── type_checking_logic │ ├── __init__.py │ ├── check_docstring.py │ ├── check_generic_classes.py │ ├── check_types.py │ └── resolve_forward_ref.py ├── requirements.txt ├── setup.py ├── test_deployment.py └── type_hint_parser_dependency_graph.graphml /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *pedantic/tests* 4 | */home/travis/virtualenv* 5 | 6 | [report] 7 | exclude_lines = 8 | # Don't complain if non-runnable code isn't run: 9 | pragma: no cover 10 | except ImportError 11 | raise ImportError 12 | if __name__ == .__main__.: 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: LostInDarkMath -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | assignees: 13 | - "LostInDarkMath" 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [3.11, 3.12, 3.13] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -U coveralls 24 | pip install -r requirements.txt 25 | - name: Run tests 26 | run: | 27 | coverage run --branch pedantic/tests/tests_main.py 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@v2.2.3 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | file: .coverage 33 | deploy: 34 | needs: test 35 | runs-on: ubuntu-latest 36 | if: github.ref == 'refs/heads/master' # only on master 37 | environment: 38 | name: pypi-deployment 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Set up Python 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: 3.12 # Specify the Python version you want to use for deployment 45 | - name: Set Twine Environment Variables 46 | run: | 47 | echo "TWINE_USERNAME=${{ secrets.PYPI_USERNAME }}" >> $GITHUB_ENV 48 | echo "TWINE_PASSWORD=${{ secrets.PYPI_PASSWORD }}" >> $GITHUB_ENV 49 | - name: Build and Upload to PyPI 50 | run: | 51 | pip install -U setuptools twine wheel 52 | python setup.py bdist_wheel 53 | twine upload dist/*.whl # Uploads the wheel file to PyPI 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pycharm 2 | .idea 3 | *.pyc 4 | 5 | # Virtuel environments 6 | venv 7 | venv35 8 | venv36 9 | venv37 10 | venv3.7 11 | venv38 12 | venv39 13 | venv310 14 | 15 | # Coverage 16 | htmlcov 17 | .coverage 18 | 19 | # wheel package 20 | build 21 | dist 22 | pedantic.egg-info 23 | 24 | STEPS_BEFORE_COMMIT.md 25 | -------------------------------------------------------------------------------- /build_wheel.bat: -------------------------------------------------------------------------------- 1 | del build /F /Q /S 2 | del dist /F /Q /S 3 | del pedantic.egg-info /F /Q /S 4 | 5 | pip install wheel 6 | python setup.py bdist_wheel 7 | pause -------------------------------------------------------------------------------- /create_pdoc.bat: -------------------------------------------------------------------------------- 1 | pip install pdoc3 2 | pdoc3 --html --output-dir docs pedantic --force 3 | "docs/pedantic/index.html" 4 | pause -------------------------------------------------------------------------------- /create_pdoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip3 install pdoc3 3 | pdoc3 --html --output-dir docs pedantic --force 4 | "docs/pedantic/index.html" 5 | -------------------------------------------------------------------------------- /create_requirements.bat: -------------------------------------------------------------------------------- 1 | "venv3.7/scripts/pip.exe" freeze > requirements.txt -------------------------------------------------------------------------------- /docs/pedantic/constants.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pedantic.constants API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pedantic.constants

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from typing import TypeVar as Tv, Callable
30 | 
31 | 
32 | TYPE_VAR_METHOD_NAME = '__pedantic_m42__'
33 | TYPE_VAR_ATTR_NAME = '__pedantic_a42__'
34 | TYPE_VAR_SELF = Tv('__pedantic_t42__')
35 | ATTR_NAME_GENERIC_INSTANCE_ALREADY_CHECKED = '__pedantic_g42__'
36 | 
37 | TypeVar = Tv
38 | ReturnType = TypeVar('ReturnType')
39 | F = Callable[..., ReturnType]
40 | C = TypeVar('C')
41 | K = TypeVar('K')
42 | V = TypeVar('V')
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 67 |
68 | 71 | 72 | -------------------------------------------------------------------------------- /docs/pedantic/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pedantic.examples API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pedantic.examples

23 |
24 |
25 |
26 |
27 |

Sub-modules

28 |
29 |
pedantic.examples.validate
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 60 |
61 | 64 | 65 | -------------------------------------------------------------------------------- /docs/pedantic/helper_methods.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pedantic.helper_methods API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pedantic.helper_methods

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
import warnings
30 | from typing import Type
31 | 
32 | 
33 | def _raise_warning(msg: str, category: Type[Warning]) -> None:
34 |     warnings.simplefilter(action='always', category=category)
35 |     warnings.warn(message=msg, category=category, stacklevel=2)
36 |     warnings.simplefilter(action='default', category=category)
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 61 |
62 | 65 | 66 | -------------------------------------------------------------------------------- /get_changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SEARCH_TERM="## Pedantic" 4 | # get line number of latest Release Header 5 | FROM=$( 6 | # get lines with the string <## Pedantic> in it (Header lines) together with line numbers 7 | grep "${SEARCH_TERM}" CHANGELOG.md --line-number | 8 | # only take the first occurrence 9 | head -n 1 | 10 | # only keep the line numbers 11 | sed 's/\(.*\):.*/\1/' 12 | ) 13 | 14 | # get line number of second latest Release Header 15 | TO=$( 16 | # same as above 17 | grep "${SEARCH_TERM}" CHANGELOG.md --line-number | 18 | # take the last of the first two lines 19 | head -n 2 | 20 | tail -n 1 | 21 | # same as above 22 | sed 's/\(.*\):.*/\1/' 23 | ) 24 | 25 | echo "Take lines ${FROM} - ${TO}" 26 | 27 | export RELEASE_BODY=$( 28 | # take content from changelog 29 | cat CHANGELOG.md | 30 | # take from [0, TO - 1] (-1 to exclude second latest release header) 31 | head -n $((TO - 1)) | 32 | # take the last TO - 1 - FROM lines to get the content of the latest release 33 | # this cuts away the first few lines before the latest release header 34 | tail -n $((TO - FROM - 1)) 35 | ) 36 | 37 | export RELEASE_NAME=$( 38 | # same as above 39 | cat CHANGELOG.md | 40 | head -n $((TO - 1)) | 41 | # throw away the body and only keep the header 42 | tail -n $((TO - FROM)) | 43 | head -n 1 | 44 | sed 's/## //' # remove leading hashtags 45 | ) 46 | 47 | echo "Found Release Details:" 48 | echo "" 49 | echo "${RELEASE_NAME}" 50 | echo "" 51 | echo "${RELEASE_BODY}" 52 | 53 | export RELEASE_BODY_NO_LINE_BREAKS=echo ${RELEASE_BODY} | tr '\n' ' ' 54 | -------------------------------------------------------------------------------- /get_coverage.bat: -------------------------------------------------------------------------------- 1 | rem @( 2 | rem echo [run] 3 | rem echo omit = *pedantic/unit_tests* 4 | rem ) > .coveragerc 5 | 6 | pip install coverage 7 | coverage run -m unittest pedantic/unit_tests/tests_main.py 8 | rem coverage report -m 9 | coverage html 10 | cd htmlcov 11 | index.html -------------------------------------------------------------------------------- /pedantic/__init__.py: -------------------------------------------------------------------------------- 1 | from pedantic.decorators import overrides, rename_kwargs, timer, count_calls, trace, trace_if_returns, \ 2 | does_same_as_function, deprecated, unimplemented, require_kwargs, pedantic, \ 3 | pedantic_require_docstring, for_all_methods, trace_class, timer_class, pedantic_class, \ 4 | pedantic_class_require_docstring, Rename, mock, frozen_dataclass, frozen_type_safe_dataclass, in_subprocess, \ 5 | calculate_in_subprocess, retry 6 | 7 | from pedantic.mixins import GenericMixin, create_decorator, DecoratorType, WithDecoratedMethods 8 | 9 | from pedantic.type_checking_logic import assert_value_matches_type, resolve_forward_ref 10 | 11 | from pedantic.exceptions import NotImplementedException 12 | 13 | from pedantic.env_var_logic import disable_pedantic, enable_pedantic, is_enabled 14 | 15 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, ReturnAs 16 | from pedantic.decorators.fn_deco_validate.exceptions import * 17 | from pedantic.decorators.fn_deco_validate.parameters import * 18 | from pedantic.decorators.fn_deco_validate.validators import * 19 | -------------------------------------------------------------------------------- /pedantic/constants.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar as Tv, Callable 2 | 3 | 4 | TYPE_VAR_METHOD_NAME = '__pedantic_m42__' 5 | TYPE_VAR_ATTR_NAME = '__pedantic_a42__' 6 | TYPE_VAR_SELF = Tv('__pedantic_t42__') 7 | ATTR_NAME_GENERIC_INSTANCE_ALREADY_CHECKED = '__pedantic_g42__' 8 | 9 | TypeVar = Tv 10 | ReturnType = TypeVar('ReturnType') 11 | F = Callable[..., ReturnType] 12 | C = TypeVar('C') 13 | K = TypeVar('K') 14 | V = TypeVar('V') 15 | -------------------------------------------------------------------------------- /pedantic/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | from .fn_deco_context_manager import safe_contextmanager, safe_async_contextmanager 2 | from .fn_deco_count_calls import count_calls 3 | from .fn_deco_deprecated import deprecated 4 | from .fn_deco_does_same_as_function import does_same_as_function 5 | from .fn_deco_in_subprocess import in_subprocess, calculate_in_subprocess 6 | from .fn_deco_mock import mock 7 | from .fn_deco_overrides import overrides 8 | from .fn_deco_pedantic import pedantic, pedantic_require_docstring 9 | from .fn_deco_rename_kwargs import rename_kwargs, Rename 10 | from .fn_deco_require_kwargs import require_kwargs 11 | from .fn_deco_retry import retry, retry_func 12 | from .fn_deco_timer import timer 13 | from .fn_deco_trace import trace 14 | from .fn_deco_trace_if_returns import trace_if_returns 15 | from .fn_deco_unimplemented import unimplemented 16 | from .class_decorators import pedantic_class, pedantic_class_require_docstring, timer_class, trace_class, \ 17 | for_all_methods 18 | from .cls_deco_frozen_dataclass import frozen_dataclass, frozen_type_safe_dataclass 19 | -------------------------------------------------------------------------------- /pedantic/decorators/class_decorators.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import types 3 | from dataclasses import is_dataclass 4 | from typing import Callable, Optional, Dict, Type 5 | 6 | from pedantic.constants import TYPE_VAR_ATTR_NAME, TYPE_VAR_METHOD_NAME, F, C, TYPE_VAR_SELF 7 | from pedantic.decorators import timer, trace 8 | from pedantic.decorators.fn_deco_pedantic import pedantic, pedantic_require_docstring 9 | from pedantic.env_var_logic import is_enabled 10 | from pedantic.exceptions import PedanticTypeCheckException 11 | from pedantic.type_checking_logic.check_generic_classes import check_instance_of_generic_class_and_get_type_vars, \ 12 | is_instance_of_generic_class 13 | 14 | 15 | def for_all_methods(decorator: F) -> Callable[[Type[C]], Type[C]]: 16 | """ 17 | Applies a decorator to all methods of a class. 18 | 19 | Example: 20 | 21 | >>> @for_all_methods(pedantic) 22 | ... class MyClass(object): 23 | ... def m1(self): pass 24 | ... def m2(self, x): pass 25 | """ 26 | def decorate(cls: C) -> C: 27 | if not is_enabled(): 28 | return cls 29 | 30 | if issubclass(cls, enum.Enum): 31 | raise PedanticTypeCheckException(f'Enum "{cls}" cannot be decorated with "@pedantic_class". ' 32 | f'Enums are not supported yet.') 33 | 34 | if is_dataclass(obj=cls): 35 | raise PedanticTypeCheckException(f'Dataclass "{cls}" cannot be decorated with "@pedantic_class". ' 36 | f'Try to write "@dataclass" over "@pedantic_class".') 37 | 38 | for attr in cls.__dict__: 39 | attr_value = getattr(cls, attr) 40 | 41 | if isinstance(attr_value, (types.FunctionType, types.MethodType)): 42 | setattr(cls, attr, decorator(attr_value)) 43 | elif isinstance(attr_value, property): 44 | prop = attr_value 45 | wrapped_getter = _get_wrapped(prop=prop.fget, decorator=decorator) 46 | wrapped_setter = _get_wrapped(prop=prop.fset, decorator=decorator) 47 | wrapped_deleter = _get_wrapped(prop=prop.fdel, decorator=decorator) 48 | new_prop = property(fget=wrapped_getter, fset=wrapped_setter, fdel=wrapped_deleter) 49 | setattr(cls, attr, new_prop) 50 | 51 | _add_type_var_attr_and_method_to_class(cls=cls) 52 | return cls 53 | return decorate 54 | 55 | 56 | def pedantic_class(cls: C) -> C: 57 | """ Shortcut for @for_all_methods(pedantic) """ 58 | return for_all_methods(decorator=pedantic)(cls=cls) 59 | 60 | 61 | def pedantic_class_require_docstring(cls: C) -> C: 62 | """ Shortcut for @for_all_methods(pedantic_require_docstring) """ 63 | return for_all_methods(decorator=pedantic_require_docstring)(cls=cls) 64 | 65 | 66 | def trace_class(cls: C) -> C: 67 | """ Shortcut for @for_all_methods(trace) """ 68 | return for_all_methods(decorator=trace)(cls=cls) 69 | 70 | 71 | def timer_class(cls: C) -> C: 72 | """ Shortcut for @for_all_methods(timer) """ 73 | return for_all_methods(decorator=timer)(cls=cls) 74 | 75 | 76 | def _get_wrapped(prop: Optional[F], decorator: F) -> Optional[F]: 77 | return decorator(prop) if prop is not None else None 78 | 79 | 80 | def _add_type_var_attr_and_method_to_class(cls: C) -> None: 81 | def type_vars(self) -> Dict: 82 | t_vars = {TYPE_VAR_SELF: cls} 83 | 84 | if is_instance_of_generic_class(instance=self): 85 | type_vars_fifo = getattr(self, TYPE_VAR_ATTR_NAME, dict()) 86 | type_vars_generics = check_instance_of_generic_class_and_get_type_vars(instance=self) 87 | setattr(self, TYPE_VAR_ATTR_NAME, {**type_vars_fifo, **type_vars_generics, **t_vars}) 88 | else: 89 | setattr(self, TYPE_VAR_ATTR_NAME, t_vars) 90 | 91 | return getattr(self, TYPE_VAR_ATTR_NAME) 92 | 93 | setattr(cls, TYPE_VAR_METHOD_NAME, type_vars) 94 | -------------------------------------------------------------------------------- /pedantic/decorators/cls_deco_frozen_dataclass.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from dataclasses import dataclass, fields, replace 3 | from typing import Type, TypeVar, Any, Union, Callable, Dict 4 | 5 | from pedantic.get_context import get_context 6 | from pedantic.type_checking_logic.check_types import assert_value_matches_type 7 | 8 | T = TypeVar('T') 9 | 10 | 11 | def frozen_type_safe_dataclass(cls: Type[T]) -> Type[T]: 12 | """ Shortcut for @frozen_dataclass(type_safe=True) """ 13 | 14 | return frozen_dataclass(type_safe=True)(cls) 15 | 16 | 17 | def frozen_dataclass( 18 | cls: Type[T] = None, 19 | type_safe: bool = False, 20 | order: bool = False, 21 | kw_only: bool = True, 22 | slots: bool = False, 23 | ) -> Union[Type[T], Callable[[Type[T]], Type[T]]]: 24 | """ 25 | Makes the decorated class immutable and a dataclass by adding the [@dataclass(frozen=True)] 26 | decorator. Also adds useful copy_with() and validate_types() instance methods to this class (see below). 27 | 28 | If [type_safe] is True, a type check is performed for each field after the __post_init__ method was called 29 | which itself s directly called after the __init__ constructor. 30 | Note this have a negative impact on the performance. It's recommend to use this for debugging and testing only. 31 | 32 | In a nutshell, the followings methods will be added to the decorated class automatically: 33 | - __init__() gives you a simple constructor like "Foo(a=6, b='hi', c=True)" 34 | - __eq__() lets you compare objects easily with "a == b" 35 | - __hash__() is also needed for instance comparison 36 | - __repr__() gives you a nice output when you call "print(foo)" 37 | - copy_with() allows you to quickly create new similar frozen instances. Use this instead of setters. 38 | - deep_copy_with() allows you to create deep copies and modify them. 39 | - validate_types() allows you to validate the types of the dataclass. 40 | This is called automatically when [type_safe] is True. 41 | 42 | If the [order] parameter is True (default is False), the following comparison methods 43 | will be added additionally: 44 | - __lt__() lets you compare instance like "a < b" 45 | - __le__() lets you compare instance like "a <= b" 46 | - __gt__() lets you compare instance like "a > b" 47 | - __ge__() lets you compare instance like "a >= b" 48 | 49 | These compare the class as if it were a tuple of its fields, in order. 50 | Both instances in the comparison must be of the identical type. 51 | 52 | The parameters slots and kw_only are only applied if the Python version is greater or equal to 3.10. 53 | 54 | Example: 55 | 56 | >>> @frozen_dataclass 57 | ... class Foo: 58 | ... a: int 59 | ... b: str 60 | ... c: bool 61 | >>> foo = Foo(a=6, b='hi', c=True) 62 | >>> print(foo) 63 | Foo(a=6, b='hi', c=True) 64 | >>> print(foo.copy_with()) 65 | Foo(a=6, b='hi', c=True) 66 | >>> print(foo.copy_with(a=42)) 67 | Foo(a=42, b='hi', c=True) 68 | >>> print(foo.copy_with(b='Hello')) 69 | Foo(a=6, b='Hello', c=True) 70 | >>> print(foo.copy_with(c=False)) 71 | Foo(a=6, b='hi', c=False) 72 | >>> print(foo.copy_with(a=676676, b='new', c=False)) 73 | Foo(a=676676, b='new', c=False) 74 | """ 75 | 76 | def decorator(cls_: Type[T]) -> Type[T]: 77 | args = {'frozen': True, 'order': order, 'kw_only': kw_only, 'slots': slots} 78 | 79 | if type_safe: 80 | old_post_init = getattr(cls_, '__post_init__', lambda _: None) 81 | 82 | def new_post_init(self) -> None: 83 | old_post_init(self) 84 | context = get_context(depth=3, increase_depth_if_name_matches=[ 85 | copy_with.__name__, 86 | deep_copy_with.__name__, 87 | ]) 88 | self.validate_types(_context=context) 89 | 90 | setattr(cls_, '__post_init__', new_post_init) # must be done before applying dataclass() 91 | 92 | new_class = dataclass(**args)(cls_) # slots = True will create a new class! 93 | 94 | def copy_with(self, **kwargs: Any) -> T: 95 | """ 96 | Creates a new immutable instance that by copying all fields of this instance replaced by the new values. 97 | Keep in mind that this is a shallow copy! 98 | """ 99 | 100 | return replace(self, **kwargs) 101 | 102 | def deep_copy_with(self, **kwargs: Any) -> T: 103 | """ 104 | Creates a new immutable instance that by deep copying all fields of 105 | this instance replaced by the new values. 106 | """ 107 | 108 | current_values = {field.name: deepcopy(getattr(self, field.name)) for field in fields(self)} 109 | return new_class(**{**current_values, **kwargs}) 110 | 111 | def validate_types(self, *, _context: Dict[str, Type] = None) -> None: 112 | """ 113 | Checks that all instance variable have the correct type. 114 | Raises a [PedanticTypeCheckException] if at least one type is incorrect. 115 | """ 116 | 117 | props = fields(new_class) 118 | 119 | if _context is None: 120 | # method was called by user 121 | _context = get_context(depth=2) 122 | 123 | _context = {**_context, **self.__init__.__globals__, self.__class__.__name__: self.__class__} 124 | 125 | for field in props: 126 | assert_value_matches_type( 127 | value=getattr(self, field.name), 128 | type_=field.type, 129 | err=f'In dataclass "{cls_.__name__}" in field "{field.name}": ', 130 | type_vars={}, 131 | context=_context, 132 | ) 133 | 134 | methods_to_add = [copy_with, deep_copy_with, validate_types] 135 | 136 | for method in methods_to_add: 137 | setattr(new_class, method.__name__, method) 138 | 139 | return new_class 140 | 141 | if cls is None: 142 | return decorator 143 | 144 | return decorator(cls_=cls) 145 | 146 | 147 | if __name__ == '__main__': 148 | import doctest 149 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 150 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_context_manager.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager, asynccontextmanager 2 | from functools import wraps 3 | from inspect import isasyncgenfunction, isgeneratorfunction 4 | from typing import Callable, TypeVar, Iterator, ContextManager, AsyncContextManager, AsyncIterator 5 | 6 | T = TypeVar('T') 7 | 8 | 9 | def safe_contextmanager(f: Callable[..., Iterator[T]]) -> Callable[..., ContextManager[T]]: 10 | """ 11 | @safe_contextmanager decorator. 12 | 13 | Typical usage: 14 | 15 | @safe_contextmanager 16 | def some_generator(): 17 | 18 | yield 19 | 20 | 21 | equivalent to this: 22 | 23 | @contextmanager 24 | def some_generator(): 25 | 26 | try: 27 | yield 28 | finally: 29 | 30 | 31 | This makes this: 32 | 33 | with some_generator() as : 34 | 35 | 36 | equivalent to this: 37 | 38 | 39 | try: 40 | = 41 | 42 | finally: 43 | 44 | """ 45 | 46 | if isasyncgenfunction(f): 47 | raise AssertionError(f'{f.__name__} is async. So you need to use "safe_async_contextmanager" instead.') 48 | if not isgeneratorfunction(f): 49 | raise AssertionError(f'{f.__name__} is not a generator.') 50 | 51 | @wraps(f) 52 | def wrapper(*args, **kwargs) -> Iterator[T]: 53 | iterator = f(*args, **kwargs) 54 | 55 | try: 56 | yield next(iterator) 57 | finally: 58 | try: 59 | next(iterator) 60 | except StopIteration: 61 | pass # this is intended 62 | 63 | return contextmanager(wrapper) # type: ignore 64 | 65 | 66 | def safe_async_contextmanager(f: Callable[..., AsyncIterator[T]]) -> Callable[..., AsyncContextManager[T]]: 67 | """ 68 | @safe_async_contextmanager decorator. 69 | 70 | Note: You need Python 3.10 or newer for this. 71 | 72 | Typical usage: 73 | 74 | @safe_async_contextmanager 75 | async def some_async_generator(): 76 | 77 | yield 78 | 79 | 80 | equivalent to this: 81 | 82 | @asynccontextmanager 83 | async def some_async_generator(): 84 | 85 | try: 86 | yield 87 | finally: 88 | 89 | 90 | This makes this: 91 | 92 | async with some_async_generator() as : 93 | 94 | 95 | equivalent to this: 96 | 97 | 98 | try: 99 | = 100 | 101 | finally: 102 | 103 | """ 104 | 105 | if not isasyncgenfunction(f): 106 | if not isgeneratorfunction(f): 107 | raise AssertionError(f'{f.__name__} is not a generator.') 108 | 109 | raise AssertionError(f'{f.__name__} is not an async generator. ' 110 | f'So you need to use "safe_contextmanager" instead.') 111 | 112 | @wraps(f) 113 | async def wrapper(*args, **kwargs) -> Iterator[T]: 114 | iterator = f(*args, **kwargs) 115 | 116 | try: 117 | yield await anext(iterator) 118 | finally: 119 | try: 120 | await anext(iterator) 121 | except StopAsyncIteration: 122 | pass # this is intended 123 | 124 | return asynccontextmanager(wrapper) # type: ignore 125 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_count_calls.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import wraps 3 | from typing import Any 4 | 5 | from pedantic.constants import F, ReturnType 6 | 7 | 8 | def count_calls(func: F) -> F: 9 | """ 10 | Prints how often the method is called during program execution. 11 | 12 | Example: 13 | 14 | >>> @count_calls 15 | ... def often_used_method(): 16 | ... return 42 17 | >>> often_used_method() 18 | Count Calls: Call 1 of function 'often_used_method' at ... 19 | >>> often_used_method() 20 | Count Calls: Call 2 of function 'often_used_method' at ... 21 | >>> often_used_method() 22 | Count Calls: Call 3 of function 'often_used_method' at ... 23 | """ 24 | 25 | @wraps(func) 26 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 27 | wrapper.num_calls += 1 28 | print(f"Count Calls: Call {wrapper.num_calls} of function {func.__name__!r} at {datetime.now()}.") 29 | return func(*args, **kwargs) 30 | 31 | wrapper.num_calls = 0 32 | return wrapper 33 | 34 | 35 | if __name__ == "__main__": 36 | import doctest 37 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 38 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_deprecated.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any 3 | 4 | from pedantic.constants import F, ReturnType 5 | from pedantic.helper_methods import _raise_warning 6 | 7 | 8 | def deprecated(func: F) -> F: 9 | """ 10 | Use this decorator to mark a function as deprecated. It will raise a warning when the function is called. 11 | 12 | Example: 13 | 14 | >>> @deprecated 15 | ... def my_function(a, b, c): 16 | ... pass 17 | >>> my_function(5, 4, 3) 18 | """ 19 | 20 | @wraps(func) 21 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 22 | _raise_warning(msg=f'Call to deprecated function {func.__qualname__}.', category=DeprecationWarning) 23 | return func(*args, **kwargs) 24 | return wrapper 25 | 26 | 27 | if __name__ == "__main__": 28 | import doctest 29 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 30 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_does_same_as_function.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import wraps 3 | from typing import Any 4 | 5 | from pedantic.constants import F, ReturnType 6 | 7 | 8 | def does_same_as_function(other_func: F) -> F: 9 | """ 10 | Each time the decorated function is executed, the function other_func is also executed and the results 11 | are compared. An AssertionError is raised if the results are not equal. 12 | 13 | Example: 14 | 15 | >>> def other_calculation(a, b, c): 16 | ... return c + b + a 17 | >>> @does_same_as_function(other_calculation) 18 | ... def some_calculation(a, b, c): 19 | ... return a + b + c 20 | >>> some_calculation(1, 2, 3) 21 | 6 22 | """ 23 | 24 | def decorator(decorated_func: F) -> F: 25 | @wraps(decorated_func) 26 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 27 | result = decorated_func(*args, **kwargs) 28 | other = other_func(*args, **kwargs) 29 | 30 | if other != result: 31 | raise AssertionError(f'Different outputs: Function "{decorated_func.__name__}" returns {result} and ' 32 | f'function "{other_func.__name__}" returns {other} for parameters {args} {kwargs}') 33 | return result 34 | 35 | @wraps(decorated_func) 36 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 37 | result = await decorated_func(*args, **kwargs) 38 | 39 | if inspect.iscoroutinefunction(other_func): 40 | other = await other_func(*args, **kwargs) 41 | else: 42 | other = other_func(*args, **kwargs) 43 | 44 | if other != result: 45 | raise AssertionError(f'Different outputs: Function "{decorated_func.__name__}" returns {result} and ' 46 | f'function "{other_func.__name__}" returns {other} for parameters {args} {kwargs}') 47 | return result 48 | 49 | if inspect.iscoroutinefunction(decorated_func): 50 | return async_wrapper 51 | else: 52 | return wrapper 53 | 54 | return decorator 55 | 56 | 57 | if __name__ == "__main__": 58 | import doctest 59 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 60 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_in_subprocess.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | from functools import wraps 4 | from typing import Callable, TypeVar, Any, Awaitable, Optional, Type, Union 5 | 6 | try: 7 | from multiprocess import Process, Pipe 8 | from multiprocess.connection import Connection 9 | except ImportError: 10 | Process: Optional[Type] = None 11 | Pipe: Optional[Type] = None 12 | Connection: Optional[Type] = None 13 | 14 | T = TypeVar('T') 15 | 16 | 17 | class SubprocessError: 18 | """ Is returned by the subprocess if an error occurs in the subprocess. """ 19 | 20 | def __init__(self, ex: Exception) -> None: 21 | self.exception = ex 22 | 23 | 24 | def in_subprocess(func: Callable[..., Union[T, Awaitable[T]]]) -> Callable[..., Awaitable[T]]: 25 | """ 26 | Executes the decorated function in a subprocess and returns the return value of it. 27 | Note that the decorated function will be replaced with an async function which returns 28 | a coroutine that needs to be awaited. 29 | This purpose of this is doing long-taking calculations without blocking the main thread 30 | of your application synchronously. That ensures that other asyncio.Tasks can work without any problem 31 | at the same time. 32 | 33 | Example: 34 | >>> import time 35 | >>> import asyncio 36 | >>> @in_subprocess 37 | ... def f(value: int) -> int: 38 | ... time.sleep(0.1) # a long taking synchronous blocking calculation 39 | ... return 2 * value 40 | >>> asyncio.run(f(value=42)) 41 | 84 42 | """ 43 | 44 | @wraps(func) 45 | async def wrapper(*args: Any, **kwargs: Any) -> T: 46 | return await calculate_in_subprocess(func, *args, **kwargs) 47 | 48 | return wrapper 49 | 50 | 51 | async def calculate_in_subprocess(func: Callable[..., Union[T, Awaitable[T]]], *args: Any, **kwargs: Any) -> T: 52 | """ 53 | Calculates the result of a synchronous function in subprocess without blocking the current thread. 54 | 55 | Arguments: 56 | func: The function that will be called in a subprocess. 57 | args: Positional arguments that will be passed to the function. 58 | kwargs: Keyword arguments that will be passed to the function. 59 | 60 | Returns: 61 | The calculated result of the function "func". 62 | 63 | Raises: 64 | Any Exception that is raised inside [func]. 65 | 66 | Further reading: https://medium.com/devopss-hole/python-multiprocessing-pickle-issue-e2d35ccf96a9 67 | 68 | Example: 69 | >>> import time 70 | >>> import asyncio 71 | >>> def f(value: int) -> int: 72 | ... time.sleep(0.1) # a long taking synchronous blocking calculation 73 | ... return 2 * value 74 | >>> asyncio.run(calculate_in_subprocess(func=f, value=42)) 75 | 84 76 | """ 77 | 78 | if Pipe is None: 79 | raise ImportError('You need to install the multiprocess package to use this: pip install multiprocess') 80 | 81 | rx, tx = Pipe(duplex=False) # receiver & transmitter ; Pipe is one-way only 82 | process = Process(target=_inner, args=(tx, func, *args), kwargs=kwargs) 83 | process.start() 84 | 85 | event = asyncio.Event() 86 | loop = asyncio.get_event_loop() 87 | loop.add_reader(fd=rx.fileno(), callback=event.set) 88 | 89 | if not rx.poll(): # do not use process.is_alive() as condition here 90 | await event.wait() 91 | 92 | loop.remove_reader(fd=rx.fileno()) 93 | event.clear() 94 | 95 | result = rx.recv() 96 | process.join() # this blocks synchronously! make sure that process is terminated before you call join() 97 | rx.close() 98 | tx.close() 99 | 100 | if isinstance(result, SubprocessError): 101 | raise result.exception 102 | 103 | return result 104 | 105 | 106 | def _inner(tx: Connection, fun: Callable[..., Union[T, Awaitable[T]]], *a, **kw_args) -> None: 107 | """ This runs in another process. """ 108 | 109 | event_loop = None 110 | if inspect.iscoroutinefunction(fun): 111 | event_loop = asyncio.new_event_loop() 112 | asyncio.set_event_loop(event_loop) 113 | 114 | try: 115 | if event_loop is not None: 116 | res = event_loop.run_until_complete(fun(*a, **kw_args)) 117 | else: 118 | res = fun(*a, **kw_args) 119 | except Exception as ex: 120 | tx.send(SubprocessError(ex=ex)) 121 | else: 122 | tx.send(res) 123 | 124 | 125 | if __name__ == '__main__': 126 | import doctest 127 | 128 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 129 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_mock.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import wraps 3 | from typing import Any 4 | 5 | from pedantic.constants import ReturnType, F 6 | 7 | 8 | def mock(return_value: ReturnType) -> F: 9 | """ 10 | Skip the execution of the function and simply return the given return value instead. 11 | This can be useful you want to check quickly if the behavior of the function causes a specific problem. 12 | Without this decorator you actually need to change the implementation of the function temporarily. 13 | 14 | Example: 15 | 16 | >>> @mock(return_value=42) 17 | ... def my_function(a, b, c): 18 | ... return a + b + c 19 | >>> my_function(1, 2, 3) 20 | 42 21 | >>> my_function(1000, 88, 204) 22 | 42 23 | """ 24 | 25 | def decorator(func: F) -> F: 26 | @wraps(func) 27 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 28 | return return_value 29 | 30 | @wraps(func) 31 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 32 | return return_value 33 | 34 | if inspect.iscoroutinefunction(func): 35 | return async_wrapper 36 | else: 37 | return wrapper 38 | return decorator 39 | 40 | 41 | if __name__ == "__main__": 42 | import doctest 43 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 44 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_overrides.py: -------------------------------------------------------------------------------- 1 | from typing import Type 2 | 3 | from pedantic.constants import F 4 | from pedantic.exceptions import PedanticOverrideException 5 | 6 | 7 | def overrides(base_class: Type) -> F: 8 | """ 9 | This is used for marking methods that overrides methods of the base class which makes the code more readable. 10 | This decorator raises an Exception if the decorated method is not a method in the parent class. 11 | 12 | Example: 13 | 14 | >>> class Parent: 15 | ... def my_instance_method(self): 16 | ... pass 17 | >>> class Child(Parent): 18 | ... @overrides(Parent) 19 | ... def my_instance_method(self): 20 | ... print('hello world') 21 | """ 22 | 23 | def decorator(func: F) -> F: 24 | name = func.__name__ 25 | 26 | if name not in dir(base_class): 27 | raise PedanticOverrideException( 28 | f'In function {func.__qualname__}:\n ' 29 | f'Base class "{base_class.__name__}" does not have such a method "{name}".') 30 | return func 31 | return decorator 32 | 33 | 34 | if __name__ == "__main__": 35 | import doctest 36 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 37 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_pedantic.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any, Optional 3 | 4 | from pedantic.get_context import get_context 5 | from pedantic.type_checking_logic.check_docstring import _check_docstring 6 | from pedantic.constants import ReturnType, F 7 | from pedantic.models.decorated_function import DecoratedFunction 8 | from pedantic.models.function_call import FunctionCall 9 | from pedantic.env_var_logic import is_enabled 10 | 11 | 12 | def pedantic(func: Optional[F] = None, require_docstring: bool = False) -> F: 13 | """ 14 | A PedanticException is raised if one of the following happened: 15 | - The decorated function is called with positional arguments. 16 | - The function has no type annotation for their return type or one or more parameters do not have type 17 | annotations. 18 | - A type annotation is incorrect. 19 | - A type annotation misses type arguments, e.g. typing.List instead of typing.List[int]. 20 | - The documented arguments do not match the argument list or their type annotations. 21 | 22 | Example: 23 | 24 | >>> @pedantic 25 | ... def my_function(a: int, b: float, c: str) -> bool: 26 | ... return float(a) == b and str(b) == c 27 | >>> my_function(a=42.0, b=14.0, c='hi') 28 | Traceback (most recent call last): 29 | ... 30 | pedantic.exceptions.PedanticTypeCheckException: In function my_function: 31 | Type hint is incorrect: Argument a=42.0 of type does not match expected type . 32 | >>> my_function(a=42, b=None, c='hi') 33 | Traceback (most recent call last): 34 | ... 35 | pedantic.exceptions.PedanticTypeCheckException: In function my_function: 36 | Type hint is incorrect: Argument b=None of type does not match expected type . 37 | >>> my_function(a=42, b=42, c='hi') 38 | Traceback (most recent call last): 39 | ... 40 | pedantic.exceptions.PedanticTypeCheckException: In function my_function: 41 | Type hint is incorrect: Argument b=42 of type does not match expected type . 42 | >>> my_function(5, 4.0, 'hi') 43 | Traceback (most recent call last): 44 | ... 45 | pedantic.exceptions.PedanticCallWithArgsException: In function my_function: 46 | Use kwargs when you call function my_function. Args: (5, 4.0, 'hi') 47 | """ 48 | 49 | def decorator(f: F) -> F: 50 | if not is_enabled(): 51 | return f 52 | 53 | decorated_func = DecoratedFunction(func=f) 54 | 55 | if decorated_func.docstring is not None and (require_docstring or len(decorated_func.docstring.params)) > 0: 56 | _check_docstring(decorated_func=decorated_func) 57 | 58 | @wraps(f) 59 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 60 | call = FunctionCall(func=decorated_func, args=args, kwargs=kwargs, context=get_context(2)) 61 | call.assert_uses_kwargs() 62 | return call.check_types() 63 | 64 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 65 | call = FunctionCall(func=decorated_func, args=args, kwargs=kwargs, context=get_context(2)) 66 | call.assert_uses_kwargs() 67 | return await call.async_check_types() 68 | 69 | if decorated_func.is_coroutine: 70 | return async_wrapper 71 | else: 72 | return wrapper 73 | 74 | return decorator if func is None else decorator(f=func) 75 | 76 | 77 | def pedantic_require_docstring(func: Optional[F] = None) -> F: 78 | """Shortcut for @pedantic(require_docstring=True) """ 79 | return pedantic(func=func, require_docstring=True) 80 | 81 | 82 | if __name__ == "__main__": 83 | import doctest 84 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 85 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_rename_kwargs.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Callable 3 | 4 | from pedantic.constants import F, ReturnType 5 | 6 | 7 | class Rename: 8 | def __init__(self, from_: str, to: str) -> None: 9 | self.from_ = from_ 10 | self.to = to 11 | 12 | 13 | def rename_kwargs(*params: Rename) -> Callable[[F], F]: 14 | """ 15 | Renames the keyword arguments based on the given "Rename" rules when the decorated function is called. 16 | You can also use this to define aliases for keyword arguments. 17 | 18 | Example: 19 | 20 | >>> @rename_kwargs( 21 | ... Rename(from_='firstname', to='a'), 22 | ... Rename(from_='lastname', to='b'), 23 | ... ) 24 | ... def my_function(a, b): 25 | ... return a + ' ' + b 26 | >>> my_function(a='egon', b='olsen') # the normal way 27 | 'egon olsen' 28 | >>> my_function(firstname='egon', lastname='olsen') # using new defined keyword arguments 29 | 'egon olsen' 30 | """ 31 | 32 | param_dict = {p.from_: p.to for p in params} 33 | 34 | def decorator(func: F) -> F: 35 | @wraps(func) 36 | def wrapper(*args, **kwargs) -> ReturnType: 37 | result_kwargs = {} 38 | 39 | for k, v in kwargs.items(): 40 | if k in param_dict: 41 | result_kwargs[param_dict[k]] = kwargs[k] 42 | else: 43 | result_kwargs[k] = kwargs[k] 44 | 45 | return func(*args, **result_kwargs) 46 | return wrapper 47 | return decorator 48 | 49 | 50 | if __name__ == "__main__": 51 | import doctest 52 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 53 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_require_kwargs.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any 3 | 4 | from pedantic.constants import F, ReturnType 5 | from pedantic.models.decorated_function import DecoratedFunction 6 | from pedantic.models.function_call import FunctionCall 7 | 8 | 9 | def require_kwargs(func: F) -> F: 10 | """ 11 | Checks that each passed argument is a keyword argument. 12 | 13 | Example: 14 | 15 | >>> @require_kwargs 16 | ... def my_function(a, b, c): 17 | ... return a + b + c 18 | >>> my_function(5, 4, 3) 19 | Traceback (most recent call last): 20 | ... 21 | pedantic.exceptions.PedanticCallWithArgsException: In function my_function: 22 | Use kwargs when you call function my_function. Args: (5, 4, 3) 23 | >>> my_function(a=5, b=4, c=3) 24 | 12 25 | """ 26 | 27 | @wraps(func) 28 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 29 | decorated_func = DecoratedFunction(func=func) 30 | call = FunctionCall(func=decorated_func, args=args, kwargs=kwargs, context={}) 31 | call.assert_uses_kwargs() 32 | return func(*args, **kwargs) 33 | return wrapper 34 | 35 | 36 | if __name__ == "__main__": 37 | import doctest 38 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 39 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_retry.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from datetime import timedelta 4 | from functools import wraps 5 | from logging import Logger 6 | from typing import Callable, TypeVar, Any, ParamSpec 7 | 8 | C = TypeVar('C', bound=Callable) 9 | P = ParamSpec('P') 10 | R = TypeVar('R') 11 | 12 | 13 | def retry( 14 | *, 15 | attempts: int, 16 | exceptions: type[Exception] | tuple[type[Exception], ...] = Exception, 17 | sleep_time: timedelta = timedelta(seconds=0), 18 | logger: Logger = None, 19 | ) -> Callable[[C], C]: 20 | """ 21 | Retries the wrapped function/method `attempts` times if the exceptions listed 22 | in [exceptions] are thrown. 23 | 24 | Parameters: 25 | attempts: The number of times to repeat the wrapped function/method 26 | exceptions: Lists of exceptions that trigger a retry attempt. 27 | sleep_time: The time to wait between the retry attempts. 28 | logger: The logger used for logging. 29 | 30 | Example: 31 | >>> @retry(attempts=3, exceptions=(ValueError, TypeError)) 32 | ... def foo(): 33 | ... raise ValueError('Some error') 34 | >>> foo() 35 | """ 36 | 37 | def decorator(func: C) -> C: 38 | @wraps(func) 39 | def wrapper(*args, **kwargs) -> Any: 40 | return retry_func( 41 | func, 42 | *args, 43 | attempts=attempts, 44 | exceptions=exceptions, 45 | sleep_time=sleep_time, 46 | logger=logger, 47 | **kwargs, 48 | ) 49 | return wrapper 50 | return decorator 51 | 52 | 53 | def retry_func( 54 | func: Callable[P, R], 55 | *args: P.args, 56 | attempts: int, 57 | exceptions: type[Exception] | tuple[type[Exception], ...] = Exception, 58 | sleep_time: timedelta = timedelta(seconds=0), 59 | logger: Logger = None, 60 | **kwargs: P.kwargs, 61 | ) -> R: 62 | attempt = 1 63 | 64 | if logger is None: 65 | logger = logging.getLogger() 66 | 67 | while attempt < attempts: 68 | try: 69 | return func(*args, **kwargs) 70 | except exceptions: 71 | logger.warning(f'Exception thrown when attempting to run {func.__name__}, ' 72 | f'attempt {attempt} of {attempts}') 73 | attempt += 1 74 | time.sleep(sleep_time.total_seconds()) 75 | 76 | return func(*args, **kwargs) 77 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_timer.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from datetime import datetime 3 | from functools import wraps 4 | from typing import Any 5 | 6 | from pedantic.constants import F, ReturnType 7 | 8 | 9 | def timer(func: F) -> F: 10 | """ 11 | Prints how long the execution of the decorated function takes. 12 | 13 | Example: 14 | 15 | >>> @timer 16 | ... def long_taking_calculation(): 17 | ... return 42 18 | >>> long_taking_calculation() 19 | Timer: Finished function "long_taking_calculation" in 0:00:00... 20 | 42 21 | """ 22 | 23 | @wraps(func) 24 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 25 | start_time: datetime = datetime.now() 26 | value = func(*args, **kwargs) 27 | end_time = datetime.now() 28 | run_time = end_time - start_time 29 | print(f'Timer: Finished function "{func.__name__}" in {run_time}.') 30 | return value 31 | 32 | @wraps(func) 33 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 34 | start_time: datetime = datetime.now() 35 | value = await func(*args, **kwargs) 36 | end_time = datetime.now() 37 | run_time = end_time - start_time 38 | print(f'Timer: Finished function "{func.__name__}" in {run_time}.') 39 | return value 40 | 41 | if inspect.iscoroutinefunction(func): 42 | return async_wrapper 43 | else: 44 | return wrapper 45 | 46 | 47 | if __name__ == "__main__": 48 | import doctest 49 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 50 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_trace.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from datetime import datetime 3 | from functools import wraps 4 | from typing import Any 5 | 6 | from pedantic.constants import F, ReturnType 7 | 8 | 9 | def trace(func: F) -> F: 10 | """ 11 | Prints the passed arguments and the returned value on each function call. 12 | 13 | Example: 14 | 15 | >>> @trace 16 | ... def my_function(a, b, c): 17 | ... return a + b + c 18 | >>> my_function(4, 5, 6) 19 | Trace: ... calling my_function() with (4, 5, 6), {} 20 | Trace: ... my_function() returned 15 21 | 15 22 | """ 23 | 24 | @wraps(func) 25 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 26 | print(f'Trace: {datetime.now()} calling {func.__name__}() with {args}, {kwargs}') 27 | original_result = func(*args, **kwargs) 28 | print(f'Trace: {datetime.now()} {func.__name__}() returned {original_result!r}') 29 | return original_result 30 | 31 | @wraps(func) 32 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 33 | print(f'Trace: {datetime.now()} calling {func.__name__}() with {args}, {kwargs}') 34 | original_result = await func(*args, **kwargs) 35 | print(f'Trace: {datetime.now()} {func.__name__}() returned {original_result!r}') 36 | return original_result 37 | 38 | if inspect.iscoroutinefunction(func): 39 | return async_wrapper 40 | else: 41 | return wrapper 42 | 43 | 44 | if __name__ == "__main__": 45 | import doctest 46 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 47 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_trace_if_returns.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import wraps 3 | from typing import Any 4 | 5 | from pedantic.constants import ReturnType, F 6 | 7 | 8 | def trace_if_returns(return_value: ReturnType) -> F: 9 | """ 10 | Prints the passed arguments if and only if the decorated function returned the given return_value. 11 | This is useful if you want to figure out which input arguments leads to a special return value. 12 | 13 | Example: 14 | 15 | >>> @trace_if_returns(42) 16 | ... def my_function(a, b, c): 17 | ... return a + b + c 18 | >>> my_function(1, 2, 3) 19 | 6 20 | >>> my_function(10, 8, 24) 21 | Function my_function returned value 42 for args: (10, 8, 24) and kwargs: {} 22 | 42 23 | """ 24 | 25 | def decorator(func: F) -> F: 26 | @wraps(func) 27 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 28 | result = func(*args, **kwargs) 29 | 30 | if result == return_value: 31 | print(f'Function {func.__name__} returned value {result} for args: {args} and kwargs: {kwargs}') 32 | 33 | return result 34 | 35 | @wraps(func) 36 | async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: 37 | result = await func(*args, **kwargs) 38 | 39 | if result == return_value: 40 | print(f'Function {func.__name__} returned value {result} for args: {args} and kwargs: {kwargs}') 41 | 42 | return result 43 | 44 | if inspect.iscoroutinefunction(func): 45 | return async_wrapper 46 | else: 47 | return wrapper 48 | return decorator 49 | 50 | 51 | if __name__ == "__main__": 52 | import doctest 53 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 54 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_unimplemented.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any 3 | 4 | from pedantic.exceptions import NotImplementedException 5 | from pedantic.constants import F, ReturnType 6 | 7 | 8 | def unimplemented(func: F) -> F: 9 | """ 10 | For documentation purposes. Throw NotImplementedException if the function is called. 11 | 12 | Example: 13 | 14 | >>> @unimplemented 15 | ... def my_function(a, b, c): 16 | ... pass 17 | >>> my_function(5, 4, 3) 18 | Traceback (most recent call last): 19 | ... 20 | pedantic.exceptions.NotImplementedException: Function "my_function" is not implemented yet! 21 | """ 22 | 23 | @wraps(func) 24 | def wrapper(*args: Any, **kwargs: Any) -> ReturnType: 25 | raise NotImplementedException(f'Function "{func.__qualname__}" is not implemented yet!') 26 | return wrapper 27 | 28 | 29 | if __name__ == "__main__": 30 | import doctest 31 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 32 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostInDarkMath/pedantic-python-decorators/f29268701e8bb48e043affe5d128040aafcf441d/pedantic/decorators/fn_deco_validate/__init__.py -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/convert_value.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Any, Union 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ConversionError 4 | 5 | T = Union[bool, int, float, str, dict, list] 6 | 7 | 8 | def convert_value(value: Any, target_type: Type[T]) -> T: 9 | if isinstance(value, target_type): 10 | return value 11 | 12 | value = str(value).strip().lower() 13 | 14 | if target_type == bool: 15 | if value in ['true', '1']: 16 | return True 17 | elif value in ['false', '0']: 18 | return False 19 | 20 | raise ConversionError(f'Value {value} cannot be converted to bool.') 21 | 22 | try: 23 | if target_type == list: 24 | return [item.strip() for item in value.split(',')] 25 | elif target_type == dict: 26 | value = {item.split(':')[0].strip(): item.partition(':')[-1].strip() for item in value.split(',')} 27 | 28 | return target_type(value) 29 | except ValueError: 30 | raise ConversionError(f'Value {value} cannot be converted to {target_type}.') 31 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | 4 | class ExceptionDictKey: 5 | VALUE = 'VALUE' 6 | MESSAGE = 'MESSAGE' 7 | PARAMETER = 'PARAMETER' 8 | VALIDATOR = 'VALIDATOR' 9 | 10 | 11 | class ValidateException(Exception): 12 | """ The base class for all exception thrown by the validate decorator. """ 13 | 14 | def __init__(self, msg: str) -> None: 15 | self.message = msg 16 | 17 | 18 | class ValidatorException(ValidateException): 19 | """ An exception that is raised inside the validate() function of a Validator. """ 20 | 21 | def __init__(self, msg: str, validator_name: str, value: Any, parameter_name: str = '') -> None: 22 | super().__init__(msg=msg) 23 | self.validator_name = validator_name 24 | self.value = value 25 | self.parameter_name = parameter_name 26 | 27 | def __str__(self) -> str: 28 | return f'{self.validator_name}: {self.message} Value: {self.value}' 29 | 30 | 31 | class ParameterException(ValidateException): 32 | """ An exception that is raised inside a Parameter. """ 33 | 34 | def __init__(self, msg: str, parameter_name: str, 35 | value: Optional[Any] = None, validator_name: Optional[str] = None) -> None: 36 | super().__init__(msg=msg) 37 | self.validator_name = validator_name 38 | self.parameter_name = parameter_name 39 | self.value = value 40 | 41 | @classmethod 42 | def from_validator_exception(cls, exception: ValidatorException, parameter_name: str = '') -> 'ParameterException': 43 | """ Creates a parameter exception from a validator exception. """ 44 | return cls( 45 | value=exception.value, 46 | msg=exception.message, 47 | validator_name=exception.validator_name, 48 | parameter_name=parameter_name or exception.parameter_name, 49 | ) 50 | 51 | def __str__(self) -> str: 52 | return str(self.to_dict) 53 | 54 | @property 55 | def to_dict(self) -> Dict[str, str]: 56 | return { 57 | ExceptionDictKey.VALUE: str(self.value), 58 | ExceptionDictKey.MESSAGE: self.message, 59 | ExceptionDictKey.VALIDATOR: self.validator_name, 60 | ExceptionDictKey.PARAMETER: self.parameter_name, 61 | } 62 | 63 | 64 | class InvalidHeader(ParameterException): 65 | """ Is raised if there is a validation error in a FlaskHeaderParameter. """ 66 | 67 | 68 | class TooManyArguments(ValidateException): 69 | """ Is raised if the function got more arguments than expected. """ 70 | 71 | 72 | class ConversionError(ValidateException): 73 | """ Is raised if a type cast failed. """ 74 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract_parameter import Parameter 2 | from .abstract_external_parameter import ExternalParameter 3 | from .deserializable import Deserializable 4 | from .environment_variable_parameter import EnvironmentVariableParameter 5 | 6 | try: 7 | from .flask_parameters import FlaskJsonParameter, FlaskFormParameter, FlaskParameter, FlaskGetParameter, \ 8 | FlaskPathParameter, FlaskHeaderParameter, GenericFlaskDeserializer 9 | except ImportError: 10 | pass 11 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/abstract_external_parameter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | from pedantic.decorators.fn_deco_validate.parameters.abstract_parameter import Parameter 5 | 6 | 7 | class ExternalParameter(Parameter, ABC): 8 | @abstractmethod 9 | def has_value(self) -> bool: 10 | """ Returns True if the value can be fetched. """ 11 | 12 | @abstractmethod 13 | def load_value(self) -> Any: 14 | """Loads a value and returns it.""" 15 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/abstract_parameter.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Any, Type, Union, NoReturn, Optional 2 | 3 | from pedantic.decorators.fn_deco_validate.convert_value import convert_value 4 | from pedantic.decorators.fn_deco_validate.exceptions import ConversionError, ValidatorException, \ 5 | ParameterException 6 | from pedantic.decorators.fn_deco_validate.validators.abstract_validator import Validator 7 | 8 | 9 | class NoValue: 10 | pass 11 | 12 | 13 | class Parameter: 14 | exception_type: Type[ParameterException] = ParameterException 15 | 16 | def __init__(self, 17 | name: str, 18 | value_type: Type[Union[bool, int, float, str, dict, list]] = None, 19 | validators: Iterable[Validator] = None, 20 | default: Any = NoValue, 21 | required: bool = True, 22 | ) -> None: 23 | self.name = name 24 | self.validators = validators if validators else [] 25 | self.default_value = default 26 | self.value_type = value_type 27 | self.is_required = False if default != NoValue else required 28 | 29 | if value_type not in [str, bool, int, float, dict, list, None]: 30 | raise AssertionError(f'value_type needs to be one of these: str, bool, int, float, dict & list') 31 | 32 | def validate(self, value: Any) -> Any: 33 | """ Apply all validators to the given value and collect all ValidationErrors. """ 34 | 35 | if value is None: 36 | if self.is_required: 37 | self.raise_exception(msg=f'Value for key {self.name} is required.') 38 | 39 | return None 40 | 41 | if self.value_type is not None: 42 | try: 43 | result_value = convert_value(value=value, target_type=self.value_type) 44 | except ConversionError as ex: 45 | return self.raise_exception(value=value, msg=ex.message) 46 | else: 47 | result_value = value 48 | 49 | for validator in self.validators: 50 | try: 51 | result_value = validator.validate(result_value) 52 | except ValidatorException as e: 53 | raise self.exception_type.from_validator_exception(exception=e, parameter_name=self.name) 54 | 55 | return result_value 56 | 57 | def raise_exception(self, msg: str, value: Any = None, validator: Optional[Validator] = None) -> NoReturn: 58 | raise self.exception_type(value=value, parameter_name=self.name, msg=msg, 59 | validator_name=validator.name if validator else None) 60 | 61 | def __str__(self) -> str: 62 | return self.__class__.__name__ + ' name=' + self.name 63 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/deserializable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict 3 | 4 | 5 | class Deserializable(ABC): 6 | """ A tiny interface which has a static from_json() method which acts like a named constructor. """ 7 | 8 | @staticmethod 9 | @abstractmethod 10 | def from_json(data: Dict[str, Any]) -> 'Deserializable': 11 | """ A named constructor which creates an object from JSON. """ 12 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/environment_variable_parameter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Type, Iterable, Union 3 | 4 | from pedantic.decorators.fn_deco_overrides import overrides 5 | from pedantic.decorators.fn_deco_validate.parameters import ExternalParameter 6 | from pedantic.decorators.fn_deco_validate.parameters.abstract_parameter import NoValue 7 | from pedantic.decorators.fn_deco_validate.validators import Validator 8 | 9 | 10 | class EnvironmentVariableParameter(ExternalParameter): 11 | def __init__(self, 12 | name: str, 13 | env_var_name: str = None, 14 | value_type: Type[Union[str, bool, int, float]] = str, 15 | validators: Iterable[Validator] = None, 16 | required: bool = True, 17 | default: Any = NoValue, 18 | ) -> None: 19 | super().__init__(name=name, validators=validators, default=default, value_type=value_type, required=required) 20 | 21 | if value_type not in [str, bool, int, float]: 22 | raise AssertionError(f'value_type needs to be one of these: str, bool, int & float') 23 | 24 | if env_var_name is None: 25 | self._env_var_name = name 26 | else: 27 | self._env_var_name = env_var_name 28 | 29 | @overrides(ExternalParameter) 30 | def has_value(self) -> bool: 31 | return self._env_var_name in os.environ 32 | 33 | @overrides(ExternalParameter) 34 | def load_value(self) -> Any: 35 | return os.environ[self._env_var_name].strip() 36 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/parameters/flask_parameters.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Dict, Type 3 | 4 | from flask import request 5 | 6 | from pedantic.decorators.fn_deco_validate.exceptions import InvalidHeader, ParameterException, ValidatorException 7 | from pedantic.decorators.fn_deco_overrides import overrides 8 | from pedantic.decorators.fn_deco_validate.parameters import ExternalParameter, Parameter 9 | from pedantic.decorators.fn_deco_validate.parameters.deserializable import Deserializable 10 | 11 | 12 | class FlaskParameter(ExternalParameter, ABC): 13 | @abstractmethod 14 | def get_dict(self) -> Dict[str, Any]: 15 | """ Returns the actual values as a dictionary. """ 16 | 17 | @overrides(ExternalParameter) 18 | def has_value(self) -> bool: 19 | dict_ = self.get_dict() 20 | return dict_ is not None and self.name in dict_ 21 | 22 | @overrides(ExternalParameter) 23 | def load_value(self) -> Any: 24 | dict_ = self.get_dict() 25 | return dict_[self.name] 26 | 27 | 28 | class FlaskJsonParameter(FlaskParameter): 29 | @overrides(FlaskParameter) 30 | def get_dict(self) -> Dict: 31 | if not request.is_json: 32 | return {} 33 | 34 | return request.json 35 | 36 | 37 | class FlaskFormParameter(FlaskParameter): 38 | @overrides(FlaskParameter) 39 | def get_dict(self) -> Dict: 40 | return request.form 41 | 42 | 43 | class FlaskPathParameter(Parameter): 44 | """ 45 | This is a special case because Flask passes path parameter as kwargs to validate(). 46 | Therefore, this doesn't need to be an ExternalParameter. 47 | """ 48 | 49 | 50 | class FlaskGetParameter(FlaskParameter): 51 | @overrides(FlaskParameter) 52 | def get_dict(self) -> Dict: 53 | return request.args 54 | 55 | @overrides(ExternalParameter) 56 | def load_value(self) -> Any: 57 | value = request.args.getlist(self.name) 58 | 59 | if self.value_type == list: 60 | return value 61 | 62 | return value[0] 63 | 64 | 65 | class FlaskHeaderParameter(FlaskParameter): 66 | exception_type = InvalidHeader 67 | 68 | @overrides(FlaskParameter) 69 | def get_dict(self) -> Dict: 70 | return request.headers 71 | 72 | 73 | class GenericFlaskDeserializer(ExternalParameter): 74 | """ 75 | A JSON deserializer for classes which implements the [Deserializable] interface. 76 | 77 | Further reading: https://github.com/LostInDarkMath/pedantic-python-decorators/issues/55 78 | """ 79 | 80 | def __init__(self, cls: Type[Deserializable], catch_exception: bool = True, **kwargs) -> None: 81 | super().__init__(**kwargs) 82 | self._cls = cls 83 | self._catch_exceptions = catch_exception 84 | 85 | @overrides(ExternalParameter) 86 | def has_value(self) -> bool: 87 | return request.is_json 88 | 89 | @overrides(ExternalParameter) 90 | def load_value(self) -> Any: 91 | try: 92 | return self._cls.from_json(request.json) 93 | except ValidatorException as ex: 94 | raise ParameterException.from_validator_exception(exception=ex, parameter_name='') 95 | except Exception as ex: 96 | if self._catch_exceptions: 97 | self.raise_exception(msg=str(ex)) 98 | 99 | raise ex 100 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract_validator import Validator 2 | from .composite_validator import Composite 3 | from .datetime_isoformat import DatetimeIsoFormat 4 | from .datetime_unix_timestamp import DateTimeUnixTimestamp 5 | from .email import Email 6 | from .enum import IsEnum 7 | from .for_each import ForEach 8 | from .is_uuid import IsUuid 9 | from .match_pattern import MatchPattern 10 | from .max import Max 11 | from .max_length import MaxLength 12 | from .min import Min 13 | from .min_length import MinLength 14 | from .not_empty import NotEmpty 15 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/abstract_validator.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, NoReturn 3 | 4 | from pedantic.decorators.fn_deco_validate.exceptions import ValidatorException 5 | 6 | 7 | class Validator(ABC): 8 | @abstractmethod 9 | def validate(self, value: Any) -> Any: 10 | """ 11 | Validates and convert the value. 12 | Raises an [ValidatorException] in case of an invalid value. 13 | To raise this you can simply call self.raise_exception(). 14 | """ 15 | 16 | def validate_param(self, value: Any, parameter_name: str) -> Any: 17 | """ 18 | Validates and converts the value, just like [validate()]. 19 | The difference is that a parameter_name is included in the exception, if an exception is raised. 20 | """ 21 | 22 | try: 23 | return self.validate(value=value) 24 | except ValidatorException as ex: 25 | ex.parameter_name = parameter_name 26 | raise ex 27 | 28 | def raise_exception(self, value: Any, msg: str) -> NoReturn: 29 | raise ValidatorException(value=value, validator_name=self.name, msg=msg) 30 | 31 | @property 32 | def name(self) -> str: 33 | return self.__class__.__name__ 34 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/composite_validator.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Iterator, Any 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class Composite(Validator): 8 | def __init__(self, validators: Iterable[Validator]) -> None: 9 | self._validators = validators 10 | 11 | def __iter__(self) -> Iterator[Validator]: 12 | for validator in self._validators: 13 | yield validator 14 | 15 | @overrides(Validator) 16 | def validate(self, value: Any) -> Any: 17 | for validator in self: 18 | validator.validate(value) 19 | 20 | return value 21 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/datetime_isoformat.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class DatetimeIsoFormat(Validator): 8 | @overrides(Validator) 9 | def validate(self, value: str) -> datetime: 10 | try: 11 | value = datetime.fromisoformat(value) 12 | except (TypeError, ValueError, AttributeError): 13 | self.raise_exception(msg=f'invalid value: {value} is not a datetime in ISO format', value=value) 14 | 15 | return value 16 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/datetime_unix_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Union 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators import Validator 6 | 7 | 8 | class DateTimeUnixTimestamp(Validator): 9 | @overrides(Validator) 10 | def validate(self, value: Union[int, float, str]) -> datetime: 11 | if not isinstance(value, (int, float, str)): 12 | self.raise_exception(msg=f'Invalid seconds since 1970: {value}', value=value) 13 | 14 | try: 15 | seconds = float(value) 16 | except ValueError: 17 | return self.raise_exception(msg=f'Could parse {value} to float.', value=value) 18 | 19 | try: 20 | return datetime(year=1970, month=1, day=1) + timedelta(seconds=seconds) 21 | except OverflowError: 22 | return self.raise_exception( 23 | msg=f'Date value out of range. Make sure you send SECONDS since 1970. Got: {value}', value=value) 24 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/email.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Callable 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators import Validator 6 | 7 | REGEX_EMAIL = r"[^@\s]+@[^@\s]+\.[a-zA-Z0-9]+$" 8 | 9 | 10 | class Email(Validator): 11 | def __init__(self, email_pattern: str = REGEX_EMAIL, post_processor: Callable[[str], str] = lambda x: x) -> None: 12 | self._pattern = email_pattern 13 | self._post_processor = post_processor 14 | 15 | @overrides(Validator) 16 | def validate(self, value: str) -> str: 17 | if not re.fullmatch(pattern=self._pattern, string=value): 18 | self.raise_exception(msg=f'invalid email address: {value}', value=value) 19 | 20 | return self._post_processor(value) 21 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/enum.py: -------------------------------------------------------------------------------- 1 | from enum import EnumMeta, IntEnum 2 | from typing import Any 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators import Validator 6 | 7 | 8 | class IsEnum(Validator): 9 | def __init__(self, enum: EnumMeta, convert: bool = True, to_upper_case: bool = True) -> None: 10 | self._enum = enum 11 | self._convert = convert 12 | self._to_upper_case = to_upper_case 13 | 14 | @overrides(Validator) 15 | def validate(self, value: Any) -> Any: 16 | try: 17 | if isinstance(value, str) and self._to_upper_case: 18 | value = value.upper() 19 | 20 | if issubclass(self._enum, IntEnum): 21 | enum_value = self._enum(int(value)) 22 | else: 23 | enum_value = self._enum(value) 24 | except (ValueError, TypeError): 25 | return self.raise_exception(msg=f'Incorrect value {value} for enum {self._enum}.', value=value) 26 | 27 | if self._convert: 28 | return enum_value 29 | 30 | return value 31 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/for_each.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from typing import Any, Iterable, List, Union 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators import Validator 6 | 7 | 8 | class ForEach(Validator): 9 | def __init__(self, validators: Union[Validator, Iterable[Validator]]) -> None: 10 | if isinstance(validators, Validator): 11 | self._validators = [validators] 12 | else: 13 | self._validators = validators 14 | 15 | @overrides(Validator) 16 | def validate(self, value: Iterable[Any]) -> List[Any]: 17 | if not isinstance(value, collections.abc.Iterable): 18 | self.raise_exception(msg=f'{value} is not iterable.', value=value) 19 | 20 | results = [] 21 | 22 | for item in value: 23 | for validator in self._validators: 24 | item = validator.validate(item) 25 | 26 | results.append(item) 27 | 28 | return results 29 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/is_uuid.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class IsUuid(Validator): 8 | def __init__(self, convert: bool = False) -> None: 9 | self._convert = convert 10 | 11 | @overrides(Validator) 12 | def validate(self, value: str) -> str: 13 | try: 14 | converted_value = UUID(str(value)) 15 | except ValueError: 16 | return self.raise_exception(msg=f'{value} is not a valid UUID', value=value) 17 | 18 | return converted_value if self._convert else value 19 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/match_pattern.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class MatchPattern(Validator): 8 | def __init__(self, pattern: str) -> None: 9 | self._pattern = re.compile(pattern=pattern) 10 | 11 | @overrides(Validator) 12 | def validate(self, value: str) -> str: 13 | if not self._pattern.search(string=str(value)): 14 | self.raise_exception(msg=f'Value "{value}" does not match pattern {self._pattern.pattern}.', value=value) 15 | 16 | return value 17 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/max.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class Max(Validator): 8 | def __init__(self, value: Union[int, float], include_boundary: bool = True) -> None: 9 | """ 10 | >>> Max(7, True).validate(7) 11 | True 12 | >>> Max(7, False).validate(7) 13 | False 14 | >>> Max(7, False).validate(6.999) 15 | True 16 | """ 17 | self._value = value 18 | self._include_boundary = include_boundary 19 | 20 | @overrides(Validator) 21 | def validate(self, value: Union[int, float]) -> Union[int, float]: 22 | if value > self._value and self._include_boundary: 23 | self.raise_exception(msg=f'greater then allowed: {value} is not <= {self._value}', value=value) 24 | elif value >= self._value and not self._include_boundary: 25 | self.raise_exception(msg=f'greater then allowed: {value} is not < {self._value}', value=value) 26 | 27 | return value 28 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/max_length.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from typing import Sized, Any 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators.abstract_validator import Validator 6 | 7 | 8 | class MaxLength(Validator): 9 | def __init__(self, length: int) -> None: 10 | self._length = length 11 | 12 | @overrides(Validator) 13 | def validate(self, value: Sized) -> Any: 14 | if not isinstance(value, collections.abc.Sized): 15 | self.raise_exception(msg=f'{value} has no length.', value=value) 16 | 17 | if len(value) > self._length: 18 | self.raise_exception(msg=f'{value} is too long with length {len(value)}.', value=value) 19 | 20 | return value 21 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/min.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pedantic import overrides 4 | from pedantic.decorators.fn_deco_validate.validators import Validator 5 | 6 | 7 | class Min(Validator): 8 | def __init__(self, value: Union[int, float], include_boundary: bool = True) -> None: 9 | """ 10 | >>> Min(7, True).validate(7) 11 | True 12 | >>> Min(7, False).validate(7) 13 | False 14 | >>> Min(7, False).validate(7.001) 15 | True 16 | """ 17 | self._value = value 18 | self._include_boundary = include_boundary 19 | 20 | @overrides(Validator) 21 | def validate(self, value: Union[int, float]) -> Union[int, float]: 22 | if value < self._value and self._include_boundary: 23 | self.raise_exception(msg=f'smaller then allowed: {value} is not >= {self._value}', value=value) 24 | elif value <= self._value and not self._include_boundary: 25 | self.raise_exception(msg=f'smaller then allowed: {value} is not > {self._value}', value=value) 26 | 27 | return value 28 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/min_length.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from typing import Sized, Any 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators.abstract_validator import Validator 6 | 7 | 8 | class MinLength(Validator): 9 | def __init__(self, length: int) -> None: 10 | self._length = length 11 | 12 | @overrides(Validator) 13 | def validate(self, value: Sized) -> Any: 14 | if not isinstance(value, collections.abc.Sized): 15 | self.raise_exception(msg=f'{value} has no length.', value=value) 16 | 17 | if len(value) < self._length: 18 | self.raise_exception(msg=f'{value} is too long with length {len(value)}.', value=value) 19 | 20 | return value 21 | -------------------------------------------------------------------------------- /pedantic/decorators/fn_deco_validate/validators/not_empty.py: -------------------------------------------------------------------------------- 1 | import collections 2 | from typing import Sequence 3 | 4 | from pedantic import overrides 5 | from pedantic.decorators.fn_deco_validate.validators.abstract_validator import Validator 6 | 7 | 8 | class NotEmpty(Validator): 9 | def __init__(self, strip: bool = True) -> None: 10 | self.strip = strip 11 | 12 | @overrides(Validator) 13 | def validate(self, value: Sequence) -> Sequence: 14 | """ 15 | Throws a ValidationError if the sequence is empty. 16 | If the sequence is a string, it removes all leading and trailing whitespace. 17 | """ 18 | 19 | if isinstance(value, str): 20 | if not value.strip(): 21 | self.raise_exception(msg=f'Got empty String which is invalid.', value=value) 22 | 23 | return value.strip() if self.strip else value 24 | elif isinstance(value, collections.abc.Sequence): 25 | if len(value) == 0: 26 | raise self.raise_exception(msg=f'Got empty which is invalid.', value=value) 27 | 28 | return value 29 | 30 | self.raise_exception(msg=f'Got {type(value)} which is not a Sequence.', value=value) 31 | -------------------------------------------------------------------------------- /pedantic/env_var_logic.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | ENVIRONMENT_VARIABLE_NAME = 'ENABLE_PEDANTIC' 4 | 5 | 6 | def enable_pedantic() -> None: 7 | os.environ[ENVIRONMENT_VARIABLE_NAME] = '1' 8 | 9 | 10 | def disable_pedantic() -> None: 11 | os.environ[ENVIRONMENT_VARIABLE_NAME] = '0' 12 | 13 | 14 | def is_enabled() -> bool: 15 | if ENVIRONMENT_VARIABLE_NAME not in os.environ: 16 | return True 17 | 18 | return os.environ[ENVIRONMENT_VARIABLE_NAME] == '1' 19 | -------------------------------------------------------------------------------- /pedantic/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostInDarkMath/pedantic-python-decorators/f29268701e8bb48e043affe5d128040aafcf441d/pedantic/examples/__init__.py -------------------------------------------------------------------------------- /pedantic/examples/config.csv: -------------------------------------------------------------------------------- 1 | 42 2 | 0.26 3 | -------------------------------------------------------------------------------- /pedantic/examples/validate.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | 4 | from pedantic import validate, ExternalParameter, overrides, Validator, Parameter, Min, ReturnAs 5 | 6 | 7 | @dataclass(frozen=True) 8 | class Configuration: 9 | iterations: int 10 | max_error: float 11 | 12 | 13 | class ConfigurationValidator(Validator): 14 | @overrides(Validator) 15 | def validate(self, value: Configuration) -> Configuration: 16 | if value.iterations < 1 or value.max_error < 0: 17 | self.raise_exception(msg=f'Invalid configuration: {value}', value=value) 18 | 19 | return value 20 | 21 | 22 | class ConfigFromEnvVar(ExternalParameter): 23 | """ Reads the configuration from environment variables. """ 24 | 25 | @overrides(ExternalParameter) 26 | def has_value(self) -> bool: 27 | return 'iterations' in os.environ and 'max_error' in os.environ 28 | 29 | @overrides(ExternalParameter) 30 | def load_value(self) -> Configuration: 31 | return Configuration( 32 | iterations=int(os.environ['iterations']), 33 | max_error=float(os.environ['max_error']), 34 | ) 35 | 36 | 37 | class ConfigFromFile(ExternalParameter): 38 | """ Reads the configuration from a config file. """ 39 | 40 | @overrides(ExternalParameter) 41 | def has_value(self) -> bool: 42 | return os.path.isfile('config.csv') 43 | 44 | @overrides(ExternalParameter) 45 | def load_value(self) -> Configuration: 46 | with open(file='config.csv', mode='r') as file: 47 | content = file.readlines() 48 | return Configuration( 49 | iterations=int(content[0].strip('\n')), 50 | max_error=float(content[1]), 51 | ) 52 | 53 | 54 | # choose your configuration source here: 55 | @validate(ConfigFromEnvVar(name='config', validators=[ConfigurationValidator()]), strict=False, return_as=ReturnAs.KWARGS_WITH_NONE) 56 | # @validate(ConfigFromFile(name='config', validators=[ConfigurationValidator()]), strict=False) 57 | 58 | # with strict_mode = True (which is the default) 59 | # you need to pass a Parameter for each parameter of the decorated function 60 | # @validate( 61 | # Parameter(name='value', validators=[Min(5, include_boundary=False)]), 62 | # ConfigFromFile(name='config', validators=[ConfigurationValidator()]), 63 | # ) 64 | def my_algorithm(value: float, config: Configuration) -> float: 65 | """ 66 | This method calculates something that depends on the given value with considering the configuration. 67 | Note how well this small piece of code is designed: 68 | - Fhe function my_algorithm() need a Configuration but has no knowledge where this come from. 69 | - Furthermore, it need does not care about parameter validation. 70 | - The ConfigurationValidator doesn't now anything about the creation of the data. 71 | - The @validate decorator is the only you need to change, if you want a different configuration source. 72 | """ 73 | print(value) 74 | print(config) 75 | return value 76 | 77 | 78 | if __name__ == '__main__': 79 | # we can call the function with a config like there is no decorator. 80 | # This makes testing extremely easy: no config files, no environment variables or stuff like that 81 | print(my_algorithm(value=2, config=Configuration(iterations=3, max_error=4.4))) 82 | 83 | os.environ['iterations'] = '12' 84 | os.environ['max_error'] = '3.1415' 85 | 86 | # but we also can omit the config and load it implicitly by our custom Parameters 87 | print(my_algorithm(value=42.0)) 88 | -------------------------------------------------------------------------------- /pedantic/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class NotImplementedException(Exception): 3 | pass 4 | 5 | 6 | class PedanticException(Exception): 7 | pass 8 | 9 | 10 | class PedanticTypeCheckException(PedanticException): 11 | pass 12 | 13 | 14 | class PedanticDocstringException(PedanticException): 15 | pass 16 | 17 | 18 | class PedanticOverrideException(PedanticException): 19 | pass 20 | 21 | 22 | class PedanticCallWithArgsException(PedanticException): 23 | pass 24 | 25 | 26 | class PedanticTypeVarMismatchException(PedanticException): 27 | pass 28 | -------------------------------------------------------------------------------- /pedantic/get_context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Type, Dict, List 3 | 4 | 5 | def get_context(depth: int = 1, increase_depth_if_name_matches: List[str] = None) -> Dict[str, Type]: 6 | """ 7 | Get the context of a frame at the given depth of the current call stack. 8 | See also: https://docs.python.org/3/library/sys.html#sys._getframe 9 | """ 10 | 11 | frame = sys._getframe(depth) 12 | name = frame.f_code.co_name 13 | 14 | if name in (increase_depth_if_name_matches or []): 15 | frame = sys._getframe(depth + 1) 16 | 17 | return {**frame.f_globals, **frame.f_locals} 18 | -------------------------------------------------------------------------------- /pedantic/helper_methods.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Type 3 | 4 | 5 | def _raise_warning(msg: str, category: Type[Warning]) -> None: 6 | warnings.simplefilter(action='always', category=category) 7 | warnings.warn(message=msg, category=category, stacklevel=2) 8 | warnings.simplefilter(action='default', category=category) 9 | -------------------------------------------------------------------------------- /pedantic/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .generic_mixin import GenericMixin 2 | from .with_decorated_methods import create_decorator, DecoratorType, WithDecoratedMethods 3 | -------------------------------------------------------------------------------- /pedantic/mixins/generic_mixin.py: -------------------------------------------------------------------------------- 1 | from types import GenericAlias 2 | from typing import List, Type, TypeVar, Dict, Generic, Any, Optional 3 | 4 | 5 | class GenericMixin: 6 | """ 7 | A mixin that provides easy access to given type variables. 8 | 9 | Example: 10 | >>> from typing import Generic, TypeVar 11 | >>> T = TypeVar('T') 12 | >>> U = TypeVar('U') 13 | >>> class Foo(Generic[T, U], GenericMixin): 14 | ... values: List[T] 15 | ... value: U 16 | >>> f = Foo[str, int]() 17 | >>> f.type_vars 18 | {~T: , ~U: } 19 | """ 20 | 21 | @property 22 | def type_var(self) -> Type: 23 | """ 24 | Get the type variable for this class. 25 | Use this for convenience if your class has only one type parameter. 26 | 27 | Example: 28 | >>> from typing import Generic, TypeVar 29 | >>> T = TypeVar('T') 30 | >>> class Foo(Generic[T], GenericMixin): 31 | ... value: T 32 | >>> f = Foo[float]() 33 | >>> f.type_var 34 | 35 | """ 36 | 37 | types = self._get_types() 38 | assert len(types) == 1, f'You have multiple type parameters. Please use "type_vars" instead of "type_var".' 39 | return list(types.values())[0] # type: ignore 40 | 41 | @property 42 | def type_vars(self) -> Dict[TypeVar, Type]: 43 | """ 44 | Returns the mapping of type variables to types. 45 | 46 | Example: 47 | >>> from typing import Generic, TypeVar 48 | >>> T = TypeVar('T') 49 | >>> U = TypeVar('U') 50 | >>> class Foo(Generic[T, U], GenericMixin): 51 | ... values: List[T] 52 | ... value: U 53 | >>> f = Foo[str, int]() 54 | >>> f.type_vars 55 | {~T: , ~U: } 56 | """ 57 | 58 | return self._get_types() 59 | 60 | def _get_types(self) -> Dict[TypeVar, Type]: 61 | """ 62 | See https://stackoverflow.com/questions/57706180/generict-base-class-how-to-get-type-of-t-from-within-instance/60984681#60984681 63 | """ 64 | 65 | non_generic_error = AssertionError( 66 | f'{self.class_name} is not a generic class. To make it generic, declare it like: ' 67 | f'class {self.class_name}(Generic[T], GenericMixin):...') 68 | 69 | if not hasattr(self, '__orig_bases__'): 70 | raise non_generic_error 71 | 72 | generic_base = get_generic_base(obj=self) 73 | 74 | if not generic_base: 75 | for base in self.__orig_bases__: # type: ignore # (we checked existence above) 76 | if not hasattr(base, '__origin__'): 77 | continue 78 | 79 | generic_base = get_generic_base(base.__origin__) 80 | 81 | if generic_base: 82 | types = base.__args__ 83 | break 84 | else: 85 | if not hasattr(self, '__orig_class__'): 86 | raise AssertionError( 87 | f'You need to instantiate this class with type parameters! Example: {self.class_name}[int]()\n' 88 | f'Also make sure that you do not call this in the __init__() method of your class! ' 89 | f'See also https://github.com/python/cpython/issues/90899') 90 | 91 | types = self.__orig_class__.__args__ # type: ignore 92 | 93 | type_vars = generic_base.__args__ 94 | return {v: t for v, t in zip(type_vars, types)} 95 | 96 | @property 97 | def class_name(self) -> str: 98 | """ Get the name of the class of this instance. """ 99 | 100 | return type(self).__name__ 101 | 102 | 103 | def get_generic_base(obj: Any) -> Optional[GenericAlias]: 104 | generic_bases = [c for c in obj.__orig_bases__ if hasattr(c, '__origin__') and c.__origin__ == Generic] 105 | 106 | if generic_bases: 107 | return generic_bases[0] # this is safe because a class can have at most one "Generic" superclass 108 | 109 | return None 110 | 111 | if __name__ == '__main__': 112 | import doctest 113 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 114 | -------------------------------------------------------------------------------- /pedantic/mixins/with_decorated_methods.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from enum import StrEnum 3 | from typing import TypeVar, Callable, Generic 4 | 5 | from pedantic.mixins.generic_mixin import GenericMixin 6 | 7 | 8 | class DecoratorType(StrEnum): 9 | """ 10 | The interface that defines all possible decorators types. 11 | 12 | The values of this enum are used as property names and the properties are added to the decorated functions. 13 | So I would recommend naming them with a leading underscore to keep them private and also write it lowercase. 14 | Example: 15 | >>> class Decorators(DecoratorType): 16 | ... FOO = '_foo' 17 | """ 18 | 19 | 20 | E = TypeVar('E', bound=DecoratorType) 21 | T = TypeVar('T') 22 | C = TypeVar('C', bound=Callable) 23 | 24 | 25 | def create_decorator( 26 | decorator_type: DecoratorType, 27 | transformation: Callable[[C, DecoratorType, T], C] = None, 28 | ) -> Callable[[T], Callable[[C], C]]: 29 | """ 30 | Creates a new decorator that is parametrized with one argument of an arbitrary type. 31 | You can also pass an arbitrary [transformation] to add custom behavior to the decorator. 32 | """ 33 | 34 | def decorator(value: T) -> Callable[[C], C]: 35 | def fun(f: C) -> C: 36 | setattr(f, decorator_type, value) 37 | 38 | if transformation is None: 39 | return f 40 | 41 | return transformation(f, decorator_type, value) 42 | 43 | return fun # we do not need functools.wraps, because we return the original function here 44 | 45 | return decorator 46 | 47 | 48 | class WithDecoratedMethods(ABC, Generic[E], GenericMixin): 49 | """ 50 | A mixin that is used to figure out which method is decorated with custom parameterized decorators. 51 | Example: 52 | >>> class Decorators(DecoratorType): 53 | ... FOO = '_foo' 54 | ... BAR = '_bar' 55 | >>> foo = create_decorator(decorator_type=Decorators.FOO) 56 | >>> bar = create_decorator(decorator_type=Decorators.BAR) 57 | >>> class MyClass(WithDecoratedMethods[Decorators]): 58 | ... @foo(42) 59 | ... def m1(self) -> None: 60 | ... print('bar') 61 | ... 62 | ... @foo(value=43) 63 | ... def m2(self) -> None: 64 | ... print('bar') 65 | ... 66 | ... @bar(value=44) 67 | ... def m3(self) -> None: 68 | ... print('bar') 69 | >>> instance = MyClass() 70 | >>> instance.get_decorated_functions() 71 | { 72 | : { 73 | >: 42, 74 | >: 43, 75 | }, 76 | : { 77 | >: 44, 78 | } 79 | } 80 | """ 81 | 82 | def get_decorated_functions(self) -> dict[E, dict[C, T]]: 83 | decorator_types = self.type_var 84 | decorated_functions = {t: dict() for t in decorator_types} # type: ignore 85 | 86 | for attribute_name in dir(self): 87 | if attribute_name.startswith('__'): 88 | continue 89 | 90 | attribute = getattr(self, attribute_name) 91 | 92 | for decorator_type in decorator_types: # type: ignore 93 | if hasattr(attribute, decorator_type): 94 | decorated_functions[decorator_type][attribute] = getattr(attribute, decorator_type) 95 | 96 | return decorated_functions 97 | -------------------------------------------------------------------------------- /pedantic/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .decorated_function import DecoratedFunction 2 | from .function_call import FunctionCall 3 | from .generator_wrapper import GeneratorWrapper 4 | -------------------------------------------------------------------------------- /pedantic/models/decorated_function.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import re 3 | import types 4 | from typing import Any, Callable, Dict, Optional 5 | 6 | try: 7 | from docstring_parser import parse, Docstring 8 | IS_DOCSTRING_PARSER_INSTALLED = True 9 | except ImportError: 10 | IS_DOCSTRING_PARSER_INSTALLED = False 11 | Docstring = None 12 | parse = None 13 | 14 | from pedantic.exceptions import PedanticTypeCheckException 15 | 16 | FUNCTIONS_THAT_REQUIRE_KWARGS = [ 17 | '__new__', '__init__', '__str__', '__del__', '__int__', '__float__', '__complex__', '__oct__', '__hex__', 18 | '__index__', '__trunc__', '__repr__', '__unicode__', '__hash__', '__nonzero__', '__dir__', '__sizeof__' 19 | ] 20 | 21 | 22 | class DecoratedFunction: 23 | def __init__(self, func: Callable[..., Any]) -> None: 24 | self._func = func 25 | 26 | if not isinstance(func, (types.FunctionType, types.MethodType)): 27 | raise PedanticTypeCheckException(f'{self.full_name} should be a method or function.') 28 | 29 | self._full_arg_spec = inspect.getfullargspec(func) 30 | self._signature = inspect.signature(func) 31 | self._err = f'In function {func.__qualname__}:\n' 32 | self._source: str = inspect.getsource(object=func) 33 | 34 | if IS_DOCSTRING_PARSER_INSTALLED: 35 | self._docstring = parse(func.__doc__) 36 | else: # pragma: no cover 37 | self._docstring = None 38 | 39 | @property 40 | def func(self) -> Callable[..., Any]: 41 | return self._func 42 | 43 | @property 44 | def annotations(self) -> Dict[str, Any]: 45 | return self._full_arg_spec.annotations 46 | 47 | @property 48 | def docstring(self) -> Optional[Docstring]: 49 | """ 50 | Returns the docstring if the docstring-parser package is installed else None. 51 | See also https://pypi.org/project/docstring-parser/ 52 | """ 53 | 54 | return self._docstring 55 | 56 | @property 57 | def raw_doc(self) -> Optional[str]: 58 | return self._func.__doc__ 59 | 60 | @property 61 | def signature(self) -> inspect.Signature: 62 | return self._signature 63 | 64 | @property 65 | def err(self) -> str: 66 | return self._err 67 | 68 | @property 69 | def source(self) -> str: 70 | return self._source 71 | 72 | @property 73 | def name(self) -> str: 74 | return self._func.__name__ 75 | 76 | @property 77 | def full_name(self) -> str: 78 | return self._func.__qualname__ 79 | 80 | @property 81 | def is_static_method(self) -> bool: 82 | """ I honestly have no idea how to do this better :( """ 83 | 84 | return '@staticmethod' in self.source 85 | 86 | @property 87 | def wants_args(self) -> bool: 88 | return '*args' in self.source 89 | 90 | @property 91 | def is_property_setter(self) -> bool: 92 | return f'@{self.name}.setter' in self.source 93 | 94 | @property 95 | def should_have_kwargs(self) -> bool: 96 | if self.is_property_setter or self.wants_args: 97 | return False 98 | elif not self.name.startswith('__') or not self.name.endswith('__'): 99 | return True 100 | return self.name in FUNCTIONS_THAT_REQUIRE_KWARGS 101 | 102 | @property 103 | def is_instance_method(self) -> bool: 104 | return self._full_arg_spec.args != [] and self._full_arg_spec.args[0] == 'self' 105 | 106 | @property 107 | def is_class_method(self) -> bool: 108 | """ 109 | Returns true if the function is decoratorated with the @classmethod decorator. 110 | See also: https://stackoverflow.com/questions/19227724/check-if-a-function-uses-classmethod 111 | """ 112 | 113 | return inspect.ismethod(self._func) 114 | 115 | @property 116 | def num_of_decorators(self) -> int: 117 | return len(re.findall('@', self.source.split('def')[0])) 118 | 119 | @property 120 | def is_pedantic(self) -> bool: 121 | return '@pedantic' in self.source or '@require_kwargs' in self.source 122 | 123 | @property 124 | def is_coroutine(self) -> bool: 125 | return inspect.iscoroutinefunction(self._func) 126 | 127 | @property 128 | def is_generator(self) -> bool: 129 | return inspect.isgeneratorfunction(self._func) 130 | -------------------------------------------------------------------------------- /pedantic/models/generator_wrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Any, Dict, Iterable, Iterator, TypeVar 2 | 3 | from pedantic.type_checking_logic.check_types import assert_value_matches_type, get_type_arguments, get_base_generic 4 | from pedantic.exceptions import PedanticTypeCheckException 5 | 6 | 7 | class GeneratorWrapper: 8 | def __init__(self, wrapped: Generator, expected_type: Any, err_msg: str, type_vars: Dict[TypeVar, Any]) -> None: 9 | self._generator = wrapped 10 | self._err = err_msg 11 | self._yield_type = None 12 | self._send_type = None 13 | self._return_type = None 14 | self._type_vars = type_vars 15 | self._initialized = False 16 | 17 | self._set_and_check_return_types(expected_return_type=expected_type) 18 | 19 | def __iter__(self) -> 'GeneratorWrapper': 20 | return self 21 | 22 | def __next__(self) -> Any: 23 | return self.send(obj=None) 24 | 25 | def __getattr__(self, name: str) -> Any: 26 | return getattr(self._generator, name) 27 | 28 | def throw(self, *args) -> Any: 29 | return self._generator.throw(*args) 30 | 31 | def close(self) -> None: 32 | self._generator.close() 33 | 34 | def send(self, obj) -> Any: 35 | if self._initialized: 36 | assert_value_matches_type(value=obj, type_=self._send_type, type_vars=self._type_vars, err=self._err) 37 | else: 38 | self._initialized = True 39 | 40 | try: 41 | returned_value = self._generator.send(obj) 42 | except StopIteration as ex: 43 | assert_value_matches_type(value=ex.value, 44 | type_=self._return_type, 45 | type_vars=self._type_vars, 46 | err=self._err) 47 | raise ex 48 | 49 | assert_value_matches_type(value=returned_value, 50 | type_=self._yield_type, 51 | type_vars=self._type_vars, 52 | err=self._err) 53 | return returned_value 54 | 55 | def _set_and_check_return_types(self, expected_return_type: Any) -> Any: 56 | base_generic = get_base_generic(cls=expected_return_type) 57 | 58 | if base_generic not in [Generator, Iterable, Iterator]: 59 | raise PedanticTypeCheckException( 60 | f'{self._err}Generator should have type annotation "typing.Generator[]", "typing.Iterator[]" or ' 61 | f'"typing.Iterable[]". Got "{expected_return_type}" instead.') 62 | 63 | result = get_type_arguments(expected_return_type) 64 | 65 | if len(result) == 1: 66 | self._yield_type = result[0] 67 | elif len(result) == 3: 68 | self._yield_type = result[0] 69 | self._send_type = result[1] 70 | self._return_type = result[2] 71 | else: 72 | raise PedanticTypeCheckException(f'{self._err}Generator should have a type argument. Got: {result}') 73 | return result[0] 74 | -------------------------------------------------------------------------------- /pedantic/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pedantic/tests/test_assert_value_matches_type.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from abc import ABC 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | from typing import Callable, Awaitable, Coroutine, Any, Union, Optional, Generic, TypeVar, Tuple 6 | 7 | from pedantic.exceptions import PedanticTypeCheckException 8 | from pedantic.type_checking_logic.check_types import assert_value_matches_type 9 | 10 | 11 | @dataclass 12 | class Foo: 13 | value: int 14 | 15 | 16 | class TestAssertValueMatchesType(unittest.TestCase): 17 | def test_callable(self): 18 | def _cb(foo: Foo) -> str: 19 | return str(foo.value) 20 | 21 | assert_value_matches_type( 22 | value=_cb, 23 | type_=Callable[..., str], 24 | err='', 25 | type_vars={}, 26 | ) 27 | 28 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 29 | assert_value_matches_type( 30 | value=_cb, 31 | type_=Callable[..., int], 32 | err='', 33 | type_vars={}, 34 | ) 35 | 36 | def test_callable_return_type_none(self): 37 | def _cb(foo: Foo) -> None: 38 | return print(foo) 39 | 40 | assert_value_matches_type( 41 | value=_cb, 42 | type_=Callable[..., None], 43 | err='', 44 | type_vars={}, 45 | ) 46 | 47 | def test_callable_awaitable(self): 48 | async def _cb(foo: Foo) -> str: 49 | return str(foo.value) 50 | 51 | assert_value_matches_type( 52 | value=_cb, 53 | type_=Callable[..., Awaitable[str]], 54 | err='', 55 | type_vars={}, 56 | ) 57 | 58 | assert_value_matches_type( 59 | value=_cb, 60 | type_=Callable[..., Awaitable[Any]], 61 | err='', 62 | type_vars={}, 63 | ) 64 | 65 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 66 | assert_value_matches_type( 67 | value=_cb, 68 | type_=Callable[..., Awaitable[int]], 69 | err='', 70 | type_vars={}, 71 | ) 72 | 73 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 74 | assert_value_matches_type( 75 | value=_cb, 76 | type_=Callable[..., str], 77 | err='', 78 | type_vars={}, 79 | ) 80 | 81 | def test_callable_coroutine(self): 82 | async def _cb(foo: Foo) -> str: 83 | return str(foo.value) 84 | 85 | assert_value_matches_type( 86 | value=_cb, 87 | type_=Callable[..., Coroutine[None, None, str]], 88 | err='', 89 | type_vars={}, 90 | ) 91 | 92 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 93 | assert_value_matches_type( 94 | value=_cb, 95 | type_=Callable[..., Coroutine[None, None, int]], 96 | err='', 97 | type_vars={}, 98 | ) 99 | 100 | def test_callable_awaitable_with_none_return_type(self): 101 | async def _cb(foo: Foo) -> None: 102 | print(foo) 103 | 104 | assert_value_matches_type( 105 | value=_cb, 106 | type_=Callable[..., Awaitable[None]], 107 | err='', 108 | type_vars={}, 109 | ) 110 | 111 | assert_value_matches_type( 112 | value=_cb, 113 | type_=Callable[..., Awaitable[Any]], 114 | err='', 115 | type_vars={}, 116 | ) 117 | 118 | def test_callable_with_old_union_type_hint(self): 119 | async def _cb(machine_id: str) -> Union[int, None]: 120 | return 42 121 | 122 | assert_value_matches_type( 123 | value=_cb, 124 | type_=Callable[..., Awaitable[Union[int, None]]], 125 | err='', 126 | type_vars={}, 127 | ) 128 | 129 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 130 | assert_value_matches_type( 131 | value=_cb, 132 | type_=Callable[..., Awaitable[int]], 133 | err='', 134 | type_vars={}, 135 | ) 136 | 137 | def test_callable_with_new_union_type_hint(self): 138 | async def _cb(machine_id: str) -> int | None: 139 | return 42 140 | 141 | assert_value_matches_type( 142 | value=_cb, 143 | type_=Callable[..., Awaitable[int | None]], 144 | err='', 145 | type_vars={}, 146 | ) 147 | 148 | assert_value_matches_type( 149 | value=_cb, 150 | type_=Callable[..., Awaitable[Any]], 151 | err='', 152 | type_vars={}, 153 | ) 154 | 155 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 156 | assert_value_matches_type( 157 | value=_cb, 158 | type_=Callable[..., Awaitable[int]], 159 | err='', 160 | type_vars={}, 161 | ) 162 | 163 | def test_forward_ref_inheritance(self): 164 | T = TypeVar('T') 165 | 166 | class State(Generic[T], ABC): 167 | pass 168 | 169 | class StateMachine(Generic[T], ABC): 170 | pass 171 | 172 | class MachineState(State['MachineStateMachine']): 173 | pass 174 | 175 | class OfflineMachineState(MachineState): 176 | pass 177 | 178 | class MachineStateMachine(StateMachine[MachineState]): 179 | pass 180 | 181 | assert_value_matches_type( 182 | value=OfflineMachineState(), 183 | type_=Optional['MachineState'], 184 | err='', 185 | type_vars={}, 186 | context=locals(), 187 | ) 188 | 189 | def test_tuple_with_ellipsis(self): 190 | """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/75 """ 191 | 192 | assert_value_matches_type( 193 | value=(1, 2.0, 'hello'), 194 | type_=Tuple[Any, ...], 195 | err='', 196 | type_vars={}, 197 | context=locals(), 198 | ) 199 | 200 | def test_union_of_callable(self): 201 | """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/74 """ 202 | 203 | assert_value_matches_type( 204 | value=datetime.now(), 205 | type_=Union[datetime, Callable[[], datetime]], 206 | err='', 207 | type_vars={}, 208 | context=locals(), 209 | ) 210 | -------------------------------------------------------------------------------- /pedantic/tests/test_async_context_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic.decorators import safe_async_contextmanager 4 | 5 | 6 | class TestAsyncContextManager(unittest.IsolatedAsyncioTestCase): 7 | async def test_safe_context_manager_no_exception(self): 8 | before = False 9 | after = False 10 | 11 | @safe_async_contextmanager 12 | async def foo(): 13 | nonlocal before, after 14 | before = True 15 | yield 42 16 | after = True 17 | 18 | self.assertFalse(before) 19 | self.assertFalse(after) 20 | 21 | async with foo() as f: 22 | self.assertTrue(before) 23 | self.assertFalse(after) 24 | self.assertEqual(42, f) 25 | 26 | self.assertTrue(before) 27 | self.assertTrue(after) 28 | 29 | async def test_safe_context_manager_with_exception(self): 30 | before = False 31 | after = False 32 | 33 | @safe_async_contextmanager 34 | async def foo(): 35 | nonlocal before, after 36 | before = True 37 | yield 42 38 | after = True 39 | 40 | self.assertFalse(before) 41 | self.assertFalse(after) 42 | 43 | with self.assertRaises(expected_exception=ValueError): 44 | async with foo() as f: 45 | self.assertTrue(before) 46 | self.assertFalse(after) 47 | self.assertEqual(42, f) 48 | raise ValueError('oh no') 49 | 50 | self.assertTrue(before) 51 | self.assertTrue(after) 52 | 53 | async def test_safe_context_manager_with_args_kwargs(self): 54 | @safe_async_contextmanager 55 | async def foo(a, b): 56 | yield a, b 57 | 58 | async with foo(42, b=43) as f: 59 | self.assertEqual((42, 43), f) 60 | 61 | def test_safe_context_manager_async(self): 62 | with self.assertRaises(expected_exception=AssertionError) as e: 63 | @safe_async_contextmanager 64 | def foo(a, b): 65 | yield a, b 66 | 67 | expected = 'foo is not an async generator. So you need to use "safe_contextmanager" instead.' 68 | self.assertEqual(expected, e.exception.args[0]) 69 | 70 | async def test_safe_context_manager_non_generator(self): 71 | with self.assertRaises(expected_exception=AssertionError) as e: 72 | @safe_async_contextmanager 73 | async def foo(a, b): 74 | return a, b 75 | 76 | expected = 'foo is not a generator.' 77 | self.assertEqual(expected, e.exception.args[0]) 78 | -------------------------------------------------------------------------------- /pedantic/tests/test_context_manager.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators import safe_contextmanager 4 | 5 | 6 | class TestContextManager(TestCase): 7 | def test_safe_context_manager_no_exception(self): 8 | before = False 9 | after = False 10 | 11 | @safe_contextmanager 12 | def foo(): 13 | nonlocal before, after 14 | before = True 15 | yield 42 16 | after = True 17 | 18 | self.assertFalse(before) 19 | self.assertFalse(after) 20 | 21 | with foo() as f: 22 | self.assertTrue(before) 23 | self.assertFalse(after) 24 | self.assertEqual(42, f) 25 | 26 | self.assertTrue(before) 27 | self.assertTrue(after) 28 | 29 | def test_safe_context_manager_with_exception(self): 30 | before = False 31 | after = False 32 | 33 | @safe_contextmanager 34 | def foo(): 35 | nonlocal before, after 36 | before = True 37 | yield 42 38 | after = True 39 | 40 | self.assertFalse(before) 41 | self.assertFalse(after) 42 | 43 | with self.assertRaises(expected_exception=ValueError): 44 | with foo() as f: 45 | self.assertTrue(before) 46 | self.assertFalse(after) 47 | self.assertEqual(42, f) 48 | raise ValueError('oh no') 49 | 50 | self.assertTrue(before) 51 | self.assertTrue(after) 52 | 53 | def test_safe_context_manager_with_args_kwargs(self): 54 | @safe_contextmanager 55 | def foo(a, b): 56 | yield a, b 57 | 58 | with foo(42, b=43) as f: 59 | self.assertEqual((42, 43), f) 60 | 61 | def test_safe_context_manager_async(self): 62 | with self.assertRaises(expected_exception=AssertionError) as e: 63 | @safe_contextmanager 64 | async def foo(a, b): 65 | yield a, b 66 | 67 | expected = 'foo is async. So you need to use "safe_async_contextmanager" instead.' 68 | self.assertEqual(expected, e.exception.args[0]) 69 | 70 | def test_safe_context_manager_non_generator(self): 71 | with self.assertRaises(expected_exception=AssertionError) as e: 72 | @safe_contextmanager 73 | def foo(a, b): 74 | return a, b 75 | 76 | expected = 'foo is not a generator.' 77 | self.assertEqual(expected, e.exception.args[0]) 78 | -------------------------------------------------------------------------------- /pedantic/tests/test_generator_wrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Iterator 2 | from unittest import TestCase 3 | 4 | from pedantic.models import GeneratorWrapper 5 | 6 | 7 | class TestGeneratorWrapper(TestCase): 8 | def test_generator_wrapper(self) -> None: 9 | def gen_func() -> Iterator[int]: 10 | num = 0 11 | 12 | while num < 100: 13 | yield num 14 | num += 1 15 | 16 | generator = gen_func() 17 | 18 | g = GeneratorWrapper( 19 | wrapped=generator, 20 | expected_type=Iterator[int], 21 | err_msg='msg', 22 | type_vars={}, 23 | ) 24 | 25 | print(sum([x for x in g])) 26 | 27 | with self.assertRaises(expected_exception=Exception): 28 | g.throw(Exception('error')) 29 | 30 | with self.assertRaises(expected_exception=AttributeError): 31 | g.invalid 32 | 33 | g.close() 34 | -------------------------------------------------------------------------------- /pedantic/tests/test_generic_mixin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import TypeVar, Generic, List, Type 3 | 4 | from pedantic import GenericMixin 5 | 6 | T = TypeVar('T') 7 | U = TypeVar('U') 8 | 9 | 10 | class TestGenericMixin(unittest.TestCase): 11 | def test_single_type_var(self): 12 | class Foo(Generic[T], GenericMixin): 13 | value: T 14 | 15 | foo = Foo[str]() 16 | assert foo.type_var == str 17 | assert foo.type_vars == {T: str} 18 | 19 | invalid = Foo() 20 | 21 | with self.assertRaises(expected_exception=AssertionError) as err: 22 | invalid.type_var 23 | 24 | assert f'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.exception.args[0] 25 | 26 | def test_multiple_type_vars(self): 27 | class Foo(Generic[T, U], GenericMixin): 28 | value: T 29 | values: List[U] 30 | 31 | foo = Foo[str, int]() 32 | 33 | with self.assertRaises(expected_exception=AssertionError) as err: 34 | foo.type_var 35 | 36 | self.assertEqual(err.exception.args[0], 'You have multiple type parameters. ' 37 | 'Please use "type_vars" instead of "type_var".') 38 | 39 | assert foo.type_vars == {T: str, U: int} 40 | 41 | invalid = Foo() 42 | 43 | with self.assertRaises(expected_exception=AssertionError) as err: 44 | invalid.type_var 45 | 46 | assert f'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.exception.args[0] 47 | 48 | def test_non_generic_class(self): 49 | class Foo(GenericMixin): 50 | value: int 51 | 52 | invalid = Foo() 53 | 54 | with self.assertRaises(expected_exception=AssertionError) as err: 55 | invalid.type_var 56 | 57 | self.assertEqual(err.exception.args[0], f'Foo is not a generic class. To make it generic, declare it like: ' 58 | f'class Foo(Generic[T], GenericMixin):...') 59 | 60 | def test_call_type_var_in_constructor(self): 61 | class Foo(Generic[T], GenericMixin): 62 | def __init__(self) -> None: 63 | self.x = self.type_var() 64 | 65 | with self.assertRaises(expected_exception=AssertionError) as err: 66 | Foo[str]() 67 | 68 | assert 'make sure that you do not call this in the __init__() method' in err.exception.args[0] 69 | 70 | def test_subclass_set_type_variable(self): 71 | class Gen(Generic[T], GenericMixin): 72 | def __init__(self, value: T) -> None: 73 | self.value = value 74 | 75 | def get_type(self) -> dict[TypeVar, Type]: 76 | return self.type_vars 77 | 78 | class MyClass(Gen[int]): 79 | pass 80 | 81 | bar = Gen[int](value=4) 82 | assert bar.get_type() == {T: int} 83 | 84 | foo = MyClass(value=4) 85 | assert foo.get_type() == {T: int} 86 | 87 | def test_subclass_with_multiple_parents(self): 88 | class Gen(Generic[T], GenericMixin): 89 | def __init__(self, value: T) -> None: 90 | self.value = value 91 | 92 | def get_type(self) -> dict[TypeVar, Type]: 93 | return self.type_vars 94 | 95 | class MyMixin: 96 | value = 42 97 | 98 | class MyClass(MyMixin, Gen[int]): 99 | pass 100 | 101 | bar = Gen[int](value=4) 102 | assert bar.get_type() == {T: int} 103 | 104 | foo = MyClass(value=4) 105 | assert foo.get_type() == {T: int} 106 | -------------------------------------------------------------------------------- /pedantic/tests/test_in_subprocess.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import unittest 4 | from typing import NoReturn 5 | 6 | from multiprocess import Pipe 7 | 8 | from pedantic import in_subprocess 9 | from pedantic.decorators.fn_deco_in_subprocess import _inner, SubprocessError 10 | 11 | 12 | class TestInSubprocess(unittest.IsolatedAsyncioTestCase): 13 | async def test_in_subprocess_simple(self): 14 | @in_subprocess 15 | def f() -> int: 16 | return 42 17 | 18 | assert await f() == 42 19 | 20 | async def test_in_subprocess_custom_object(self): 21 | class Foo: 22 | def __init__(self, v) -> None: 23 | self._value = v 24 | 25 | @in_subprocess 26 | def f() -> Foo: 27 | return Foo(v=42) 28 | 29 | assert (await f())._value == 42 30 | 31 | async def test_in_subprocess_simple_async(self): 32 | @in_subprocess 33 | async def f() -> int: 34 | return 42 35 | 36 | assert await f() == 42 37 | 38 | async def test_in_subprocess_no_args(self): 39 | @in_subprocess 40 | def f() -> int: 41 | time.sleep(0.1) 42 | return 42 43 | 44 | async def t() -> None: 45 | for _ in range(6): 46 | await asyncio.sleep(0.01) 47 | nonlocal counter 48 | counter += 1 49 | 50 | counter = 0 51 | task = asyncio.create_task(t()) 52 | assert await f() == 42 53 | assert counter >= 5 54 | await task 55 | 56 | async def test_in_subprocess_no_args_no_return(self): 57 | @in_subprocess 58 | def f() -> None: 59 | time.sleep(0.1) 60 | 61 | assert await f() is None 62 | 63 | async def test_in_subprocess_exception(self): 64 | @in_subprocess 65 | def f() -> NoReturn: 66 | raise RuntimeError('foo') 67 | 68 | with self.assertRaises(expected_exception=RuntimeError): 69 | await f() 70 | 71 | async def test_not_in_subprocess_blocks(self): 72 | async def f() -> int: 73 | time.sleep(0.1) 74 | return 42 75 | 76 | async def t() -> None: 77 | for _ in range(6): 78 | await asyncio.sleep(0.05) 79 | nonlocal counter 80 | counter += 1 81 | 82 | counter = 0 83 | task = asyncio.create_task(t()) 84 | assert await f() == 42 85 | assert counter == 0 86 | await task 87 | 88 | async def test_in_subprocess_with_arguments(self): 89 | @in_subprocess 90 | def f(a: int, b: int) -> int: 91 | return a + b 92 | 93 | assert await f(4, 5) == 9 94 | assert await f(a=4, b=5) == 9 95 | 96 | def test_inner_function_sync(self): 97 | """ Needed for line coverage""" 98 | 99 | rx, tx = Pipe(duplex=False) 100 | _inner(tx, lambda x: 1 / x, x=42) 101 | assert rx.recv() == 1 / 42 102 | 103 | _inner(tx, lambda x: 1 / x, x=0) 104 | ex = rx.recv() 105 | assert isinstance(ex, SubprocessError) 106 | 107 | def test_inner_function_async(self): 108 | """ Needed for line coverage""" 109 | 110 | async def foo(x): 111 | return 1/x 112 | 113 | rx, tx = Pipe(duplex=False) 114 | _inner(tx, foo, x=42) 115 | assert rx.recv() == 1 / 42 116 | 117 | _inner(tx, foo, x=0) 118 | ex = rx.recv() 119 | assert isinstance(ex, SubprocessError) 120 | 121 | async def test_in_subprocess_instance_method(self): 122 | class Foo: 123 | async def pos_args(self) -> int: 124 | return await self.f(4, 5) 125 | 126 | async def kw_args(self) -> int: 127 | return await self.f(a=4, b=5) 128 | 129 | @in_subprocess 130 | def f(self, a: int, b: int) -> int: 131 | return a + b 132 | 133 | foo = Foo() 134 | assert await foo.pos_args() == 9 135 | assert await foo.kw_args() == 9 136 | 137 | async def test_in_subprocess_static_method(self): 138 | class Foo: 139 | async def pos_args(self) -> int: 140 | return await self.f(4, 5) 141 | 142 | async def kw_args(self) -> int: 143 | return await self.f(a=4, b=5) 144 | 145 | @staticmethod 146 | @in_subprocess 147 | def f(a: int, b: int) -> int: 148 | return a + b 149 | 150 | foo = Foo() 151 | assert await foo.pos_args() == 9 152 | assert await foo.kw_args() == 9 153 | -------------------------------------------------------------------------------- /pedantic/tests/test_rename_kwargs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic import rename_kwargs, Rename 4 | 5 | 6 | class TestRenameKwargs(unittest.TestCase): 7 | def test_rename_kwargs(self): 8 | @rename_kwargs( 9 | Rename(from_='x', to='a'), 10 | Rename(from_='y', to='b'), 11 | ) 12 | def operation(a: int, b: int) -> int: 13 | return a + b 14 | 15 | operation(4, 5) 16 | operation(a=4, b=5) 17 | operation(x=4, y=5) 18 | operation(x=4, b=5) 19 | -------------------------------------------------------------------------------- /pedantic/tests/test_resolve_forward_ref.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | from unittest import TestCase 3 | 4 | from pedantic.type_checking_logic.resolve_forward_ref import resolve_forward_ref 5 | 6 | 7 | class TestResolveForwardRef(TestCase): 8 | def test_resolve_forward_ref_primitive_types(self): 9 | assert resolve_forward_ref(type_='int') == int 10 | assert resolve_forward_ref(type_='float') == float 11 | assert resolve_forward_ref(type_='str') == str 12 | assert resolve_forward_ref(type_='bool') == bool 13 | 14 | def test_resolve_forward_ref_typing_types(self): 15 | assert resolve_forward_ref(type_='List[int]') == List[int] 16 | assert resolve_forward_ref(type_='Optional[List[Union[str, float]]]') == Optional[List[Union[str, float]]] 17 | 18 | def test_unresolvable_type(self): 19 | with self.assertRaises(NameError): 20 | resolve_forward_ref(type_='Invalid') 21 | 22 | def test_resolve_forward_ref_custom_class(self): 23 | class Foo: 24 | pass 25 | 26 | context = locals() 27 | assert resolve_forward_ref(type_='Foo', context=context) == Foo 28 | assert resolve_forward_ref(type_='Optional[Foo]', context=context) == Optional[Foo] 29 | -------------------------------------------------------------------------------- /pedantic/tests/test_retry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic import retry 4 | from pedantic.decorators.fn_deco_retry import retry_func 5 | 6 | 7 | class TestRetry(unittest.TestCase): 8 | def test_retry_positive_no_args(self): 9 | count = 0 10 | 11 | @retry(attempts=5) 12 | def foo(): 13 | nonlocal count 14 | count += 1 15 | 16 | foo() 17 | assert count == 1 18 | 19 | def test_retry_positive_args_and_kwargs(self): 20 | count = 0 21 | 22 | @retry(attempts=5) 23 | def foo(x, y): 24 | nonlocal count 25 | count += x + y 26 | 27 | foo(12, y=42) 28 | assert count == 54 29 | 30 | def test_retry_positive_no_args_fails_every_time(self): 31 | count = 0 32 | 33 | @retry(attempts=5) 34 | def foo(): 35 | nonlocal count 36 | count += 1 37 | raise ValueError('foo') 38 | 39 | with self.assertRaises(ValueError): 40 | foo() 41 | 42 | assert count == 5 43 | 44 | def test_retry_positive_no_args_fails_different_exception_type(self): 45 | count = 0 46 | 47 | @retry(attempts=5, exceptions=AssertionError) 48 | def foo(): 49 | nonlocal count 50 | count += 1 51 | raise ValueError('foo') 52 | 53 | with self.assertRaises(ValueError): 54 | foo() 55 | 56 | assert count == 1 57 | 58 | def test_retry_fails_same_exception_type(self): 59 | count = 0 60 | 61 | @retry(attempts=5, exceptions=AssertionError) 62 | def foo(): 63 | nonlocal count 64 | count += 1 65 | raise AssertionError('foo') 66 | 67 | with self.assertRaises(AssertionError): 68 | foo() 69 | 70 | assert count == 5 71 | 72 | def test_retry_positive_no_args_fails_on_first_times(self): 73 | count = 0 74 | 75 | @retry(attempts=5) 76 | def foo() -> int: 77 | nonlocal count 78 | count += 1 79 | 80 | if count < 3: 81 | raise ValueError('foo') 82 | 83 | return count 84 | 85 | assert foo() == 3 86 | assert count == 3 87 | 88 | 89 | class TestRetryFunc(unittest.TestCase): 90 | def test_retry_positive_no_args(self): 91 | count = 0 92 | 93 | def foo(): 94 | nonlocal count 95 | count += 1 96 | 97 | retry_func(func=foo, attempts=5) 98 | assert count == 1 99 | 100 | def test_retry_positive_args_and_kwargs(self): 101 | count = 0 102 | 103 | def foo(x, y): 104 | nonlocal count 105 | count += x + y 106 | 107 | retry_func(foo, 12, attempts=5, y=42) 108 | assert count == 54 109 | 110 | def test_retry_positive_no_args_fails_every_time(self): 111 | count = 0 112 | 113 | def foo(): 114 | nonlocal count 115 | count += 1 116 | raise ValueError('foo') 117 | 118 | with self.assertRaises(ValueError): 119 | retry_func(func=foo, attempts=5) 120 | 121 | assert count == 5 122 | 123 | def test_retry_positive_no_args_fails_different_exception_type(self): 124 | count = 0 125 | 126 | def foo(): 127 | nonlocal count 128 | count += 1 129 | raise ValueError('foo') 130 | 131 | with self.assertRaises(ValueError): 132 | retry_func(func=foo, attempts=5, exceptions=AssertionError) 133 | 134 | assert count == 1 135 | 136 | def test_retry_fails_same_exception_type(self): 137 | count = 0 138 | 139 | def foo(): 140 | nonlocal count 141 | count += 1 142 | raise AssertionError('foo') 143 | 144 | with self.assertRaises(AssertionError): 145 | retry_func(func=foo, attempts=5, exceptions=AssertionError) 146 | 147 | assert count == 5 148 | 149 | def test_retry_positive_no_args_fails_on_first_times(self): 150 | count = 0 151 | 152 | def foo() -> int: 153 | nonlocal count 154 | count += 1 155 | 156 | if count < 3: 157 | raise ValueError('foo') 158 | 159 | return count 160 | 161 | assert retry_func(func=foo, attempts=5) == 3 162 | assert count == 3 163 | -------------------------------------------------------------------------------- /pedantic/tests/test_with_decorated_methods.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from functools import wraps 3 | 4 | from pedantic import DecoratorType, create_decorator, WithDecoratedMethods 5 | 6 | 7 | class Decorators(DecoratorType): 8 | FOO = '_foo' 9 | BAR = '_bar' 10 | 11 | 12 | foo = create_decorator(decorator_type=Decorators.FOO) 13 | bar = create_decorator(decorator_type=Decorators.BAR) 14 | 15 | 16 | class TestWithDecoratedMethods(unittest.TestCase): 17 | def test_with_decorated_methods_sync(self): 18 | class MyClass(WithDecoratedMethods[Decorators]): 19 | @foo(42) 20 | def m1(self) -> None: 21 | print('bar') 22 | 23 | @foo(value=43) 24 | def m2(self) -> None: 25 | print('bar') 26 | 27 | @bar(value=44) 28 | def m3(self) -> None: 29 | print('bar') 30 | 31 | instance = MyClass() 32 | expected = { 33 | Decorators.FOO: { 34 | instance.m1: 42, 35 | instance.m2: 43, 36 | }, 37 | Decorators.BAR: { 38 | instance.m3: 44, 39 | } 40 | } 41 | assert instance.get_decorated_functions() == expected 42 | 43 | def test_with_decorated_methods_async(self): 44 | class MyClass(WithDecoratedMethods[Decorators]): 45 | @foo(42) 46 | async def m1(self) -> None: 47 | print('bar') 48 | 49 | @foo(value=43) 50 | async def m2(self) -> None: 51 | print('bar') 52 | 53 | @bar(value=44) 54 | async def m3(self) -> None: 55 | print('bar') 56 | 57 | instance = MyClass() 58 | expected = { 59 | Decorators.FOO: { 60 | instance.m1: 42, 61 | instance.m2: 43, 62 | }, 63 | Decorators.BAR: { 64 | instance.m3: 44, 65 | } 66 | } 67 | assert instance.get_decorated_functions() == expected 68 | 69 | 70 | def test_with_custom_transformation(self): 71 | def my_transformation(f, decorator_type, value): 72 | assert decorator_type == Decorators.BAR 73 | assert value == 42 74 | 75 | @wraps(f) 76 | def wrapper(*args, **kwargs): 77 | f(*args, **kwargs) 78 | return 4422 # we add a return value 79 | 80 | return wrapper 81 | 82 | my_decorator = create_decorator(decorator_type=Decorators.BAR, transformation=my_transformation) 83 | 84 | class MyClass(WithDecoratedMethods[Decorators]): 85 | @my_decorator(42) 86 | def m1(self) -> int: 87 | return 1 88 | 89 | instance = MyClass() 90 | expected = { 91 | Decorators.BAR: { 92 | instance.m1: 42, 93 | }, 94 | Decorators.FOO: {}, 95 | } 96 | assert instance.get_decorated_functions() == expected 97 | 98 | assert instance.m1() == 4422 # check that transformation was applied 99 | -------------------------------------------------------------------------------- /pedantic/tests/tests_class_decorators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | # local file imports 4 | from pedantic.decorators.class_decorators import trace_class, timer_class 5 | 6 | 7 | class TestClassDecorators(unittest.TestCase): 8 | 9 | def test_trace_class(self): 10 | @trace_class 11 | class MyClass: 12 | def __init__(self, s: str) -> None: 13 | self.s = s 14 | 15 | def double(self, b: int) -> str: 16 | return self.s + str(b) 17 | 18 | @staticmethod 19 | def generator() -> 'MyClass': 20 | return MyClass(s='generated') 21 | 22 | m = MyClass.generator() 23 | m.double(b=42) 24 | 25 | def test_timer_class(self): 26 | @timer_class 27 | class MyClass: 28 | def __init__(self, s: str) -> None: 29 | self.s = s 30 | 31 | def double(self, b: int) -> str: 32 | return self.s + str(b) 33 | 34 | @staticmethod 35 | def generator() -> 'MyClass': 36 | return MyClass(s='generated') 37 | 38 | m = MyClass.generator() 39 | m.double(b=42) 40 | -------------------------------------------------------------------------------- /pedantic/tests/tests_combination_of_decorators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from abc import ABC, abstractmethod 3 | 4 | from pedantic.decorators.class_decorators import pedantic_class, for_all_methods 5 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 6 | from pedantic.decorators.fn_deco_validate.validators import Min 7 | from pedantic.exceptions import PedanticException, PedanticTypeCheckException, PedanticCallWithArgsException 8 | from pedantic.decorators.fn_deco_pedantic import pedantic 9 | from pedantic import overrides 10 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, Parameter, ReturnAs 11 | 12 | 13 | class TestCombinationOfDecorators(unittest.TestCase): 14 | def test_pedantic_overrides(self): 15 | class MyClass(ABC): 16 | @pedantic 17 | @abstractmethod 18 | def op(self, a: int) -> None: 19 | pass 20 | 21 | class Child(MyClass): 22 | a = 0 23 | 24 | @pedantic 25 | @overrides(MyClass) 26 | def op(self, a: int) -> None: 27 | self.a = a 28 | 29 | c = Child() 30 | c.op(a=42) 31 | 32 | def test_pedantic_below_validate(self): 33 | @validate( 34 | Parameter(name='x', validators=[Min(0)]), 35 | return_as=ReturnAs.KWARGS_WITH_NONE, 36 | ) 37 | @pedantic 38 | def some_calculation(x: int) -> int: 39 | return x 40 | 41 | some_calculation(x=42) 42 | some_calculation(42) 43 | 44 | with self.assertRaises(expected_exception=ParameterException): 45 | some_calculation(x=-1) 46 | with self.assertRaises(expected_exception=ParameterException): 47 | some_calculation(x=-42) 48 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 49 | some_calculation(x=1.0) 50 | 51 | def test_pedantic_above_validate(self): 52 | @pedantic 53 | @validate( 54 | Parameter(name='x', validators=[Min(0)]), 55 | ) 56 | def some_calculation(x: int) -> int: 57 | return x 58 | 59 | some_calculation(x=42) 60 | 61 | with self.assertRaises(expected_exception=ParameterException): 62 | some_calculation(x=-1) 63 | with self.assertRaises(expected_exception=ParameterException): 64 | some_calculation(x=-42) 65 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 66 | some_calculation(x=1.0) 67 | with self.assertRaises(expected_exception=PedanticException): 68 | some_calculation(42) 69 | 70 | def test_pedantic_above_validate_on_instance_method(self): 71 | class MyClass: 72 | @pedantic 73 | @validate( 74 | Parameter(name='x', validators=[Min(0)]), 75 | ) 76 | def some_calculation(self, x: int) -> int: 77 | return x 78 | 79 | m = MyClass() 80 | m.some_calculation(x=42) 81 | with self.assertRaises(expected_exception=ParameterException): 82 | m.some_calculation(x=-1) 83 | with self.assertRaises(expected_exception=ParameterException): 84 | m.some_calculation(x=-42) 85 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 86 | m.some_calculation(x=1.0) 87 | with self.assertRaises(expected_exception=PedanticCallWithArgsException): 88 | m.some_calculation(42) 89 | 90 | def test_pedantic_below_validate_on_instance_method(self): 91 | class MyClass: 92 | @validate( 93 | Parameter(name='x', validators=[Min(0)]), 94 | ) 95 | @pedantic 96 | def some_calculation(self, x: int) -> int: 97 | return x 98 | 99 | m = MyClass() 100 | m.some_calculation(x=42) 101 | m.some_calculation(42) 102 | 103 | with self.assertRaises(expected_exception=ParameterException): 104 | m.some_calculation(x=-1) 105 | with self.assertRaises(expected_exception=ParameterException): 106 | m.some_calculation(x=-42) 107 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 108 | m.some_calculation(x=1.0) 109 | 110 | def test_pedantic_class_with_validate_instance_method(self): 111 | @pedantic_class 112 | class MyClass: 113 | @validate( 114 | Parameter(name='x', validators=[Min(0)]), 115 | ) 116 | def some_calculation(self, x: int) -> int: 117 | return x 118 | 119 | m = MyClass() 120 | m.some_calculation(x=42) 121 | with self.assertRaises(expected_exception=ParameterException): 122 | m.some_calculation(x=-1) 123 | with self.assertRaises(expected_exception=ParameterException): 124 | m.some_calculation(x=-42) 125 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 126 | m.some_calculation(x=1.0) 127 | with self.assertRaises(expected_exception=PedanticCallWithArgsException): 128 | m.some_calculation(42) 129 | 130 | def test_pedantic_class_static_method_1(self): 131 | @pedantic_class 132 | class MyClass: 133 | @staticmethod 134 | def some_calculation(x: int) -> int: 135 | return x 136 | 137 | m = MyClass() 138 | m.some_calculation(x=42) 139 | MyClass.some_calculation(x=45) 140 | 141 | def test_pedantic_class_static_method_2(self): 142 | """Never do this, but it works""" 143 | @for_all_methods(staticmethod) 144 | @pedantic_class 145 | class MyClass: 146 | def some_calculation(x: int) -> int: 147 | return x 148 | 149 | m = MyClass() 150 | m.some_calculation(x=42) 151 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 152 | m.some_calculation(x=42.0) 153 | MyClass.some_calculation(x=45) 154 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 155 | MyClass.some_calculation(x=45.0) 156 | 157 | def test_pedantic_static_method_1(self): 158 | class MyClass: 159 | @staticmethod 160 | @pedantic 161 | def some_calculation(x: int) -> int: 162 | return x 163 | 164 | m = MyClass() 165 | m.some_calculation(x=42) 166 | MyClass.some_calculation(x=45) 167 | -------------------------------------------------------------------------------- /pedantic/tests/tests_decorated_function.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic.models.decorated_function import DecoratedFunction 4 | 5 | 6 | class TestDecoratedFunction(unittest.TestCase): 7 | def test_static_method(self): 8 | def f_1(): pass 9 | 10 | deco_f = DecoratedFunction(f_1) 11 | self.assertFalse(deco_f.is_static_method) 12 | 13 | class MyClass: 14 | def f_1(self): pass 15 | 16 | @staticmethod 17 | def f_2(): pass 18 | 19 | @classmethod 20 | def f_3(cls): pass 21 | 22 | deco_f_1 = DecoratedFunction(MyClass.f_1) 23 | deco_f_2 = DecoratedFunction(MyClass.f_2) 24 | deco_f_3 = DecoratedFunction(MyClass.f_3) 25 | 26 | self.assertFalse(deco_f_1.is_static_method) 27 | self.assertTrue(deco_f_2.is_static_method) 28 | self.assertFalse(deco_f_3.is_static_method) 29 | 30 | def test_function_wants_args(self): 31 | def f_1(*args, **kwargs): pass 32 | 33 | def f_2(a, b, *args, **kwargs): pass 34 | 35 | def f_3(a, b, *args): pass 36 | 37 | def f_4(*args): pass 38 | 39 | def f_5(): pass 40 | 41 | self.assertTrue(DecoratedFunction(f_1).wants_args) 42 | self.assertTrue(DecoratedFunction(f_2).wants_args) 43 | self.assertTrue(DecoratedFunction(f_3).wants_args) 44 | self.assertTrue(DecoratedFunction(f_4).wants_args) 45 | self.assertFalse(DecoratedFunction(f_5).wants_args) 46 | 47 | class MyClass: 48 | def f(self): pass 49 | 50 | @staticmethod 51 | def g(): pass 52 | 53 | self.assertFalse(DecoratedFunction(MyClass.f).wants_args) 54 | self.assertFalse(DecoratedFunction(MyClass.g).wants_args) 55 | 56 | def test_is_property_setter(self): 57 | def f_1(): pass 58 | 59 | self.assertFalse(DecoratedFunction(f_1).is_property_setter) 60 | 61 | class MyClass: 62 | _h = 42 63 | 64 | def f_1(self): pass 65 | 66 | @staticmethod 67 | def f_2(): pass 68 | 69 | self.assertFalse(DecoratedFunction(MyClass.f_1).is_property_setter) 70 | self.assertFalse(DecoratedFunction(MyClass.f_2).is_property_setter) 71 | 72 | def test_wants_kwargs(self): 73 | def f_1(*args, **kwargs): pass 74 | 75 | def f_2(a, b, *args, **kwargs): pass 76 | 77 | def f_3(a, b, *args): pass 78 | 79 | def f_4(*args): pass 80 | 81 | def f_5(): pass 82 | 83 | def f_6(a, b, c): pass 84 | 85 | self.assertFalse(DecoratedFunction(f_1).should_have_kwargs) 86 | self.assertFalse(DecoratedFunction(f_2).should_have_kwargs) 87 | self.assertFalse(DecoratedFunction(f_3).should_have_kwargs) 88 | self.assertFalse(DecoratedFunction(f_4).should_have_kwargs) 89 | self.assertTrue(DecoratedFunction(f_5).should_have_kwargs) 90 | self.assertTrue(DecoratedFunction(f_6).should_have_kwargs) 91 | 92 | class A: 93 | def f(self): pass 94 | 95 | @staticmethod 96 | def g(): pass 97 | 98 | def __compare__(self, other): pass 99 | 100 | self.assertTrue(DecoratedFunction(A.f).should_have_kwargs) 101 | self.assertTrue(DecoratedFunction(A.g).should_have_kwargs) 102 | self.assertFalse(DecoratedFunction(A.__compare__).should_have_kwargs) 103 | 104 | def test_instance_method(self): 105 | def h(): pass 106 | 107 | self.assertFalse(DecoratedFunction(h).is_instance_method) 108 | 109 | class A: 110 | def f(self): pass 111 | 112 | @staticmethod 113 | def g(): pass 114 | 115 | self.assertTrue(DecoratedFunction(A.f).is_instance_method) 116 | self.assertFalse(DecoratedFunction(A.g).is_instance_method) 117 | 118 | def test_num_decorators(self): 119 | def decorator(f): 120 | return f 121 | 122 | def f_1(): pass 123 | 124 | @decorator 125 | def f_2(): pass 126 | 127 | @decorator 128 | @decorator 129 | def f_3(): pass 130 | 131 | @decorator 132 | @decorator 133 | @decorator 134 | def f_4(): 135 | pass 136 | 137 | self.assertEqual(DecoratedFunction(f_1).num_of_decorators, 0) 138 | self.assertEqual(DecoratedFunction(f_2).num_of_decorators, 1) 139 | self.assertEqual(DecoratedFunction(f_3).num_of_decorators, 2) 140 | self.assertEqual(DecoratedFunction(f_4).num_of_decorators, 3) 141 | -------------------------------------------------------------------------------- /pedantic/tests/tests_doctests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import doctest 3 | 4 | 5 | def run_doctests() -> None: 6 | unittest.TextTestRunner().run(get_doctest_test_suite()) 7 | 8 | 9 | def get_doctest_test_suite() -> unittest.TestSuite: 10 | parent_module = __import__('pedantic') 11 | modules = [ 12 | parent_module.decorators.fn_deco_count_calls, 13 | parent_module.decorators.fn_deco_deprecated, 14 | parent_module.decorators.fn_deco_does_same_as_function, 15 | parent_module.decorators.fn_deco_in_subprocess, 16 | parent_module.decorators.fn_deco_overrides, 17 | parent_module.decorators.fn_deco_pedantic, 18 | parent_module.decorators.fn_deco_rename_kwargs, 19 | parent_module.decorators.fn_deco_timer, 20 | parent_module.decorators.fn_deco_trace, 21 | parent_module.decorators.fn_deco_trace_if_returns, 22 | parent_module.decorators.fn_deco_unimplemented, 23 | parent_module.mixins.generic_mixin, 24 | parent_module.type_checking_logic.check_types, 25 | parent_module.type_checking_logic.check_generic_classes, 26 | parent_module.type_checking_logic.check_docstring, 27 | parent_module.decorators.cls_deco_frozen_dataclass, 28 | ] 29 | test_suites = [doctest.DocTestSuite(module=module, optionflags=doctest.ELLIPSIS) for module in modules] 30 | return unittest.TestSuite(test_suites) 31 | 32 | 33 | if __name__ == '__main__': 34 | run_doctests() 35 | -------------------------------------------------------------------------------- /pedantic/tests/tests_environment_variables.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic.exceptions import PedanticTypeCheckException 4 | from pedantic.env_var_logic import enable_pedantic, disable_pedantic, is_enabled 5 | from pedantic.decorators.fn_deco_pedantic import pedantic 6 | 7 | 8 | class TestEnvironmentVariables(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.state = is_enabled() 11 | enable_pedantic() 12 | 13 | def tearDown(self) -> None: 14 | enable_pedantic() 15 | 16 | def test_pedantic_enabled(self): 17 | enable_pedantic() 18 | 19 | @pedantic 20 | def some_method(): 21 | return 42 22 | 23 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 24 | some_method() 25 | 26 | def test_pedantic_disabled(self): 27 | disable_pedantic() 28 | 29 | @pedantic 30 | def some_method(): 31 | return 42 32 | 33 | some_method() 34 | 35 | def test_enable_disable(self): 36 | enable_pedantic() 37 | self.assertTrue(is_enabled()) 38 | disable_pedantic() 39 | self.assertFalse(is_enabled()) 40 | enable_pedantic() 41 | self.assertTrue(is_enabled()) 42 | -------------------------------------------------------------------------------- /pedantic/tests/tests_generator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Generator, Iterator, Iterable, List 3 | 4 | from pedantic.exceptions import PedanticTypeCheckException 5 | from pedantic.decorators.fn_deco_pedantic import pedantic 6 | 7 | 8 | class TestGenerator(unittest.TestCase): 9 | def test_iterator(self): 10 | @pedantic 11 | def gen_func() -> Iterator[int]: 12 | num = 0 13 | 14 | while num < 100: 15 | yield num 16 | num += 1 17 | 18 | gen = gen_func() 19 | next(gen) 20 | 21 | def test_iterator_wrong_type_hint(self): 22 | @pedantic 23 | def genfunc() -> Iterator[float]: 24 | num = 0 25 | 26 | while num < 100: 27 | yield num 28 | num += 1 29 | 30 | gen = genfunc() 31 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 32 | next(gen) 33 | 34 | def test_iterator_no_type_args(self): 35 | @pedantic 36 | def genfunc() -> Iterator: 37 | num = 0 38 | 39 | while num < 100: 40 | yield num 41 | num += 1 42 | 43 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 44 | genfunc() 45 | 46 | def test_iterator_completely_wrong_type_hint(self): 47 | @pedantic 48 | def gen_func() -> List[int]: 49 | num = 0 50 | 51 | while num < 100: 52 | yield num 53 | num += 1 54 | 55 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 56 | gen_func() 57 | 58 | def test_iterable(self): 59 | @pedantic 60 | def gen_func() -> Iterable[int]: 61 | num = 0 62 | 63 | while num < 100: 64 | yield num 65 | num += 1 66 | 67 | gen = gen_func() 68 | next(gen) 69 | 70 | def test_iterable_no_type_args(self): 71 | @pedantic 72 | def gen_func() -> Iterable: 73 | num = 0 74 | 75 | while num < 100: 76 | yield num 77 | num += 1 78 | 79 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 80 | gen_func() 81 | 82 | def test_generator(self): 83 | @pedantic 84 | def gen_func() -> Generator[int, None, str]: 85 | num = 0 86 | 87 | while num < 100: 88 | yield num 89 | num += 1 90 | return 'Done' 91 | 92 | gen = gen_func() 93 | next(gen) 94 | 95 | def test_invalid_no_type_args_generator(self): 96 | @pedantic 97 | def gen_func() -> Generator: 98 | num = 0 99 | 100 | while num < 100: 101 | yield num 102 | num += 1 103 | return 'Done' 104 | 105 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 106 | gen_func() 107 | -------------------------------------------------------------------------------- /pedantic/tests/tests_main.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | 5 | sys.path.append(os.getcwd()) 6 | 7 | from pedantic.tests.test_retry import TestRetry, TestRetryFunc 8 | from pedantic.tests.test_with_decorated_methods import TestWithDecoratedMethods 9 | from pedantic.tests.validate.test_convert_value import TestConvertValue 10 | from pedantic.tests.test_rename_kwargs import TestRenameKwargs 11 | from pedantic.tests.validate.test_datetime_isoformat import TestValidatorDatetimeIsoformat 12 | from pedantic.tests.validate.test_flask_parameters import TestFlaskParameters 13 | from pedantic.tests.validate.test_parameter_environment_variable import TestParameterEnvironmentVariable 14 | from pedantic.tests.validate.test_validate import TestValidate 15 | from pedantic.tests.validate.test_validate import AsyncValidateTests 16 | from pedantic.tests.tests_small_method_decorators import AsyncSmallDecoratorTests 17 | from pedantic.tests.tests_pedantic_async import TestPedanticAsyncio 18 | from pedantic.tests.test_in_subprocess import TestInSubprocess 19 | from pedantic.tests.test_async_context_manager import TestAsyncContextManager 20 | from pedantic.tests.tests_pedantic_python_311 import TestPedanticPython311AddedStuff 21 | from pedantic.tests.test_resolve_forward_ref import TestResolveForwardRef 22 | from pedantic.tests.test_generic_mixin import TestGenericMixin 23 | from pedantic.tests.test_assert_value_matches_type import TestAssertValueMatchesType 24 | from pedantic.tests.validate.test_validator_composite import TestValidatorComposite 25 | from pedantic.tests.validate.test_validator_datetime_unix_timestamp import TestValidatorDatetimeUnixTimestamp 26 | from pedantic.tests.validate.test_validator_email import TestValidatorEmail 27 | from pedantic.tests.validate.test_validator_for_each import TestValidatorForEach 28 | from pedantic.tests.validate.test_validator_is_enum import TestValidatorIsEnum 29 | from pedantic.tests.validate.test_validator_is_uuid import TestValidatorIsUUID 30 | from pedantic.tests.validate.test_validator_match_pattern import TestValidatorMatchPattern 31 | from pedantic.tests.validate.test_validator_max import TestValidatorMax 32 | from pedantic.tests.validate.test_validator_max_length import TestValidatorMaxLength 33 | from pedantic.tests.validate.test_validator_min import TestValidatorMin 34 | from pedantic.tests.validate.test_validator_min_length import TestValidatorMinLength 35 | from pedantic.tests.validate.test_validator_not_empty import TestValidatorNotEmpty 36 | from pedantic.tests.test_generator_wrapper import TestGeneratorWrapper 37 | from pedantic.tests.tests_mock import TestMock 38 | from pedantic.tests.tests_doctests import get_doctest_test_suite 39 | from pedantic.tests.test_frozen_dataclass import TestFrozenDataclass 40 | from pedantic.tests.tests_require_kwargs import TestRequireKwargs 41 | from pedantic.tests.tests_class_decorators import TestClassDecorators 42 | from pedantic.tests.tests_pedantic_class import TestPedanticClass 43 | from pedantic.tests.tests_pedantic import TestDecoratorRequireKwargsAndTypeCheck 44 | from pedantic.tests.tests_small_method_decorators import TestSmallDecoratorMethods 45 | from pedantic.tests.tests_combination_of_decorators import TestCombinationOfDecorators 46 | from pedantic.tests.tests_docstring import TestRequireDocstringGoogleFormat 47 | from pedantic.tests.tests_pedantic_class_docstring import TestPedanticClassDocstring 48 | from pedantic.tests.tests_decorated_function import TestDecoratedFunction 49 | from pedantic.tests.tests_environment_variables import TestEnvironmentVariables 50 | from pedantic.tests.tests_generic_classes import TestGenericClasses 51 | from pedantic.tests.tests_generator import TestGenerator 52 | from pedantic.tests.test_context_manager import TestContextManager 53 | 54 | 55 | def run_all_tests() -> None: 56 | test_classes_to_run = [ 57 | TestAssertValueMatchesType, 58 | TestGenericMixin, 59 | TestWithDecoratedMethods, 60 | TestRequireKwargs, 61 | TestClassDecorators, 62 | TestContextManager, 63 | TestFrozenDataclass, 64 | TestPedanticClass, 65 | TestDecoratorRequireKwargsAndTypeCheck, 66 | TestSmallDecoratorMethods, 67 | TestCombinationOfDecorators, 68 | TestRequireDocstringGoogleFormat, 69 | TestPedanticClassDocstring, 70 | TestDecoratedFunction, 71 | TestEnvironmentVariables, 72 | TestGenericClasses, 73 | TestGenerator, 74 | TestMock, 75 | TestGeneratorWrapper, 76 | TestRenameKwargs, 77 | TestRetry, 78 | TestRetryFunc, 79 | TestResolveForwardRef, 80 | # validate 81 | TestValidatorDatetimeIsoformat, 82 | TestFlaskParameters, 83 | TestParameterEnvironmentVariable, 84 | TestConvertValue, 85 | TestValidate, 86 | TestValidatorComposite, 87 | TestValidatorDatetimeUnixTimestamp, 88 | TestValidatorEmail, 89 | TestValidatorForEach, 90 | TestValidatorIsEnum, 91 | TestValidatorIsUUID, 92 | TestValidatorMatchPattern, 93 | TestValidatorMax, 94 | TestValidatorMaxLength, 95 | TestValidatorMin, 96 | TestValidatorMinLength, 97 | TestValidatorNotEmpty, 98 | 99 | # async 100 | AsyncValidateTests, 101 | AsyncSmallDecoratorTests, 102 | TestPedanticAsyncio, 103 | TestInSubprocess, 104 | TestAsyncContextManager, 105 | 106 | TestPedanticPython311AddedStuff, 107 | ] 108 | 109 | loader = unittest.TestLoader() 110 | suites_list = [get_doctest_test_suite()] 111 | 112 | for test_class in test_classes_to_run: 113 | suite = loader.loadTestsFromTestCase(test_class) 114 | suites_list.append(suite) 115 | 116 | big_suite = unittest.TestSuite(suites_list) 117 | runner = unittest.TextTestRunner() 118 | result = runner.run(big_suite) 119 | assert not result.errors and not result.failures, f'Some tests failed!' 120 | 121 | 122 | if __name__ == '__main__': 123 | run_all_tests() 124 | -------------------------------------------------------------------------------- /pedantic/tests/tests_mock.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic import mock 4 | 5 | 6 | class TestMock(TestCase): 7 | def test_mock(self) -> None: 8 | @mock(return_value=42) 9 | def my_function(a, b, c): 10 | return a + b + c 11 | 12 | assert my_function(1, 2, 3) == 42 13 | assert my_function(100, 200, 300) == 42 14 | -------------------------------------------------------------------------------- /pedantic/tests/tests_pedantic_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | from pedantic.decorators.class_decorators import pedantic_class 5 | from pedantic.exceptions import PedanticTypeCheckException 6 | from pedantic.decorators.fn_deco_pedantic import pedantic 7 | 8 | 9 | class TestPedanticAsyncio(unittest.IsolatedAsyncioTestCase): 10 | async def test_coroutine_correct_return_type(self): 11 | @pedantic 12 | async def foo() -> str: 13 | await asyncio.sleep(0) 14 | return 'foo' 15 | 16 | await foo() 17 | 18 | async def test_coroutine_wrong_return_type(self): 19 | @pedantic 20 | async def foo() -> str: 21 | await asyncio.sleep(0) 22 | return 1 23 | 24 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 25 | await foo() 26 | 27 | async def test_coroutine_wrong_argument_type(self): 28 | @pedantic 29 | async def foo(x: int) -> int: 30 | await asyncio.sleep(0) 31 | return 1 + x 32 | 33 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 34 | await foo(x=4.5) 35 | 36 | async def test_static_async(self): 37 | @pedantic_class 38 | class Foo: 39 | @staticmethod 40 | async def staticmethod() -> int: 41 | await asyncio.sleep(0) 42 | return 'foo' 43 | 44 | @classmethod 45 | async def classmethod(cls) -> int: 46 | await asyncio.sleep(0) 47 | return 'foo' 48 | 49 | async def method(self) -> int: 50 | await asyncio.sleep(0) 51 | return 'foo' 52 | 53 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 54 | await Foo.staticmethod() 55 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 56 | await Foo.classmethod() 57 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 58 | await Foo().method() 59 | -------------------------------------------------------------------------------- /pedantic/tests/tests_pedantic_class_docstring.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic import pedantic_class_require_docstring 4 | from pedantic.exceptions import PedanticDocstringException 5 | 6 | 7 | class TestPedanticClassDocstring(unittest.TestCase): 8 | def test_require_docstring(self): 9 | @pedantic_class_require_docstring 10 | class MyClass: 11 | def __init__(self, s: str) -> None: 12 | """Constructor 13 | 14 | Args: 15 | s (str): name 16 | """ 17 | self.s = s 18 | 19 | def double(self, b: int) -> str: 20 | """some method 21 | 22 | Args: 23 | b (int): magic number 24 | 25 | Returns: 26 | str: cool stuff 27 | 28 | """ 29 | return self.s + str(b) 30 | 31 | @staticmethod 32 | def generator() -> 'MyClass': 33 | """Static 34 | 35 | Returns: 36 | MyClass: instance 37 | """ 38 | return MyClass(s='generated') 39 | 40 | m = MyClass.generator() 41 | m.double(b=42) 42 | 43 | def test_typo_docstring(self): 44 | with self.assertRaises(expected_exception=PedanticDocstringException): 45 | @pedantic_class_require_docstring 46 | class MyClass: 47 | def __init__(self, s: str) -> None: 48 | """Constructor 49 | 50 | Args: 51 | s (str): name 52 | """ 53 | self.s = s 54 | 55 | @staticmethod 56 | def generator() -> 'MyClass': 57 | """Static 58 | 59 | Returns: 60 | MyClas: instance 61 | """ 62 | return MyClass(s='generated') 63 | 64 | def test_wrong_docstring(self): 65 | with self.assertRaises(expected_exception=PedanticDocstringException): 66 | @pedantic_class_require_docstring 67 | class MyClass: 68 | def __init__(self, s: str) -> None: 69 | """Constructor 70 | 71 | Args: 72 | s (str): name 73 | """ 74 | self.s = s 75 | 76 | def double(self, b: int) -> str: 77 | """some method 78 | 79 | Args: 80 | b (float): magic number 81 | 82 | Returns: 83 | str: cool stuff 84 | 85 | """ 86 | return self.s + str(b) 87 | -------------------------------------------------------------------------------- /pedantic/tests/tests_pedantic_python_311.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pedantic import pedantic, pedantic_class 4 | from pedantic.exceptions import PedanticTypeCheckException 5 | 6 | 7 | class TestPedanticPython311AddedStuff(unittest.TestCase): 8 | def test_typing_never(self): 9 | from typing import Never 10 | 11 | @pedantic 12 | def never_call_me(arg: Never) -> None: 13 | pass 14 | 15 | @pedantic 16 | def foo() -> Never: 17 | pass 18 | 19 | @pedantic 20 | def bar() -> Never: 21 | raise ZeroDivisionError('bar') 22 | 23 | with self.assertRaises(expected_exception=ZeroDivisionError): 24 | bar() 25 | 26 | with self.assertRaises(PedanticTypeCheckException): 27 | foo() 28 | 29 | with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc: 30 | never_call_me(arg='42') 31 | 32 | def test_literal_string(self): 33 | from typing import LiteralString 34 | 35 | @pedantic 36 | def foo(s: LiteralString) -> None: 37 | pass 38 | 39 | foo(s='Hi') 40 | foo(s=2 * 'Hi') 41 | 42 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 43 | foo(s=3) 44 | 45 | def test_self_type(self): 46 | from typing import Self 47 | 48 | class Bar: 49 | pass 50 | 51 | @pedantic_class 52 | class Foo: 53 | def f(self) -> Self: 54 | return self 55 | 56 | @staticmethod 57 | def g() -> Self: 58 | return Foo() 59 | 60 | @classmethod 61 | def h(cls) -> Self: 62 | return cls() 63 | 64 | def f_2(self) -> Self: 65 | return Bar() 66 | 67 | @staticmethod 68 | def g_2() -> Self: 69 | return Bar() 70 | 71 | @classmethod 72 | def h_2(cls) -> Self: 73 | return Bar() 74 | 75 | f = Foo() 76 | assert f.f() == f 77 | f.g() 78 | f.h() 79 | Foo.g() 80 | Foo.h() 81 | 82 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 83 | f.f_2() 84 | 85 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 86 | f.g_2() 87 | 88 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 89 | f.h_2() 90 | 91 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 92 | Foo.g_2() 93 | 94 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 95 | Foo.h_2() 96 | 97 | def test_using_self_type_annotation_outside_class(self): 98 | from typing import Self 99 | 100 | @pedantic 101 | def f() -> Self: 102 | return 'hi' 103 | 104 | with self.assertRaises(expected_exception=PedanticTypeCheckException): 105 | f() 106 | 107 | def test_type_var_tuple(self): 108 | from typing import TypeVarTuple, Generic 109 | 110 | Ts = TypeVarTuple('Ts') 111 | 112 | @pedantic_class 113 | class Array(Generic[*Ts]): 114 | def __init__(self, *args: *Ts) -> None: 115 | self._values = args 116 | 117 | @pedantic 118 | def add_dimension(a: Array[*Ts], value: int) -> Array[int, *Ts]: 119 | return Array[int, *Ts](value, *a._values) 120 | 121 | array = Array[int, float](42, 3.4) 122 | array_2 = Array[bool, int, float, str](True, 4, 3.4, 'hi') 123 | extended_array = add_dimension(a=array, value=42) 124 | assert extended_array._values == (42, 42, 3.4) 125 | 126 | # this is too complicated at the moment 127 | # with self.assertRaises(expected_exception=PedanticTypeCheckException): 128 | # Array[int, float](4.2, 3.4) -------------------------------------------------------------------------------- /pedantic/tests/tests_require_kwargs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | # local file imports 4 | from pedantic.exceptions import PedanticCallWithArgsException 5 | from pedantic import require_kwargs 6 | 7 | 8 | class TestRequireKwargs(unittest.TestCase): 9 | 10 | def test_kwargs(self): 11 | @require_kwargs 12 | def calc(n: int, m: int, i: int) -> int: 13 | return n + m + i 14 | 15 | calc(n=1, m=2, i=3) 16 | 17 | def test_no_kwargs_1(self): 18 | @require_kwargs 19 | def calc(n: int, m: int, i: int) -> int: 20 | return n + m + i 21 | 22 | with self.assertRaises(expected_exception=PedanticCallWithArgsException): 23 | calc(1, m=2, i=3) 24 | 25 | def test_no_kwargs_2(self): 26 | @require_kwargs 27 | def calc(n: int, m: int, i: int) -> int: 28 | return n + m + i 29 | 30 | with self.assertRaises(expected_exception=PedanticCallWithArgsException): 31 | calc(1, 2, 3) 32 | -------------------------------------------------------------------------------- /pedantic/tests/validate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostInDarkMath/pedantic-python-decorators/f29268701e8bb48e043affe5d128040aafcf441d/pedantic/tests/validate/__init__.py -------------------------------------------------------------------------------- /pedantic/tests/validate/test_convert_value.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ConversionError 4 | from pedantic.decorators.fn_deco_validate.convert_value import convert_value 5 | 6 | 7 | class TestConvertValue(TestCase): 8 | def test_convert_to_bool(self): 9 | for value in [True, 1, '1', ' 1 ', ' tRuE ', 'TRUE']: 10 | self.assertTrue(convert_value(value=value, target_type=bool)) 11 | 12 | for value in [False, 0, '0', ' 0 ', ' fAlSe ', 'FALSE']: 13 | self.assertFalse(convert_value(value=value, target_type=bool)) 14 | 15 | for value in ['alse', 0.1, '0.2', ' 0000 ', 'Talse', 'Frue', 42]: 16 | with self.assertRaises(expected_exception=ConversionError): 17 | self.assertFalse(convert_value(value=value, target_type=bool)) 18 | 19 | def test_convert_to_int(self): 20 | for value in range(-4, 4): 21 | self.assertEqual(value, convert_value(value=value, target_type=int)) 22 | 23 | self.assertEqual(42, convert_value(value='42', target_type=int)) 24 | self.assertEqual(0, convert_value(value=' 0000 ', target_type=int)) 25 | 26 | for value in ['alse', 'Talse', 'Frue', 0.2, '0.2']: 27 | with self.assertRaises(expected_exception=ConversionError): 28 | self.assertFalse(convert_value(value=value, target_type=int)) 29 | 30 | def test_convert_to_float(self): 31 | for value in range(-4, 4): 32 | self.assertEqual(value, convert_value(value=value, target_type=float)) 33 | 34 | self.assertEqual(0.2, convert_value(value=0.2, target_type=float)) 35 | self.assertEqual(0.2, convert_value(value='0.2', target_type=float)) 36 | self.assertEqual(42, convert_value(value='42', target_type=float)) 37 | self.assertEqual(0, convert_value(value=' 0000 ', target_type=float)) 38 | 39 | for value in ['alse', 'Talse', 'Frue']: 40 | with self.assertRaises(expected_exception=ConversionError): 41 | self.assertFalse(convert_value(value=value, target_type=float)) 42 | 43 | def test_convert_to_list(self): 44 | for value in [[], [1], ['1', ' 1 '], [' tRuE ', 'TRUE']]: 45 | self.assertEqual(value, convert_value(value=value, target_type=list)) 46 | 47 | self.assertEqual(['1', '2', '3'], convert_value(value='1,2,3', target_type=list)) 48 | 49 | def test_convert_to_dict(self): 50 | for value in [{}, {1: 2}, {'1': ' 1 '}, {1: ' tRuE ', 2: 'TRUE'}]: 51 | self.assertEqual(value, convert_value(value=value, target_type=dict)) 52 | 53 | self.assertEqual({'1': '1', '2': '4', '3': '7'}, convert_value(value='1:1,2:4,3:7', target_type=dict)) 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_datetime_isoformat.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from unittest import TestCase 3 | 4 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 5 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 6 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 7 | from pedantic.decorators.fn_deco_validate.validators import DatetimeIsoFormat 8 | 9 | 10 | class TestValidatorDatetimeIsoformat(TestCase): 11 | def test_validator_datetime_isoformat(self) -> None: 12 | @validate(Parameter(name='x', validators=[DatetimeIsoFormat()])) 13 | def foo(x): 14 | return x 15 | 16 | now = datetime.now() 17 | self.assertTrue(abs((now - foo(now.isoformat()) < timedelta(milliseconds=1)))) 18 | 19 | with self.assertRaises(expected_exception=ParameterException): 20 | foo('12.12.2020') 21 | 22 | with self.assertRaises(expected_exception=ParameterException): 23 | foo('invalid') 24 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_parameter_environment_variable.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 5 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 6 | from pedantic.decorators.fn_deco_validate.parameters import EnvironmentVariableParameter 7 | 8 | 9 | class TestParameterEnvironmentVariable(TestCase): 10 | def setUp(self) -> None: 11 | if 'foo' in os.environ: 12 | del os.environ['foo'] 13 | 14 | def test_parameter_environment_variable_str(self) -> None: 15 | @validate(EnvironmentVariableParameter(name='foo', value_type=str)) 16 | def bar(foo): 17 | return foo 18 | 19 | os.environ['foo'] = '42' 20 | self.assertEqual('42', bar()) 21 | 22 | def test_parameter_environment_variable_int(self) -> None: 23 | @validate(EnvironmentVariableParameter(name='foo', value_type=int)) 24 | def bar(foo): 25 | return foo 26 | 27 | os.environ['foo'] = '42' 28 | self.assertEqual(42, bar()) 29 | 30 | def test_parameter_environment_variable_float(self) -> None: 31 | @validate(EnvironmentVariableParameter(name='foo', value_type=float)) 32 | def bar(foo): 33 | return foo 34 | 35 | os.environ['foo'] = '42.7' 36 | self.assertEqual(42.7, bar()) 37 | 38 | def test_parameter_environment_variable_bool(self) -> None: 39 | @validate(EnvironmentVariableParameter(name='foo', value_type=bool)) 40 | def bar(foo): 41 | return foo 42 | 43 | for value in ['true', 'True', 'TRUE']: 44 | os.environ['foo'] = value 45 | self.assertTrue(bar()) 46 | 47 | for value in ['false', 'False', 'FALSE']: 48 | os.environ['foo'] = value 49 | self.assertFalse(bar()) 50 | 51 | for value in ['invalid', 'frue', 'talse']: 52 | os.environ['foo'] = value 53 | 54 | with self.assertRaises(expected_exception=ParameterException): 55 | bar() 56 | 57 | def test_parameter_environment_variable_not_set(self) -> None: 58 | @validate(EnvironmentVariableParameter(name='foo')) 59 | def bar(foo): 60 | return foo 61 | 62 | with self.assertRaises(expected_exception=ParameterException): 63 | bar() 64 | 65 | def test_invalid_value_type(self) -> None: 66 | with self.assertRaises(expected_exception=AssertionError): 67 | @validate(EnvironmentVariableParameter(name='foo', value_type=dict)) 68 | def bar(foo): 69 | return foo 70 | 71 | def test_parameter_environment_variable_different_name(self) -> None: 72 | @validate(EnvironmentVariableParameter(name='foo', env_var_name='fuu', value_type=str)) 73 | def bar(foo): 74 | return foo 75 | 76 | os.environ['fuu'] = '42' 77 | self.assertEqual('42', bar()) 78 | 79 | def test_two_parameters(self) -> None: 80 | @validate(EnvironmentVariableParameter(name='a'), strict=False) 81 | def foo(a: float, b: int): 82 | print(f'{a} and {b}') 83 | 84 | os.environ['a'] = '42' 85 | foo(b=42) 86 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_composite.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import Composite, Max, Min 7 | 8 | 9 | class TestValidatorComposite(TestCase): 10 | def test_validator_composite(self) -> None: 11 | @validate(Parameter(name='x', validators=[Composite([Min(3), Max(5)])])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual(3, foo(3)) 16 | self.assertEqual(4, foo(4)) 17 | self.assertEqual(5, foo(5)) 18 | 19 | with self.assertRaises(expected_exception=ParameterException): 20 | foo(5.0001) 21 | 22 | with self.assertRaises(expected_exception=ParameterException): 23 | foo(2.9999) 24 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_datetime_unix_timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import TestCase 3 | 4 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 5 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 6 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 7 | from pedantic.decorators.fn_deco_validate.validators import DateTimeUnixTimestamp 8 | 9 | 10 | class TestValidatorDatetimeUnixTimestamp(TestCase): 11 | def test_validator_datetime_unix_timestamp(self) -> None: 12 | @validate(Parameter(name='x', validators=[DateTimeUnixTimestamp()])) 13 | def foo(x): 14 | return x 15 | 16 | now = datetime.now() 17 | unix_timestamp = (now - datetime(year=1970, month=1, day=1)).total_seconds() 18 | self.assertEqual(now, foo(unix_timestamp)) 19 | self.assertEqual(now, foo(str(unix_timestamp))) 20 | 21 | with self.assertRaises(expected_exception=ParameterException): 22 | foo('12.12.2020') 23 | 24 | with self.assertRaises(expected_exception=ParameterException): 25 | foo('invalid') 26 | 27 | with self.assertRaises(expected_exception=ParameterException): 28 | foo({'a': 1}) 29 | 30 | with self.assertRaises(expected_exception=ParameterException): 31 | foo(unix_timestamp * 1000) 32 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_email.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import Email 7 | 8 | 9 | class TestValidatorEmail(TestCase): 10 | def test_validator_email(self) -> None: 11 | @validate(Parameter(name='x', validators=[Email()])) 12 | def foo(x): 13 | return x 14 | 15 | for value in ['fred@web.de', 'genial@gmail.com', 'test@test.co.uk']: 16 | self.assertEqual(value, foo(value)) 17 | 18 | for value in ['fred', 'fred@web', 'fred@w@eb.de', 'fred@@web.de', 'invalid@invalid']: 19 | with self.assertRaises(expected_exception=ParameterException): 20 | foo(value) 21 | 22 | def test_validator_email_converts_to_lower_case(self) -> None: 23 | @validate(Parameter(name='x', validators=[Email(post_processor=lambda x: x.lower())])) 24 | def foo(x): 25 | return x 26 | 27 | for value in ['Fred@Web.de', 'GENIAL@GMAIL.com', 'test@test.CO.UK']: 28 | self.assertEqual(value.lower(), foo(value)) 29 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_for_each.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import ForEach, Min, Max 7 | 8 | 9 | class TestValidatorForEach(TestCase): 10 | def test_validator_for_each_single_child(self) -> None: 11 | @validate(Parameter(name='x', validators=[ForEach(Min(3))])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual([3, 4, 5], foo([3, 4, 5])) 16 | 17 | for value in [42, [3, 2, 5]]: 18 | with self.assertRaises(expected_exception=ParameterException): 19 | foo(value) 20 | 21 | def test_validator_for_each_multiple_children(self) -> None: 22 | @validate(Parameter(name='x', validators=[ForEach([Min(3), Max(4)])])) 23 | def foo(x): 24 | return x 25 | 26 | self.assertEqual([3, 4], foo([3, 4])) 27 | 28 | for value in [42, [3, 2, 5]]: 29 | with self.assertRaises(expected_exception=ParameterException): 30 | foo(value) 31 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_is_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | from unittest import TestCase 3 | 4 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 5 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 6 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 7 | from pedantic.decorators.fn_deco_validate.validators import IsEnum 8 | 9 | 10 | class MyEnum(Enum): 11 | RED = 'RED' 12 | BLUE = 'BLUE' 13 | 14 | 15 | class MyIntEnum(IntEnum): 16 | RED = 1 17 | BLUE = 2 18 | 19 | 20 | class TestValidatorIsEnum(TestCase): 21 | def test_validator_is_enum_convert_true(self) -> None: 22 | @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=True)])) 23 | def foo(x): 24 | return x 25 | 26 | self.assertEqual(MyEnum.RED, foo('RED')) 27 | self.assertEqual(MyEnum.BLUE, foo('BLUE')) 28 | 29 | for value in ['fred', 1, 'GREEN']: 30 | with self.assertRaises(expected_exception=ParameterException): 31 | foo(value) 32 | 33 | def test_validator_is_enum_int_enum_convert_true(self) -> None: 34 | @validate(Parameter(name='x', validators=[IsEnum(MyIntEnum, convert=True)])) 35 | def foo(x): 36 | return x 37 | 38 | self.assertEqual(MyIntEnum.RED, foo('1')) 39 | self.assertEqual(MyIntEnum.BLUE, foo('2')) 40 | self.assertEqual(MyIntEnum.RED, foo(1)) 41 | self.assertEqual(MyIntEnum.BLUE, foo(2)) 42 | 43 | for value in ['fred', 3, 'GREEN']: 44 | with self.assertRaises(expected_exception=ParameterException): 45 | foo(value) 46 | 47 | def test_validator_is_enum_convert_false(self) -> None: 48 | @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) 49 | def foo(x): 50 | return x 51 | 52 | self.assertEqual('RED', foo('RED')) 53 | self.assertEqual('BLUE', foo('BLUE')) 54 | 55 | for value in ['fred', 1, 'GREEN']: 56 | with self.assertRaises(expected_exception=ParameterException): 57 | foo(value) 58 | 59 | def test_validator_is_enum_to_upper_case(self) -> None: 60 | @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) 61 | def foo(x): 62 | return x 63 | 64 | self.assertEqual('RED', foo('red')) 65 | self.assertEqual('BLUE', foo('blue')) 66 | self.assertEqual('BLUE', foo('bLUe')) 67 | 68 | def test_validator_is_enum_to_upper_case_disabled(self) -> None: 69 | @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False, to_upper_case=False)])) 70 | def foo(x): print(x) 71 | 72 | for value in ['red', 'blue', 'Red', 'bLUe']: 73 | with self.assertRaises(expected_exception=ParameterException): 74 | foo(value) 75 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_is_uuid.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from uuid import uuid1, uuid3, uuid4, uuid5 3 | 4 | from pedantic import ForEach 5 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 6 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 7 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 8 | from pedantic.decorators.fn_deco_validate.validators import IsUuid 9 | 10 | 11 | class TestValidatorIsUUID(TestCase): 12 | def test_validator_is_uuid(self): 13 | @validate(Parameter(name='x', validators=[IsUuid()], required=False)) 14 | def foo(x): 15 | return x 16 | 17 | for id_ in [str(uuid1()), str(uuid3(uuid1(), 'b')), str(uuid4()), str(uuid5(uuid1(), 'b'))]: 18 | self.assertEqual(id_, foo(id_)) 19 | 20 | for no_id in ['invalid', 12]: 21 | with self.assertRaises(expected_exception=ParameterException): 22 | foo(no_id) 23 | 24 | def test_validator_is_uuid_with_for_each_and_none_value(self): 25 | @validate(Parameter(name='x', validators=[ForEach(IsUuid())])) 26 | def foo(x): 27 | return x 28 | 29 | uuid = str(uuid1()) 30 | self.assertEqual([], foo([])) 31 | self.assertEqual([uuid], foo([uuid])) 32 | 33 | with self.assertRaises(expected_exception=ParameterException): 34 | foo([None]) 35 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_match_pattern.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import MatchPattern 7 | 8 | 9 | class TestValidatorMatchPattern(TestCase): 10 | def test_validator_match_pattern(self) -> None: 11 | pattern = r'^(([01]\d|2[0-3]):([0-5]\d)|24:00)$' 12 | 13 | @validate(Parameter(name='x', validators=[MatchPattern(pattern)])) 14 | def foo(x): 15 | return x 16 | 17 | for value in ['00:00', '02:45', '14:59', '23:59']: 18 | self.assertEqual(value, foo(value)) 19 | 20 | for value in ['00:70', '24:00', '30:00', 'invalid']: 21 | with self.assertRaises(expected_exception=ParameterException): 22 | foo([3, 2, 5]) 23 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_max.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators.max import Max 7 | 8 | 9 | class TestValidatorMax(TestCase): 10 | def test_validator_max_length_include_boundary_true(self) -> None: 11 | @validate(Parameter(name='x', validators=[Max(3, include_boundary=True)])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual(3, foo(3)) 16 | self.assertEqual(2, foo(2)) 17 | 18 | with self.assertRaises(expected_exception=ParameterException): 19 | foo(4) 20 | 21 | with self.assertRaises(expected_exception=ParameterException): 22 | foo(3.001) 23 | 24 | def test_validator_max_length_include_boundary_false(self) -> None: 25 | @validate(Parameter(name='x', validators=[Max(3, include_boundary=False)])) 26 | def foo(x): 27 | return x 28 | 29 | self.assertEqual(2.9999, foo(2.9999)) 30 | self.assertEqual(2, foo(2)) 31 | 32 | with self.assertRaises(expected_exception=ParameterException): 33 | foo(4) 34 | 35 | with self.assertRaises(expected_exception=ParameterException): 36 | foo(3) 37 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_max_length.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import MaxLength 7 | 8 | 9 | class TestValidatorMaxLength(TestCase): 10 | def test_validator_max_length(self) -> None: 11 | @validate(Parameter(name='x', validators=[MaxLength(3)])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual('hi', foo('hi')) 16 | self.assertEqual('hi!', foo('hi!')) 17 | self.assertEqual([1, 2, 3], foo([1, 2, 3])) 18 | 19 | with self.assertRaises(expected_exception=ParameterException): 20 | foo('hi!!') 21 | 22 | with self.assertRaises(expected_exception=ParameterException): 23 | foo([1, 2, 3, 4]) 24 | 25 | with self.assertRaises(expected_exception=ParameterException): 26 | foo(42) 27 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_min.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import Min 7 | 8 | 9 | class TestValidatorMin(TestCase): 10 | def test_validator_min_length_include_boundary_true(self) -> None: 11 | @validate(Parameter(name='x', validators=[Min(3, include_boundary=True)])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual(3, foo(3)) 16 | self.assertEqual(4, foo(4)) 17 | 18 | with self.assertRaises(expected_exception=ParameterException): 19 | foo(2) 20 | 21 | with self.assertRaises(expected_exception=ParameterException): 22 | foo(2.9999) 23 | 24 | def test_validator_min_length_include_boundary_false(self) -> None: 25 | @validate(Parameter(name='x', validators=[Min(3, include_boundary=False)])) 26 | def foo(x): 27 | return x 28 | 29 | self.assertEqual(3.0001, foo(3.0001)) 30 | self.assertEqual(4, foo(4)) 31 | 32 | with self.assertRaises(expected_exception=ParameterException): 33 | foo(2) 34 | 35 | with self.assertRaises(expected_exception=ParameterException): 36 | foo(3) 37 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_min_length.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import MinLength 7 | 8 | 9 | class TestValidatorMinLength(TestCase): 10 | def test_validator_min_length(self) -> None: 11 | @validate(Parameter(name='x', validators=[MinLength(3)])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual('hi!', foo('hi!')) 16 | self.assertEqual('hello', foo('hello')) 17 | self.assertEqual([1, 2, 3], foo([1, 2, 3])) 18 | 19 | with self.assertRaises(expected_exception=ParameterException): 20 | foo('hi') 21 | 22 | with self.assertRaises(expected_exception=ParameterException): 23 | foo([1, 2]) 24 | 25 | with self.assertRaises(expected_exception=ParameterException): 26 | foo(42) 27 | -------------------------------------------------------------------------------- /pedantic/tests/validate/test_validator_not_empty.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pedantic.decorators.fn_deco_validate.exceptions import ParameterException 4 | from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate 5 | from pedantic.decorators.fn_deco_validate.parameters import Parameter 6 | from pedantic.decorators.fn_deco_validate.validators import NotEmpty 7 | 8 | 9 | class TestValidatorNotEmpty(TestCase): 10 | def test_validator_not_empty(self) -> None: 11 | @validate(Parameter(name='x', validators=[NotEmpty()])) 12 | def foo(x): 13 | return x 14 | 15 | self.assertEqual('hi', foo('hi')) 16 | self.assertEqual('hi', foo(' hi ')) 17 | self.assertEqual([1], foo([1])) 18 | 19 | for value in ['', ' ', [], {}, set()]: 20 | with self.assertRaises(expected_exception=ParameterException): 21 | foo(value) 22 | -------------------------------------------------------------------------------- /pedantic/type_checking_logic/__init__.py: -------------------------------------------------------------------------------- 1 | from .check_types import assert_value_matches_type 2 | from .resolve_forward_ref import resolve_forward_ref 3 | -------------------------------------------------------------------------------- /pedantic/type_checking_logic/check_generic_classes.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Any, Generic, Dict 3 | 4 | from pedantic.exceptions import PedanticTypeVarMismatchException 5 | from pedantic.type_checking_logic.check_types import get_type_arguments 6 | from pedantic.constants import TypeVar, ATTR_NAME_GENERIC_INSTANCE_ALREADY_CHECKED 7 | 8 | 9 | def check_instance_of_generic_class_and_get_type_vars(instance: Any) -> Dict[TypeVar, Any]: 10 | """ 11 | >>> from typing import TypeVar, Generic, List 12 | >>> T = TypeVar('T') 13 | >>> class A(Generic[T]): pass 14 | >>> a = A() # would normally raise an error due to _assert_constructor_called_with_generics, but not in doctest 15 | >>> check_instance_of_generic_class_and_get_type_vars(a) 16 | {} 17 | >>> b = A[int]() 18 | >>> check_instance_of_generic_class_and_get_type_vars(b) 19 | {~T: } 20 | >>> c = A[List[int]]() 21 | >>> check_instance_of_generic_class_and_get_type_vars(c) 22 | {~T: typing.List[int]} 23 | >>> S = TypeVar('S') 24 | >>> class B(Generic[T, S]): pass 25 | >>> d = B() 26 | >>> check_instance_of_generic_class_and_get_type_vars(d) 27 | {} 28 | >>> e = B[int]() 29 | Traceback (most recent call last): 30 | ... 31 | TypeError: Too few ...; actual 1, expect... 2 32 | >>> f = B[int, float]() 33 | >>> check_instance_of_generic_class_and_get_type_vars(f) 34 | {~T: , ~S: } 35 | >>> class C(B): pass 36 | >>> g = C() 37 | >>> check_instance_of_generic_class_and_get_type_vars(g) 38 | {} 39 | """ 40 | type_vars = dict() 41 | _assert_constructor_called_with_generics(instance=instance) 42 | 43 | # The information I need is set after the object construction in the __orig_class__ attribute. 44 | # This method is called before construction, and therefore it returns if the value isn't set 45 | # https://stackoverflow.com/questions/60985221/how-can-i-access-t-from-a-generict-instance-early-in-its-lifecycle 46 | if not hasattr(instance, '__orig_class__'): 47 | return type_vars 48 | 49 | type_variables = get_type_arguments(type(instance).__orig_bases__[0]) 50 | actual_types = get_type_arguments(instance.__orig_class__) 51 | 52 | for i, type_var in enumerate(type_variables): 53 | type_vars[type_var] = actual_types[i] 54 | return type_vars 55 | 56 | 57 | def _assert_constructor_called_with_generics(instance: Any) -> None: 58 | """ 59 | This is very hacky. Therefore, it is kind of non-aggressive and raises only an error if is sure. 60 | 61 | >>> from typing import TypeVar, Generic, List 62 | >>> T = TypeVar('T') 63 | >>> class A(Generic[T]): pass 64 | >>> a = A() # would normally raise an error due to _assert_constructor_called_with_generics, but not in doctest 65 | >>> _assert_constructor_called_with_generics(a) 66 | >>> b = A[int]() 67 | >>> _assert_constructor_called_with_generics(b) 68 | >>> c = A[List[int]]() 69 | >>> _assert_constructor_called_with_generics(c) 70 | >>> S = TypeVar('S') 71 | >>> class B(Generic[T, S]): pass 72 | >>> d = B() 73 | >>> _assert_constructor_called_with_generics(d) 74 | >>> f = B[int, float]() 75 | >>> _assert_constructor_called_with_generics(f) 76 | >>> class C(B): pass 77 | >>> g = C() 78 | >>> _assert_constructor_called_with_generics(g) 79 | """ 80 | 81 | if hasattr(instance, ATTR_NAME_GENERIC_INSTANCE_ALREADY_CHECKED): 82 | return 83 | 84 | name = instance.__class__.__name__ 85 | q_name = instance.__class__.__qualname__ 86 | call_stack_frames = inspect.stack() 87 | frame_of_wrapper = list(filter(lambda f: f.function == 'wrapper', call_stack_frames)) 88 | if not frame_of_wrapper: 89 | return 90 | 91 | frame = call_stack_frames[call_stack_frames.index(frame_of_wrapper[-1]) + 1] 92 | while frame.filename.endswith('typing.py'): 93 | frame = call_stack_frames[call_stack_frames.index(frame) + 1] 94 | 95 | src = [_remove_comments_and_spaces_from_src_line(line) for line in inspect.getsource(frame.frame).split('\n')] 96 | target = '=' + name 97 | filtered_src = list(filter(lambda line: target in line, src)) 98 | if not filtered_src: 99 | return 100 | 101 | for match in filtered_src: 102 | constructor_call = match.split(target)[1] 103 | generics = constructor_call.split('(')[0] 104 | if '[' not in generics or ']' not in generics: 105 | raise PedanticTypeVarMismatchException( 106 | f'Use generics when you create an instance of the generic class "{q_name}". \n ' 107 | f'Your call: {match} \n How it should be called: {name}[YourType]({constructor_call.split("(")[1]}') 108 | 109 | setattr(instance, ATTR_NAME_GENERIC_INSTANCE_ALREADY_CHECKED, True) 110 | 111 | 112 | def is_instance_of_generic_class(instance: Any) -> bool: 113 | """ 114 | >>> class A: pass 115 | >>> a = A() 116 | >>> is_instance_of_generic_class(a) 117 | False 118 | >>> from typing import TypeVar, Generic 119 | >>> T = TypeVar('T') 120 | >>> class B(Generic[T]): pass 121 | >>> b = B() 122 | >>> is_instance_of_generic_class(b) 123 | True 124 | >>> b2 = B[int]() 125 | >>> is_instance_of_generic_class(b2) 126 | True 127 | """ 128 | return Generic in instance.__class__.__bases__ 129 | 130 | 131 | def _remove_comments_and_spaces_from_src_line(line: str) -> str: 132 | """ 133 | >>> _remove_comments_and_spaces_from_src_line('a = 42 # this is a comment') 134 | 'a=42' 135 | >>> _remove_comments_and_spaces_from_src_line('m = MyClass[Parent](a=Child1())') 136 | 'm=MyClass[Parent](a=Child1())' 137 | """ 138 | return line.split('#')[0].replace(' ', '') 139 | 140 | 141 | if __name__ == '__main__': 142 | import doctest 143 | doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) 144 | -------------------------------------------------------------------------------- /pedantic/type_checking_logic/resolve_forward_ref.py: -------------------------------------------------------------------------------- 1 | from typing import * # useful for globals(), see below 2 | 3 | 4 | def resolve_forward_ref(type_: str, globals_: Dict[str, Any] = None, context: Dict = None) -> Type: 5 | """ 6 | Resolve a type annotation that is a string. 7 | 8 | Raises: 9 | NameError: in case of [type_] cannot be resolved. 10 | """ 11 | 12 | return eval(str(type_), globals_ or globals(), context or {}) 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docstring-parser==0.16 2 | Flask[async]==3.0.2 # you do not need flask to use this decorators. Flask is only needed you want to use @validate for Flask endpoints 3 | multiprocess==0.70.16 4 | Werkzeug==3.0.1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | def get_content_from_readme(file_name: str = 'README.md') -> str: 7 | this_directory = path.abspath(path.dirname(__file__)) 8 | 9 | with open(path.join(this_directory, file_name), encoding='utf-8') as file: 10 | return file.read() 11 | 12 | 13 | url = "https://github.com/LostInDarkMath/pedantic-python-decorators" 14 | author = "Willi Sontopski" 15 | 16 | setup( 17 | name="pedantic", 18 | version="2.1.9", 19 | python_requires='>=3.11.0', 20 | packages=find_packages(), 21 | install_requires=[], 22 | author=author, 23 | author_email="willi_sontopski@arcor.de", 24 | license="Apache-2.0 License", 25 | maintainer=author, 26 | description="Some useful Python decorators for cleaner software development.", 27 | long_description=get_content_from_readme(), 28 | long_description_content_type='text/markdown', 29 | keywords="decorators tools helpers type-checking pedantic type annotations", 30 | url=url, 31 | project_urls={ 32 | "Bug Tracker": f'{url}/issues', 33 | "Documentation": 'https://lostindarkmath.github.io/pedantic-python-decorators/pedantic/', 34 | "Source Code": url, 35 | }, 36 | include_package_data=False, 37 | zip_safe=True, 38 | ) 39 | -------------------------------------------------------------------------------- /test_deployment.py: -------------------------------------------------------------------------------- 1 | from pedantic.tests.tests_main import run_all_tests 2 | 3 | if __name__ == '__main__': 4 | run_all_tests() 5 | --------------------------------------------------------------------------------