├── .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 |
47 |
49 |
51 |
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 |
26 |
35 |
37 |
39 |
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 |
41 |
43 |
45 |
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 |
--------------------------------------------------------------------------------