diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b5826981..7d38642f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,11 +23,11 @@ jobs: python -m pip install --upgrade pip pip install -U coveralls pip install -e .[dev] - - name: Run tests + - name: Run tests with pytest run: | - coverage run --branch pedantic/tests/tests_main.py + pytest --doctest-modules --cov=pedantic --cov-branch --cov-report= --cov-report=term - name: Coveralls - uses: coverallsapp/github-action@v2.2.3 + uses: coverallsapp/github-action@v2.3.6 with: github-token: ${{ secrets.GITHUB_TOKEN }} file: .coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 79faf4b1..0851406c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +## Pedantic 2.4.0 +- migrate from unittest to pytest +- exclude tests from package deployment +- add an optional `message` argument to `@deprecated` decorator + ## Pedantic 2.3.3 - [fixed validation error message in MinLength validator](https://github.com/LostInDarkMath/pedantic-python-decorators/issues/111) - removed unused scripts diff --git a/docs/pedantic/decorators/fn_deco_deprecated.html b/docs/pedantic/decorators/fn_deco_deprecated.html index 7e3dab10..5dd14fcd 100644 --- a/docs/pedantic/decorators/fn_deco_deprecated.html +++ b/docs/pedantic/decorators/fn_deco_deprecated.html @@ -45,37 +45,53 @@

Module pedantic.decorators.fn_deco_deprecated

Functions
-def deprecated(func: Callable[..., ~ReturnType]) ‑> Callable[..., ~ReturnType] +def deprecated(func: Callable[..., ~ReturnType] | None = None, message: str = '') ‑> Callable[..., ~ReturnType] | Callable[[Callable[..., ~ReturnType]], Callable[..., ~ReturnType]]
Expand source code -
def deprecated(func: F) -> F:
+
def deprecated(func: F | None = None, message: str = '') -> F | Callable[[F], F]:
     """
         Use this decorator to mark a function as deprecated. It will raise a warning when the function is called.
+        You can specify an optional reason or message to display with the warning.
 
         Example:
-
         >>> @deprecated
         ... def my_function(a, b, c):
         ...     pass
-        >>> my_function(5, 4, 3)
+        >>> my_function(5, 4, 3)  # doctest: +SKIP
+        >>> @deprecated(message='Will be removed soon. Please use my_function_new_instead.')
+        ... def my_function(a, b, c):
+        ...     pass
+        >>> my_function(5, 4, 3)  # doctest: +SKIP
     """
 
-    @wraps(func)
-    def wrapper(*args: Any, **kwargs: Any) -> ReturnType:
-        _raise_warning(msg=f'Call to deprecated function {func.__qualname__}.', category=DeprecationWarning)
-        return func(*args, **kwargs)
-    return wrapper
+ def decorator(fun: F) -> F: + @wraps(fun) + def wrapper(*args: Any, **kwargs: Any) -> ReturnType: + msg = f'Call to deprecated function {fun.__qualname__}.' + + if message: + msg += f'\nReason: {message}' + + warnings.warn(message=msg, category=DeprecationWarning, stacklevel=2) + return fun(*args, **kwargs) + return wrapper + return decorator if func is None else decorator(func)
-

Use this decorator to mark a function as deprecated. It will raise a warning when the function is called.

+

Use this decorator to mark a function as deprecated. It will raise a warning when the function is called. +You can specify an optional reason or message to display with the warning.

Example:

>>> @deprecated
 ... def my_function(a, b, c):
 ...     pass
->>> my_function(5, 4, 3)
+>>> my_function(5, 4, 3)  # doctest: +SKIP
+>>> @deprecated(message='Will be removed soon. Please use my_function_new_instead.')
+... def my_function(a, b, c):
+...     pass
+>>> my_function(5, 4, 3)  # doctest: +SKIP
 
diff --git a/docs/pedantic/decorators/fn_deco_retry.html b/docs/pedantic/decorators/fn_deco_retry.html index 72c65185..c55e6503 100644 --- a/docs/pedantic/decorators/fn_deco_retry.html +++ b/docs/pedantic/decorators/fn_deco_retry.html @@ -73,7 +73,7 @@

Functions

>>> @retry(attempts=3, exceptions=(ValueError, TypeError)) ... def foo(): ... raise ValueError('Some error') - >>> foo() + >>> foo() # doctest: +SKIP """ def decorator(func: C) -> C: @@ -102,7 +102,7 @@

Example

>>> @retry(attempts=3, exceptions=(ValueError, TypeError))
 ... def foo():
 ...     raise ValueError('Some error')
->>> foo()
+>>> foo()  # doctest: +SKIP
 
diff --git a/docs/pedantic/decorators/fn_deco_validate/validators/max.html b/docs/pedantic/decorators/fn_deco_validate/validators/max.html index 38045f83..419900da 100644 --- a/docs/pedantic/decorators/fn_deco_validate/validators/max.html +++ b/docs/pedantic/decorators/fn_deco_validate/validators/max.html @@ -59,11 +59,12 @@

Classes

def __init__(self, value: Union[int, float], include_boundary: bool = True) -> None: """ >>> Max(7, True).validate(7) - True - >>> Max(7, False).validate(7) - False + 7 + >>> Max(7, False).validate(7) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValidatorException: ... >>> Max(7, False).validate(6.999) - True + 6.999 """ self._value = value self._include_boundary = include_boundary @@ -80,11 +81,12 @@

Classes

Helper class that provides a standard way to create an ABC using inheritance.

>>> Max(7, True).validate(7)
-True
->>> Max(7, False).validate(7)
-False
+7
+>>> Max(7, False).validate(7)  # doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+ValidatorException: ...
 >>> Max(7, False).validate(6.999)
-True
+6.999
 

Ancestors

-

Subclasses

-
class WithDecoratedMethods @@ -152,7 +148,7 @@

Subclasses

... def m3(self) -> None: ... print('bar') >>> instance = MyClass() - >>> instance.get_decorated_functions() + >>> instance.get_decorated_functions() # doctest: +SKIP { <Decorators.FOO: '_foo'>: { <bound method MyClass.m1 of <__main__.MyClass object at 0x7fea7a6e2610>>: 42, @@ -203,7 +199,7 @@

Example

... def m3(self) -> None: ... print('bar') >>> instance = MyClass() ->>> instance.get_decorated_functions() +>>> instance.get_decorated_functions() # doctest: +SKIP { <Decorators.FOO: '_foo'>: { <bound method MyClass.m1 of <__main__.MyClass object at 0x7fea7a6e2610>>: 42, diff --git a/pedantic/decorators/cls_deco_frozen_dataclass.py b/pedantic/decorators/cls_deco_frozen_dataclass.py index aeff42e6..70e90354 100644 --- a/pedantic/decorators/cls_deco_frozen_dataclass.py +++ b/pedantic/decorators/cls_deco_frozen_dataclass.py @@ -142,8 +142,3 @@ def validate_types(self, *, _context: Dict[str, Type] = None) -> None: return decorator return decorator(cls_=cls) - - -if __name__ == '__main__': - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_count_calls.py b/pedantic/decorators/fn_deco_count_calls.py index 7fed2abb..3fc1ebd0 100644 --- a/pedantic/decorators/fn_deco_count_calls.py +++ b/pedantic/decorators/fn_deco_count_calls.py @@ -30,8 +30,3 @@ def wrapper(*args: Any, **kwargs: Any) -> ReturnType: wrapper.num_calls = 0 return wrapper - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_deprecated.py b/pedantic/decorators/fn_deco_deprecated.py index 4a0bae9f..62772466 100644 --- a/pedantic/decorators/fn_deco_deprecated.py +++ b/pedantic/decorators/fn_deco_deprecated.py @@ -1,29 +1,40 @@ +import warnings from functools import wraps -from typing import Any +from typing import Any, Callable, overload from pedantic.constants import F, ReturnType -from pedantic.helper_methods import _raise_warning +@overload +def deprecated(func: F) -> F: ... -def deprecated(func: F) -> F: +@overload +def deprecated(*, message: str = '') -> Callable[[F], F]: ... + +def deprecated(func: F | None = None, message: str = '') -> F | Callable[[F], F]: """ Use this decorator to mark a function as deprecated. It will raise a warning when the function is called. + You can specify an optional reason or message to display with the warning. Example: - >>> @deprecated ... def my_function(a, b, c): ... pass - >>> my_function(5, 4, 3) + >>> my_function(5, 4, 3) # doctest: +SKIP + >>> @deprecated(message='Will be removed soon. Please use my_function_new_instead.') + ... def my_function(a, b, c): + ... pass + >>> my_function(5, 4, 3) # doctest: +SKIP """ - @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> ReturnType: - _raise_warning(msg=f'Call to deprecated function {func.__qualname__}.', category=DeprecationWarning) - return func(*args, **kwargs) - return wrapper + def decorator(fun: F) -> F: + @wraps(fun) + def wrapper(*args: Any, **kwargs: Any) -> ReturnType: + msg = f'Call to deprecated function {fun.__qualname__}.' + if message: + msg += f'\nReason: {message}' -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) + warnings.warn(message=msg, category=DeprecationWarning, stacklevel=2) + return fun(*args, **kwargs) + return wrapper + return decorator if func is None else decorator(func) diff --git a/pedantic/decorators/fn_deco_does_same_as_function.py b/pedantic/decorators/fn_deco_does_same_as_function.py index 5629cabd..0ce47eaa 100644 --- a/pedantic/decorators/fn_deco_does_same_as_function.py +++ b/pedantic/decorators/fn_deco_does_same_as_function.py @@ -52,8 +52,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: return wrapper return decorator - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_in_subprocess.py b/pedantic/decorators/fn_deco_in_subprocess.py index 8809d49e..b308acb0 100644 --- a/pedantic/decorators/fn_deco_in_subprocess.py +++ b/pedantic/decorators/fn_deco_in_subprocess.py @@ -120,9 +120,3 @@ def _inner(tx: Connection, fun: Callable[..., Union[T, Awaitable[T]]], *a, **kw_ tx.send(SubprocessError(ex=ex)) else: tx.send(res) - - -if __name__ == '__main__': - import doctest - - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_mock.py b/pedantic/decorators/fn_deco_mock.py index 47ca926d..75639183 100644 --- a/pedantic/decorators/fn_deco_mock.py +++ b/pedantic/decorators/fn_deco_mock.py @@ -36,8 +36,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: else: return wrapper return decorator - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_overrides.py b/pedantic/decorators/fn_deco_overrides.py index ed6727b6..cdfa850f 100644 --- a/pedantic/decorators/fn_deco_overrides.py +++ b/pedantic/decorators/fn_deco_overrides.py @@ -29,8 +29,3 @@ def decorator(func: F) -> F: f'Base class "{base_class.__name__}" does not have such a method "{name}".') return func return decorator - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_pedantic.py b/pedantic/decorators/fn_deco_pedantic.py index 44a8ccfd..1ea79359 100644 --- a/pedantic/decorators/fn_deco_pedantic.py +++ b/pedantic/decorators/fn_deco_pedantic.py @@ -77,8 +77,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: def pedantic_require_docstring(func: Optional[F] = None) -> F: """Shortcut for @pedantic(require_docstring=True) """ return pedantic(func=func, require_docstring=True) - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_rename_kwargs.py b/pedantic/decorators/fn_deco_rename_kwargs.py index e7a416b5..b529a22c 100644 --- a/pedantic/decorators/fn_deco_rename_kwargs.py +++ b/pedantic/decorators/fn_deco_rename_kwargs.py @@ -45,8 +45,3 @@ def wrapper(*args, **kwargs) -> ReturnType: return func(*args, **result_kwargs) return wrapper return decorator - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_require_kwargs.py b/pedantic/decorators/fn_deco_require_kwargs.py index 4ddc0d37..f763b31c 100644 --- a/pedantic/decorators/fn_deco_require_kwargs.py +++ b/pedantic/decorators/fn_deco_require_kwargs.py @@ -31,8 +31,3 @@ def wrapper(*args: Any, **kwargs: Any) -> ReturnType: call.assert_uses_kwargs() return func(*args, **kwargs) return wrapper - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_retry.py b/pedantic/decorators/fn_deco_retry.py index 211006c2..10fdb75b 100644 --- a/pedantic/decorators/fn_deco_retry.py +++ b/pedantic/decorators/fn_deco_retry.py @@ -31,7 +31,7 @@ def retry( >>> @retry(attempts=3, exceptions=(ValueError, TypeError)) ... def foo(): ... raise ValueError('Some error') - >>> foo() + >>> foo() # doctest: +SKIP """ def decorator(func: C) -> C: diff --git a/pedantic/decorators/fn_deco_timer.py b/pedantic/decorators/fn_deco_timer.py index c1751b08..843fc477 100644 --- a/pedantic/decorators/fn_deco_timer.py +++ b/pedantic/decorators/fn_deco_timer.py @@ -42,8 +42,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: return async_wrapper else: return wrapper - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_trace.py b/pedantic/decorators/fn_deco_trace.py index 2c4cb498..aab72668 100644 --- a/pedantic/decorators/fn_deco_trace.py +++ b/pedantic/decorators/fn_deco_trace.py @@ -39,8 +39,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: return async_wrapper else: return wrapper - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_trace_if_returns.py b/pedantic/decorators/fn_deco_trace_if_returns.py index c7f3954e..5ef3a693 100644 --- a/pedantic/decorators/fn_deco_trace_if_returns.py +++ b/pedantic/decorators/fn_deco_trace_if_returns.py @@ -46,8 +46,3 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> ReturnType: else: return wrapper return decorator - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_unimplemented.py b/pedantic/decorators/fn_deco_unimplemented.py index 3db7a9e0..e84e513d 100644 --- a/pedantic/decorators/fn_deco_unimplemented.py +++ b/pedantic/decorators/fn_deco_unimplemented.py @@ -24,8 +24,3 @@ def unimplemented(func: F) -> F: def wrapper(*args: Any, **kwargs: Any) -> ReturnType: raise NotImplementedException(f'Function "{func.__qualname__}" is not implemented yet!') return wrapper - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/decorators/fn_deco_validate/validators/max.py b/pedantic/decorators/fn_deco_validate/validators/max.py index 19506116..c687d0f1 100644 --- a/pedantic/decorators/fn_deco_validate/validators/max.py +++ b/pedantic/decorators/fn_deco_validate/validators/max.py @@ -8,11 +8,12 @@ class Max(Validator): def __init__(self, value: Union[int, float], include_boundary: bool = True) -> None: """ >>> Max(7, True).validate(7) - True - >>> Max(7, False).validate(7) - False + 7 + >>> Max(7, False).validate(7) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValidatorException: ... >>> Max(7, False).validate(6.999) - True + 6.999 """ self._value = value self._include_boundary = include_boundary diff --git a/pedantic/decorators/fn_deco_validate/validators/min.py b/pedantic/decorators/fn_deco_validate/validators/min.py index e5966bdc..8a8e3550 100644 --- a/pedantic/decorators/fn_deco_validate/validators/min.py +++ b/pedantic/decorators/fn_deco_validate/validators/min.py @@ -8,11 +8,12 @@ class Min(Validator): def __init__(self, value: Union[int, float], include_boundary: bool = True) -> None: """ >>> Min(7, True).validate(7) - True - >>> Min(7, False).validate(7) - False + 7 + >>> Min(7, False).validate(7) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValidatorException: ... >>> Min(7, False).validate(7.001) - True + 7.001 """ self._value = value self._include_boundary = include_boundary diff --git a/pedantic/helper_methods.py b/pedantic/helper_methods.py deleted file mode 100644 index 34487999..00000000 --- a/pedantic/helper_methods.py +++ /dev/null @@ -1,8 +0,0 @@ -import warnings -from typing import Type - - -def _raise_warning(msg: str, category: Type[Warning]) -> None: - warnings.simplefilter(action='always', category=category) - warnings.warn(message=msg, category=category, stacklevel=2) - warnings.simplefilter(action='default', category=category) diff --git a/pedantic/mixins/generic_mixin.py b/pedantic/mixins/generic_mixin.py index 7f542622..94ed2987 100644 --- a/pedantic/mixins/generic_mixin.py +++ b/pedantic/mixins/generic_mixin.py @@ -140,8 +140,3 @@ def class_name(self) -> str: """ Get the name of the class of this instance. """ return type(self).__name__ - - -if __name__ == '__main__': - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/mixins/with_decorated_methods.py b/pedantic/mixins/with_decorated_methods.py index 79bd478d..f5ecded3 100644 --- a/pedantic/mixins/with_decorated_methods.py +++ b/pedantic/mixins/with_decorated_methods.py @@ -68,7 +68,7 @@ class WithDecoratedMethods(ABC, Generic[DecoratorTypeVar], GenericMixin): ... def m3(self) -> None: ... print('bar') >>> instance = MyClass() - >>> instance.get_decorated_functions() + >>> instance.get_decorated_functions() # doctest: +SKIP { : { >: 42, diff --git a/pedantic/tests/__init__.py b/pedantic/tests/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/pedantic/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pedantic/tests/test_assert_value_matches_type.py b/pedantic/tests/test_assert_value_matches_type.py deleted file mode 100644 index 79a91046..00000000 --- a/pedantic/tests/test_assert_value_matches_type.py +++ /dev/null @@ -1,209 +0,0 @@ -import unittest -from abc import ABC -from dataclasses import dataclass -from datetime import datetime -from typing import Callable, Awaitable, Coroutine, Any, Union, Optional, Generic, TypeVar, Tuple - -from pedantic.exceptions import PedanticTypeCheckException -from pedantic.type_checking_logic.check_types import assert_value_matches_type - - -@dataclass -class Foo: - value: int - - -class TestAssertValueMatchesType(unittest.TestCase): - def test_callable(self): - def _cb(foo: Foo) -> str: - return str(foo.value) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., str], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., int], - err='', - type_vars={}, - ) - - def test_callable_return_type_none(self): - def _cb(foo: Foo) -> None: - return print(foo) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., None], - err='', - type_vars={}, - ) - - def test_callable_awaitable(self): - async def _cb(foo: Foo) -> str: - return str(foo.value) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[str]], - err='', - type_vars={}, - ) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[Any]], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[int]], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., str], - err='', - type_vars={}, - ) - - def test_callable_coroutine(self): - async def _cb(foo: Foo) -> str: - return str(foo.value) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Coroutine[None, None, str]], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., Coroutine[None, None, int]], - err='', - type_vars={}, - ) - - def test_callable_awaitable_with_none_return_type(self): - async def _cb(foo: Foo) -> None: - print(foo) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[None]], - err='', - type_vars={}, - ) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[Any]], - err='', - type_vars={}, - ) - - def test_callable_with_old_union_type_hint(self): - async def _cb(machine_id: str) -> Union[int, None]: - return 42 - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[Union[int, None]]], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[int]], - err='', - type_vars={}, - ) - - def test_callable_with_new_union_type_hint(self): - async def _cb(machine_id: str) -> int | None: - return 42 - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[int | None]], - err='', - type_vars={}, - ) - - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[Any]], - err='', - type_vars={}, - ) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - assert_value_matches_type( - value=_cb, - type_=Callable[..., Awaitable[int]], - err='', - type_vars={}, - ) - - def test_forward_ref_inheritance(self): - T = TypeVar('T') - - class State(Generic[T], ABC): - pass - - class StateMachine(Generic[T], ABC): - pass - - class MachineState(State['MachineStateMachine']): - pass - - class OfflineMachineState(MachineState): - pass - - class MachineStateMachine(StateMachine[MachineState]): - pass - - assert_value_matches_type( - value=OfflineMachineState(), - type_=Optional['MachineState'], - err='', - type_vars={}, - context=locals(), - ) - - def test_tuple_with_ellipsis(self): - """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/75 """ - - assert_value_matches_type( - value=(1, 2.0, 'hello'), - type_=Tuple[Any, ...], - err='', - type_vars={}, - context=locals(), - ) - - def test_union_of_callable(self): - """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/74 """ - - assert_value_matches_type( - value=datetime.now(), - type_=Union[datetime, Callable[[], datetime]], - err='', - type_vars={}, - context=locals(), - ) diff --git a/pedantic/tests/test_async_context_manager.py b/pedantic/tests/test_async_context_manager.py deleted file mode 100644 index c5c6e4a9..00000000 --- a/pedantic/tests/test_async_context_manager.py +++ /dev/null @@ -1,77 +0,0 @@ -import unittest - -from pedantic.decorators import safe_async_contextmanager - - -class TestAsyncContextManager(unittest.IsolatedAsyncioTestCase): - async def test_safe_context_manager_no_exception(self): - before = False - after = False - - @safe_async_contextmanager - async def foo(): - nonlocal before, after - before = True - yield 42 - after = True - - self.assertFalse(before) - self.assertFalse(after) - - async with foo() as f: - self.assertTrue(before) - self.assertFalse(after) - self.assertEqual(42, f) - - self.assertTrue(before) - self.assertTrue(after) - - async def test_safe_context_manager_with_exception(self): - before = False - after = False - - @safe_async_contextmanager - async def foo(): - nonlocal before, after - before = True - yield 42 - after = True - - self.assertFalse(before) - self.assertFalse(after) - - with self.assertRaises(expected_exception=ValueError): - async with foo() as f: - self.assertTrue(before) - self.assertFalse(after) - self.assertEqual(42, f) - raise ValueError('oh no') - - self.assertTrue(before) - self.assertTrue(after) - - async def test_safe_context_manager_with_args_kwargs(self): - @safe_async_contextmanager - async def foo(a, b): - yield a, b - - async with foo(42, b=43) as f: - self.assertEqual((42, 43), f) - - def test_safe_context_manager_async(self): - with self.assertRaises(expected_exception=AssertionError) as e: - @safe_async_contextmanager - def foo(a, b): - yield a, b - - expected = 'foo is not an async generator. So you need to use "safe_contextmanager" instead.' - self.assertEqual(expected, e.exception.args[0]) - - async def test_safe_context_manager_non_generator(self): - with self.assertRaises(expected_exception=AssertionError) as e: - @safe_async_contextmanager - async def foo(a, b): - return a, b - - expected = 'foo is not a generator.' - self.assertEqual(expected, e.exception.args[0]) diff --git a/pedantic/tests/test_context_manager.py b/pedantic/tests/test_context_manager.py deleted file mode 100644 index 7162e426..00000000 --- a/pedantic/tests/test_context_manager.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators import safe_contextmanager - - -class TestContextManager(TestCase): - def test_safe_context_manager_no_exception(self): - before = False - after = False - - @safe_contextmanager - def foo(): - nonlocal before, after - before = True - yield 42 - after = True - - self.assertFalse(before) - self.assertFalse(after) - - with foo() as f: - self.assertTrue(before) - self.assertFalse(after) - self.assertEqual(42, f) - - self.assertTrue(before) - self.assertTrue(after) - - def test_safe_context_manager_with_exception(self): - before = False - after = False - - @safe_contextmanager - def foo(): - nonlocal before, after - before = True - yield 42 - after = True - - self.assertFalse(before) - self.assertFalse(after) - - with self.assertRaises(expected_exception=ValueError): - with foo() as f: - self.assertTrue(before) - self.assertFalse(after) - self.assertEqual(42, f) - raise ValueError('oh no') - - self.assertTrue(before) - self.assertTrue(after) - - def test_safe_context_manager_with_args_kwargs(self): - @safe_contextmanager - def foo(a, b): - yield a, b - - with foo(42, b=43) as f: - self.assertEqual((42, 43), f) - - def test_safe_context_manager_async(self): - with self.assertRaises(expected_exception=AssertionError) as e: - @safe_contextmanager - async def foo(a, b): - yield a, b - - expected = 'foo is async. So you need to use "safe_async_contextmanager" instead.' - self.assertEqual(expected, e.exception.args[0]) - - def test_safe_context_manager_non_generator(self): - with self.assertRaises(expected_exception=AssertionError) as e: - @safe_contextmanager - def foo(a, b): - return a, b - - expected = 'foo is not a generator.' - self.assertEqual(expected, e.exception.args[0]) diff --git a/pedantic/tests/test_frozen_dataclass.py b/pedantic/tests/test_frozen_dataclass.py deleted file mode 100644 index a5ec5c04..00000000 --- a/pedantic/tests/test_frozen_dataclass.py +++ /dev/null @@ -1,373 +0,0 @@ -import unittest -from abc import ABC -from dataclasses import dataclass, FrozenInstanceError -from typing import List, Dict, Set, Tuple, Awaitable, Callable, Generic, TypeVar, Optional - -from pedantic.decorators.cls_deco_frozen_dataclass import frozen_dataclass, frozen_type_safe_dataclass -from pedantic.exceptions import PedanticTypeCheckException - - -@frozen_dataclass -class Foo: - a: int - b: str - c: bool - - -@frozen_type_safe_dataclass -class B: - v: Set[int] - - -@frozen_type_safe_dataclass -class A: - foo: List[int] - bar: Dict[str, str] - values: Tuple[B, B] - - -class TestFrozenDataclass(unittest.TestCase): - def test_equals_and_hash(self): - a = Foo(a=6, b='hi', c=True) - b = Foo(a=6, b='hi', c=True) - c = Foo(a=7, b='hi', c=True) - - self.assertEqual(a, b) - self.assertEqual(hash(a), hash(b)) - - self.assertNotEqual(a, c) - self.assertNotEqual(hash(a), hash(c)) - - def test_copy_with(self): - foo = Foo(a=6, b='hi', c=True) - - copy_1 = foo.copy_with() - self.assertEqual(foo, copy_1) - - copy_2 = foo.copy_with(a=42) - self.assertNotEqual(foo, copy_2) - self.assertEqual(42, copy_2.a) - self.assertEqual(foo.b, copy_2.b) - self.assertEqual(foo.c, copy_2.c) - - copy_3 = foo.copy_with(b='Hello') - self.assertNotEqual(foo, copy_3) - self.assertEqual(foo.a, copy_3.a) - self.assertEqual('Hello', copy_3.b) - self.assertEqual(foo.c, copy_3.c) - - copy_4 = foo.copy_with(c=False) - self.assertNotEqual(foo, copy_4) - self.assertEqual(foo.a, copy_4.a) - self.assertEqual(foo.b, copy_4.b) - self.assertEqual(False, copy_4.c) - - copy_5 = foo.copy_with(a=676676, b='new', c=False) - self.assertNotEqual(foo, copy_5) - self.assertEqual(676676, copy_5.a) - self.assertEqual('new', copy_5.b) - self.assertEqual(False, copy_5.c) - - def test_validate_types(self): - foo = Foo(a=6, b='hi', c=True) - foo.validate_types() - - bar = Foo(a=6.6, b='hi', c=True) - - with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc: - bar.validate_types() - - expected = 'In dataclass "Foo" in field "a": Type hint is incorrect: Argument 6.6 of type ' \ - 'does not match expected type .' - self.assertEqual(str(exc.exception), expected) - - def test_frozen_dataclass_above_dataclass(self): - # This is the same behavior like - # >>> @dataclass(frozen=True) - # ... @dataclass - # ... class C: - # ... foo: int - - @frozen_dataclass - @dataclass - class A: - foo: int - - with self.assertRaises(expected_exception=TypeError): - A() - - with self.assertRaises(expected_exception=FrozenInstanceError): - A(foo=3) - - def test_frozen_dataclass_below_dataclass(self): - @dataclass - @frozen_dataclass - class A: - foo: int - - with self.assertRaises(expected_exception=TypeError): - A() - - a = A(foo=3) - - with self.assertRaises(expected_exception=FrozenInstanceError): - a.foo = 4 - - b = a.copy_with(foo=4) - self.assertEqual(4, b.foo) - - - def test_frozen_typesafe_dataclass_with_post_init(self): - b = 3 - - @frozen_dataclass(type_safe=True) - class A: - foo: int - - def __post_init__(self) -> None: - nonlocal b - b = 33 - - with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc: - A(foo=42.7) - - self.assertEqual( - 'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type' - ' does not match expected type .', - str(exc.exception) - ) - - # we check that the __post_init__ method is executed - self.assertEqual(33, b) - - def test_frozen_typesafe_dataclass_without_post_init(self): - @frozen_dataclass(type_safe=True) - class A: - foo: int - - with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc: - A(foo=42.7) - - self.assertEqual( - 'In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type' - ' does not match expected type .', - str(exc.exception) - ) - - def test_frozen_dataclass_with_empty_braces(self): - @frozen_dataclass() - class A: - foo: int - - a = A(foo=42) - self.assertEqual(42, a.foo) - - def test_frozen_dataclass_no_braces(self): - @frozen_dataclass - class A: - foo: int - - a = A(foo=42) - self.assertEqual(42, a.foo) - - def test_frozen_dataclass_order(self): - @frozen_dataclass(order=True) - class A: - foo: int - bar: int - - a = A(foo=42, bar=43) - b = A(foo=42, bar=42) - c = A(foo=41, bar=44) - d = A(foo=44, bar=0) - self.assertLess(b, a) - self.assertLess(c, b) - self.assertLess(a, d) - - def test_frozen_type_safe_dataclass_copy_with_check(self): - @frozen_type_safe_dataclass - class A: - foo: int - bar: bool - - a = A(foo=42, bar=False) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - a.copy_with(foo=1.1) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - a.copy_with(bar=11) - - a.copy_with(foo=11, bar=True) - - def test_copy_with_is_shallow(self): - a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6}))) - shallow = a.copy_with() - - # manipulation - shallow.bar['hello'] = 'pedantic' - shallow.foo.append(3) - - self.assertEqual([1, 2, 3], a.foo) - self.assertEqual([1, 2, 3], shallow.foo) - self.assertEqual('pedantic', a.bar['hello']) - self.assertEqual('pedantic', shallow.bar['hello']) - - def test_copy_with_and_deep_copy_with(self): - a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6}))) - deep = a.deep_copy_with() - - # manipulation - deep.bar['hello'] = 'pedantic' - deep.foo.append(3) - - self.assertEqual([1, 2], a.foo) - self.assertEqual([1, 2, 3], deep.foo) - self.assertEqual('world', a.bar['hello']) - self.assertEqual('pedantic', deep.bar['hello']) - - def test_frozen_dataclass_inheritance_override_post_init(self): - i = 1 - - @frozen_type_safe_dataclass - class A: - bar: int - - def __post_init__(self): - nonlocal i - i += 1 - print('hello a') - - @frozen_type_safe_dataclass - class B(A): - foo: int - - def __post_init__(self): - nonlocal i - i *= 10 - print('hello b') - - A(bar=3) - self.assertEqual(2, i) - - b = B(bar=3, foo=42) - self.assertEqual(20, i) # post init of A was not called - self.assertEqual(3, b.bar) - self.assertEqual(42, b.foo) - - a = b.copy_with() - self.assertEqual(b, a) - self.assertEqual(200, i) - - def test_frozen_dataclass_inheritance_not_override_post_init(self): - i = 1 - - @frozen_type_safe_dataclass - class A: - bar: int - - def __post_init__(self): - nonlocal i - i += 1 - print('hello a') - - @frozen_type_safe_dataclass - class B(A): - foo: int - - A(bar=3) - self.assertEqual(2, i) - - b = B(bar=3, foo=42) - self.assertEqual(3, i) # post init of A was called - self.assertEqual(3, b.bar) - self.assertEqual(42, b.foo) - - a = b.copy_with() - self.assertEqual(b, a) - self.assertEqual(4, i) - - def test_type_safe_frozen_dataclass_with_awaitable(self): - @frozen_type_safe_dataclass - class A: - f: Callable[..., Awaitable[int]] - - async def _cb() -> int: - return 42 - - async def _cb_2() -> str: - return '42' - - A(f=_cb) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - A(f=_cb_2) - - def test_type_safe_frozen_dataclass_with_forward_ref(self): - T = TypeVar('T') - - class State(Generic[T], ABC): - pass - - class StateMachine(Generic[T], ABC): - pass - - @frozen_type_safe_dataclass - class StateChangeResult: - new_state: Optional['MachineState'] - - class MachineState(State['MachineStateMachine']): - pass - - class OfflineMachineState(MachineState): - pass - - class OnlineMachineState: - pass - - class MachineStateMachine(StateMachine[MachineState]): - pass - - s = StateChangeResult(new_state=OfflineMachineState()) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - StateChangeResult(new_state=OnlineMachineState()) - - s.validate_types() - - def test_forward_ref_to_itself(self): - """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """ - - @frozen_type_safe_dataclass - class Comment: - replies: List['Comment'] - - comment = Comment(replies=[Comment(replies=[])]) - comment.copy_with(replies=[Comment(replies=[])]) - comment.validate_types() - - def test_forward_ref_to_itself_while_class_not_in_scope(self): - """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """ - - def _scope(): - @frozen_type_safe_dataclass - class Comment: - replies: List['Comment'] - - def _make(replies=None): - return Comment(replies=replies or []) - - return _make - - make = _scope() - - comment = make(replies=[make(replies=[])]) - comment.copy_with(replies=[make(replies=[])]) - comment.validate_types() - - def test_slots_work_with_equals(self): - @frozen_dataclass(slots=True) - class Foo: - a: int - - o = Foo(a=0) - assert o == o.copy_with() diff --git a/pedantic/tests/test_generator_wrapper.py b/pedantic/tests/test_generator_wrapper.py deleted file mode 100644 index 24256442..00000000 --- a/pedantic/tests/test_generator_wrapper.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Iterator -from unittest import TestCase - -from pedantic.models import GeneratorWrapper - - -class TestGeneratorWrapper(TestCase): - def test_generator_wrapper(self) -> None: - def gen_func() -> Iterator[int]: - num = 0 - - while num < 100: - yield num - num += 1 - - generator = gen_func() - - g = GeneratorWrapper( - wrapped=generator, - expected_type=Iterator[int], - err_msg='msg', - type_vars={}, - ) - - print(sum([x for x in g])) - - with self.assertRaises(expected_exception=Exception): - g.throw(Exception('error')) - - with self.assertRaises(expected_exception=AttributeError): - g.invalid - - g.close() diff --git a/pedantic/tests/test_generic_mixin.py b/pedantic/tests/test_generic_mixin.py deleted file mode 100644 index 7211dbad..00000000 --- a/pedantic/tests/test_generic_mixin.py +++ /dev/null @@ -1,165 +0,0 @@ -import unittest -from typing import TypeVar, Generic, List, Type - -from pedantic import GenericMixin - -A = TypeVar('A') -E = TypeVar('E') -S = TypeVar('S') -T = TypeVar('T') -U = TypeVar('U') - - -class TestGenericMixin(unittest.TestCase): - def test_single_type_var(self): - class Foo(Generic[T], GenericMixin): - value: T - - foo = Foo[str]() - assert foo.type_var == str - assert foo.type_vars == {T: str} - - invalid = Foo() - - with self.assertRaises(expected_exception=AssertionError) as err: - invalid.type_var - - assert f'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.exception.args[0] - - def test_multiple_type_vars(self): - class Foo(Generic[T, U], GenericMixin): - value: T - values: List[U] - - foo = Foo[str, int]() - - with self.assertRaises(expected_exception=AssertionError) as err: - foo.type_var - - self.assertEqual(err.exception.args[0], 'You have multiple type parameters. ' - 'Please use "type_vars" instead of "type_var".') - - assert foo.type_vars == {T: str, U: int} - - invalid = Foo() - - with self.assertRaises(expected_exception=AssertionError) as err: - invalid.type_var - - assert f'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.exception.args[0] - - def test_non_generic_class(self): - class Foo(GenericMixin): - value: int - - invalid = Foo() - - with self.assertRaises(expected_exception=AssertionError) as err: - invalid.type_var - - self.assertEqual(err.exception.args[0], f'Foo is not a generic class. To make it generic, declare it like: ' - f'class Foo(Generic[T], GenericMixin):...') - - def test_call_type_var_in_constructor(self): - class Foo(Generic[T], GenericMixin): - def __init__(self) -> None: - self.x = self.type_var() - - with self.assertRaises(expected_exception=AssertionError) as err: - Foo[str]() - - assert 'make sure that you do not call this in the __init__() method' in err.exception.args[0] - - def test_subclass_set_type_variable(self): - class Gen(Generic[T], GenericMixin): - def __init__(self, value: T) -> None: - self.value = value - - def get_type(self) -> dict[TypeVar, Type]: - return self.type_vars - - class MyClass(Gen[int]): - pass - - bar = Gen[int](value=4) - assert bar.get_type() == {T: int} - - foo = MyClass(value=4) - assert foo.get_type() == {T: int} - - def test_subclass_with_multiple_parents(self): - class Gen(Generic[T], GenericMixin): - def __init__(self, value: T) -> None: - self.value = value - - def get_type(self) -> dict[TypeVar, Type]: - return self.type_vars - - class MyMixin: - value = 42 - - class MyClass(MyMixin, Gen[int]): - pass - - bar = Gen[int](value=4) - assert bar.get_type() == {T: int} - - foo = MyClass(value=4) - assert foo.get_type() == {T: int} - - def test_resolved_type_var_inheritance(self): - class Foo(Generic[T]): ... - - class Bar(Foo[int], Generic[U], GenericMixin): ... - - bar = Bar[str]() - assert bar.type_vars == {T: int, U: str} - - def test_resolved_type_var_inheritance_2(self): - class Foo(Generic[T], GenericMixin): ... - - class Bar(Foo[int], Generic[U]): ... - - bar = Bar[str]() - assert bar.type_vars == {T: int, U: str} - - def test_very_complex_inheritance(self): - class Foo(Generic[E], GenericMixin): ... - class Bar(Foo[int], Generic[S]): ... - class Baz(Foo[int]): ... - class Deep(Baz): ... - class Deeper(Baz, Generic[T]): ... - - foo = Foo[str]() - actual = foo.type_vars - assert actual == {E: str} - - bar = Bar[str]() - actual = bar.type_vars - assert actual == {E: int, S: str} - - baz = Baz() - actual = baz.type_vars - assert actual == {E: int} - - deep = Deep() - actual = deep.type_vars - assert actual == {E: int} - - deeper = Deeper[bool]() - actual = deeper.type_vars - assert actual == {E: int, T: bool} - - with self.assertRaises(expected_exception=AssertionError) as err: - Foo().type_vars - - assert 'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.exception.args[0] - - def test_substitution_lookup_hits(self): - class Base(Generic[A], GenericMixin): ... - class Mid(Base[A], Generic[A]): ... - class Final(Mid[int]): ... - - obj = Final() - actual = obj.type_vars - assert actual == {A: int} diff --git a/pedantic/tests/test_in_subprocess.py b/pedantic/tests/test_in_subprocess.py deleted file mode 100644 index f3f8d3ed..00000000 --- a/pedantic/tests/test_in_subprocess.py +++ /dev/null @@ -1,152 +0,0 @@ -import asyncio -import time -import unittest -from typing import NoReturn - -from multiprocess import Pipe - -from pedantic import in_subprocess -from pedantic.decorators.fn_deco_in_subprocess import _inner, SubprocessError - - -class TestInSubprocess(unittest.IsolatedAsyncioTestCase): - async def test_in_subprocess_simple(self): - @in_subprocess - def f() -> int: - return 42 - - assert await f() == 42 - - async def test_in_subprocess_custom_object(self): - class Foo: - def __init__(self, v) -> None: - self._value = v - - @in_subprocess - def f() -> Foo: - return Foo(v=42) - - assert (await f())._value == 42 - - async def test_in_subprocess_simple_async(self): - @in_subprocess - async def f() -> int: - return 42 - - assert await f() == 42 - - async def test_in_subprocess_no_args(self): - @in_subprocess - def f() -> int: - time.sleep(0.1) - return 42 - - async def t() -> None: - for _ in range(6): - await asyncio.sleep(0.01) - nonlocal counter - counter += 1 - - counter = 0 - task = asyncio.create_task(t()) - assert await f() == 42 - assert counter >= 5 - await task - - async def test_in_subprocess_no_args_no_return(self): - @in_subprocess - def f() -> None: - time.sleep(0.1) - - assert await f() is None - - async def test_in_subprocess_exception(self): - @in_subprocess - def f() -> NoReturn: - raise RuntimeError('foo') - - with self.assertRaises(expected_exception=RuntimeError): - await f() - - async def test_not_in_subprocess_blocks(self): - async def f() -> int: - time.sleep(0.1) - return 42 - - async def t() -> None: - for _ in range(6): - await asyncio.sleep(0.05) - nonlocal counter - counter += 1 - - counter = 0 - task = asyncio.create_task(t()) - assert await f() == 42 - assert counter == 0 - await task - - async def test_in_subprocess_with_arguments(self): - @in_subprocess - def f(a: int, b: int) -> int: - return a + b - - assert await f(4, 5) == 9 - assert await f(a=4, b=5) == 9 - - def test_inner_function_sync(self): - """ Needed for line coverage""" - - rx, tx = Pipe(duplex=False) - _inner(tx, lambda x: 1 / x, x=42) - assert rx.recv() == 1 / 42 - - _inner(tx, lambda x: 1 / x, x=0) - ex = rx.recv() - assert isinstance(ex, SubprocessError) - - def test_inner_function_async(self): - """ Needed for line coverage""" - - async def foo(x): - return 1/x - - rx, tx = Pipe(duplex=False) - _inner(tx, foo, x=42) - assert rx.recv() == 1 / 42 - - _inner(tx, foo, x=0) - ex = rx.recv() - assert isinstance(ex, SubprocessError) - - async def test_in_subprocess_instance_method(self): - class Foo: - async def pos_args(self) -> int: - return await self.f(4, 5) - - async def kw_args(self) -> int: - return await self.f(a=4, b=5) - - @in_subprocess - def f(self, a: int, b: int) -> int: - return a + b - - foo = Foo() - assert await foo.pos_args() == 9 - assert await foo.kw_args() == 9 - - async def test_in_subprocess_static_method(self): - class Foo: - async def pos_args(self) -> int: - return await self.f(4, 5) - - async def kw_args(self) -> int: - return await self.f(a=4, b=5) - - @staticmethod - @in_subprocess - def f(a: int, b: int) -> int: - return a + b - - foo = Foo() - assert await foo.pos_args() == 9 - assert await foo.kw_args() == 9 diff --git a/pedantic/tests/test_rename_kwargs.py b/pedantic/tests/test_rename_kwargs.py deleted file mode 100644 index aca62679..00000000 --- a/pedantic/tests/test_rename_kwargs.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest - -from pedantic import rename_kwargs, Rename - - -class TestRenameKwargs(unittest.TestCase): - def test_rename_kwargs(self): - @rename_kwargs( - Rename(from_='x', to='a'), - Rename(from_='y', to='b'), - ) - def operation(a: int, b: int) -> int: - return a + b - - operation(4, 5) - operation(a=4, b=5) - operation(x=4, y=5) - operation(x=4, b=5) diff --git a/pedantic/tests/test_resolve_forward_ref.py b/pedantic/tests/test_resolve_forward_ref.py deleted file mode 100644 index 7f67841e..00000000 --- a/pedantic/tests/test_resolve_forward_ref.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List, Optional, Union -from unittest import TestCase - -from pedantic.type_checking_logic.resolve_forward_ref import resolve_forward_ref - - -class TestResolveForwardRef(TestCase): - def test_resolve_forward_ref_primitive_types(self): - assert resolve_forward_ref(type_='int') == int - assert resolve_forward_ref(type_='float') == float - assert resolve_forward_ref(type_='str') == str - assert resolve_forward_ref(type_='bool') == bool - - def test_resolve_forward_ref_typing_types(self): - assert resolve_forward_ref(type_='List[int]') == List[int] - assert resolve_forward_ref(type_='Optional[List[Union[str, float]]]') == Optional[List[Union[str, float]]] - - def test_unresolvable_type(self): - with self.assertRaises(NameError): - resolve_forward_ref(type_='Invalid') - - def test_resolve_forward_ref_custom_class(self): - class Foo: - pass - - context = locals() - assert resolve_forward_ref(type_='Foo', context=context) == Foo - assert resolve_forward_ref(type_='Optional[Foo]', context=context) == Optional[Foo] diff --git a/pedantic/tests/test_retry.py b/pedantic/tests/test_retry.py deleted file mode 100644 index 197977a2..00000000 --- a/pedantic/tests/test_retry.py +++ /dev/null @@ -1,162 +0,0 @@ -import unittest - -from pedantic import retry -from pedantic.decorators.fn_deco_retry import retry_func - - -class TestRetry(unittest.TestCase): - def test_retry_positive_no_args(self): - count = 0 - - @retry(attempts=5) - def foo(): - nonlocal count - count += 1 - - foo() - assert count == 1 - - def test_retry_positive_args_and_kwargs(self): - count = 0 - - @retry(attempts=5) - def foo(x, y): - nonlocal count - count += x + y - - foo(12, y=42) - assert count == 54 - - def test_retry_positive_no_args_fails_every_time(self): - count = 0 - - @retry(attempts=5) - def foo(): - nonlocal count - count += 1 - raise ValueError('foo') - - with self.assertRaises(ValueError): - foo() - - assert count == 5 - - def test_retry_positive_no_args_fails_different_exception_type(self): - count = 0 - - @retry(attempts=5, exceptions=AssertionError) - def foo(): - nonlocal count - count += 1 - raise ValueError('foo') - - with self.assertRaises(ValueError): - foo() - - assert count == 1 - - def test_retry_fails_same_exception_type(self): - count = 0 - - @retry(attempts=5, exceptions=AssertionError) - def foo(): - nonlocal count - count += 1 - raise AssertionError('foo') - - with self.assertRaises(AssertionError): - foo() - - assert count == 5 - - def test_retry_positive_no_args_fails_on_first_times(self): - count = 0 - - @retry(attempts=5) - def foo() -> int: - nonlocal count - count += 1 - - if count < 3: - raise ValueError('foo') - - return count - - assert foo() == 3 - assert count == 3 - - -class TestRetryFunc(unittest.TestCase): - def test_retry_positive_no_args(self): - count = 0 - - def foo(): - nonlocal count - count += 1 - - retry_func(func=foo, attempts=5) - assert count == 1 - - def test_retry_positive_args_and_kwargs(self): - count = 0 - - def foo(x, y): - nonlocal count - count += x + y - - retry_func(foo, 12, attempts=5, y=42) - assert count == 54 - - def test_retry_positive_no_args_fails_every_time(self): - count = 0 - - def foo(): - nonlocal count - count += 1 - raise ValueError('foo') - - with self.assertRaises(ValueError): - retry_func(func=foo, attempts=5) - - assert count == 5 - - def test_retry_positive_no_args_fails_different_exception_type(self): - count = 0 - - def foo(): - nonlocal count - count += 1 - raise ValueError('foo') - - with self.assertRaises(ValueError): - retry_func(func=foo, attempts=5, exceptions=AssertionError) - - assert count == 1 - - def test_retry_fails_same_exception_type(self): - count = 0 - - def foo(): - nonlocal count - count += 1 - raise AssertionError('foo') - - with self.assertRaises(AssertionError): - retry_func(func=foo, attempts=5, exceptions=AssertionError) - - assert count == 5 - - def test_retry_positive_no_args_fails_on_first_times(self): - count = 0 - - def foo() -> int: - nonlocal count - count += 1 - - if count < 3: - raise ValueError('foo') - - return count - - assert retry_func(func=foo, attempts=5) == 3 - assert count == 3 diff --git a/pedantic/tests/test_with_decorated_methods.py b/pedantic/tests/test_with_decorated_methods.py deleted file mode 100644 index 1f17e8e1..00000000 --- a/pedantic/tests/test_with_decorated_methods.py +++ /dev/null @@ -1,125 +0,0 @@ -import unittest -from functools import wraps -from typing import TypeVar, Generic, NoReturn - -from pedantic import DecoratorType, create_decorator, WithDecoratedMethods - -T = TypeVar('T') - -class Decorators(DecoratorType): - FOO = '_foo' - BAR = '_bar' - - -foo = create_decorator(decorator_type=Decorators.FOO) -bar = create_decorator(decorator_type=Decorators.BAR) - - -class TestWithDecoratedMethods(unittest.TestCase): - def test_no_decorated_methods(self): - class MyClass(WithDecoratedMethods[Decorators]): - pass - - instance = MyClass() - assert instance.get_decorated_functions() == {Decorators.FOO: {}, Decorators.BAR: {}} - - def test_class_with_bad_property(self): - class MyClass(WithDecoratedMethods[Decorators]): - @property - def bad(self) -> NoReturn: - raise RuntimeError('bad man') - - instance = MyClass() - assert instance.get_decorated_functions() == {Decorators.FOO: {}, Decorators.BAR: {}} - - def test_with_decorated_methods_sync(self): - class MyClass(WithDecoratedMethods[Decorators]): - @foo(42) - def m1(self) -> None: - print('bar') - - @foo(value=43) - def m2(self) -> None: - print('bar') - - @bar(value=44) - def m3(self) -> None: - print('bar') - - instance = MyClass() - expected = { - Decorators.FOO: { - instance.m1: 42, - instance.m2: 43, - }, - Decorators.BAR: { - instance.m3: 44, - } - } - assert instance.get_decorated_functions() == expected - - def test_with_decorated_methods_async(self): - class MyClass(WithDecoratedMethods[Decorators]): - @foo(42) - async def m1(self) -> None: - print('bar') - - @foo(value=43) - async def m2(self) -> None: - print('bar') - - @bar(value=44) - async def m3(self) -> None: - print('bar') - - instance = MyClass() - expected = { - Decorators.FOO: { - instance.m1: 42, - instance.m2: 43, - }, - Decorators.BAR: { - instance.m3: 44, - } - } - assert instance.get_decorated_functions() == expected - - - def test_with_custom_transformation(self): - def my_transformation(f, decorator_type, value): - assert decorator_type == Decorators.BAR - assert value == 42 - - @wraps(f) - def wrapper(*args, **kwargs): - f(*args, **kwargs) - return 4422 # we add a return value - - return wrapper - - my_decorator = create_decorator(decorator_type=Decorators.BAR, transformation=my_transformation) - - class MyClass(WithDecoratedMethods[Decorators]): - @my_decorator(42) - def m1(self) -> int: - return 1 - - instance = MyClass() - expected = { - Decorators.BAR: { - instance.m1: 42, - }, - Decorators.FOO: {}, - } - assert instance.get_decorated_functions() == expected - - assert instance.m1() == 4422 # check that transformation was applied - - def test_with_decorated_methods_can_have_generic_child_class(self): - class MyClass(Generic[T], WithDecoratedMethods[Decorators]): - @foo(42) - def m1(self) -> None: ... - - instance = MyClass[int]() - actual = instance.get_decorated_functions() - assert actual == {Decorators.FOO: {instance.m1: 42}, Decorators.BAR: {}} \ No newline at end of file diff --git a/pedantic/tests/tests_class_decorators.py b/pedantic/tests/tests_class_decorators.py deleted file mode 100644 index 3a67ce13..00000000 --- a/pedantic/tests/tests_class_decorators.py +++ /dev/null @@ -1,39 +0,0 @@ -import unittest - -# local file imports -from pedantic.decorators.class_decorators import trace_class, timer_class - - -class TestClassDecorators(unittest.TestCase): - - def test_trace_class(self): - @trace_class - class MyClass: - def __init__(self, s: str) -> None: - self.s = s - - def double(self, b: int) -> str: - return self.s + str(b) - - @staticmethod - def generator() -> 'MyClass': - return MyClass(s='generated') - - m = MyClass.generator() - m.double(b=42) - - def test_timer_class(self): - @timer_class - class MyClass: - def __init__(self, s: str) -> None: - self.s = s - - def double(self, b: int) -> str: - return self.s + str(b) - - @staticmethod - def generator() -> 'MyClass': - return MyClass(s='generated') - - m = MyClass.generator() - m.double(b=42) diff --git a/pedantic/tests/tests_combination_of_decorators.py b/pedantic/tests/tests_combination_of_decorators.py deleted file mode 100644 index 8a8ff775..00000000 --- a/pedantic/tests/tests_combination_of_decorators.py +++ /dev/null @@ -1,166 +0,0 @@ -import unittest -from abc import ABC, abstractmethod - -from pedantic.decorators.class_decorators import pedantic_class, for_all_methods -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.validators import Min -from pedantic.exceptions import PedanticException, PedanticTypeCheckException, PedanticCallWithArgsException -from pedantic.decorators.fn_deco_pedantic import pedantic -from pedantic import overrides -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, Parameter, ReturnAs - - -class TestCombinationOfDecorators(unittest.TestCase): - def test_pedantic_overrides(self): - class MyClass(ABC): - @pedantic - @abstractmethod - def op(self, a: int) -> None: - pass - - class Child(MyClass): - a = 0 - - @pedantic - @overrides(MyClass) - def op(self, a: int) -> None: - self.a = a - - c = Child() - c.op(a=42) - - def test_pedantic_below_validate(self): - @validate( - Parameter(name='x', validators=[Min(0)]), - return_as=ReturnAs.KWARGS_WITH_NONE, - ) - @pedantic - def some_calculation(x: int) -> int: - return x - - some_calculation(x=42) - some_calculation(42) - - with self.assertRaises(expected_exception=ParameterException): - some_calculation(x=-1) - with self.assertRaises(expected_exception=ParameterException): - some_calculation(x=-42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_calculation(x=1.0) - - def test_pedantic_above_validate(self): - @pedantic - @validate( - Parameter(name='x', validators=[Min(0)]), - ) - def some_calculation(x: int) -> int: - return x - - some_calculation(x=42) - - with self.assertRaises(expected_exception=ParameterException): - some_calculation(x=-1) - with self.assertRaises(expected_exception=ParameterException): - some_calculation(x=-42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_calculation(x=1.0) - with self.assertRaises(expected_exception=PedanticException): - some_calculation(42) - - def test_pedantic_above_validate_on_instance_method(self): - class MyClass: - @pedantic - @validate( - Parameter(name='x', validators=[Min(0)]), - ) - def some_calculation(self, x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-1) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_calculation(x=1.0) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.some_calculation(42) - - def test_pedantic_below_validate_on_instance_method(self): - class MyClass: - @validate( - Parameter(name='x', validators=[Min(0)]), - ) - @pedantic - def some_calculation(self, x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - m.some_calculation(42) - - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-1) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_calculation(x=1.0) - - def test_pedantic_class_with_validate_instance_method(self): - @pedantic_class - class MyClass: - @validate( - Parameter(name='x', validators=[Min(0)]), - ) - def some_calculation(self, x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-1) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(x=-42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_calculation(x=1.0) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.some_calculation(42) - - def test_pedantic_class_static_method_1(self): - @pedantic_class - class MyClass: - @staticmethod - def some_calculation(x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - MyClass.some_calculation(x=45) - - def test_pedantic_class_static_method_2(self): - """Never do this, but it works""" - @for_all_methods(staticmethod) - @pedantic_class - class MyClass: - def some_calculation(x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_calculation(x=42.0) - MyClass.some_calculation(x=45) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.some_calculation(x=45.0) - - def test_pedantic_static_method_1(self): - class MyClass: - @staticmethod - @pedantic - def some_calculation(x: int) -> int: - return x - - m = MyClass() - m.some_calculation(x=42) - MyClass.some_calculation(x=45) diff --git a/pedantic/tests/tests_decorated_function.py b/pedantic/tests/tests_decorated_function.py deleted file mode 100644 index e5266e79..00000000 --- a/pedantic/tests/tests_decorated_function.py +++ /dev/null @@ -1,140 +0,0 @@ -import unittest - -from pedantic.models.decorated_function import DecoratedFunction - - -class TestDecoratedFunction(unittest.TestCase): - def test_static_method(self): - def f_1(): pass - - deco_f = DecoratedFunction(f_1) - self.assertFalse(deco_f.is_static_method) - - class MyClass: - def f_1(self): pass - - @staticmethod - def f_2(): pass - - @classmethod - def f_3(cls): pass - - deco_f_1 = DecoratedFunction(MyClass.f_1) - deco_f_2 = DecoratedFunction(MyClass.f_2) - deco_f_3 = DecoratedFunction(MyClass.f_3) - - self.assertFalse(deco_f_1.is_static_method) - self.assertTrue(deco_f_2.is_static_method) - self.assertFalse(deco_f_3.is_static_method) - - def test_function_wants_args(self): - def f_1(*args, **kwargs): pass - - def f_2(a, b, *args, **kwargs): pass - - def f_3(a, b, *args): pass - - def f_4(*args): pass - - def f_5(): pass - - self.assertTrue(DecoratedFunction(f_1).wants_args) - self.assertTrue(DecoratedFunction(f_2).wants_args) - self.assertTrue(DecoratedFunction(f_3).wants_args) - self.assertTrue(DecoratedFunction(f_4).wants_args) - self.assertFalse(DecoratedFunction(f_5).wants_args) - - class MyClass: - def f(self): pass - - @staticmethod - def g(): pass - - self.assertFalse(DecoratedFunction(MyClass.f).wants_args) - self.assertFalse(DecoratedFunction(MyClass.g).wants_args) - - def test_is_property_setter(self): - def f_1(): pass - - self.assertFalse(DecoratedFunction(f_1).is_property_setter) - - class MyClass: - _h = 42 - - def f_1(self): pass - - @staticmethod - def f_2(): pass - - self.assertFalse(DecoratedFunction(MyClass.f_1).is_property_setter) - self.assertFalse(DecoratedFunction(MyClass.f_2).is_property_setter) - - def test_wants_kwargs(self): - def f_1(*args, **kwargs): pass - - def f_2(a, b, *args, **kwargs): pass - - def f_3(a, b, *args): pass - - def f_4(*args): pass - - def f_5(): pass - - def f_6(a, b, c): pass - - self.assertFalse(DecoratedFunction(f_1).should_have_kwargs) - self.assertFalse(DecoratedFunction(f_2).should_have_kwargs) - self.assertFalse(DecoratedFunction(f_3).should_have_kwargs) - self.assertFalse(DecoratedFunction(f_4).should_have_kwargs) - self.assertTrue(DecoratedFunction(f_5).should_have_kwargs) - self.assertTrue(DecoratedFunction(f_6).should_have_kwargs) - - class A: - def f(self): pass - - @staticmethod - def g(): pass - - def __compare__(self, other): pass - - self.assertTrue(DecoratedFunction(A.f).should_have_kwargs) - self.assertTrue(DecoratedFunction(A.g).should_have_kwargs) - self.assertFalse(DecoratedFunction(A.__compare__).should_have_kwargs) - - def test_instance_method(self): - def h(): pass - - self.assertFalse(DecoratedFunction(h).is_instance_method) - - class A: - def f(self): pass - - @staticmethod - def g(): pass - - self.assertTrue(DecoratedFunction(A.f).is_instance_method) - self.assertFalse(DecoratedFunction(A.g).is_instance_method) - - def test_num_decorators(self): - def decorator(f): - return f - - def f_1(): pass - - @decorator - def f_2(): pass - - @decorator - @decorator - def f_3(): pass - - @decorator - @decorator - @decorator - def f_4(): - pass - - self.assertEqual(DecoratedFunction(f_1).num_of_decorators, 0) - self.assertEqual(DecoratedFunction(f_2).num_of_decorators, 1) - self.assertEqual(DecoratedFunction(f_3).num_of_decorators, 2) - self.assertEqual(DecoratedFunction(f_4).num_of_decorators, 3) diff --git a/pedantic/tests/tests_docstring.py b/pedantic/tests/tests_docstring.py deleted file mode 100644 index ca89a7bb..00000000 --- a/pedantic/tests/tests_docstring.py +++ /dev/null @@ -1,481 +0,0 @@ -from unittest import TestCase -from typing import List, Optional - -from pedantic.exceptions import PedanticTypeCheckException, PedanticDocstringException -from pedantic.decorators.fn_deco_pedantic import pedantic_require_docstring, pedantic -from pedantic.decorators.class_decorators import pedantic_class_require_docstring, pedantic_class - - -class TestRequireDocstringGoogleFormat(TestCase): - - def test_no_docstring(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - def test_one_line_doc_string_missing_arguments_and_return(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(n: int, m: int, i: int) -> int: - """Returns the sum of the three args.""" - return n + m + i - - def test_one_line_doc_string_corrected(self): - @pedantic_require_docstring - def calc(n: int, m: int, i: int) -> int: - """Returns the sum of the three args. - - Args: - n (int): something - m (int): something - i (int): something - - Returns: - int: bar - """ - return n + m + i - - calc(n=42, m=40, i=38) - - def test_list_vs_typing_list(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - list: a list of strings representing the header columns - """ - return [file_loc, str(print_cols)] - - def test_google_docstring_2(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - List[str]: a list of strings representing the header columns - """ - - return [file_loc, str(print_cols)] - - calc(file_loc='Hi', print_cols=False) - - def test_google_docstring_3(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - List[str]: a list of strings representing the header columns - """ - - return [file_loc, str(print_cols)] - - calc(file_loc='Hi', print_cols=False) - - def test_more_parameter_documented_than_the_function_takes(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - amount (int): THIS ARGUMENT IS NOT TAKEN BY THE FUNCTION - - Returns: - List[str]: a list of strings representing the header columns - """ - return [file_loc, str(print_cols)] - - def test_google_docstring_corrected(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool, amount: int) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - amount (int): now it is - - Returns: - List[str]: a list of strings representing the header columns - """ - - return [file_loc, str(print_cols), str(amount)] - - calc(file_loc='Hi', print_cols=False, amount=42) - - def test_no_args_keyword_before_documented_arguments(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> list: - """Gets and prints the spreadsheet's header columns - - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - list: a list of strings representing the header columns - """ - return [file_loc, str(print_cols)] - - def test_google_no_return_keyword(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> list: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - list: a list of strings representing the header columns - """ - return [file_loc, str(print_cols)] - - def test_keep_it_simple(self): - @pedantic_require_docstring - def calc() -> None: - """Gets and prints the spreadsheet's header columns""" - pass - - calc() - - def test_docstring_misses_argument(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(name: str) -> None: - """Gets and prints the spreadsheet's header columns""" - print('hi ' + name) - - def test_keep_it_simple_2_corrected(self): - @pedantic_require_docstring - def calc(name: str) -> None: - """Gets and prints the spreadsheet's header columns - - Args: - name (str): the name - """ - print('hi ' + name) - - calc(name='maria') - - def test_undocumented_arg(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool, number: int) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - List[str]: a list of strings representing the header columns - """ - return [file_loc, str(print_cols), str(number)] - - def test_undocumented_arg_corrected(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool, number: int) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - number (int): magic number - - Returns: - List[str]: a list of strings representing the header columns - """ - - return [file_loc, str(print_cols), str(number)] - - calc(file_loc='Hi', print_cols=False, number=42) - - def test_restructured_text_style_doctsring_cannot_be_parsed_yet(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - :param file_loc: The file location of the spreadsheet - :type file_loc: str - :param print_cols: A flag used to print the columns to the console - :type print_cols: bool - :returns: a list of strings representing the header column - :rtype: List[str] - """ - return [file_loc, str(print_cols)] - - def test_return_nothing_but_document_return_value(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool): - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - list: a list of strings representing the header columns - """ - print([file_loc, str(print_cols)]) - - def test_return_nothing_but_document_return_value_2(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> None: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - list: a list of strings representing the header columns - """ - print([file_loc, str(print_cols)]) - - def test_return_value_1_corrected(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> None: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - """ - - a = [file_loc, str(print_cols)] - calc(file_loc='Hi', print_cols=False) - - def test_return_value_is_not_documented(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - # the error message here is actually wrong due to the behavior of the docstring-parser package - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console (default is False) - - Returns: - """ - return [file_loc, str(print_cols)] - - def test_return_value_2_corrected(self): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - - Returns: - List[str]: results - """ - - return [file_loc, str(print_cols)] - - calc(file_loc='Hi', print_cols=False) - - def test_return_value_is_not_documented_3(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc(file_loc: str, print_cols: bool) -> List[str]: - """Gets and prints the spreadsheet's header columns - - Args: - file_loc (str): The file location of the spreadsheet - print_cols (bool): A flag used to print the columns to the console - (default is False) - """ - return [file_loc, str(print_cols)] - - def test_wrong_format_1(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - class MyText: - text = 'hi' - - @pedantic_require_docstring - def __contains__(self, substring: str) -> bool: - """ - Checks if contains substring. - Overriding __contains__ build in functions allows to use the 'in' operator blah readability - - Example: - my_text = MyText('abc') - if 'ab' in my_text -> true - :param: substring: substring - :return: True if substring is stored, False otherwise. - """ - return substring in self.text - - def test_undocumented_arg_3(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic - def calc(a: int, b: float, c: str) -> str: - """Returns some cool string - - Args: - a (int): something - b (float): something - - Returns: - str: something - """ - return str(a) + str(b) + c - - calc(a=42, b=3.14, c='hi') - - def test_pedantic_1_corrected(self): - @pedantic - def calc(a: int, b: float, c: str) -> str: - """Returns some cool string - - Args: - a (int): something - b (float): something - c (str): something - - Returns: - str: something - """ - return str(a) + str(b) + c - - calc(a=42, b=3.14, c='hi') - - def test_documented_none_as_return_type(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def calc() -> None: - """some cool stuff - - Returns: - None: the evil void - """ - pass - - def test_exception_in_docstring_parser(self): - @pedantic_class - class Foo: - def func(self, b: str) -> str: - """ - Function with docstring syntax error below. - Args: - b (str): - simple string - Returns: - str: simple string - """ - return b - - def test_user_class(self): - class BPMNEnum: - attr = 'BPMNEnum' - - class BPMNElement: - attr = 'BPMNElement' - - @pedantic_class_require_docstring - class MyClass: - def make_element(self, element_type: BPMNEnum, - src_tgt_elements: Optional[List[BPMNElement]] = None) -> List[BPMNElement]: - """ - Searches all element_types in XML-DOM and returns corresponding - BPMN-Objects. - Args: - element_type(BPMNEnum): abc - src_tgt_elements (Optional[List[BPMNElement]]): abc - - Returns: - List[BPMNElement]: abc - """ - element_type.attr = '42' - return src_tgt_elements - - m = MyClass() - m.make_element(element_type=BPMNEnum(), src_tgt_elements=[BPMNElement()]) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.make_element(element_type=BPMNElement(), src_tgt_elements=[BPMNEnum()]) - - def test_user_class_with_typing(self): - class BPMNEnum: - attr = 'BPMNEnum' - - class BPMNElement: - attr = 'BPMNElement' - - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_class_require_docstring - class MyClass: - - def make_element(self, element_type: BPMNEnum, - src_tgt_elements: Optional[List[BPMNElement]] = None) -> List[BPMNElement]: - """ - Searches all element_types in XML-DOM and returns corresponding - BPMN-Objects. - Args: - element_type(BPMNEnum): abc - src_tgt_elements (typing.Optional[List[BPMNElement]]): abc - - Returns: - List[BPMNElement]: abc - """ - element_type.attr = '42' - return src_tgt_elements - - def test_factory(self): - @pedantic_require_docstring - def get_instance() -> TestCase: - """ - Returns: - TestCase: A new TestCase - """ - return TestCase() - - get_instance() - - def test_pedantic_args(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic(require_docstring=True) - def no_docstrings() -> None: - print('.') - - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_require_docstring - def no_docstrings() -> None: - print('.') diff --git a/pedantic/tests/tests_doctests.py b/pedantic/tests/tests_doctests.py deleted file mode 100644 index 193311f5..00000000 --- a/pedantic/tests/tests_doctests.py +++ /dev/null @@ -1,34 +0,0 @@ -import unittest -import doctest - - -def run_doctests() -> None: - unittest.TextTestRunner().run(get_doctest_test_suite()) - - -def get_doctest_test_suite() -> unittest.TestSuite: - parent_module = __import__('pedantic') - modules = [ - parent_module.decorators.fn_deco_count_calls, - parent_module.decorators.fn_deco_deprecated, - parent_module.decorators.fn_deco_does_same_as_function, - parent_module.decorators.fn_deco_in_subprocess, - parent_module.decorators.fn_deco_overrides, - parent_module.decorators.fn_deco_pedantic, - parent_module.decorators.fn_deco_rename_kwargs, - parent_module.decorators.fn_deco_timer, - parent_module.decorators.fn_deco_trace, - parent_module.decorators.fn_deco_trace_if_returns, - parent_module.decorators.fn_deco_unimplemented, - parent_module.mixins.generic_mixin, - parent_module.type_checking_logic.check_types, - parent_module.type_checking_logic.check_generic_classes, - parent_module.type_checking_logic.check_docstring, - parent_module.decorators.cls_deco_frozen_dataclass, - ] - test_suites = [doctest.DocTestSuite(module=module, optionflags=doctest.ELLIPSIS) for module in modules] - return unittest.TestSuite(test_suites) - - -if __name__ == '__main__': - run_doctests() diff --git a/pedantic/tests/tests_environment_variables.py b/pedantic/tests/tests_environment_variables.py deleted file mode 100644 index 1405260e..00000000 --- a/pedantic/tests/tests_environment_variables.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest - -from pedantic.exceptions import PedanticTypeCheckException -from pedantic.env_var_logic import enable_pedantic, disable_pedantic, is_enabled -from pedantic.decorators.fn_deco_pedantic import pedantic - - -class TestEnvironmentVariables(unittest.TestCase): - def setUp(self) -> None: - self.state = is_enabled() - enable_pedantic() - - def tearDown(self) -> None: - enable_pedantic() - - def test_pedantic_enabled(self): - enable_pedantic() - - @pedantic - def some_method(): - return 42 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_method() - - def test_pedantic_disabled(self): - disable_pedantic() - - @pedantic - def some_method(): - return 42 - - some_method() - - def test_enable_disable(self): - enable_pedantic() - self.assertTrue(is_enabled()) - disable_pedantic() - self.assertFalse(is_enabled()) - enable_pedantic() - self.assertTrue(is_enabled()) diff --git a/pedantic/tests/tests_generator.py b/pedantic/tests/tests_generator.py deleted file mode 100644 index 0f016c6b..00000000 --- a/pedantic/tests/tests_generator.py +++ /dev/null @@ -1,106 +0,0 @@ -import unittest -from typing import Generator, Iterator, Iterable, List - -from pedantic.exceptions import PedanticTypeCheckException -from pedantic.decorators.fn_deco_pedantic import pedantic - - -class TestGenerator(unittest.TestCase): - def test_iterator(self): - @pedantic - def gen_func() -> Iterator[int]: - num = 0 - - while num < 100: - yield num - num += 1 - - gen = gen_func() - next(gen) - - def test_iterator_wrong_type_hint(self): - @pedantic - def genfunc() -> Iterator[float]: - num = 0 - - while num < 100: - yield num - num += 1 - - gen = genfunc() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - next(gen) - - def test_iterator_no_type_args(self): - @pedantic - def genfunc() -> Iterator: - num = 0 - - while num < 100: - yield num - num += 1 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - genfunc() - - def test_iterator_completely_wrong_type_hint(self): - @pedantic - def gen_func() -> List[int]: - num = 0 - - while num < 100: - yield num - num += 1 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - gen_func() - - def test_iterable(self): - @pedantic - def gen_func() -> Iterable[int]: - num = 0 - - while num < 100: - yield num - num += 1 - - gen = gen_func() - next(gen) - - def test_iterable_no_type_args(self): - @pedantic - def gen_func() -> Iterable: - num = 0 - - while num < 100: - yield num - num += 1 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - gen_func() - - def test_generator(self): - @pedantic - def gen_func() -> Generator[int, None, str]: - num = 0 - - while num < 100: - yield num - num += 1 - return 'Done' - - gen = gen_func() - next(gen) - - def test_invalid_no_type_args_generator(self): - @pedantic - def gen_func() -> Generator: - num = 0 - - while num < 100: - yield num - num += 1 - return 'Done' - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - gen_func() diff --git a/pedantic/tests/tests_generic_classes.py b/pedantic/tests/tests_generic_classes.py deleted file mode 100644 index 1263eeeb..00000000 --- a/pedantic/tests/tests_generic_classes.py +++ /dev/null @@ -1,317 +0,0 @@ -import unittest -from typing import Generic, TypeVar, Any, List, Optional, Union - -from pedantic.constants import TYPE_VAR_METHOD_NAME, TYPE_VAR_SELF -from pedantic.decorators.class_decorators import pedantic_class -from pedantic.exceptions import PedanticTypeVarMismatchException - - -class TestGenericClasses(unittest.TestCase): - def test_pedantic_generic_class(self): - T = TypeVar('T') - - @pedantic_class - class LoggedVar(Generic[T]): - def __init__(self, value: T, name: str, logger: Any) -> None: - self.name = name - self.logger = logger - self.value = value - - def set(self, new: T) -> None: - self.log(message='Set ' + repr(self.value)) - self.value = new - - def get(self) -> T: - self.log(message='Get ' + repr(self.value)) - return self.value - - def log(self, message: str) -> None: - self.logger = self.name + message - - o = LoggedVar[int](value=42, name='hi', logger='test') - o.set(new=57) - self.assertTrue(isinstance(o.get(), int)) - - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - o.set(new=3.14) - - def test_stack(self): - T = TypeVar('T') - - @pedantic_class - class Stack(Generic[T]): - def __init__(self) -> None: - self.items: List[T] = [] - - def push(self, item: T) -> None: - self.items.append(item) - - def pop(self) -> T: - return self.items.pop() - - def empty(self) -> bool: - return not self.items - - def top(self) -> Optional[T]: - if len(self.items) > 0: - return self.items[len(self.items) - 1] - else: - return None - - my_stack = Stack[str]() - get_type_vars = getattr(my_stack, TYPE_VAR_METHOD_NAME) - self.assertEqual(get_type_vars(), {T: str, TYPE_VAR_SELF: Stack}) - with self.assertRaises(expected_exception=IndexError): - my_stack.pop() - self.assertIsNone(my_stack.top()) - self.assertIsNone(my_stack.top()) - # self.assertFalse(T in get_type_vars()) - my_stack.push(item='hi') - self.assertTrue(T in get_type_vars()) - my_stack.push(item='world') - self.assertTrue(T in get_type_vars()) - self.assertTrue(len(get_type_vars()), 1) - self.assertEqual(my_stack.pop(), 'world') - self.assertEqual(my_stack.pop(), 'hi') - self.assertIsNone(my_stack.top()) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - my_stack.push(item=42) - - my_other_stack = Stack[int]() - get_type_vars = getattr(my_other_stack, TYPE_VAR_METHOD_NAME) - self.assertEqual(get_type_vars(), {T: int, TYPE_VAR_SELF: Stack}) - with self.assertRaises(expected_exception=IndexError): - my_other_stack.pop() - self.assertIsNone(my_other_stack.top()) - self.assertIsNone(my_other_stack.top()) - my_other_stack.push(item=100) - self.assertTrue(len(get_type_vars()), 1) - my_other_stack.push(item=142) - self.assertTrue(len(get_type_vars()), 1) - self.assertEqual(my_other_stack.pop(), 142) - self.assertEqual(my_other_stack.pop(), 100) - self.assertIsNone(my_other_stack.top()) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - my_other_stack.push(item='42') - - def test_generic_class_initialised_without_generics(self): - T = TypeVar('T') - - @pedantic_class - class MyClass(Generic[T]): - def __init__(self, a: T) -> None: - self.a = a - - def get_a(self) -> T: - return self.a - - def set_a(self, val: T) -> None: - self.a = val - - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - m = MyClass(a=42) - - def test_generic_class_initialised_without_generics_2(self): - T = TypeVar('T') - - @pedantic_class - class MyClass(Generic[T]): - def __init__(self, a: T) -> None: - self.a = a - - def get_a(self) -> T: - return self.a - - def set_a(self, val: T) -> None: - self.a = val - - MyClass(a=42) # it is not recognized if it isn't assigned - - def test_generic_class_inheritance(self): - class Parent: - pass - - class Child1(Parent): - pass - - class Child2(Parent): - pass - - T = TypeVar('T') - - @pedantic_class - class MyClass(Generic[T]): - def __init__(self, a: T) -> None: - self.a = a - - def get_a(self) -> T: - return self.a - - def set_a(self, val: T) -> None: - self.a = val - - m = MyClass[Parent](a=Child1()) - self.assertTrue(isinstance(m.get_a(), Child1)) - self.assertFalse(isinstance(m.get_a(), Child2)) - m.set_a(val=Child2()) - self.assertTrue(isinstance(m.get_a(), Child2)) - self.assertFalse(isinstance(m.get_a(), Child1)) - - def test_merge_dicts(self): - def create(): - T = TypeVar('T') - - @pedantic_class - class MyClass(Generic[T]): - def __init__(self, a: T) -> None: - self.a = a - - def get_a(self) -> T: - return self.a - - def set_a(self, val: T) -> None: - self.a = val - return MyClass(a=42) - a = create() - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - a.set_a(val='hi') - - def test_recursion_depth_exceeded(self): - T = TypeVar('T') - - @pedantic_class - class Stack(Generic[T]): - def __init__(self) -> None: - self.items: List[T] = [] - - def len(self) -> int: - return len(self.items) - - def push(self, item: T) -> None: - self.items.append(item) - - def pop(self) -> T: - if len(self.items) > 0: - return self.items.pop() - else: - raise ValueError() - - def empty(self) -> bool: - return not self.items - - def top(self) -> Optional[T]: - if len(self.items) > 0: - return self.items[len(self.items) - 1] - else: - return None - - def __len__(self) -> int: - return len(self.items) - - def create_stack(): - stack = Stack[int]() - return stack - - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - stack: Stack[int] = Stack() - self.assertTrue(stack.empty()) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - stack = Stack() - self.assertTrue(stack.empty()) - stack = create_stack() - self.assertTrue(stack.empty()) - - def test_generic_union(self): - T = TypeVar('T') - - @pedantic_class - class Stack(Generic[T]): - def __init__(self) -> None: - self.items: List[T] = [] - - def len(self) -> int: - return len(self.items) - - def push(self, item: T) -> None: - self.items.append(item) - - def pop(self) -> T: - if len(self.items) > 0: - return self.items.pop() - else: - raise ValueError() - - def empty(self) -> bool: - return not self.items - - def top(self) -> Optional[T]: - if len(self.items) > 0: - return self.items[len(self.items) - 1] - else: - return None - - def __len__(self) -> int: - return len(self.items) - - s = Stack[Union[int, float, str]]() - s.push(item=42) - s.push(item='hello') - s.push(item=3.1415) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - s.push(item=[1, 2]) - - def test_inheritance(self): - T = TypeVar('T') - - @pedantic_class - class Stack(Generic[T]): - def __init__(self) -> None: - self.items: List[T] = [] - - def len(self) -> int: - return len(self.items) - - def push(self, item: T) -> None: - self.items.append(item) - - def pop(self) -> T: - if len(self.items) > 0: - return self.items.pop() - else: - raise ValueError() - - def empty(self) -> bool: - return not self.items - - def top(self) -> Optional[T]: - if len(self.items) > 0: - return self.items[len(self.items) - 1] - else: - return None - - def __len__(self) -> int: - return len(self.items) - - @pedantic_class - class Parent: - pass - - @pedantic_class - class Child1(Parent): - pass - - @pedantic_class - class Child2(Parent): - pass - - parent_stack = Stack[Parent]() - parent_stack.push(item=Child1()) - parent_stack.push(item=Child2()) - parent_stack.push(item=Parent()) - - child_1_stack = Stack[Child1]() - child_1_stack.push(item=Child1()) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - child_1_stack.push(item=Child2()) - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - child_1_stack.push(item=Parent()) diff --git a/pedantic/tests/tests_main.py b/pedantic/tests/tests_main.py deleted file mode 100644 index d8f0be9f..00000000 --- a/pedantic/tests/tests_main.py +++ /dev/null @@ -1,123 +0,0 @@ -import unittest -import sys -import os - -sys.path.append(os.getcwd()) - -from pedantic.tests.test_retry import TestRetry, TestRetryFunc -from pedantic.tests.test_with_decorated_methods import TestWithDecoratedMethods -from pedantic.tests.validate.test_convert_value import TestConvertValue -from pedantic.tests.test_rename_kwargs import TestRenameKwargs -from pedantic.tests.validate.test_datetime_isoformat import TestValidatorDatetimeIsoformat -from pedantic.tests.validate.test_flask_parameters import TestFlaskParameters -from pedantic.tests.validate.test_parameter_environment_variable import TestParameterEnvironmentVariable -from pedantic.tests.validate.test_validate import TestValidate -from pedantic.tests.validate.test_validate import AsyncValidateTests -from pedantic.tests.tests_small_method_decorators import AsyncSmallDecoratorTests -from pedantic.tests.tests_pedantic_async import TestPedanticAsyncio -from pedantic.tests.test_in_subprocess import TestInSubprocess -from pedantic.tests.test_async_context_manager import TestAsyncContextManager -from pedantic.tests.tests_pedantic_python_311 import TestPedanticPython311AddedStuff -from pedantic.tests.test_resolve_forward_ref import TestResolveForwardRef -from pedantic.tests.test_generic_mixin import TestGenericMixin -from pedantic.tests.test_assert_value_matches_type import TestAssertValueMatchesType -from pedantic.tests.validate.test_validator_composite import TestValidatorComposite -from pedantic.tests.validate.test_validator_datetime_unix_timestamp import TestValidatorDatetimeUnixTimestamp -from pedantic.tests.validate.test_validator_email import TestValidatorEmail -from pedantic.tests.validate.test_validator_for_each import TestValidatorForEach -from pedantic.tests.validate.test_validator_is_enum import TestValidatorIsEnum -from pedantic.tests.validate.test_validator_is_uuid import TestValidatorIsUUID -from pedantic.tests.validate.test_validator_match_pattern import TestValidatorMatchPattern -from pedantic.tests.validate.test_validator_max import TestValidatorMax -from pedantic.tests.validate.test_validator_max_length import TestValidatorMaxLength -from pedantic.tests.validate.test_validator_min import TestValidatorMin -from pedantic.tests.validate.test_validator_min_length import TestValidatorMinLength -from pedantic.tests.validate.test_validator_not_empty import TestValidatorNotEmpty -from pedantic.tests.test_generator_wrapper import TestGeneratorWrapper -from pedantic.tests.tests_mock import TestMock -from pedantic.tests.tests_doctests import get_doctest_test_suite -from pedantic.tests.test_frozen_dataclass import TestFrozenDataclass -from pedantic.tests.tests_require_kwargs import TestRequireKwargs -from pedantic.tests.tests_class_decorators import TestClassDecorators -from pedantic.tests.tests_pedantic_class import TestPedanticClass -from pedantic.tests.tests_pedantic import TestDecoratorRequireKwargsAndTypeCheck -from pedantic.tests.tests_small_method_decorators import TestSmallDecoratorMethods -from pedantic.tests.tests_combination_of_decorators import TestCombinationOfDecorators -from pedantic.tests.tests_docstring import TestRequireDocstringGoogleFormat -from pedantic.tests.tests_pedantic_class_docstring import TestPedanticClassDocstring -from pedantic.tests.tests_decorated_function import TestDecoratedFunction -from pedantic.tests.tests_environment_variables import TestEnvironmentVariables -from pedantic.tests.tests_generic_classes import TestGenericClasses -from pedantic.tests.tests_generator import TestGenerator -from pedantic.tests.test_context_manager import TestContextManager - - -def run_all_tests() -> None: - test_classes_to_run = [ - TestAssertValueMatchesType, - TestGenericMixin, - TestWithDecoratedMethods, - TestRequireKwargs, - TestClassDecorators, - TestContextManager, - TestFrozenDataclass, - TestPedanticClass, - TestDecoratorRequireKwargsAndTypeCheck, - TestSmallDecoratorMethods, - TestCombinationOfDecorators, - TestRequireDocstringGoogleFormat, - TestPedanticClassDocstring, - TestDecoratedFunction, - TestEnvironmentVariables, - TestGenericClasses, - TestGenerator, - TestMock, - TestGeneratorWrapper, - TestRenameKwargs, - TestRetry, - TestRetryFunc, - TestResolveForwardRef, - # validate - TestValidatorDatetimeIsoformat, - TestFlaskParameters, - TestParameterEnvironmentVariable, - TestConvertValue, - TestValidate, - TestValidatorComposite, - TestValidatorDatetimeUnixTimestamp, - TestValidatorEmail, - TestValidatorForEach, - TestValidatorIsEnum, - TestValidatorIsUUID, - TestValidatorMatchPattern, - TestValidatorMax, - TestValidatorMaxLength, - TestValidatorMin, - TestValidatorMinLength, - TestValidatorNotEmpty, - - # async - AsyncValidateTests, - AsyncSmallDecoratorTests, - TestPedanticAsyncio, - TestInSubprocess, - TestAsyncContextManager, - - TestPedanticPython311AddedStuff, - ] - - loader = unittest.TestLoader() - suites_list = [get_doctest_test_suite()] - - for test_class in test_classes_to_run: - suite = loader.loadTestsFromTestCase(test_class) - suites_list.append(suite) - - big_suite = unittest.TestSuite(suites_list) - runner = unittest.TextTestRunner() - result = runner.run(big_suite) - assert not result.errors and not result.failures, f'Some tests failed!' - - -if __name__ == '__main__': - run_all_tests() diff --git a/pedantic/tests/tests_mock.py b/pedantic/tests/tests_mock.py deleted file mode 100644 index dcbe24b7..00000000 --- a/pedantic/tests/tests_mock.py +++ /dev/null @@ -1,13 +0,0 @@ -from unittest import TestCase - -from pedantic import mock - - -class TestMock(TestCase): - def test_mock(self) -> None: - @mock(return_value=42) - def my_function(a, b, c): - return a + b + c - - assert my_function(1, 2, 3) == 42 - assert my_function(100, 200, 300) == 42 diff --git a/pedantic/tests/tests_pedantic.py b/pedantic/tests/tests_pedantic.py deleted file mode 100644 index fc0d6925..00000000 --- a/pedantic/tests/tests_pedantic.py +++ /dev/null @@ -1,2462 +0,0 @@ -import os.path -import types -import typing -import unittest -from dataclasses import dataclass -from datetime import datetime, date -from functools import wraps, partial -from io import BytesIO, StringIO -from typing import List, Tuple, Callable, Any, Optional, Union, Dict, Set, FrozenSet, NewType, TypeVar, Sequence, \ - AbstractSet, Iterator, NamedTuple, Collection, Type, Generator, Generic, BinaryIO, TextIO, Iterable, Container, \ - NoReturn, ClassVar, Literal -from enum import Enum, IntEnum - -from pedantic import pedantic_class -from pedantic.exceptions import PedanticTypeCheckException, PedanticException, PedanticCallWithArgsException, \ - PedanticTypeVarMismatchException -from pedantic.decorators.fn_deco_pedantic import pedantic - -TEST_FILE = 'test.txt' - - -class Parent: - pass - - -class Child(Parent): - def method(self, a: int): - pass - - -class TestDecoratorRequireKwargsAndTypeCheck(unittest.TestCase): - def tearDown(self) -> None: - if os.path.isfile(TEST_FILE): - os.remove(TEST_FILE) - - def test_no_kwargs(self): - @pedantic - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - calc(42, 40, 38) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - calc(42, m=40, i=38) - calc(n=42, m=40, i=38) - - def test_nested_type_hints_1(self): - @pedantic - def calc(n: int) -> List[List[float]]: - return [0.0 * n] - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42) - - def test_nested_type_hints_1_corrected(self): - @pedantic - def calc(n: int) -> List[List[float]]: - return [[0.0 * n]] - - calc(n=42) - - def test_nested_type_hints_2(self): - """Problem here: int != float""" - @pedantic - def calc(n: int) -> List[Tuple[float, str]]: - return [(n, str(n))] - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42) - - def test_nested_type_hints_2_corrected(self): - @pedantic - def calc(n: int) -> List[Tuple[int, str]]: - return [(n, str(n))] - - @pedantic - def calc_2(n: float) -> List[Tuple[float, str]]: - return [(n, str(n))] - - calc(n=42) - calc_2(n=42.0) - - def test_nested_type_hints_3(self): - """Problem here: inner function actually returns Tuple[int, str]""" - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: - @pedantic - def f(x: int, y: float) -> Tuple[float, str]: - return n * x, str(y) - return f - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42)(x=3, y=3.14) - - def test_nested_type_hints_3_corrected(self): - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[int, str]]: - @pedantic - def f(x: int, y: float) -> Tuple[int, str]: - return n * x, str(y) - - return f - - calc(n=42)(x=3, y=3.14) - - def test_nested_type_hints_4(self): - """Problem here: return type is actually float""" - @pedantic - def calc(n: List[List[float]]) -> int: - return n[0][0] - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=[[42.0]]) - - def test_nested_type_hints_corrected(self): - @pedantic - def calc(n: List[List[float]]) -> int: - return int(n[0][0]) - - calc(n=[[42.0]]) - - def test_nested_type_hints_5(self): - """Problem here: Tuple[float, str] != Tuple[float, float]""" - - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: - @pedantic - def f(x: int, y: float) -> Tuple[float, float]: - return n * float(x), y - return f - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42) - - def test_nested_type_hints_5_corrected(self): - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[float, float]]: - @pedantic - def f(x: int, y: float) -> Tuple[float, float]: - return n * float(x), y - return f - - calc(n=42) - - def test_missing_type_hint_1(self): - """Problem here: type hint for n missed""" - @pedantic - def calc(n) -> float: - return 42.0 * n - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42) - - def test_missing_type_hint_1_corrected(self): - @pedantic - def calc(n: int) -> float: - return 42.0 * n - - calc(n=42) - - def test_missing_type_hint_2(self): - """Problem here: Return type annotation missed""" - @pedantic - def calc(n: int): - return 'Hi' + str(n) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42) - - def test_missing_type_hint_2_corrected(self): - @pedantic - def calc(n: int) -> str: - return 'Hi' + str(n) - - calc(n=42) - - def test_missing_type_hint_3(self): - """Problem here: type hint for i missed""" - @pedantic - def calc(n: int, m: int, i) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=38) - - def test_missing_type_hint_3_corrected(self): - @pedantic - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - calc(n=42, m=40, i=38) - - def test_all_ok_2(self): - @pedantic - def calc(n: int, m: int, i: int) -> str: - return str(n + m + i) - - calc(n=42, m=40, i=38) - - def test_all_ok_3(self): - @pedantic - def calc(n: int, m: int, i: int) -> None: - str(n + m + i) - - calc(n=42, m=40, i=38) - - def test_all_ok_4(self): - @pedantic - def calc(n: int) -> List[List[int]]: - return [[n]] - - calc(n=42) - - def test_all_ok_5(self): - @pedantic - def calc(n: int) -> List[Tuple[float, str]]: - return [(float(n), str(n))] - - calc(n=42) - - def test_all_ok_6(self): - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: - @pedantic - def f(x: int, y: float) -> Tuple[float, str]: - return n * float(x), str(y) - return f - - calc(n=42)(x=72, y=3.14) - - def test_all_ok_7(self): - @pedantic - def calc(n: List[List[float]]) -> Any: - return n[0][0] - - calc(n=[[42.0]]) - - def test_all_ok_8(self): - @pedantic - def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: - @pedantic - def f(x: int, y: float) -> Tuple[float, str]: - return n * float(x), str(y) - - return f - - calc(n=42)(x=3, y=3.14) - - def test_wrong_type_hint_1(self): - """Problem here: str != int""" - @pedantic - def calc(n: int, m: int, i: int) -> str: - return n + m + i - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_1_corrected(self): - @pedantic - def calc(n: int, m: int, i: int) -> str: - return str(n + m + i) - - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_2(self): - """Problem here: str != int""" - @pedantic - def calc(n: int, m: int, i: str) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_2_corrected(self): - @pedantic - def calc(n: int, m: int, i: str) -> int: - return n + m + int(i) - - calc(n=42, m=40, i='38') - - def test_wrong_type_hint_3(self): - """Problem here: None != int""" - @pedantic - def calc(n: int, m: int, i: int) -> None: - return n + m + i - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_corrected(self): - @pedantic - def calc(n: int, m: int, i: int) -> None: - print(n + m + i) - - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_4(self): - """Problem here: None != int""" - @pedantic - def calc(n: int, m: int, i: int) -> int: - print(n + m + i) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=38) - - def test_wrong_type_hint_4_corrected(self): - @pedantic - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - calc(n=42, m=40, i=38) - - def test_none_1(self): - """Problem here: None is not accepted""" - @pedantic - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=None) - - def test_none_2(self): - @pedantic - def calc(n: int, m: int, i: Optional[int]) -> int: - return n + m + i if i is not None else n + m - - calc(n=42, m=40, i=None) - - def test_none_3(self): - @pedantic - def calc(n: int, m: int, i: Union[int, None]) -> int: - return n + m + i if i is not None else n + m - - calc(n=42, m=40, i=None) - - def test_none_4(self): - """Problem here: function may return None""" - @pedantic - def calc(n: int, m: int, i: Union[int, None]) -> int: - return n + m + i if i is not None else None - - calc(n=42, m=40, i=42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(n=42, m=40, i=None) - - def test_none_5(self): - @pedantic - def calc(n: int, m: int, i: Union[int, None]) -> Optional[int]: - return n + m + i if i is not None else None - - calc(n=42, m=40, i=None) - - def test_inheritance_1(self): - class MyClassA: - pass - - class MyClassB(MyClassA): - pass - - @pedantic - def calc(a: MyClassA) -> str: - return str(a) - - calc(a=MyClassA()) - calc(a=MyClassB()) - - def test_inheritance_2(self): - """Problem here: A is not a subtype of B""" - class MyClassA: - pass - - class MyClassB(MyClassA): - pass - - @pedantic - def calc(a: MyClassB) -> str: - return str(a) - - calc(a=MyClassB()) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(a=MyClassA()) - - def test_instance_method_1(self): - class MyClassA: - @pedantic - def calc(self, i: int) -> str: - return str(i) - - a = MyClassA() - a.calc(i=42) - - def test_instance_method_2(self): - """Problem here: 'i' has no type annotation""" - class MyClassA: - @pedantic - def calc(self, i) -> str: - return str(i) - - a = MyClassA() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - a.calc(i=42) - - def test_instance_method_2_corrected(self): - class MyClassA: - @pedantic - def calc(self, i: int) -> str: - return str(i) - - a = MyClassA() - a.calc(i=42) - - def test_instance_method_int_is_not_float(self): - class MyClassA: - @pedantic - def calc(self, i: float) -> str: - return str(i) - - a = MyClassA() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - a.calc(i=42) - - def test_instance_method_3_corrected(self): - class MyClassA: - @pedantic - def calc(self, i: float) -> str: - return str(i) - - a = MyClassA() - a.calc(i=42.0) - - def test_instance_method_no_kwargs(self): - class MyClassA: - @pedantic - def calc(self, i: int) -> str: - return str(i) - - a = MyClassA() - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - a.calc(42) - - def test_instance_method_5(self): - """Problem here: instance methods is not called with kwargs""" - class MyClassA: - @pedantic - def calc(self, i: int) -> str: - return str(i) - - a = MyClassA() - a.calc(i=42) - - def test_lambda_1(self): - @pedantic - def calc(i: float) -> Callable[[float], str]: - return lambda x: str(x * i) - - calc(i=42.0)(10.0) - - def test_lambda_3(self): - @pedantic - def calc(i: float) -> Callable[[float], str]: - def res(x: float) -> str: - return str(x * i) - return res - - calc(i=42.0)(10.0) - - def test_lambda_int_is_not_float(self): - @pedantic - def calc(i: float) -> Callable[[float], str]: - def res(x: int) -> str: - return str(x * i) - return res - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=42.0)(x=10) - - def test_lambda_4_almost_corrected(self): - """Problem here: float != str""" - @pedantic - def calc(i: float) -> Callable[[float], str]: - @pedantic - def res(x: int) -> str: - return str(x * i) - return res - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=42.0)(x=10) - - def test_lambda_4_almost_corrected_2(self): - @pedantic - def calc(i: float) -> Callable[[int], str]: - @pedantic - def res(x: int) -> str: - return str(x * i) - return res - - calc(i=42.0)(x=10) - - def test_lambda_5(self): - """Problem here: float != int""" - @pedantic - def calc(i: float) -> Callable[[float], str]: - @pedantic - def res(x: float) -> str: - return str(x * i) - return res - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=42.0)(x=10) - - def test_lambda_corrected(self): - @pedantic - def calc(i: float) -> Callable[[float], str]: - @pedantic - def res(x: float) -> str: - return str(x * i) - - return res - - calc(i=42.0)(x=10.0) - - def test_tuple_without_type_args(self): - @pedantic - def calc(i: Tuple) -> str: - return str(i) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=(42.0, 43, 'hi')) - - def test_tuple_without_args_corrected(self): - @pedantic - def calc(i: Tuple[Any, ...]) -> str: - return str(i) - - calc(i=(42.0, 43, 'hi')) - - def test_callable_without_type_args(self): - @pedantic - def calc(i: Callable) -> str: - return str(i(' you')) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=lambda x: (42.0, 43, 'hi', x)) - - def test_callable_without_args_correct_with_lambdas(self): - @pedantic - def calc(i: Callable[[Any], Tuple[Any, ...]]) -> str: - return str(i(x=' you')) - - calc(i=lambda x: (42.0, 43, 'hi', x)) - - def test_callable_without_args_corrected(self): - @pedantic - def calc(i: Callable[[Any], Tuple[Any, ...]]) -> str: - return str(i(x=' you')) - - @pedantic - def arg(x: Any) -> Tuple[Any, ...]: - return 42.0, 43, 'hi', x - calc(i=arg) - - def test_list_without_args(self): - @pedantic - def calc(i: List) -> Any: - return [i] - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(i=[42.0, 43, 'hi']) - - def test_list_without_args_corrected(self): - @pedantic - def calc(i: List[Any]) -> List[List[Any]]: - return [i] - - calc(i=[42.0, 43, 'hi']) - - def test_ellipsis_in_callable_1(self): - @pedantic - def calc(i: Callable[..., int]) -> int: - return i() - - @pedantic - def call() -> int: - return 42 - - calc(i=call) - - def test_ellipsis_in_callable_2(self): - @pedantic - def calc(i: Callable[..., int]) -> int: - return i(x=3.14, y=5) - - @pedantic - def call(x: float, y: int) -> int: - return 42 - - calc(i=call) - - def test_ellipsis_in_callable_3(self): - """Problem here: call to "call" misses one argument""" - @pedantic - def calc(i: Callable[..., int]) -> int: - return i(x=3.14) - - @pedantic - def call(x: float, y: int) -> int: - return 42 - - with self.assertRaises(expected_exception=PedanticException): - calc(i=call) - - def test_optional_args_1(self): - @pedantic - def calc(a: int, b: int = 42) -> int: - return a + b - - calc(a=2) - - def test_optional_args_2(self): - @pedantic - def calc(a: int = 3, b: int = 42, c: float = 5.0) -> float: - return a + b + c - - calc() - calc(a=1) - calc(b=1) - calc(c=1.0) - calc(a=1, b=1) - calc(a=1, c=1.0) - calc(b=1, c=1.0) - calc(a=1, b=1, c=1.0) - - def test_optional_args_3(self): - """Problem here: optional argument c: 5 is not a float""" - @pedantic - def calc(a: int = 3, b: int = 42, c: float = 5) -> float: - return a + b + c - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc() - - def test_optional_args_3_corrected(self): - @pedantic - def calc(a: int = 3, b: int = 42, c: float = 5.0) -> float: - return a + b + c - - calc() - - def test_optional_args_4(self): - class MyClass: - @pedantic - def foo(self, a: int, b: Optional[int] = 1) -> int: - return a + b - - my_class = MyClass() - my_class.foo(a=10) - - def test_optional_args_5(self): - @pedantic - def calc(d: Optional[Dict[int, int]] = None) -> Optional[int]: - if d is None: - return None - return sum(d.keys()) - - calc(d=None) - calc() - calc(d={42: 3}) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(d={42: 3.14}) - - def test_optional_args_6(self): - """"Problem here: str != int""" - @pedantic - def calc(d: int = 42) -> int: - return int(d) - - calc(d=99999) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(d='999999') - - def test_enum_1(self): - """Problem here: Type hint for 'a' should be MyEnum instead of MyEnum.GAMMA""" - class MyEnum(Enum): - ALPHA = 'startEvent' - BETA = 'task' - GAMMA = 'sequenceFlow' - - class MyClass: - @pedantic - def operation(self, a: MyEnum.GAMMA) -> None: - print(a) - - m = MyClass() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.operation(a=MyEnum.GAMMA) - - def test_enum_1_corrected(self): - class MyEnum(Enum): - ALPHA = 'startEvent' - BETA = 'task' - GAMMA = 'sequenceFlow' - - @pedantic - def operation(a: MyEnum) -> None: - print(a) - - operation(a=MyEnum.GAMMA) - - def test_sloppy_types_dict(self): - @pedantic - def operation(d: dict) -> int: - return len(d.keys()) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d={1: 1, 2: 2}) - - def test_sloppy_types_dict_almost_corrected_no_type_args(self): - @pedantic - def operation(d: Dict) -> int: - return len(d.keys()) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d={1: 1, 2: 2}) - - def test_sloppy_types_dict_corrected(self): - @pedantic - def operation(d: Dict[int, int]) -> int: - return len(d.keys()) - - operation(d={1: 1, 2: 2}) - - def test_sloppy_types_list(self): - @pedantic - def operation(d: list) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=[1, 2, 3, 4]) - - def test_sloppy_types_list_almost_corrected_no_type_args(self): - @pedantic - def operation(d: List) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=[1, 2, 3, 4]) - - def test_sloppy_types_list_corrected(self): - @pedantic - def operation(d: List[int]) -> int: - return len(d) - - operation(d=[1, 2, 3, 4]) - - def test_sloppy_types_tuple(self): - @pedantic - def operation(d: tuple) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=(1, 2, 3)) - - def test_sloppy_types_tuple_almost_corrected_no_type_args(self): - @pedantic - def operation(d: Tuple) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=(1, 2, 3)) - - def test_sloppy_types_tuple_corrected(self): - @pedantic - def operation(d: Tuple[int, int, int]) -> int: - return len(d) - - operation(d=(1, 2, 3)) - - def test_sloppy_types_set(self): - @pedantic - def operation(d: set) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d={1, 2, 3}) - - def test_sloppy_types_set_almost_corrected_to_type_args(self): - @pedantic - def operation(d: Set) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d={1, 2, 3}) - - def test_sloppy_types_set_corrected(self): - @pedantic - def operation(d: Set[int]) -> int: - return len(d) - - operation(d={1, 2, 3}) - - def test_sloppy_types_frozenset(self): - @pedantic - def operation(d: frozenset) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=frozenset({1, 2, 3})) - - def test_sloppy_types_frozenset_almost_corrected_no_type_args(self): - @pedantic - def operation(d: FrozenSet) -> int: - return len(d) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - operation(d=frozenset({1, 2, 3})) - - def test_sloppy_types_frozenset_corrected(self): - @pedantic - def operation(d: FrozenSet[int]) -> int: - return len(d) - - operation(d=frozenset({1, 2, 3})) - - def test_type_list_but_got_tuple(self): - @pedantic - def calc(ls: List[Any]) -> int: - return len(ls) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - calc(ls=(1, 2, 3)) - - def test_type_list_corrected(self): - @pedantic - def calc(ls: Tuple[Any, ...]) -> int: - return len(ls) - - calc(ls=(1, 2, 3)) - - def test_any(self): - @pedantic - def calc(ls: List[Any]) -> Dict[int, Any]: - return {i: ls[i] for i in range(0, len(ls))} - - calc(ls=[1, 2, 3]) - calc(ls=[1.11, 2.0, 3.0]) - calc(ls=['1', '2', '3']) - calc(ls=[10.5, '2', (3, 4, 5)]) - - def test_aliases(self): - Vector = List[float] - - @pedantic - def scale(scalar: float, vector: Vector) -> Vector: - return [scalar * num for num in vector] - - scale(scalar=2.0, vector=[1.0, -4.2, 5.4]) - - def test_new_type(self): - UserId = NewType('UserId', int) - - @pedantic - def get_user_name(user_id: UserId) -> str: - return str(user_id) - - some_id = UserId(524313) - get_user_name(user_id=some_id) - - # the following would be desirable but impossible to check at runtime: - # with self.assertRaises(expected_exception=AssertionError): - # get_user_name(user_id=-1) - - def test_list_of_new_type(self): - UserId = NewType('UserId', int) - - @pedantic - def get_user_name(user_ids: List[UserId]) -> str: - return str(user_ids) - - get_user_name(user_ids=[UserId(524313), UserId(42)]) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - get_user_name(user_ids=[UserId(524313), UserId(42), 430.0]) - - def test_callable_no_args(self): - @pedantic - def f(g: Callable[[], str]) -> str: - return g() - - @pedantic - def greetings() -> str: - return 'hello world' - - f(g=greetings) - - def test_type_var(self): - T = TypeVar('T') - - @pedantic - def first(ls: List[T]) -> T: - return ls[0] - - first(ls=[1, 2, 3]) - - def test_type_var_wrong(self): - T = TypeVar('T') - - @pedantic - def first(ls: List[T]) -> T: - return str(ls[0]) - - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - first(ls=[1, 2, 3]) - - def test_type_var_wrong_sequence(self): - T = TypeVar('T') - - @pedantic - def first(ls: Sequence[T]) -> T: - return str(ls[0]) - - with self.assertRaises(expected_exception=PedanticTypeVarMismatchException): - first(ls=[1, 2, 3]) - - def test_double_pedantic(self): - @pedantic - @pedantic - def f(x: int, y: float) -> Tuple[float, str]: - return float(x), str(y) - - f(x=5, y=3.14) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f(x=5.0, y=3.14) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - f(5, 3.14) - - def test_args_kwargs(self): - @pedantic - def some_method(a: int = 0, b: float = 0.0) -> float: - return a * b - - @pedantic - def wrapper_method(*args: Union[int, float], **kwargs: Union[int, float]) -> float: - return some_method(*args, **kwargs) - - some_method() - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - some_method(3, 3.0) - some_method(a=3, b=3.0) - wrapper_method() - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - wrapper_method(3, 3.0) - wrapper_method(a=3, b=3.0) - - def test_args_kwargs_no_type_hint(self): - @pedantic - def method_no_type_hint(*args, **kwargs) -> None: - print(args) - print(kwargs) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - method_no_type_hint(a=3, b=3.0) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - method_no_type_hint() - - def test_args_kwargs_wrong_type_hint(self): - """See: https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values""" - @pedantic - def wrapper_method(*args: str, **kwargs: str) -> None: - print(args) - print(kwargs) - - wrapper_method() - wrapper_method('hi', 'you', ':)') - wrapper_method(a='hi', b='you', c=':)') - with self.assertRaises(expected_exception=PedanticTypeCheckException): - wrapper_method('hi', 'you', ':)', 7) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - wrapper_method(3, 3.0) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - wrapper_method(a=3, b=3.0) - - def test_additional_kwargs(self): - @pedantic - def some_method(a: int, b: float = 0.0, **kwargs: int) -> float: - return sum([a, b]) - - some_method(a=5) - some_method(a=5, b=0.1) - some_method(a=5, b=0.1, c=4) - some_method(a=5, b=0.1, c=4, d=5, e=6) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_method(a=5, b=0.1, c=4, d=5.0, e=6) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_method(a=5.0, b=0.1, c=4, d=5, e=6) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - some_method(a=5, b=0, c=4, d=5, e=6) - - def test_args_kwargs_different_types(self): - @pedantic - def foo(*args: str, **kwds: int) -> None: - print(args) - print(kwds) - - foo('a', 'b', 'c') - foo(x=1, y=2) - foo('', z=0) - - def test_pedantic_on_class(self): - with self.assertRaises(expected_exception=PedanticTypeCheckException): - @pedantic - class MyClass: - pass - MyClass() - - def test_is_subtype_tuple(self): - with self.assertRaises(expected_exception=PedanticTypeCheckException): - @pedantic - def foo() -> Callable[[Tuple[float, str]], Tuple[int]]: - def bar(a: Tuple[float]) -> Tuple[int]: - return len(a[1]) + int(a[0]), - return bar - foo() - - def test_is_subtype_tuple_corrected(self): - @pedantic - def foo() -> Callable[[Tuple[float, str]], Tuple[int]]: - def bar(a: Tuple[float, str]) -> Tuple[int]: - return len(a[1]) + int(a[0]), - return bar - foo() - - def test_forward_ref(self): - class Conversation: - pass - - @pedantic - def get_conversations() -> List['Conversation']: - return [Conversation(), Conversation()] - - get_conversations() - - def test_alternative_list_type_hint(self): - @pedantic - def _is_digit_in_int(digit: [int], num: int) -> bool: - num_str = str(num) - for i in num_str: - if int(i) == digit: - return True - return False - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - _is_digit_in_int(digit=4, num=42) - - def test_callable_with_union_return(self): - class MyClass: - pass - - @pedantic - def admin_required(func: Callable[..., Union[str, MyClass]]) -> Callable[..., Union[str, MyClass]]: - @wraps(func) - def decorated_function(*args, **kwargs): - return func(*args, **kwargs) - return decorated_function - - @admin_required - @pedantic - def get_server_info() -> str: - return 'info' - - get_server_info() - - def test_pedantic(self): - @pedantic - def foo(a: int, b: str) -> str: - return 'abc' - - self.assertEqual('abc', foo(a=4, b='abc')) - - def test_pedantic_always(self): - @pedantic - def foo(a: int, b: str) -> str: - return 'abc' - - self.assertEqual('abc', foo(a=4, b='abc')) - - def test_pedantic_arguments_fail(self): - @pedantic - def foo(a: int, b: str) -> str: - return 'abc' - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - foo(a=4, b=5) - - def test_pedantic_return_type_fail(self): - @pedantic - def foo(a: int, b: str) -> str: - return 6 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - foo(a=4, b='abc') - - def test_return_type_none(self): - @pedantic - def foo() -> None: - return 'a' - with self.assertRaises(expected_exception=PedanticTypeCheckException): - foo() - - def test_marco(self): - @pedantic_class - class A: - def __init__(self, val: int) -> None: - self.val = val - - def __eq__(self, other: 'A') -> bool: # other: A and all subclasses - return self.val == other.val - - @pedantic_class - class B(A): - def __init__(self, val: int) -> None: - super().__init__(val=val) - - @pedantic_class - class C(A): - def __init__(self, val: int) -> None: - super().__init__(val=val) - - a = A(val=42) - b = B(val=42) - c = C(val=42) - - assert a == b # works - assert a == c # works - assert b == c # error - - def test_date_datetime(self): - @pedantic - def foo(a: datetime, b: date) -> None: - pass - - foo(a=datetime(1995, 2, 5), b=date(1987, 8, 7)) - foo(a=datetime(1995, 2, 5), b=datetime(1987, 8, 7)) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - foo(a=date(1995, 2, 5), b=date(1987, 8, 7)) - - def test_any_type(self): - @pedantic - def foo(a: Any) -> None: - pass - - foo(a='aa') - - def test_callable_exact_arg_count(self): - @pedantic - def foo(a: Callable[[int, str], int]) -> None: - pass - - def some_callable(x: int, y: str) -> int: - pass - - foo(a=some_callable) - - def test_callable_bad_type(self): - @pedantic - def foo(a: Callable[..., int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_callable_too_few_arguments(self): - @pedantic - def foo(a: Callable[[int, str], int]) -> None: - pass - - def some_callable(x: int) -> int: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=some_callable) - - def test_callable_mandatory_kwonlyargs(self): - @pedantic - def foo(a: Callable[[int, str], int]) -> None: - pass - - def some_callable(x: int, y: str, *, z: float, bar: str) -> int: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=some_callable) - - def test_callable_class(self): - """ - Test that passing a class as a callable does not count the "self" argument "a"gainst the - ones declared in the Callable specification. - - """ - @pedantic - def foo(a: Callable[[int, str], Any]) -> None: - pass - - class SomeClass: - def __init__(self, x: int, y: str): - pass - - foo(a=SomeClass) - - def test_callable_plain(self): - @pedantic - def foo(a: Callable[..., Any]) -> None: - pass - - def callback(a): - pass - - foo(a=callback) - - def test_callable_bound_method(self): - @pedantic - def foo(callback: Callable[[int], Any]) -> None: - pass - - foo(callback=Child().method) - - def test_callable_defaults(self): - """ - Test that a callable having "too many" arguments don't raise an error if the extra - arguments have default values. - - """ - @pedantic - def foo(callback: Callable[[int, str], Any]) -> None: - pass - - def some_callable(x: int, y: str, z: float = 1.2) -> int: - pass - - foo(callback=some_callable) - - def test_callable_builtin(self): - @pedantic - def foo(callback: types.BuiltinFunctionType) -> None: - pass - - foo(callback=[].append) - - def test_dict_bad_type(self): - @pedantic - def foo(a: Dict[str, int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_dict_bad_key_type(self): - @pedantic - def foo(a: Dict[str, int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a={1: 2}) - - def test_dict_bad_value_type(self): - @pedantic - def foo(a: Dict[str, int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a={'x': 'a'}) - - def test_list_bad_type(self): - @pedantic - def foo(a: List[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_list_bad_element(self): - @pedantic - def foo(a: List[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=[1, 2, 'bb']) - - def test_sequence_bad_type(self): - @pedantic - def foo(a: Sequence[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_sequence_bad_element(self): - @pedantic - def foo(a: Sequence[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=[1, 2, 'bb']) - - def test_abstractset_custom_type(self): - T = TypeVar('T') - - @pedantic_class - class DummySet(AbstractSet[T]): - def __contains__(self, x: object) -> bool: - return x == 1 - - def __len__(self) -> T: - return 1 - - def __iter__(self) -> Iterator[T]: - yield 1 - - @pedantic - def foo(a: AbstractSet[int]) -> None: - pass - - foo(a=DummySet[int]()) - - def test_abstractset_bad_type(self): - @pedantic - def foo(a: AbstractSet[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_set_bad_type(self): - @pedantic - def foo(a: Set[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_abstractset_bad_element(self): - @pedantic - def foo(a: AbstractSet[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a={1, 2, 'bb'}) - - def test_set_bad_element(self): - @pedantic - def foo(a: Set[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a={1, 2, 'bb'}) - - def test_tuple_bad_type(self): - @pedantic - def foo(a: Tuple[int]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=5) - - def test_tuple_too_many_elements(self): - @pedantic - def foo(a: Tuple[int, str]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=(1, 'aa', 2)) - - def test_tuple_too_few_elements(self): - @pedantic - def foo(a: Tuple[int, str]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=(1,)) - - def test_tuple_bad_element(self): - @pedantic - def foo(a: Tuple[int, str]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=(1, 2)) - - def test_tuple_ellipsis_bad_element(self): - @pedantic - def foo(a: Tuple[int, ...]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=(1, 2, 'blah')) - - def test_namedtuple(self): - Employee = NamedTuple('Employee', [('name', str), ('id', int)]) - - @pedantic - def foo(bar: Employee) -> None: - print(bar) - - foo(bar=Employee('bob', 1)) - - def test_namedtuple_key_mismatch(self): - Employee1 = NamedTuple('Employee', [('name', str), ('id', int)]) - Employee2 = NamedTuple('Employee', [('firstname', str), ('id', int)]) - - @pedantic - def foo(bar: Employee1) -> None: - print(bar) - - with self.assertRaises(PedanticTypeCheckException): - foo(bar=Employee2('bob', 1)) - - def test_namedtuple_type_mismatch(self): - Employee = NamedTuple('Employee', [('name', str), ('id', int)]) - - @pedantic - def foo(bar: Employee) -> None: - print(bar) - - with self.assertRaises(PedanticTypeCheckException): - foo(bar=('bob', 1)) - - def test_namedtuple_huge_type_mismatch(self): - Employee = NamedTuple('Employee', [('name', str), ('id', int)]) - - @pedantic - def foo(bar: int) -> None: - print(bar) - - with self.assertRaises(PedanticTypeCheckException): - foo(bar=foo(bar=Employee('bob', 1))) - - def test_namedtuple_wrong_field_type(self): - Employee = NamedTuple('Employee', [('name', str), ('id', int)]) - - @pedantic - def foo(bar: Employee) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(bar=Employee(2, 1)) - - def test_union(self): - @pedantic - def foo(a: Union[str, int]) -> None: - pass - - for value in [6, 'xa']: - foo(a=value) - - def test_union_new_syntax(self): - @pedantic - def foo(a: str | int) -> None: - pass - - for value in [6, 'xa']: - foo(a=value) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1.7) - - def test_union_typing_type(self): - @pedantic - def foo(a: Union[str, Collection]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1) - - def test_union_fail(self): - @pedantic - def foo(a: Union[str, int]) -> None: - pass - - for value in [5.6, b'xa']: - with self.assertRaises(PedanticTypeCheckException): - foo(a=value) - - def test_type_var_constraints(self): - T = TypeVar('T', int, str) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - for values in [ - {'a': 6, 'b': 7}, - {'a': 'aa', 'b': "bb"}, - ]: - foo(**values) - - def test_type_var_constraints_fail_typing_type(self): - T = TypeVar('T', int, Collection) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a='aa', b='bb') - - def test_typevar_constraints_fail(self): - T = TypeVar('T', int, str) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=2.5, b='aa') - - def test_typevar_bound(self): - T = TypeVar('T', bound=Parent) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - foo(a=Child(), b=Child()) - - def test_type_var_bound_fail(self): - T = TypeVar('T', bound=Child) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=Parent(), b=Parent()) - - def test_type_var_invariant_fail(self): - T = TypeVar('T', int, str) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=2, b=3.6) - - def test_type_var_covariant(self): - T = TypeVar('T', covariant=True) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - foo(a=Parent(), b=Child()) - - def test_type_var_covariant_fail(self): - T = TypeVar('T', covariant=True) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeVarMismatchException): - foo(a=Child(), b=Parent()) - - def test_type_var_contravariant(self): - T = TypeVar('T', contravariant=True) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - foo(a=Child(), b=Parent()) - - def test_type_var_contravariant_fail(self): - T = TypeVar('T', contravariant=True) - - @pedantic - def foo(a: T, b: T) -> None: - pass - - with self.assertRaises(PedanticTypeVarMismatchException): - foo(a=Parent(), b=Child()) - - def test_class_bad_subclass(self): - @pedantic - def foo(a: Type[Child]) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=Parent) - - def test_class_any(self): - @pedantic - def foo(a: Type[Any]) -> None: - pass - - foo(a=str) - - def test_wrapped_function(self): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - - @pedantic - @decorator - def foo(a: 'Child') -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=Parent()) - - def test_mismatching_default_type(self): - @pedantic - def foo(a: str = 1) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo() - - def test_implicit_default_none(self): - """ - Test that if the default value is ``None``, a ``None`` argument can be passed. - - """ - @pedantic - def foo(a: Optional[str] = None) -> None: - pass - - foo() - - def test_generator_simple(self): - """Test that argument type checking works in a generator function too.""" - @pedantic - def generate(a: int) -> Generator[int, int, None]: - yield a - yield a + 1 - - gen = generate(a=1) - next(gen) - - def test_wrapped_generator_no_return_type_annotation(self): - """Test that return type checking works in a generator function too.""" - @pedantic - def generate(a: int) -> Generator[int, int, None]: - yield a - yield a + 1 - - gen = generate(a=1) - next(gen) - - def test_varargs(self): - @pedantic - def foo(*args: int) -> None: - pass - - foo(1, 2) - - def test_varargs_fail(self): - @pedantic - def foo(*args: int) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(1, 'a') - - def test_kwargs(self): - @pedantic - def foo(**kwargs: int) -> None: - pass - - foo(a=1, b=2) - - def test_kwargs_fail(self): - @pedantic - def foo(**kwargs: int) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1, b='a') - - def test_generic(self): - T_Foo = TypeVar('T_Foo') - - class FooGeneric(Generic[T_Foo]): - pass - - @pedantic - def foo(a: FooGeneric[str]) -> None: - print(a) - - foo(a=FooGeneric[str]()) - - def test_newtype(self): - myint = NewType("myint", int) - - @pedantic - def foo(a: myint) -> int: - return 42 - - assert foo(a=1) == 42 - - with self.assertRaises(PedanticTypeCheckException): - foo(a="a") - - def test_collection(self): - @pedantic - def foo(a: Collection) -> None: - pass - - with self.assertRaises(PedanticTypeCheckException): - foo(a=True) - - def test_binary_io(self): - @pedantic - def foo(a: BinaryIO) -> None: - print(a) - - foo(a=BytesIO()) - - def test_text_io(self): - @pedantic - def foo(a: TextIO) -> None: - print(a) - - foo(a=StringIO()) - - def test_binary_io_fail(self): - @pedantic - def foo(a: TextIO) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=BytesIO()) - - def test_text_io_fail(self): - @pedantic - def foo(a: BinaryIO) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=StringIO()) - - def test_binary_io_real_file(self): - @pedantic - def foo(a: BinaryIO) -> None: - print(a) - - with open(file=TEST_FILE, mode='wb') as f: - foo(a=f) - - def test_text_io_real_file(self): - @pedantic - def foo(a: TextIO) -> None: - print(a) - - with open(file=TEST_FILE, mode='w') as f: - foo(a=f) - - def test_pedantic_return_type_var_fail(self): - T = TypeVar('T', int, float) - - @pedantic - def foo(a: T, b: T) -> T: - return 'a' - - with self.assertRaises(PedanticTypeCheckException): - foo(a=4, b=2) - - def test_callable(self): - @pedantic - def foo_1(a: Callable[..., int]) -> None: - print(a) - - @pedantic - def foo_2(a: Callable) -> None: - print(a) - - def some_callable() -> int: - return 4 - - foo_1(a=some_callable) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a=some_callable) - - def test_list(self): - @pedantic - def foo_1(a: List[int]) -> None: - print(a) - - @pedantic - def foo_2(a: List) -> None: - print(a) - - @pedantic - def foo_3(a: list) -> None: - print(a) - - @pedantic - def foo_4(a: list[int]) -> None: - print(a) - - foo_1(a=[1, 2]) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a=[1, 2]) - - with self.assertRaises(PedanticTypeCheckException): - foo_3(a=[1, 2]) - - - foo_4(a=[1, 2]) - - def test_dict(self): - @pedantic - def foo_1(a: Dict[str, int]) -> None: - print(a) - - @pedantic - def foo_2(a: Dict) -> None: - print(a) - - @pedantic - def foo_3(a: dict) -> None: - print(a) - - @pedantic - def foo_4(a: dict[str, int]) -> None: - print(a) - - foo_1(a={'x': 2}) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a={'x': 2}) - - with self.assertRaises(PedanticTypeCheckException): - foo_3(a={'x': 2}) - - foo_4(a={'x': 2}) - - def test_sequence(self): - @pedantic - def foo(a: Sequence[str]) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - foo(a=value) - - def test_sequence_no_type_args(self): - @pedantic - def foo(a: Sequence) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - with self.assertRaises(PedanticTypeCheckException): - foo(a=value) - - def test_iterable(self): - @pedantic - def foo(a: Iterable[str]) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - foo(a=value) - - def test_iterable_no_type_args(self): - @pedantic - def foo(a: Iterable) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - with self.assertRaises(PedanticTypeCheckException): - foo(a=value) - - def test_container(self): - @pedantic - def foo(a: Container[str]) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - foo(a=value) - - def test_container_no_type_args(self): - @pedantic - def foo(a: Container) -> None: - print(a) - - for value in [('a', 'b'), ['a', 'b'], 'abc']: - with self.assertRaises(PedanticTypeCheckException): - foo(a=value) - - def test_set(self): - @pedantic - def foo_1(a: AbstractSet[int]) -> None: - print(a) - - @pedantic - def foo_2(a: Set[int]) -> None: - print(a) - - for value in [set(), {6}]: - foo_1(a=value) - foo_2(a=value) - - def test_set_no_type_args(self): - @pedantic - def foo_1(a: AbstractSet) -> None: - print(a) - - @pedantic - def foo_2(a: Set) -> None: - print(a) - - @pedantic - def foo_3(a: set) -> None: - print(a) - - for value in [set(), {6}]: - with self.assertRaises(PedanticTypeCheckException): - foo_1(a=value) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a=value) - - with self.assertRaises(PedanticTypeCheckException): - foo_3(a=value) - - def test_tuple(self): - @pedantic - def foo_1(a: Tuple[int, int]) -> None: - print(a) - - @pedantic - def foo_2(a: Tuple[int, ...]) -> None: - print(a) - - foo_1(a=(1, 2)) - foo_2(a=(1, 2)) - - def test_tuple_no_type_args(self): - @pedantic - def foo_1(a: Tuple) -> None: - print(a) - - @pedantic - def foo_2(a: tuple) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo_1(a=(1, 2)) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a=(1, 2)) - - def test_empty_tuple(self): - @pedantic - def foo(a: Tuple[()]) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=()) - - def test_class(self): - @pedantic - def foo_1(a: Type[Parent]) -> None: - print(a) - - @pedantic - def foo_2(a: Type[TypeVar('UnboundType')]) -> None: - print(a) - - @pedantic - def foo_3(a: Type[TypeVar('BoundType', bound=Parent)]) -> None: - print(a) - - foo_1(a=Child) - foo_2(a=Child) - foo_3(a=Child) - - def test_class_no_type_vars(self): - @pedantic - def foo_1(a: Type) -> None: - print(a) - - @pedantic - def foo_2(a: type) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo_1(a=Child) - - with self.assertRaises(PedanticTypeCheckException): - foo_2(a=Child) - - def test_class_not_a_class(self): - @pedantic - def foo(a: Type[Parent]) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1) - - def test_complex(self): - @pedantic - def foo(a: complex) -> None: - print(a) - - foo(a=complex(1, 5)) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1.0) - - def test_float(self): - @pedantic - def foo(a: float) -> None: - print(a) - - foo(a=1.5) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=1) - - def test_coroutine_correct_return_type(self): - @pedantic - async def foo() -> str: - return 'foo' - - coro = foo() - - with self.assertRaises(StopIteration): - coro.send(None) - - def test_coroutine_wrong_return_type(self): - @pedantic - async def foo() -> str: - return 1 - - coro = foo() - - with self.assertRaises(PedanticTypeCheckException): - coro.send(None) - - def test_bytearray_bytes(self): - @pedantic - def foo(x: bytearray) -> None: - pass - - foo(x=bytearray([1])) - - def test_class_decorator(self): - @pedantic_class - class Foo: - @staticmethod - def staticmethod() -> int: - return 'foo' - - @classmethod - def classmethod(cls) -> int: - return 'foo' - - def method(self) -> int: - return 'foo' - - with self.assertRaises(PedanticTypeCheckException): - Foo.staticmethod() - - with self.assertRaises(PedanticTypeCheckException): - Foo.classmethod() - - with self.assertRaises(PedanticTypeCheckException): - Foo().method() - - def test_generator(self): - @pedantic - def genfunc() -> Generator[int, str, List[str]]: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - gen = genfunc() - - with self.assertRaises(StopIteration): - value = next(gen) - while True: - value = gen.send(str(value)) - assert isinstance(value, int) - - def test_generator_no_type_args(self): - @pedantic - def genfunc() -> Generator: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - with self.assertRaises(PedanticTypeCheckException): - genfunc() - - def test_iterator(self): - @pedantic - def genfunc() -> Iterator[int]: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - gen = genfunc() - - with self.assertRaises(PedanticTypeCheckException): - value = next(gen) - while True: - value = gen.send(str(value)) - assert isinstance(value, int) - - def test_iterator_no_type_args(self): - @pedantic - def genfunc() -> Iterator: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - with self.assertRaises(PedanticTypeCheckException): - genfunc() - - def test_iterable_advanced(self): - @pedantic - def genfunc() -> Iterable[int]: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - gen = genfunc() - - with self.assertRaises(PedanticTypeCheckException): - value = next(gen) - while True: - value = gen.send(str(value)) - assert isinstance(value, int) - - def test_iterable_advanced_no_type_args(self): - @pedantic - def genfunc() -> Iterable: - val1 = yield 2 - val2 = yield 3 - val3 = yield 4 - return [val1, val2, val3] - - with self.assertRaises(PedanticTypeCheckException): - genfunc() - - def test_generator_bad_yield(self): - @pedantic - def genfunc_1() -> Generator[int, str, None]: - yield 'foo' - - @pedantic - def genfunc_2() -> Iterable[int]: - yield 'foo' - - @pedantic - def genfunc_3() -> Iterator[int]: - yield 'foo' - - gen = genfunc_1() - - with self.assertRaises(PedanticTypeCheckException): - next(gen) - - gen = genfunc_2() - - with self.assertRaises(PedanticTypeCheckException): - next(gen) - - gen = genfunc_3() - - with self.assertRaises(PedanticTypeCheckException): - next(gen) - - def test_generator_bad_send(self): - @pedantic - def genfunc() -> Generator[int, str, None]: - yield 1 - yield 2 - - gen = genfunc() - next(gen) - - with self.assertRaises(PedanticTypeCheckException): - gen.send(2) - - def test_generator_bad_return(self): - @pedantic - def genfunc() -> Generator[int, str, str]: - yield 1 - return 6 - - gen = genfunc() - next(gen) - - with self.assertRaises(PedanticTypeCheckException): - gen.send('foo') - - def test_return_generator(self): - @pedantic - def genfunc() -> Generator[int, None, None]: - yield 1 - - @pedantic - def foo() -> Generator[int, None, None]: - return genfunc() - - foo() - - def test_local_class(self): - @pedantic_class - class LocalClass: - class Inner: - pass - - def create_inner(self) -> 'Inner': - return self.Inner() - - retval = LocalClass().create_inner() - assert isinstance(retval, LocalClass.Inner) - - def test_local_class_async(self): - @pedantic_class - class LocalClass: - class Inner: - pass - - async def create_inner(self) -> 'Inner': - return self.Inner() - - coro = LocalClass().create_inner() - - with self.assertRaises(StopIteration): - coro.send(None) - - def test_callable_nonmember(self): - class CallableClass: - def __call__(self): - pass - - @pedantic_class - class LocalClass: - some_callable = CallableClass() - - def test_inherited_class_method(self): - @pedantic_class - class Parent: - @classmethod - def foo(cls, x: str) -> str: - return cls.__name__ - - @pedantic_class - class Child(Parent): - pass - - self.assertEqual('Parent', Child.foo(x='bar')) - - with self.assertRaises(PedanticTypeCheckException): - Child.foo(x=1) - - def test_type_var_forward_ref_bound(self): - TBound = TypeVar('TBound', bound='Parent') - - @pedantic - def func(x: TBound) -> None: - pass - - func(x=Parent()) - - with self.assertRaises(PedanticTypeCheckException): - func(x='foo') - - def test_noreturn(self): - @pedantic - def foo() -> NoReturn: - pass - - @pedantic - def bar() -> NoReturn: - raise ZeroDivisionError('bar') - - with self.assertRaises(expected_exception=ZeroDivisionError): - bar() - - with self.assertRaises(PedanticTypeCheckException): - foo() - - def test_literal(self): - @pedantic - def foo(a: Literal[1, True, 'x', b'y', 404]) -> None: - print(a) - - foo(a=404) - foo(a=True) - foo(a='x') - - with self.assertRaises(PedanticTypeCheckException): - foo(a=4) - - def test_literal_union(self): - @pedantic - def foo(a: Union[str, Literal[1, 6, 8]]) -> None: - print(a) - - foo(a=6) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=4) - - def test_literal_illegal_value(self): - @pedantic - def foo(a: Literal[1, 1.1]) -> None: - print(a) - - with self.assertRaises(PedanticTypeCheckException): - foo(a=4) - - def test_enum(self): - with self.assertRaises(PedanticTypeCheckException): - @pedantic_class - class MyEnum(Enum): - A = 'a' - - def test_enum_aggregate(self): - T = TypeVar('T', bound=IntEnum) - - @pedantic_class - class EnumAggregate(Generic[T]): - enum: ClassVar[Type[T]] - - def __init__(self, value: Union[int, str, List[T]]) -> None: - assert len(self.enum) < 10 - - if value == '': - raise ValueError(f'Parameter "value" cannot be empty!') - - if isinstance(value, list): - self._value = ''.join([str(x.value) for x in value]) - else: - self._value = str(value) - - self._value = ''.join(sorted(self._value)) # sort characters in string - self.to_list() # check if is valid - - def __contains__(self, item: T) -> bool: - return item in self.to_list() - - def __eq__(self, other: Union['EnumAggregate', str]) -> bool: - if isinstance(other, str): - return self._value == other - - return self._value == other._value - - def __str__(self) -> str: - return self._value - - def to_list(self) -> List[T]: - return [self.enum(int(character)) for character in self._value] - - @property - def value(self) -> str: - return self._value - - @classmethod - def all(cls) -> str: - return ''.join([str(x.value) for x in cls.enum]) - - class Gender(IntEnum): - MALE = 1 - FEMALE = 2 - DIVERS = 3 - - @pedantic_class - class Genders(EnumAggregate[Gender]): - enum = Gender - - Genders(value=12) - - with self.assertRaises(PedanticTypeCheckException): - Genders(value=Child()) - - def test_primitive_list_dict_tuple(self): - @pedantic - def f(x: list[dict[int, tuple[float, str]]]) -> list[Any]: - return x - - f(x=[{3: (3.24, 'hi')}]) - - for value in [ - [{3, (3, 'hi')}], - [{3: (3, 'hi')}], - [{3: (3.24, 3)}], - [{3: (3.24, 'hi')}, {0}], - ]: - with self.assertRaises(PedanticTypeCheckException): - f(x=value) - - def test_dataclass_protocol(self): - class IsDataclass(typing.Protocol): - __dataclass_fields__: ClassVar[Dict] - - @dataclass - class Foo: - v: int - - @pedantic - def foo(x: IsDataclass) -> IsDataclass: - return x - - foo(x=Foo(v=42)) - - def test_dataclass_protocol_in_type(self): - class IsDataclass(typing.Protocol): - __dataclass_fields__: ClassVar[Dict] - - @dataclass - class Foo: - v: int - - @pedantic - def foo(x: type[IsDataclass]) -> IsDataclass: - return x - - assert foo(x=Foo) == Foo - - def test_dataclass_protocol_in_type_with_union(self): - class IsDataclass(typing.Protocol): - __dataclass_fields__: ClassVar[Dict] - - @dataclass - class Foo: - v: int - - @pedantic - def foo(x: type[None | bool | IsDataclass]) -> IsDataclass: - return x - - assert foo(x=Foo) == Foo - - def test_partial_function(self): - @pedantic - def f(a: int, b: int) -> int: - return a + b - - g = pedantic(partial(f, a=1)) - - assert f(a=2, b=3) == 5 - assert g(b=3) == 4 - - with self.assertRaises(PedanticTypeCheckException): - f(a='2', b=3) - - with self.assertRaises(PedanticTypeCheckException): - g(b='2') diff --git a/pedantic/tests/tests_pedantic_async.py b/pedantic/tests/tests_pedantic_async.py deleted file mode 100644 index 057ae358..00000000 --- a/pedantic/tests/tests_pedantic_async.py +++ /dev/null @@ -1,58 +0,0 @@ -import asyncio -import unittest - -from pedantic.decorators.class_decorators import pedantic_class -from pedantic.exceptions import PedanticTypeCheckException -from pedantic.decorators.fn_deco_pedantic import pedantic - - -class TestPedanticAsyncio(unittest.IsolatedAsyncioTestCase): - async def test_coroutine_correct_return_type(self): - @pedantic - async def foo() -> str: - await asyncio.sleep(0) - return 'foo' - - await foo() - - async def test_coroutine_wrong_return_type(self): - @pedantic - async def foo() -> str: - await asyncio.sleep(0) - return 1 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - await foo() - - async def test_coroutine_wrong_argument_type(self): - @pedantic - async def foo(x: int) -> int: - await asyncio.sleep(0) - return 1 + x - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - await foo(x=4.5) - - async def test_static_async(self): - @pedantic_class - class Foo: - @staticmethod - async def staticmethod() -> int: - await asyncio.sleep(0) - return 'foo' - - @classmethod - async def classmethod(cls) -> int: - await asyncio.sleep(0) - return 'foo' - - async def method(self) -> int: - await asyncio.sleep(0) - return 'foo' - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - await Foo.staticmethod() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - await Foo.classmethod() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - await Foo().method() diff --git a/pedantic/tests/tests_pedantic_class.py b/pedantic/tests/tests_pedantic_class.py deleted file mode 100644 index bf203605..00000000 --- a/pedantic/tests/tests_pedantic_class.py +++ /dev/null @@ -1,537 +0,0 @@ -import sys -import unittest -from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import IntEnum -from typing import Any, Optional, Callable, Union, Dict, List - -from pedantic.env_var_logic import disable_pedantic, enable_pedantic -from pedantic import overrides -from pedantic.decorators.class_decorators import pedantic_class -from pedantic.exceptions import PedanticOverrideException, PedanticTypeCheckException, \ - PedanticCallWithArgsException - - -class TestPedanticClass(unittest.TestCase): - def tearDown(self) -> None: - enable_pedantic() - - def test_constructor(self): - @pedantic_class - class MyClass: - def __init__(self, a: int) -> None: - self.a = a - - MyClass(a=42) - - def test_constructor_with_list(self): - class Foo(IntEnum): - A = 1 - B = 2 - - @pedantic_class - class MyClass: - def __init__(self, b: int, a: List[Foo]) -> None: - self.a = a - self.b = b - - MyClass(b=42, a=[Foo.A, Foo.B]) - - def test_constructor_param_without_type_hint(self): - @pedantic_class - class MyClass: - def __init__(self, a) -> None: - self.a = a - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass(a=42) - - def test_constructor_without_return_type(self): - @pedantic_class - class MyClass: - def __init__(self, a: int): - self.a = a - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass(a=42) - - def test_constructor_wrong_return_type(self): - @pedantic_class - class MyClass: - def __init__(self, a: int) -> int: - self.a = a - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass(a=42) - - def test_constructor_must_be_called_with_kwargs(self): - @pedantic_class - class MyClass: - def __init__(self, a: int) -> None: - self.a = a - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - MyClass(42) - - def test_multiple_methods(self): - @pedantic_class - class MyClass: - def __init__(self, a: int) -> None: - self.a = a - - def calc(self, b: int) -> int: - return self.a - b - - def print(self, s: str) -> None: - print(f'{self.a} and {s}') - - m = MyClass(a=5) - m.calc(b=42) - m.print(s='Hi') - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.calc(45) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.calc(b=45.0) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.print('Hi') - - def test_multiple_methods_with_missing_and_wrong_type_hints(self): - @pedantic_class - class MyClass: - def __init__(self, a: int) -> None: - self.a = a - - def calc(self, b: int) -> float: - return self.a - b - - def dream(self, b) -> int: - return self.a * b - - def print(self, s: str): - print(f'{self.a} and {s}') - - m = MyClass(a=5) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.calc(b=42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.print(s='Hi') - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.dream(b=2) - - def test_type_annotation_string(self): - @pedantic_class - class MyClass: - def __init__(self, s: str) -> None: - self.s = s - - @staticmethod - def generator() -> 'MyClass': - return MyClass(s='generated') - - MyClass.generator() - - def test_typo_in_type_annotation_string(self): - @pedantic_class - class MyClass: - def __init__(self, s: str) -> None: - self.s = s - - @staticmethod - def generator() -> 'MyClas': - return MyClass(s='generated') - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.generator() - - def test_overriding_contains(self): - @pedantic_class - class MyClass(list): - def __contains__(self, item: int) -> bool: - return True - - m = MyClass() - print(42 in m) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - print('something' in m) - - def test_type_annotation_string_typo(self): - @pedantic_class - class MyClass: - def compare(self, other: 'MyClas') -> bool: - return self == other - - def fixed_compare(self, other: 'MyClass') -> bool: - return self == other - - m = MyClass() - m.fixed_compare(other=m) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.compare(other=m) - - def test_docstring_not_required(self): - @pedantic_class - class Foo: - def __init__(self, a: int) -> None: - self.a = a - - def bunk(self) -> int: - ''' - Function with correct docstring. Yes, single-quoted docstrings are allowed too. - Returns: - int: 42 - ''' - return self.a - - foo = Foo(a=10) - foo.bunk() - - def test_overrides(self): - @pedantic_class - class Abstract: - def func(self, b: str) -> str: - pass - - def bunk(self) -> int: - pass - - @pedantic_class - class Foo(Abstract): - def __init__(self, a: int) -> None: - self.a = a - - @overrides(Abstract) - def func(self, b: str) -> str: - return b - - @overrides(Abstract) - def bunk(self) -> int: - return 42 - - f = Foo(a=42) - f.func(b='Hi') - f.bunk() - - def test_overrides_abc(self): - @pedantic_class - class Abstract(ABC): - @abstractmethod - def func(self, b: str) -> str: - pass - - @abstractmethod - def bunk(self) -> int: - pass - - @pedantic_class - class Foo(Abstract): - def __init__(self, a: int) -> None: - self.a = a - - @overrides(Abstract) - def func(self, b: str) -> str: - return b - - @overrides(Abstract) - def bunk(self) -> int: - return 42 - - f = Foo(a=42) - f.func(b='Hi') - f.bunk() - - def test_overrides_with_type_errors_and_call_by_args3(self): - @pedantic_class - class Abstract: - def func(self, b: str) -> str: - pass - - def bunk(self) -> int: - pass - - @pedantic_class - class Foo(Abstract): - def __init__(self, a: int) -> None: - self.a = a - - @overrides(Abstract) - def func(self, b: str) -> str: - return b - - @overrides(Abstract) - def bunk(self) -> int: - return self.a - - f = Foo(a=42) - f.func(b='Hi') - f.bunk() - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - f.func('Hi') - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo(a=3.1415) - f.a = 3.145 - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f.bunk() - - def test_overrides_goes_wrong(self): - @pedantic_class - class Parent: - def func(self, b: str) -> str: - return b + b + b - - def bunk(self) -> int: - return 42 - - with self.assertRaises(expected_exception=PedanticOverrideException): - @pedantic_class - class Foo(Parent): - def __init__(self, a: int) -> None: - self.a = a - - @overrides(Parent) - def funcy(self, b: str) -> str: - return b - - @overrides(Parent) - def bunk(self) -> int: - return self.a - - f = Foo(a=40002) - f.func(b='Hi') - f.bunk() - - p = Parent() - p.func(b='Hi') - p.bunk() - - def test_static_method_with_sloppy_type_annotation(self): - @pedantic_class - class MyStaticClass: - @staticmethod - def double_func(a: int) -> int: - x, y = MyStaticClass.static_bar() - return x - - @staticmethod - def static_bar() -> (int, int): # this is wrong. Correct would be Tuple[int, int] - return 0, 1 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - print(MyStaticClass.double_func(a=0)) - - def test_property(self): - @pedantic_class - class MyClass(object): - def __init__(self, some_arg: Any) -> None: - self._some_attribute = some_arg - - @property - def some_attribute(self) -> int: - return self._some_attribute - - @some_attribute.setter - def some_attribute(self, value: str) -> None: - self._some_attribute = value - - def calc(self, value: float) -> float: - return 2 * value - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - MyClass(42) - - m = MyClass(some_arg=42) - self.assertEqual(m.some_attribute, 42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_attribute = 100 - self.assertEqual(m.some_attribute, 42) - m.some_attribute = '100' - self.assertEqual(m._some_attribute, '100') - m.calc(value=42.0) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - print(m.some_attribute) - - def test_property_getter_and_setter_misses_type_hints(self): - @pedantic_class - class MyClass(object): - def __init__(self, some_arg: int) -> None: - self._some_attribute = some_arg - - @property - def some_attribute(self): - return self._some_attribute - - @some_attribute.setter - def some_attribute(self, value: int): - self._some_attribute = value - - def calc(self, value: float) -> float: - return 2 * value - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - MyClass(42) - - m = MyClass(some_arg=42) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.some_attribute = 100 - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - print(m.some_attribute) - m.calc(value=42.0) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.calc(value=42) - - def test_default_constructor(self): - @pedantic_class - class MyClass: - def fun(self) -> int: - return 42 - m = MyClass() - m.fun() - - def test_optional_callable(self): - @pedantic_class - class SemanticSimilarity: - def __init__(self, post_processing: bool = True, val: Optional[Callable[[float], float]] = None) -> None: - if post_processing is None: - self.post_processing = val - else: - self.post_processing = lambda x: x - - SemanticSimilarity() - - def test_optional_lambda(self): - @pedantic_class - class SemanticSimilarity: - def __init__(self, val: Callable[[float], float] = lambda x: x) -> None: - self.post_processing = val - - SemanticSimilarity() - - def test_class_method_type_annotation_missing(self): - @pedantic_class - class MyClass: - @classmethod - def do(cls): - print('i did something') - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.do() - - def test_class_method_type_annotation(self): - @pedantic_class - class MyClass: - @classmethod - def do(cls) -> None: - print('i did something') - - @classmethod - def calc(cls, x: Union[int, float]) -> int: - return x * x - - MyClass.do() - MyClass.calc(x=5) - m = MyClass() - m.do() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.calc(5) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.calc(x=5.1) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - MyClass.calc('hi') - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.calc(5) - with self.assertRaises(expected_exception=PedanticTypeCheckException): - m.calc(x=5.1) - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - m.calc('hi') - - def test_dataclass_inside(self): - """Pedantic cannot be used on dataclasses.""" - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - @pedantic_class - @dataclass - class MyClass: - name: str - unit_price: float - quantity_on_hand: int = 0 - - def test_dataclass_outside(self): - """Pedantic cannot check the constructor of dataclasses""" - - @dataclass - @pedantic_class - class MyClass: - name: str - unit_price: float - quantity_on_hand: int = 0 - - def total_cost(self) -> int: - return self.unit_price * self.quantity_on_hand - - MyClass(name='name', unit_price=5.1) - a = MyClass(name='name', unit_price=5.0, quantity_on_hand=42) - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - a.total_cost() - - def test_class_decorator_static_class_method(self): - @pedantic_class - class Foo: - @staticmethod - def staticmethod() -> int: - return 'foo' - - @classmethod - def classmethod(cls) -> int: - return 'foo' - - def method(self) -> int: - return 'foo' - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo.staticmethod() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo.classmethod() - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo().method() - - def test_pedantic_class_disable_pedantic(self): - disable_pedantic() - - @pedantic_class - class MyClass: - def __init__(self, pw, **kwargs): - self._validate_str_len(new_values=kwargs) - - @staticmethod - def _validate_str_len(new_values: Dict[str, Any]) -> None: - return 42 - - def method(pw, **kwargs): - MyClass._validate_str_len(new_values=kwargs) - - MyClass._validate_str_len(None) - MyClass._validate_str_len(new_values={1: 1, 2: 2}) - MyClass(name='hi', age=12, pw='123') - - def test_disable_pedantic_2(self): - """ https://github.com/LostInDarkMath/pedantic-python-decorators/issues/37 """ - - disable_pedantic() - - @pedantic_class - class Foo: - def __init__(self) -> None: - self._value = 42 - - def do(self) -> None: - print(self.bar(value=self._value)) - - @staticmethod - def bar(value: int) -> int: - return value + 75 - - f = Foo() - f.do() diff --git a/pedantic/tests/tests_pedantic_class_docstring.py b/pedantic/tests/tests_pedantic_class_docstring.py deleted file mode 100644 index 61ceeaab..00000000 --- a/pedantic/tests/tests_pedantic_class_docstring.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest - -from pedantic import pedantic_class_require_docstring -from pedantic.exceptions import PedanticDocstringException - - -class TestPedanticClassDocstring(unittest.TestCase): - def test_require_docstring(self): - @pedantic_class_require_docstring - class MyClass: - def __init__(self, s: str) -> None: - """Constructor - - Args: - s (str): name - """ - self.s = s - - def double(self, b: int) -> str: - """some method - - Args: - b (int): magic number - - Returns: - str: cool stuff - - """ - return self.s + str(b) - - @staticmethod - def generator() -> 'MyClass': - """Static - - Returns: - MyClass: instance - """ - return MyClass(s='generated') - - m = MyClass.generator() - m.double(b=42) - - def test_typo_docstring(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_class_require_docstring - class MyClass: - def __init__(self, s: str) -> None: - """Constructor - - Args: - s (str): name - """ - self.s = s - - @staticmethod - def generator() -> 'MyClass': - """Static - - Returns: - MyClas: instance - """ - return MyClass(s='generated') - - def test_wrong_docstring(self): - with self.assertRaises(expected_exception=PedanticDocstringException): - @pedantic_class_require_docstring - class MyClass: - def __init__(self, s: str) -> None: - """Constructor - - Args: - s (str): name - """ - self.s = s - - def double(self, b: int) -> str: - """some method - - Args: - b (float): magic number - - Returns: - str: cool stuff - - """ - return self.s + str(b) diff --git a/pedantic/tests/tests_pedantic_python_311.py b/pedantic/tests/tests_pedantic_python_311.py deleted file mode 100644 index c21116f2..00000000 --- a/pedantic/tests/tests_pedantic_python_311.py +++ /dev/null @@ -1,128 +0,0 @@ -import unittest - -from pedantic import pedantic, pedantic_class -from pedantic.exceptions import PedanticTypeCheckException - - -class TestPedanticPython311AddedStuff(unittest.TestCase): - def test_typing_never(self): - from typing import Never - - @pedantic - def never_call_me(arg: Never) -> None: - pass - - @pedantic - def foo() -> Never: - pass - - @pedantic - def bar() -> Never: - raise ZeroDivisionError('bar') - - with self.assertRaises(expected_exception=ZeroDivisionError): - bar() - - with self.assertRaises(PedanticTypeCheckException): - foo() - - with self.assertRaises(expected_exception=PedanticTypeCheckException) as exc: - never_call_me(arg='42') - - def test_literal_string(self): - from typing import LiteralString - - @pedantic - def foo(s: LiteralString) -> None: - pass - - foo(s='Hi') - foo(s=2 * 'Hi') - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - foo(s=3) - - def test_self_type(self): - from typing import Self - - class Bar: - pass - - @pedantic_class - class Foo: - def f(self) -> Self: - return self - - @staticmethod - def g() -> Self: - return Foo() - - @classmethod - def h(cls) -> Self: - return cls() - - def f_2(self) -> Self: - return Bar() - - @staticmethod - def g_2() -> Self: - return Bar() - - @classmethod - def h_2(cls) -> Self: - return Bar() - - f = Foo() - assert f.f() == f - f.g() - f.h() - Foo.g() - Foo.h() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f.f_2() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f.g_2() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f.h_2() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo.g_2() - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - Foo.h_2() - - def test_using_self_type_annotation_outside_class(self): - from typing import Self - - @pedantic - def f() -> Self: - return 'hi' - - with self.assertRaises(expected_exception=PedanticTypeCheckException): - f() - - def test_type_var_tuple(self): - from typing import TypeVarTuple, Generic - - Ts = TypeVarTuple('Ts') - - @pedantic_class - class Array(Generic[*Ts]): - def __init__(self, *args: *Ts) -> None: - self._values = args - - @pedantic - def add_dimension(a: Array[*Ts], value: int) -> Array[int, *Ts]: - return Array[int, *Ts](value, *a._values) - - array = Array[int, float](42, 3.4) - array_2 = Array[bool, int, float, str](True, 4, 3.4, 'hi') - extended_array = add_dimension(a=array, value=42) - assert extended_array._values == (42, 42, 3.4) - - # this is too complicated at the moment - # with self.assertRaises(expected_exception=PedanticTypeCheckException): - # Array[int, float](4.2, 3.4) \ No newline at end of file diff --git a/pedantic/tests/tests_require_kwargs.py b/pedantic/tests/tests_require_kwargs.py deleted file mode 100644 index 24d8f20c..00000000 --- a/pedantic/tests/tests_require_kwargs.py +++ /dev/null @@ -1,31 +0,0 @@ -import unittest - -# local file imports -from pedantic.exceptions import PedanticCallWithArgsException -from pedantic import require_kwargs - - -class TestRequireKwargs(unittest.TestCase): - - def test_kwargs(self): - @require_kwargs - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - calc(n=1, m=2, i=3) - - def test_no_kwargs_1(self): - @require_kwargs - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - calc(1, m=2, i=3) - - def test_no_kwargs_2(self): - @require_kwargs - def calc(n: int, m: int, i: int) -> int: - return n + m + i - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - calc(1, 2, 3) diff --git a/pedantic/tests/tests_small_method_decorators.py b/pedantic/tests/tests_small_method_decorators.py deleted file mode 100644 index c547d5c6..00000000 --- a/pedantic/tests/tests_small_method_decorators.py +++ /dev/null @@ -1,276 +0,0 @@ -import asyncio -import unittest -import warnings -from abc import abstractmethod -from unittest import IsolatedAsyncioTestCase - -from pedantic import overrides, timer, count_calls, trace, trace_if_returns, does_same_as_function, deprecated, \ - unimplemented, mock, require_kwargs -from pedantic.exceptions import NotImplementedException, PedanticOverrideException, PedanticCallWithArgsException - - -class TestSmallDecoratorMethods(unittest.TestCase): - def test_overrides_parent_has_no_such_method(self): - class MyClassA: - pass - - with self.assertRaises(expected_exception=PedanticOverrideException): - class MyClassB(MyClassA): - @overrides(MyClassA) - def operation(self): pass - - def test_overrides_all_good(self): - class MyClassA: - def operation(self): pass - - class MyClassB(MyClassA): - @overrides(MyClassA) - def operation(self): - return 42 - - b = MyClassB() - b.operation() - - def test_overrides_static_method(self): - class MyClassA: - @staticmethod - def operation(): pass - - class MyClassB(MyClassA): - @staticmethod - @overrides(MyClassA) - def operation(): - return 42 - - b = MyClassB() - self.assertEqual(b.operation(), 42) - self.assertEqual(MyClassB.operation(), 42) - - def test_overrides_below_property(self): - class MyClassA: - @property - @abstractmethod - def operation(self): pass - - class MyClassB(MyClassA): - @property - @overrides(MyClassA) # Note: it does not work the other way around - def operation(self): - return 43 - - b = MyClassB() - self.assertEqual(b.operation, 43) - - def test_overrides_function(self): - class MyClassA: - pass - - with self.assertRaises(expected_exception=PedanticOverrideException): - @overrides(MyClassA) - def operation(): return 42 - - def test_deprecated_1(self): - @deprecated - def old_method(i: int) -> str: return str(i) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - old_method(42) - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated" in str(w[-1].message) - - def test_deprecated_2(self): - def old_method(i: int) -> str: - return str(i) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - old_method(42) - assert not len(w) == 1 - - def test_unimplemented(self): - @unimplemented - def dirt(i: int) -> str: - return str(i) - - with self.assertRaises(expected_exception=NotImplementedException): - dirt(42) - - def test_timer(self): - @timer - def operation(i: int) -> str: - return str(i) - - operation(42) - - def test_count_calls(self): - @count_calls - def operation(i: int) -> str: - return str(i) - - operation(42) - - def test_trace(self): - def some_method(x, y): - return x + y - - traced_method = trace(some_method) - self.assertEqual(some_method(42, 99), traced_method(42, 99)) - - def test_trace_if_returns(self): - def some_method(x, y): - return x + y - traced_method = trace_if_returns(100)(some_method) - self.assertEqual(some_method(42, 99), traced_method(42, 99)) - self.assertEqual(some_method(42, 58), traced_method(42, 58)) - - def test_does_same_as_function(self): - def some_method(x, y, z): - return x * (y + z) - - @does_same_as_function(some_method) - def other_method(x, y, z): - return x * y + x * z - - other_method(1, 2, 3) - other_method(4, 5, 6) - - def test_does_same_as_function_wrong(self): - def some_method(x, y, z): - return x * (y + z) - - @does_same_as_function(some_method) - def other_method(x, y, z): - return x * y + z - - other_method(0, 2, 0) - with self.assertRaises(expected_exception=AssertionError): - other_method(4, 5, 6) - - -class AsyncSmallDecoratorTests(IsolatedAsyncioTestCase): - async def test_overrides_async_instance_method(self) -> None: - class MyClassA: - async def operation(self): pass - - class MyClassB(MyClassA): - @overrides(MyClassA) - async def operation(self): - await asyncio.sleep(0) - return 42 - - b = MyClassB() - await b.operation() - - async def test_overrides_parent_has_no_such_method_async(self): - class MyClassA: - pass - - with self.assertRaises(expected_exception=PedanticOverrideException): - class MyClassB(MyClassA): - @overrides(MyClassA) - async def operation(self): return 42 - - async def test_count_calls_async(self): - @count_calls - async def operation(i: int) -> str: - await asyncio.sleep(0) - return str(i) - - res = await operation(42) - self.assertEqual('42', res) - - async def test_trace_async(self): - async def some_method(x, y): - await asyncio.sleep(0) - return x + y - - traced_method = trace(some_method) - self.assertEqual(await some_method(42, 99), await traced_method(42, 99)) - - async def test_trace_if_returns_async(self): - async def some_method(x, y): - await asyncio.sleep(0) - return x + y - - traced_method = trace_if_returns(100)(some_method) - self.assertEqual(await some_method(42, 99), await traced_method(42, 99)) - self.assertEqual(await some_method(42, 58), await traced_method(42, 58)) - - async def test_timer_async(self): - @timer - async def operation(i: int) -> str: - await asyncio.sleep(0.05) - return str(i) - - await operation(42) - - async def test_deprecated_async(self): - @deprecated - async def old_method(i: int) -> str: - return str(i) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - await old_method(42) - assert len(w) == 1 - assert issubclass(w[-1].category, DeprecationWarning) - assert "deprecated" in str(w[-1].message) - - async def test_does_same_as_function_async(self): - async def some_method(x, y, z): - await asyncio.sleep(0) - return x * (y + z) - - @does_same_as_function(some_method) - async def other_method(x, y, z): - await asyncio.sleep(0) - return x * y + x * z - - await other_method(1, 2, 3) - await other_method(4, 5, 6) - - async def test_does_same_as_function_async_and_sync(self): - def some_method(x, y, z): - return x * (y + z) - - @does_same_as_function(some_method) - async def other_method(x, y, z): - await asyncio.sleep(0) - return x * y + x * z - - await other_method(1, 2, 3) - await other_method(4, 5, 6) - - async def test_does_same_as_function_wrong(self): - async def some_method(x, y, z): - await asyncio.sleep(0) - return x * (y + z) - - @does_same_as_function(some_method) - async def other_method(x, y, z): - await asyncio.sleep(0) - return x * y + z - - await other_method(0, 2, 0) - - with self.assertRaises(expected_exception=AssertionError): - await other_method(4, 5, 6) - - async def test_mock_async(self) -> None: - @mock(return_value=42) - async def my_function(a, b, c): return a + b + c - - assert await my_function(1, 2, 3) == 42 - assert await my_function(100, 200, 300) == 42 - - async def test_require_kwargs(self): - @require_kwargs - async def calc(n: int, m: int, i: int) -> int: - return n + m + i - - await calc(n=1, m=2, i=3) - - with self.assertRaises(expected_exception=PedanticCallWithArgsException): - await calc(1, m=2, i=3) diff --git a/pedantic/tests/validate/test_convert_value.py b/pedantic/tests/validate/test_convert_value.py deleted file mode 100644 index 3cc39173..00000000 --- a/pedantic/tests/validate/test_convert_value.py +++ /dev/null @@ -1,56 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ConversionError -from pedantic.decorators.fn_deco_validate.convert_value import convert_value - - -class TestConvertValue(TestCase): - def test_convert_to_bool(self): - for value in [True, 1, '1', ' 1 ', ' tRuE ', 'TRUE']: - self.assertTrue(convert_value(value=value, target_type=bool)) - - for value in [False, 0, '0', ' 0 ', ' fAlSe ', 'FALSE']: - self.assertFalse(convert_value(value=value, target_type=bool)) - - for value in ['alse', 0.1, '0.2', ' 0000 ', 'Talse', 'Frue', 42]: - with self.assertRaises(expected_exception=ConversionError): - self.assertFalse(convert_value(value=value, target_type=bool)) - - def test_convert_to_int(self): - for value in range(-4, 4): - self.assertEqual(value, convert_value(value=value, target_type=int)) - - self.assertEqual(42, convert_value(value='42', target_type=int)) - self.assertEqual(0, convert_value(value=' 0000 ', target_type=int)) - - for value in ['alse', 'Talse', 'Frue', 0.2, '0.2']: - with self.assertRaises(expected_exception=ConversionError): - self.assertFalse(convert_value(value=value, target_type=int)) - - def test_convert_to_float(self): - for value in range(-4, 4): - self.assertEqual(value, convert_value(value=value, target_type=float)) - - self.assertEqual(0.2, convert_value(value=0.2, target_type=float)) - self.assertEqual(0.2, convert_value(value='0.2', target_type=float)) - self.assertEqual(42, convert_value(value='42', target_type=float)) - self.assertEqual(0, convert_value(value=' 0000 ', target_type=float)) - - for value in ['alse', 'Talse', 'Frue']: - with self.assertRaises(expected_exception=ConversionError): - self.assertFalse(convert_value(value=value, target_type=float)) - - def test_convert_to_list(self): - for value in [[], [1], ['1', ' 1 '], [' tRuE ', 'TRUE']]: - self.assertEqual(value, convert_value(value=value, target_type=list)) - - self.assertEqual(['1', '2', '3'], convert_value(value='1,2,3', target_type=list)) - - def test_convert_to_dict(self): - for value in [{}, {1: 2}, {'1': ' 1 '}, {1: ' tRuE ', 2: 'TRUE'}]: - self.assertEqual(value, convert_value(value=value, target_type=dict)) - - self.assertEqual({'1': '1', '2': '4', '3': '7'}, convert_value(value='1:1,2:4,3:7', target_type=dict)) - - - diff --git a/pedantic/tests/validate/test_datetime_isoformat.py b/pedantic/tests/validate/test_datetime_isoformat.py deleted file mode 100644 index 3c21a6e4..00000000 --- a/pedantic/tests/validate/test_datetime_isoformat.py +++ /dev/null @@ -1,23 +0,0 @@ -from datetime import datetime, timedelta -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import DatetimeIsoFormat - - -class TestValidatorDatetimeIsoformat(TestCase): - def test_validator_datetime_isoformat(self) -> None: - @validate(Parameter(name='x', validators=[DatetimeIsoFormat()])) - def foo(x): - return x - - now = datetime.now() - self.assertTrue(abs((now - foo(now.isoformat()) < timedelta(milliseconds=1)))) - - with self.assertRaises(expected_exception=ParameterException): - foo('12.12.2020') - - with self.assertRaises(expected_exception=ParameterException): - foo('invalid') diff --git a/pedantic/tests/validate/test_flask_parameters.py b/pedantic/tests/validate/test_flask_parameters.py deleted file mode 100644 index 113be5c1..00000000 --- a/pedantic/tests/validate/test_flask_parameters.py +++ /dev/null @@ -1,609 +0,0 @@ -import json -from dataclasses import dataclass -from typing import List, Dict, Any -from unittest import TestCase - -from flask import Flask, Response, jsonify - -from pedantic import Email, overrides -from pedantic.decorators.fn_deco_validate.exceptions import ValidateException, TooManyArguments, \ - ParameterException, ExceptionDictKey, InvalidHeader -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, ReturnAs -from pedantic.decorators.fn_deco_validate.parameters.flask_parameters import FlaskJsonParameter, FlaskFormParameter, \ - FlaskHeaderParameter, FlaskGetParameter, FlaskPathParameter, Deserializable, GenericFlaskDeserializer -from pedantic.decorators.fn_deco_validate.validators import NotEmpty, Min - -JSON = 'application/json' -OK = 200 -INVALID = 422 -TOO_MANY_ARGS = 400 - - -class TestFlaskParameters(TestCase): - def test_validator_flask_json(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate( - FlaskJsonParameter(name='key', validators=[NotEmpty()]), - ) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.route('/required') - @validate( - FlaskJsonParameter(name='required', required=True), - FlaskJsonParameter(name='not_required', required=False), - FlaskJsonParameter(name='not_required_with_default', required=False, default=42), - ) - def required_params(required, not_required, not_required_with_default) -> Response: - return jsonify({ - 'required': required, - 'not_required': not_required, - 'not_required_with_default': not_required_with_default, - }) - - @app.route('/types') - @validate( - FlaskJsonParameter(name='bool_param', value_type=bool), - FlaskJsonParameter(name='int_param', value_type=int), - FlaskJsonParameter(name='float_param', value_type=float), - FlaskJsonParameter(name='str_param', value_type=str), - FlaskJsonParameter(name='list_param', value_type=list), - FlaskJsonParameter(name='dict_param', value_type=dict), - ) - def different_types( - bool_param, - int_param, - float_param, - str_param, - list_param, - dict_param, - ) -> Response: - return jsonify({ - 'bool_param': bool_param, - 'int_param': int_param, - 'float_param': float_param, - 'str_param': str_param, - 'list_param': list_param, - 'dict_param': dict_param, - }) - - @app.route('/args') - @validate( - FlaskJsonParameter(name='a', validators=[NotEmpty()]), - FlaskJsonParameter(name='b', validators=[NotEmpty()]), - return_as=ReturnAs.ARGS, - ) - def names_do_not_need_to_match(my_key: str, another: str) -> Response: - return jsonify({ - 'my_key': my_key, - 'another': another, - }) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - @app.errorhandler(TooManyArguments) - def handle_validation_error(exception: TooManyArguments) -> Response: - print(str(exception)) - response = jsonify(str(exception)) - response.status_code = TOO_MANY_ARGS - return response - - with app.test_client() as client: - res = client.get('/', data=json.dumps({'key': ' hello world '}), content_type=JSON) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/', data=json.dumps({'key': ' '}), content_type=JSON) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: 'NotEmpty', - ExceptionDictKey.VALUE: ' ', - ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - data = { - 'key': ' hello world ', - 'required': '1', - } - res = client.get('/', data=json.dumps(data), content_type=JSON) - self.assertEqual(TOO_MANY_ARGS, res.status_code) - self.assertEqual("Got unexpected arguments: ['required']", res.json) - - def test_validator_flask_form_data(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='key', validators=[NotEmpty()])) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/', data={'key': ' hello world '}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/', data={'key': ' '}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: 'NotEmpty', - ExceptionDictKey.VALUE: ' ', - ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - def test_validator_flask_header(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskHeaderParameter(name='key', validators=[NotEmpty()])) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(InvalidHeader) - def handle_validation_error(exception: InvalidHeader) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/', headers={'key': ' hello world '}, data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/', headers={'key': ' '}, data={}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALUE: ' ', - ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', - ExceptionDictKey.PARAMETER: 'key', - ExceptionDictKey.VALIDATOR: 'NotEmpty', - } - self.assertEqual(expected, res.json) - - def test_validator_flask_header_optional_parameter(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskHeaderParameter(name='key', validators=[NotEmpty()], required=False)) - def hello_world(key: str = None) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get('/', headers={'key': ' hello world '}, data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/', headers={}, data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual(None, res.json) - - def test_validator_flask_get(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskGetParameter(name='key', value_type=str, validators=[NotEmpty()])) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/?key=hello_world', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello_world', res.json) - - res = client.get('/?key=hello world', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/?key= ', data={}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: 'NotEmpty', - ExceptionDictKey.VALUE: ' ', - ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - def test_validator_flask_get_multiple_values_for_same_key(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskGetParameter(name='key', value_type=list, validators=[NotEmpty()])) - def hello_world(key: List[str]) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get('/?key=hello&key=world', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual(['hello', 'world'], res.json) - - def test_validator_flask_path(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskPathParameter(name='key', validators=[NotEmpty()])) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/hello_world', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello_world', res.json) - - res = client.get('/hello world', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('hello world', res.json) - - res = client.get('/ ', data={}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: 'NotEmpty', - ExceptionDictKey.VALUE: ' ', - ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - def test_invalid_value_type(self) -> None: - app = Flask(__name__) - - with self.assertRaises(expected_exception=AssertionError): - @app.route('/') - @validate(FlaskFormParameter(name='key', value_type=tuple)) - def hello_world(key: str) -> Response: - return jsonify(key) - - def test_wrong_name(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='k', value_type=str)) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ValidateException) - def handle_validation_error(exception: ValidateException) -> Response: - print(str(exception)) - response = jsonify(str(exception)) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get(data={'key': 'k'}) - self.assertEqual(INVALID, res.status_code) - self.assertIn('Value for parameter k is required.', res.json) - - def test_default_value(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='key', value_type=str, default='42')) - def hello_world(key: str) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get(data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('42', res.json) - - def test_not_required_allows_none_kwargs_without_none(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='key', value_type=str, required=False), - return_as=ReturnAs.KWARGS_WITHOUT_NONE) - def hello_world(key: str = 'it works') -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get(data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('it works', res.json) - - def test_not_required_allows_none_kwargs_with_none(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='key', value_type=str, required=False, default=None), - return_as=ReturnAs.KWARGS_WITH_NONE) - def hello_world(key: str) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get(data={'key': None}) - self.assertEqual(OK, res.status_code) - self.assertEqual(None, res.json) - - def test_not_required_with_default(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskFormParameter(name='key', value_type=str, required=False, default='42')) - def hello_world(key: str) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get(data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('42', res.json) - - def test_validator_flask_path_type_conversion(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskPathParameter(name='key', value_type=int, validators=[Min(42)])) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/42', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual(42, res.json) - - res = client.get('/42f', data={}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: None, - ExceptionDictKey.VALUE: '42f', - ExceptionDictKey.MESSAGE: "Value 42f cannot be converted to .", - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - def test_validator_flask_json_parameter_does_not_get_json(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskJsonParameter(name='key')) - def hello_world(key: str) -> Response: - return jsonify(key) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - print(str(exception)) - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get('/', data={}) - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.VALIDATOR: None, - ExceptionDictKey.VALUE: 'None', - ExceptionDictKey.MESSAGE: 'Value for parameter key is required.', - ExceptionDictKey.PARAMETER: 'key', - } - self.assertEqual(expected, res.json) - - def test_validator_flask_json_parameter_does_not_get_json_but_default(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskJsonParameter(name='key', default='42')) - def hello_world(key: str) -> Response: - return jsonify(key) - - with app.test_client() as client: - res = client.get('/', data={}) - self.assertEqual(OK, res.status_code) - self.assertEqual('42', res.json) - - def test_too_many_arguments(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskJsonParameter(name='k', value_type=str)) - def hello_world(**kwargs) -> Response: - return jsonify(str(kwargs)) - - @app.errorhandler(TooManyArguments) - def handle_validation_error(exception: TooManyArguments) -> Response: - print(str(exception)) - response = jsonify(str(exception)) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get(data=json.dumps({'k': 'k', 'a': 1}), content_type=JSON) - self.assertEqual(INVALID, res.status_code) - self.assertEqual("Got unexpected arguments: ['a']", res.json) - - def test_exception_for_required_parameter(self) -> None: - app = Flask(__name__) - key = 'PASSWORD' - - @app.route('/') - @validate(FlaskJsonParameter(name=key, value_type=str)) - def hello_world(**kwargs) -> Response: - return jsonify(str(kwargs)) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - reason = 'required' if 'required' in exception.message else 'invalid' - response = jsonify({exception.parameter_name: [{'KEY': reason}]}) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get(data=json.dumps({}), content_type=JSON) - self.assertEqual(INVALID, res.status_code) - self.assertEqual({key: [{'KEY': 'required'}]}, res.json) - - def test_async_endpoints(self) -> None: - app = Flask(__name__) - - @app.route('/') - @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)])) - async def hello_world(k) -> Response: - return jsonify(str(k)) - - @app.route('/foo/') - @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)]), return_as=ReturnAs.ARGS) - async def return_args(k) -> Response: - return jsonify(str(k)) - - @app.route('/bar/') - @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)]), - return_as=ReturnAs.KWARGS_WITH_NONE) - async def return_kwargs_with_none(k) -> Response: - return jsonify(str(k)) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - res = client.get(path=f'/45') - self.assertEqual(OK, res.status_code) - - res = client.get(path=f'/foo/45') - self.assertEqual(OK, res.status_code) - - res = client.get(path=f'/bar/45') - self.assertEqual(OK, res.status_code) - - res = client.get(path=f'/41') - self.assertEqual(INVALID, res.status_code) - expected = { - ExceptionDictKey.MESSAGE: 'smaller then allowed: 41 is not >= 42', - ExceptionDictKey.PARAMETER: 'k', - ExceptionDictKey.VALIDATOR: 'Min', - ExceptionDictKey.VALUE: '41', - } - self.assertEqual(expected, res.json) - - def test_json_parameter_with_default_value(self) -> None: - app = Flask(__name__) - - @app.route('/testing/message/', methods=['POST']) - @validate( - FlaskPathParameter(name='email', value_type=str, validators=[Email()]), - FlaskJsonParameter(name='content', value_type=str, default='this is a fake message', required=False), - ) - def testing_send_system_message(email: str, content: str) -> Response: - return jsonify({'email': email, 'content': content}) - - with app.test_client() as client: - res = client.post(path=f'/testing/message/adam@eva.de') - self.assertEqual(OK, res.status_code) - expected = { - 'email': 'adam@eva.de', - 'content': 'this is a fake message', - } - self.assertEqual(expected, res.json) - - res = client.post(path=f'/testing/message/adam@eva.de', json={'content': 'hello world'}) - self.assertEqual(OK, res.status_code) - expected = { - 'email': 'adam@eva.de', - 'content': 'hello world', - } - self.assertEqual(expected, res.json) - - def test_generic_deserializer(self) -> None: - @dataclass(frozen=True) - class User(Deserializable): - firstname: str - lastname: str - age: int - - @staticmethod - @overrides(Deserializable) - def from_json(data: Dict[str, Any]) -> 'User': - return User( - firstname=data['firstname'], - lastname=data['lastname'], - age=Min(value=18).validate_param(value=int(data['age']), parameter_name='age'), - ) - - app = Flask(__name__) - - @app.route('/foo', methods=['POST']) - @validate( - GenericFlaskDeserializer(name='user', cls=User, catch_exception=True), - ) - def the_generic_approach(user: User) -> Response: - return jsonify({'name': user.firstname}) - - @app.route('/bar', methods=['POST']) - @validate( - GenericFlaskDeserializer(name='user', cls=User, catch_exception=False), - ) - def do_not_catch(user: User) -> Response: - return jsonify({'name': user.firstname}) - - @app.errorhandler(ParameterException) - def handle_validation_error(exception: ParameterException) -> Response: - response = jsonify(exception.to_dict) - response.status_code = INVALID - return response - - with app.test_client() as client: - data = { - 'firstname': 'Albert', - 'lastname': 'Einstein', - 'age': '56', - } - res = client.post(path=f'/foo', json=data) - assert res.status_code == 200 - assert res.json == {'name': 'Albert'} - - data.pop('age') - res = client.post(path=f'/foo', json=data) - assert res.status_code == 422 - - res = client.post(path=f'/bar', json=data) - assert res.status_code == 500 - - data['age'] = '16' - res = client.post(path=f'/foo', json=data) - assert res.status_code == 422 - expected = { - ExceptionDictKey.MESSAGE: 'smaller then allowed: 16 is not >= 18', - ExceptionDictKey.PARAMETER: 'age', - ExceptionDictKey.VALIDATOR: 'Min', - ExceptionDictKey.VALUE: '16', - } - assert res.json == expected diff --git a/pedantic/tests/validate/test_parameter_environment_variable.py b/pedantic/tests/validate/test_parameter_environment_variable.py deleted file mode 100644 index a2404d84..00000000 --- a/pedantic/tests/validate/test_parameter_environment_variable.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import EnvironmentVariableParameter - - -class TestParameterEnvironmentVariable(TestCase): - def setUp(self) -> None: - if 'foo' in os.environ: - del os.environ['foo'] - - def test_parameter_environment_variable_str(self) -> None: - @validate(EnvironmentVariableParameter(name='foo', value_type=str)) - def bar(foo): - return foo - - os.environ['foo'] = '42' - self.assertEqual('42', bar()) - - def test_parameter_environment_variable_int(self) -> None: - @validate(EnvironmentVariableParameter(name='foo', value_type=int)) - def bar(foo): - return foo - - os.environ['foo'] = '42' - self.assertEqual(42, bar()) - - def test_parameter_environment_variable_float(self) -> None: - @validate(EnvironmentVariableParameter(name='foo', value_type=float)) - def bar(foo): - return foo - - os.environ['foo'] = '42.7' - self.assertEqual(42.7, bar()) - - def test_parameter_environment_variable_bool(self) -> None: - @validate(EnvironmentVariableParameter(name='foo', value_type=bool)) - def bar(foo): - return foo - - for value in ['true', 'True', 'TRUE']: - os.environ['foo'] = value - self.assertTrue(bar()) - - for value in ['false', 'False', 'FALSE']: - os.environ['foo'] = value - self.assertFalse(bar()) - - for value in ['invalid', 'frue', 'talse']: - os.environ['foo'] = value - - with self.assertRaises(expected_exception=ParameterException): - bar() - - def test_parameter_environment_variable_not_set(self) -> None: - @validate(EnvironmentVariableParameter(name='foo')) - def bar(foo): - return foo - - with self.assertRaises(expected_exception=ParameterException): - bar() - - def test_invalid_value_type(self) -> None: - with self.assertRaises(expected_exception=AssertionError): - @validate(EnvironmentVariableParameter(name='foo', value_type=dict)) - def bar(foo): - return foo - - def test_parameter_environment_variable_different_name(self) -> None: - @validate(EnvironmentVariableParameter(name='foo', env_var_name='fuu', value_type=str)) - def bar(foo): - return foo - - os.environ['fuu'] = '42' - self.assertEqual('42', bar()) - - def test_two_parameters(self) -> None: - @validate(EnvironmentVariableParameter(name='a'), strict=False) - def foo(a: float, b: int): - print(f'{a} and {b}') - - os.environ['a'] = '42' - foo(b=42) diff --git a/pedantic/tests/validate/test_validate.py b/pedantic/tests/validate/test_validate.py deleted file mode 100644 index da6b38f9..00000000 --- a/pedantic/tests/validate/test_validate.py +++ /dev/null @@ -1,428 +0,0 @@ -import os -from datetime import datetime -from typing import Optional, Any -from unittest import IsolatedAsyncioTestCase -from unittest import TestCase - -from pedantic import DateTimeUnixTimestamp -from pedantic.decorators.fn_deco_validate.exceptions import ValidateException, ParameterException, \ - ValidatorException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, ReturnAs -from pedantic.decorators.fn_deco_validate.parameters import Parameter, EnvironmentVariableParameter -from pedantic.decorators.fn_deco_validate.validators import MaxLength, Min, Max, Email, Validator - - -class TestValidate(TestCase): - def setUp(self) -> None: - if 'foo' in os.environ: - del os.environ['foo'] - - def test_single_validator(self) -> None: - validator = MaxLength(3) - converted_value = validator.validate(value='hed') - self.assertEqual(converted_value, 'hed') - - with self.assertRaises(expected_exception=ValidatorException) as ex: - validator.validate(value='hello world') - - expected_error_msg = 'MaxLength: hello world is too long with length 11. Value: hello world' - self.assertEqual(expected_error_msg, str(ex.exception)) - - def test_single_parameter(self) -> None: - parameter = Parameter(name='x', validators=[MaxLength(3)]) - converted_value = parameter.validate(value='hed') - self.assertEqual(converted_value, 'hed') - - with self.assertRaises(expected_exception=ParameterException): - parameter.validate(value='hello world') - - def test_multiple_parameters(self) -> None: - @validate( - Parameter(name='a', validators=[Min(3)]), - Parameter(name='b', validators=[Max(3)]), - Parameter(name='c', validators=[Max(43)]), - ) - def bar(a, b, c): - return a + b + c - - self.assertEqual(11, bar(3, 3, 5)) - self.assertEqual(11, bar(a=3, b=3, c=5)) - - def test_validate_args(self): - @validate( - Parameter(name='a', validators=[Min(42, include_boundary=False)]), - Parameter(name='b', validators=[Min(42, include_boundary=False)]), - Parameter(name='c', validators=[Min(42, include_boundary=False)]), - ) - def some_calculation(a, b, c): - return a + b + c - - some_calculation(43, 45, 50) - with self.assertRaises(expected_exception=ParameterException): - some_calculation(30, 40, 50) - with self.assertRaises(expected_exception=ParameterException): - some_calculation(c=30, a=40, b=50) - - def test_validate_instance_method(self): - class MyClass: - @validate( - Parameter(name='x', validators=[Min(1)]), - ) - def some_calculation(self, x: int) -> int: - return x - - @validate( - Parameter(name='x', validators=[Min(1)]), - return_as=ReturnAs.KWARGS_WITHOUT_NONE, - ) - def some_calculation_2(self, x: int) -> int: - return x - - m = MyClass() - m.some_calculation(1) - m.some_calculation(42) - - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(0) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(-42) - - m.some_calculation_2(1) - m.some_calculation_2(42) - - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation_2(0) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation_2(-42) - - def test_validate_static_method(self): - """ The @staticmethod decorator have to be ABOVE the @validate decorator. """ - - class MyClass: - @staticmethod - @validate( - Parameter(name='x', validators=[Min(1)]), - ) - def some_calculation(x: int) -> int: - return x - - m = MyClass() - m.some_calculation(1) - m.some_calculation(42) - - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(0) - with self.assertRaises(expected_exception=ParameterException): - m.some_calculation(-42) - - def test_less_parameter_than_arguments(self): - @validate( - Parameter(name='b'), - strict=False, - ) - def some_calculation(a, b, c): - return a + b + c - - some_calculation(43, 0, -50) - - with self.assertRaises(expected_exception=ValidateException): - some_calculation(30, None, 50) - - def test_empty_parameter_kwargs_with_none(self): - @validate( - Parameter(name='a', required=False), - Parameter(name='b', required=True), - Parameter(name='c', required=False), - return_as=ReturnAs.KWARGS_WITH_NONE - ) - def some_calculation(a, b, c): - return str(a) + str(b) + str(c) - - self.assertEqual('430-50', some_calculation(43, 0, -50)) - self.assertEqual('None0None', some_calculation(None, 0, None)) - - def test_empty_parameter_kwargs_without_none(self): - @validate( - Parameter(name='a', required=False), - Parameter(name='b', required=True), - Parameter(name='c', required=False), - return_as=ReturnAs.KWARGS_WITHOUT_NONE - ) - def some_calculation(a: Optional[int] = 1, b: Optional[int] = 2, c: Optional[int] = 3): - return str(a) + str(b) + str(c) - - self.assertEqual('430-50', some_calculation(43, 0, -50)) - self.assertEqual('103', some_calculation(None, 0, None)) - - def test_required(self): - @validate( - Parameter(name='a', required=True), - Parameter(name='b', required=True), - Parameter(name='c', required=True), - ) - def some_calculation(a, b, c): - return a + b + c - - some_calculation(43, 0, -50) - - with self.assertRaises(expected_exception=ValidateException): - some_calculation(30, None, 50) - - def test_call_with_args(self): - @validate( - Parameter(name='x', validators=[Min(1)]), - ) - def some_calculation(x: int) -> int: - return x - - some_calculation(42) - - def test_external_parameter_accepts_value_when_given(self) -> None: - @validate(EnvironmentVariableParameter(name='foo')) - def bar(foo): - return foo - - self.assertEqual('42', bar('42')) - self.assertEqual('42', bar(foo='42')) - - def test_external_parameter_ignores_value_when_given(self) -> None: - @validate(EnvironmentVariableParameter(name='foo'), ignore_input=True) - def bar(foo): - return foo - - os.environ['foo'] = '1' - - self.assertEqual('1', bar('42')) - self.assertEqual('1', bar(foo='42')) - - def test_external_parameter_mixed_with_normal_parameter(self) -> None: - @validate( - EnvironmentVariableParameter(name='foo'), - Parameter(name='footer'), - return_as=ReturnAs.KWARGS_WITHOUT_NONE, - ) - def bar(foo, footer): - return foo, footer - - self.assertEqual(('42', 3), bar('42', 3)) - - os.environ['foo'] = '42' - self.assertEqual(('42', 3), bar(footer=3)) - - def test_too_many_arguments(self) -> None: - @validate( - Parameter(name='x'), - ) - def bar(x): - return x - - self.assertEqual(42, bar(42)) - - with self.assertRaises(expected_exception=ValidateException): - bar(42, 43) - - def test_unexpected_parameter_strict(self) -> None: - @validate(Parameter(name='y')) - def bar(x): - return x - - with self.assertRaises(expected_exception=ValidateException): - bar(42) - with self.assertRaises(expected_exception=ValidateException): - bar(x=42) - - def test_unexpected_parameter_not_strict(self) -> None: - @validate(Parameter(name='y'), strict=False) - def bar(x): - return x - - with self.assertRaises(expected_exception=ValidateException): - self.assertEqual(42, bar(42)) - - with self.assertRaises(expected_exception=ValidateException): - self.assertEqual(42, bar(x=42)) - - def test_unexpected_parameter_not_strict_external(self) -> None: - @validate(EnvironmentVariableParameter(name='foo')) - def bar(x): - return x - - with self.assertRaises(expected_exception=ValidateException): - self.assertEqual(42, bar(42)) - - with self.assertRaises(expected_exception=ValidateException): - self.assertEqual(42, bar(x=42)) - - def test_return_as_simple(self) -> None: - @validate(Parameter(name='x'), return_as=ReturnAs.ARGS) - def bar(x): - return x - - self.assertEqual(42, bar(42)) - self.assertEqual(42, bar(x=42)) - - def test_return_as_args(self) -> None: - @validate(Parameter(name='x'), return_as=ReturnAs.ARGS) - def bar(*args, **kwargs): - return args, kwargs - - self.assertEqual(((42,), {}), bar(42)) - self.assertEqual(((42,), {}), bar(x=42)) - - def test_return_as_kwargs_with_none(self) -> None: - @validate(Parameter(name='x'), return_as=ReturnAs.KWARGS_WITH_NONE) - def bar(*args, **kwargs): - return args, kwargs - - self.assertEqual(((), {'x': 42}), bar(42)) - self.assertEqual(((), {'x': 42}), bar(x=42)) - - def test_return_as_kwargs_without_none(self) -> None: - @validate(Parameter(name='x'), return_as=ReturnAs.KWARGS_WITHOUT_NONE) - def bar(*args, **kwargs): - return args, kwargs - - self.assertEqual(((), {'x': 42}), bar(42)) - self.assertEqual(((), {'x': 42}), bar(x=42)) - - def test_return_as_args_advanced(self) -> None: - @validate( - Parameter(name='a'), - Parameter(name='b'), - Parameter(name='c'), - return_as=ReturnAs.ARGS, - ) - def bar(a, b, *args, **kwargs): - return a, b, args, kwargs - - bar(a=1, b=3, c=42) - bar(1, 3, 4) - bar(1, 3, c=4) - - def test_return_as_args_advanced_different_order(self) -> None: - @validate( - Parameter(name='c'), - Parameter(name='a'), - Parameter(name='b'), - return_as=ReturnAs.ARGS, - ) - def bar(a, b, *args, **kwargs): - return a, b, args, kwargs - - self.assertEqual((1, 3, (42,), {}), bar(a=1, b=3, c=42)) - self.assertEqual((1, 3, (42,), {}), bar(1, 3, 42)) - self.assertEqual((42, 1, (3,), {}), bar(1, 3, c=42)) - - def test_return_multiple_args(self) -> None: - @validate( - Parameter(name='c'), - Parameter(name='a'), - Parameter(name='b'), - return_as=ReturnAs.KWARGS_WITH_NONE, - ) - def bar(*args, **kwargs): - return args, kwargs - - self.assertEqual(((), {'a': 1, 'b': 3, 'c': 42}), bar(a=1, b=3, c=42)) - self.assertEqual(((), {'a': 3, 'b': 42, 'c': 1}), bar(1, 3, 42)) - self.assertEqual(((), {'a': 1, 'b': 3, 'c': 42}), bar(1, 3, c=42)) - - def test_none_is_not_validated_if_not_required_kwargs_with_none(self) -> None: - @validate(Parameter(name='a', validators=[Email()], required=False), return_as=ReturnAs.KWARGS_WITH_NONE) - def bar(a: Optional[str]): - return a - - self.assertIsNone(bar(a=None)) - self.assertIsNone(bar(None)) - - with self.assertRaises(expected_exception=ParameterException): - bar('no_email') - - def test_none_is_not_validated_if_not_required_kwargs_without_none(self) -> None: - @validate(Parameter(name='a', validators=[Email()], required=False), return_as=ReturnAs.KWARGS_WITHOUT_NONE) - def bar(a: Optional[str] = None): - return a - - self.assertIsNone(bar(a=None)) - self.assertIsNone(bar(None)) - - with self.assertRaises(expected_exception=ParameterException): - bar('no_email') - - def test_allow_renaming_of_parameter_of_custom_validator(self) -> None: - class MyCustomValidator(Validator): - def validate(self, i_renamed_this_arg: Any) -> Any: - return i_renamed_this_arg - - @validate(Parameter(name='a', validators=[MyCustomValidator()])) - def bar(a: int): - return a - - self.assertEqual(42, bar(42)) - self.assertEqual(42, bar(a=42)) - - def test_none_is_removed_for_not_required_parameter(self) -> None: - @validate(Parameter(name='a', required=False)) - def bar(a: int = 42): - return a - - self.assertEqual(42, bar()) - self.assertEqual(2, bar(a=2)) - self.assertEqual(2, bar(2)) - - def test_default_value_is_not_validated_internal_parameter(self) -> None: - t = datetime(year=2021, month=11, day=24) - unix_timestamp = (t - datetime(year=1970, month=1, day=1)).total_seconds() - - @validate(Parameter(name='a', required=False, default=t, validators=[DateTimeUnixTimestamp()])) - def bar(a: datetime) -> datetime: - return a - - self.assertEqual(t, bar(a=unix_timestamp)) - self.assertEqual(t, bar()) - - def test_no_default_value(self) -> None: - @validate(Parameter(name='a', required=False)) - def bar(a: datetime) -> datetime: - return a - - with self.assertRaises(expected_exception=ValidateException): - bar() - - def test_default_value_is_not_validated_external_parameter(self) -> None: - t = datetime(year=2021, month=11, day=24) - - if 'a' in os.environ: - del os.environ['a'] - - @validate(EnvironmentVariableParameter(name='a', default=t, validators=[DateTimeUnixTimestamp()], required=False)) - def bar(a: datetime) -> datetime: - return a - - self.assertEqual(t, bar()) - - -class AsyncValidateTests(IsolatedAsyncioTestCase): - async def test_async_instance_method(self) -> None: - class Foo: - @validate(Parameter(name='k', value_type=int, validators=[Min(42)]), - return_as=ReturnAs.KWARGS_WITHOUT_NONE) - async def bar(self, k): - return k - - @validate(Parameter(name='k', value_type=int, validators=[Min(42)]), return_as=ReturnAs.ARGS) - async def bar_2(self, k): - return k - - f = Foo() - res = await f.bar(k=42) - self.assertEqual(42, res) - - with self.assertRaises(expected_exception=ParameterException): - await f.bar(k=41) - - res = await f.bar_2(k=42) - self.assertEqual(42, res) - - with self.assertRaises(expected_exception=ParameterException): - await f.bar_2(k=41) diff --git a/pedantic/tests/validate/test_validator_composite.py b/pedantic/tests/validate/test_validator_composite.py deleted file mode 100644 index 7e5c0a6e..00000000 --- a/pedantic/tests/validate/test_validator_composite.py +++ /dev/null @@ -1,23 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import Composite, Max, Min - - -class TestValidatorComposite(TestCase): - def test_validator_composite(self) -> None: - @validate(Parameter(name='x', validators=[Composite([Min(3), Max(5)])])) - def foo(x): - return x - - self.assertEqual(3, foo(3)) - self.assertEqual(4, foo(4)) - self.assertEqual(5, foo(5)) - - with self.assertRaises(expected_exception=ParameterException): - foo(5.0001) - - with self.assertRaises(expected_exception=ParameterException): - foo(2.9999) diff --git a/pedantic/tests/validate/test_validator_datetime_unix_timestamp.py b/pedantic/tests/validate/test_validator_datetime_unix_timestamp.py deleted file mode 100644 index 82ba6f75..00000000 --- a/pedantic/tests/validate/test_validator_datetime_unix_timestamp.py +++ /dev/null @@ -1,31 +0,0 @@ -from datetime import datetime -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import DateTimeUnixTimestamp - - -class TestValidatorDatetimeUnixTimestamp(TestCase): - def test_validator_datetime_unix_timestamp(self) -> None: - @validate(Parameter(name='x', validators=[DateTimeUnixTimestamp()])) - def foo(x): - return x - - now = datetime.now() - unix_timestamp = (now - datetime(year=1970, month=1, day=1)).total_seconds() - self.assertEqual(now, foo(unix_timestamp)) - self.assertEqual(now, foo(str(unix_timestamp))) - - with self.assertRaises(expected_exception=ParameterException): - foo('12.12.2020') - - with self.assertRaises(expected_exception=ParameterException): - foo('invalid') - - with self.assertRaises(expected_exception=ParameterException): - foo({'a': 1}) - - with self.assertRaises(expected_exception=ParameterException): - foo(unix_timestamp * 1000) diff --git a/pedantic/tests/validate/test_validator_email.py b/pedantic/tests/validate/test_validator_email.py deleted file mode 100644 index 71bc22e0..00000000 --- a/pedantic/tests/validate/test_validator_email.py +++ /dev/null @@ -1,28 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import Email - - -class TestValidatorEmail(TestCase): - def test_validator_email(self) -> None: - @validate(Parameter(name='x', validators=[Email()])) - def foo(x): - return x - - for value in ['fred@web.de', 'genial@gmail.com', 'test@test.co.uk']: - self.assertEqual(value, foo(value)) - - for value in ['fred', 'fred@web', 'fred@w@eb.de', 'fred@@web.de', 'invalid@invalid']: - with self.assertRaises(expected_exception=ParameterException): - foo(value) - - def test_validator_email_converts_to_lower_case(self) -> None: - @validate(Parameter(name='x', validators=[Email(post_processor=lambda x: x.lower())])) - def foo(x): - return x - - for value in ['Fred@Web.de', 'GENIAL@GMAIL.com', 'test@test.CO.UK']: - self.assertEqual(value.lower(), foo(value)) diff --git a/pedantic/tests/validate/test_validator_for_each.py b/pedantic/tests/validate/test_validator_for_each.py deleted file mode 100644 index 80532add..00000000 --- a/pedantic/tests/validate/test_validator_for_each.py +++ /dev/null @@ -1,30 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import ForEach, Min, Max - - -class TestValidatorForEach(TestCase): - def test_validator_for_each_single_child(self) -> None: - @validate(Parameter(name='x', validators=[ForEach(Min(3))])) - def foo(x): - return x - - self.assertEqual([3, 4, 5], foo([3, 4, 5])) - - for value in [42, [3, 2, 5]]: - with self.assertRaises(expected_exception=ParameterException): - foo(value) - - def test_validator_for_each_multiple_children(self) -> None: - @validate(Parameter(name='x', validators=[ForEach([Min(3), Max(4)])])) - def foo(x): - return x - - self.assertEqual([3, 4], foo([3, 4])) - - for value in [42, [3, 2, 5]]: - with self.assertRaises(expected_exception=ParameterException): - foo(value) diff --git a/pedantic/tests/validate/test_validator_is_enum.py b/pedantic/tests/validate/test_validator_is_enum.py deleted file mode 100644 index e496b564..00000000 --- a/pedantic/tests/validate/test_validator_is_enum.py +++ /dev/null @@ -1,74 +0,0 @@ -from enum import Enum, IntEnum -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import IsEnum - - -class MyEnum(Enum): - RED = 'RED' - BLUE = 'BLUE' - - -class MyIntEnum(IntEnum): - RED = 1 - BLUE = 2 - - -class TestValidatorIsEnum(TestCase): - def test_validator_is_enum_convert_true(self) -> None: - @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=True)])) - def foo(x): - return x - - self.assertEqual(MyEnum.RED, foo('RED')) - self.assertEqual(MyEnum.BLUE, foo('BLUE')) - - for value in ['fred', 1, 'GREEN']: - with self.assertRaises(expected_exception=ParameterException): - foo(value) - - def test_validator_is_enum_int_enum_convert_true(self) -> None: - @validate(Parameter(name='x', validators=[IsEnum(MyIntEnum, convert=True)])) - def foo(x): - return x - - self.assertEqual(MyIntEnum.RED, foo('1')) - self.assertEqual(MyIntEnum.BLUE, foo('2')) - self.assertEqual(MyIntEnum.RED, foo(1)) - self.assertEqual(MyIntEnum.BLUE, foo(2)) - - for value in ['fred', 3, 'GREEN']: - with self.assertRaises(expected_exception=ParameterException): - foo(value) - - def test_validator_is_enum_convert_false(self) -> None: - @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) - def foo(x): - return x - - self.assertEqual('RED', foo('RED')) - self.assertEqual('BLUE', foo('BLUE')) - - for value in ['fred', 1, 'GREEN']: - with self.assertRaises(expected_exception=ParameterException): - foo(value) - - def test_validator_is_enum_to_upper_case(self) -> None: - @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) - def foo(x): - return x - - self.assertEqual('RED', foo('red')) - self.assertEqual('BLUE', foo('blue')) - self.assertEqual('BLUE', foo('bLUe')) - - def test_validator_is_enum_to_upper_case_disabled(self) -> None: - @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False, to_upper_case=False)])) - def foo(x): print(x) - - for value in ['red', 'blue', 'Red', 'bLUe']: - with self.assertRaises(expected_exception=ParameterException): - foo(value) diff --git a/pedantic/tests/validate/test_validator_is_uuid.py b/pedantic/tests/validate/test_validator_is_uuid.py deleted file mode 100644 index 2bc07118..00000000 --- a/pedantic/tests/validate/test_validator_is_uuid.py +++ /dev/null @@ -1,34 +0,0 @@ -from unittest import TestCase -from uuid import uuid1, uuid3, uuid4, uuid5 - -from pedantic import ForEach -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import IsUuid - - -class TestValidatorIsUUID(TestCase): - def test_validator_is_uuid(self): - @validate(Parameter(name='x', validators=[IsUuid()], required=False)) - def foo(x): - return x - - for id_ in [str(uuid1()), str(uuid3(uuid1(), 'b')), str(uuid4()), str(uuid5(uuid1(), 'b'))]: - self.assertEqual(id_, foo(id_)) - - for no_id in ['invalid', 12]: - with self.assertRaises(expected_exception=ParameterException): - foo(no_id) - - def test_validator_is_uuid_with_for_each_and_none_value(self): - @validate(Parameter(name='x', validators=[ForEach(IsUuid())])) - def foo(x): - return x - - uuid = str(uuid1()) - self.assertEqual([], foo([])) - self.assertEqual([uuid], foo([uuid])) - - with self.assertRaises(expected_exception=ParameterException): - foo([None]) diff --git a/pedantic/tests/validate/test_validator_match_pattern.py b/pedantic/tests/validate/test_validator_match_pattern.py deleted file mode 100644 index 1cc310d2..00000000 --- a/pedantic/tests/validate/test_validator_match_pattern.py +++ /dev/null @@ -1,22 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import MatchPattern - - -class TestValidatorMatchPattern(TestCase): - def test_validator_match_pattern(self) -> None: - pattern = r'^(([01]\d|2[0-3]):([0-5]\d)|24:00)$' - - @validate(Parameter(name='x', validators=[MatchPattern(pattern)])) - def foo(x): - return x - - for value in ['00:00', '02:45', '14:59', '23:59']: - self.assertEqual(value, foo(value)) - - for value in ['00:70', '24:00', '30:00', 'invalid']: - with self.assertRaises(expected_exception=ParameterException): - foo([3, 2, 5]) diff --git a/pedantic/tests/validate/test_validator_max.py b/pedantic/tests/validate/test_validator_max.py deleted file mode 100644 index 08f74b48..00000000 --- a/pedantic/tests/validate/test_validator_max.py +++ /dev/null @@ -1,36 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators.max import Max - - -class TestValidatorMax(TestCase): - def test_validator_max_length_include_boundary_true(self) -> None: - @validate(Parameter(name='x', validators=[Max(3, include_boundary=True)])) - def foo(x): - return x - - self.assertEqual(3, foo(3)) - self.assertEqual(2, foo(2)) - - with self.assertRaises(expected_exception=ParameterException): - foo(4) - - with self.assertRaises(expected_exception=ParameterException): - foo(3.001) - - def test_validator_max_length_include_boundary_false(self) -> None: - @validate(Parameter(name='x', validators=[Max(3, include_boundary=False)])) - def foo(x): - return x - - self.assertEqual(2.9999, foo(2.9999)) - self.assertEqual(2, foo(2)) - - with self.assertRaises(expected_exception=ParameterException): - foo(4) - - with self.assertRaises(expected_exception=ParameterException): - foo(3) diff --git a/pedantic/tests/validate/test_validator_max_length.py b/pedantic/tests/validate/test_validator_max_length.py deleted file mode 100644 index b56281cb..00000000 --- a/pedantic/tests/validate/test_validator_max_length.py +++ /dev/null @@ -1,32 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import MaxLength - - -class TestValidatorMaxLength(TestCase): - def test_validator_max_length(self) -> None: - @validate(Parameter(name='x', validators=[MaxLength(3)])) - def foo(x): - return x - - self.assertEqual('hi', foo('hi')) - self.assertEqual('hi!', foo('hi!')) - self.assertEqual([1, 2, 3], foo([1, 2, 3])) - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo('hi!!') - - assert ex.exception.message == 'hi!! is too long with length 4.' - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo([1, 2, 3, 4]) - - assert ex.exception.message == '[1, 2, 3, 4] is too long with length 4.' - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo(42) - - assert ex.exception.message == '42 has no length.' diff --git a/pedantic/tests/validate/test_validator_min.py b/pedantic/tests/validate/test_validator_min.py deleted file mode 100644 index a97d0f8b..00000000 --- a/pedantic/tests/validate/test_validator_min.py +++ /dev/null @@ -1,36 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import Min - - -class TestValidatorMin(TestCase): - def test_validator_min_length_include_boundary_true(self) -> None: - @validate(Parameter(name='x', validators=[Min(3, include_boundary=True)])) - def foo(x): - return x - - self.assertEqual(3, foo(3)) - self.assertEqual(4, foo(4)) - - with self.assertRaises(expected_exception=ParameterException): - foo(2) - - with self.assertRaises(expected_exception=ParameterException): - foo(2.9999) - - def test_validator_min_length_include_boundary_false(self) -> None: - @validate(Parameter(name='x', validators=[Min(3, include_boundary=False)])) - def foo(x): - return x - - self.assertEqual(3.0001, foo(3.0001)) - self.assertEqual(4, foo(4)) - - with self.assertRaises(expected_exception=ParameterException): - foo(2) - - with self.assertRaises(expected_exception=ParameterException): - foo(3) diff --git a/pedantic/tests/validate/test_validator_min_length.py b/pedantic/tests/validate/test_validator_min_length.py deleted file mode 100644 index d25f5e3c..00000000 --- a/pedantic/tests/validate/test_validator_min_length.py +++ /dev/null @@ -1,32 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import MinLength - - -class TestValidatorMinLength(TestCase): - def test_validator_min_length(self) -> None: - @validate(Parameter(name='x', validators=[MinLength(3)])) - def foo(x): - return x - - self.assertEqual('hi!', foo('hi!')) - self.assertEqual('hello', foo('hello')) - self.assertEqual([1, 2, 3], foo([1, 2, 3])) - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo('hi') - - assert ex.exception.message == 'hi is too short with length 2.' - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo([1, 2]) - - assert ex.exception.message == '[1, 2] is too short with length 2.' - - with self.assertRaises(expected_exception=ParameterException) as ex: - foo(42) - - assert ex.exception.message == '42 has no length.' diff --git a/pedantic/tests/validate/test_validator_not_empty.py b/pedantic/tests/validate/test_validator_not_empty.py deleted file mode 100644 index 0ba3d493..00000000 --- a/pedantic/tests/validate/test_validator_not_empty.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest import TestCase - -from pedantic.decorators.fn_deco_validate.exceptions import ParameterException -from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate -from pedantic.decorators.fn_deco_validate.parameters import Parameter -from pedantic.decorators.fn_deco_validate.validators import NotEmpty - - -class TestValidatorNotEmpty(TestCase): - def test_validator_not_empty(self) -> None: - @validate(Parameter(name='x', validators=[NotEmpty()])) - def foo(x): - return x - - self.assertEqual('hi', foo('hi')) - self.assertEqual('hi', foo(' hi ')) - self.assertEqual([1], foo([1])) - - for value in ['', ' ', [], {}, set()]: - with self.assertRaises(expected_exception=ParameterException): - foo(value) diff --git a/pedantic/type_checking_logic/check_generic_classes.py b/pedantic/type_checking_logic/check_generic_classes.py index e6ba3c17..d0904a6b 100644 --- a/pedantic/type_checking_logic/check_generic_classes.py +++ b/pedantic/type_checking_logic/check_generic_classes.py @@ -136,8 +136,3 @@ def _remove_comments_and_spaces_from_src_line(line: str) -> str: 'm=MyClass[Parent](a=Child1())' """ return line.split('#')[0].replace(' ', '') - - -if __name__ == '__main__': - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/pedantic/type_checking_logic/check_types.py b/pedantic/type_checking_logic/check_types.py index 4e2e1646..31f70540 100644 --- a/pedantic/type_checking_logic/check_types.py +++ b/pedantic/type_checking_logic/check_types.py @@ -1063,8 +1063,3 @@ def convert_to_typing_types(x: typing.Type) -> typing.Type: return x # new since Python 3.14 raise RuntimeError(x) - - -if __name__ == "__main__": - import doctest - doctest.testmod(verbose=False, optionflags=doctest.ELLIPSIS) diff --git a/poetry.lock b/poetry.lock index ceda8982..cb350437 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,12 +52,132 @@ description = "Cross-platform colored terminal text." optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] -markers = "extra == \"dev\" and platform_system == \"Windows\"" +markers = "extra == \"dev\" and (sys_platform == \"win32\" or platform_system == \"Windows\")" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.13.4" +description = "Code coverage measurement for Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "dill" version = "0.4.1" @@ -119,6 +239,19 @@ werkzeug = ">=3.1.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -281,6 +414,130 @@ files = [ [package.dependencies] dill = ">=0.4.1" +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\"" +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"dev\" and python_version < \"3.13\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + [[package]] name = "werkzeug" version = "3.1.5" @@ -301,9 +558,9 @@ markupsafe = ">=2.1.1" watchdog = ["watchdog (>=2.3)"] [extras] -dev = ["Flask", "Werkzeug", "docstring-parser", "multiprocess"] +dev = ["Flask", "Werkzeug", "docstring-parser", "multiprocess", "pytest", "pytest-asyncio", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "627e37896f3041972658be45847057888285034f7602690eb35b5b6d52506353" +content-hash = "e0f6b68dabc193cb34037f3420dec19fd1b2babea9d700de9471a810499546d8" diff --git a/pyproject.toml b/pyproject.toml index 5a53c7f6..6a29fcb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pedantic" -version = "2.3.3" +version = "2.4.0" description = "Some useful Python decorators for cleaner software development." readme = "README.md" requires-python = ">=3.11" @@ -31,6 +31,9 @@ classifiers = [ # pip install .[dev] dev = [ "docstring-parser==0.17", + "pytest==9.0.2", + "pytest-asyncio==1.3.0", + "pytest-cov==7.0.0", "Flask[async]==3.1.2", "multiprocess==0.70.19", "Werkzeug==3.1.5", @@ -38,3 +41,7 @@ dev = [ [tool.setuptools.packages.find] where = ["."] + +[tool.pytest.ini_options] +addopts = "--doctest-modules" +python_files = ["*.py"] diff --git a/pedantic/tests/validate/__init__.py b/tests/__init__.py similarity index 100% rename from pedantic/tests/validate/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f9c8b682 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import os + +import pytest + +from pedantic.env_var_logic import ENVIRONMENT_VARIABLE_NAME + + +@pytest.fixture(autouse=True) +def cleanup_env_vars() -> None: + for env_var in [ENVIRONMENT_VARIABLE_NAME, 'foo']: + if env_var in os.environ: + del os.environ[env_var] diff --git a/tests/decorators/__init__.py b/tests/decorators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/decorators/pedantic/__init__.py b/tests/decorators/pedantic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/decorators/pedantic/test_generator.py b/tests/decorators/pedantic/test_generator.py new file mode 100644 index 00000000..ed64717d --- /dev/null +++ b/tests/decorators/pedantic/test_generator.py @@ -0,0 +1,113 @@ +from typing import Generator, Iterator, Iterable, List + +import pytest + +from pedantic.exceptions import PedanticTypeCheckException +from pedantic.decorators.fn_deco_pedantic import pedantic + + +def test_iterator(): + @pedantic + def gen_func() -> Iterator[int]: + num = 0 + + while num < 100: + yield num + num += 1 + + gen = gen_func() + next(gen) + + +def test_iterator_wrong_type_hint(): + @pedantic + def genfunc() -> Iterator[float]: + num = 0 + + while num < 100: + yield num + num += 1 + + gen = genfunc() + with pytest.raises(expected_exception=PedanticTypeCheckException): + next(gen) + + +def test_iterator_no_type_args(): + @pedantic + def genfunc() -> Iterator: + num = 0 + + while num < 100: + yield num + num += 1 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + genfunc() + + +def test_iterator_completely_wrong_type_hint(): + @pedantic + def gen_func() -> List[int]: + num = 0 + + while num < 100: + yield num + num += 1 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + gen_func() + + +def test_iterable(): + @pedantic + def gen_func() -> Iterable[int]: + num = 0 + + while num < 100: + yield num + num += 1 + + gen = gen_func() + next(gen) + + +def test_iterable_no_type_args(): + @pedantic + def gen_func() -> Iterable: + num = 0 + + while num < 100: + yield num + num += 1 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + gen_func() + + +def test_generator(): + @pedantic + def gen_func() -> Generator[int, None, str]: + num = 0 + + while num < 100: + yield num + num += 1 + return 'Done' + + gen = gen_func() + next(gen) + + +def test_invalid_no_type_args_generator(): + @pedantic + def gen_func() -> Generator: + num = 0 + + while num < 100: + yield num + num += 1 + return 'Done' + + with pytest.raises(expected_exception=PedanticTypeCheckException): + gen_func() diff --git a/tests/decorators/pedantic/test_generic_classes.py b/tests/decorators/pedantic/test_generic_classes.py new file mode 100644 index 00000000..f487d8c3 --- /dev/null +++ b/tests/decorators/pedantic/test_generic_classes.py @@ -0,0 +1,315 @@ +from typing import Generic, TypeVar, Any, List, Optional, Union + +import pytest + +from pedantic.constants import TYPE_VAR_METHOD_NAME, TYPE_VAR_SELF +from pedantic.decorators.class_decorators import pedantic_class +from pedantic.exceptions import PedanticTypeVarMismatchException + +T = TypeVar('T') + + +def test_pedantic_generic_class(): + @pedantic_class + class LoggedVar(Generic[T]): + def __init__(self, value: T, name: str, logger: Any) -> None: + self.name = name + self.logger = logger + self.value = value + + def set(self, new: T) -> None: + self.log(message='Set ' + repr(self.value)) + self.value = new + + def get(self) -> T: + self.log(message='Get ' + repr(self.value)) + return self.value + + def log(self, message: str) -> None: + self.logger = self.name + message + + o = LoggedVar[int](value=42, name='hi', logger='test') + o.set(new=57) + assert isinstance(o.get(), int) + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + o.set(new=3.14) + + +def test_stack(): + @pedantic_class + class Stack(Generic[T]): + def __init__(self) -> None: + self.items: List[T] = [] + + def push(self, item: T) -> None: + self.items.append(item) + + def pop(self) -> T: + return self.items.pop() + + def empty(self) -> bool: + return not self.items + + def top(self) -> Optional[T]: + if len(self.items) > 0: + return self.items[len(self.items) - 1] + else: + return None + + my_stack = Stack[str]() + get_type_vars = getattr(my_stack, TYPE_VAR_METHOD_NAME) + assert get_type_vars() == {T: str, TYPE_VAR_SELF: Stack} + with pytest.raises(expected_exception=IndexError): + my_stack.pop() + assert my_stack.top() is None + assert my_stack.top() is None + assert T in get_type_vars() + my_stack.push(item='hi') + assert T in get_type_vars() + my_stack.push(item='world') + assert T in get_type_vars() + assert my_stack.pop() == 'world' + assert my_stack.pop() == 'hi' + assert my_stack.top() is None + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + my_stack.push(item=42) + + my_other_stack = Stack[int]() + get_type_vars = getattr(my_other_stack, TYPE_VAR_METHOD_NAME) + assert get_type_vars() == {T: int, TYPE_VAR_SELF: Stack} + + with pytest.raises(expected_exception=IndexError): + my_other_stack.pop() + + assert my_other_stack.top() is None + assert my_other_stack.top() is None + my_other_stack.push(item=100) + assert get_type_vars() == {T: int, TYPE_VAR_SELF: Stack} + my_other_stack.push(item=142) + assert get_type_vars() == {T: int, TYPE_VAR_SELF: Stack} + assert my_other_stack.pop() == 142 + assert my_other_stack.pop() == 100 + assert my_other_stack.top() is None + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + my_other_stack.push(item='42') + + +def test_generic_class_initialised_without_generics(): + @pedantic_class + class MyClass(Generic[T]): + def __init__(self, a: T) -> None: + self.a = a + + def get_a(self) -> T: + return self.a + + def set_a(self, val: T) -> None: + self.a = val + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + _ = MyClass(a=42) # for some reason the exception is only raised when I assign it to a variable + + +def test_generic_class_initialised_without_generics_2(): + @pedantic_class + class MyClass(Generic[T]): + def __init__(self, a: T) -> None: + self.a = a + + def get_a(self) -> T: + return self.a + + def set_a(self, val: T) -> None: + self.a = val + + MyClass(a=42) # it is not recognized if it isn't assigned + + +def test_generic_class_inheritance(): + class Parent: + pass + + class Child1(Parent): + pass + + class Child2(Parent): + pass + + @pedantic_class + class MyClass(Generic[T]): + def __init__(self, a: T) -> None: + self.a = a + + def get_a(self) -> T: + return self.a + + def set_a(self, val: T) -> None: + self.a = val + + m = MyClass[Parent](a=Child1()) + assert isinstance(m.get_a(), Child1) + assert not isinstance(m.get_a(), Child2) + m.set_a(val=Child2()) + assert isinstance(m.get_a(), Child2) + assert not isinstance(m.get_a(), Child1) + + +def test_merge_dicts(): + def create(): + @pedantic_class + class MyClass(Generic[T]): + def __init__(self, a: T) -> None: + self.a = a + + def get_a(self) -> T: + return self.a + + def set_a(self, val: T) -> None: + self.a = val + return MyClass(a=42) + a = create() + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + a.set_a(val='hi') + + +def test_recursion_depth_exceeded(): + @pedantic_class + class Stack(Generic[T]): + def __init__(self) -> None: + self.items: List[T] = [] + + def len(self) -> int: + return len(self.items) + + def push(self, item: T) -> None: + self.items.append(item) + + def pop(self) -> T: + if len(self.items) > 0: + return self.items.pop() + else: + raise ValueError() + + def empty(self) -> bool: + return not self.items + + def top(self) -> Optional[T]: + if len(self.items) > 0: + return self.items[len(self.items) - 1] + else: + return None + + def __len__(self) -> int: + return len(self.items) + + def create_stack(): + stack = Stack[int]() + return stack + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + stack: Stack[int] = Stack() + stack.empty() + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + stack = Stack() + stack.empty() + + stack = create_stack() + assert stack.empty() + + +def test_generic_union(): + @pedantic_class + class Stack(Generic[T]): + def __init__(self) -> None: + self.items: List[T] = [] + + def len(self) -> int: + return len(self.items) + + def push(self, item: T) -> None: + self.items.append(item) + + def pop(self) -> T: + if len(self.items) > 0: + return self.items.pop() + else: + raise ValueError() + + def empty(self) -> bool: + return not self.items + + def top(self) -> Optional[T]: + if len(self.items) > 0: + return self.items[len(self.items) - 1] + else: + return None + + def __len__(self) -> int: + return len(self.items) + + s = Stack[Union[int, float, str]]() + s.push(item=42) + s.push(item='hello') + s.push(item=3.1415) + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + s.push(item=[1, 2]) + + +def test_inheritance(): + @pedantic_class + class Stack(Generic[T]): + def __init__(self) -> None: + self.items: List[T] = [] + + def len(self) -> int: + return len(self.items) + + def push(self, item: T) -> None: + self.items.append(item) + + def pop(self) -> T: + if len(self.items) > 0: + return self.items.pop() + else: + raise ValueError() + + def empty(self) -> bool: + return not self.items + + def top(self) -> Optional[T]: + if len(self.items) > 0: + return self.items[len(self.items) - 1] + else: + return None + + def __len__(self) -> int: + return len(self.items) + + @pedantic_class + class Parent: + pass + + @pedantic_class + class Child1(Parent): + pass + + @pedantic_class + class Child2(Parent): + pass + + parent_stack = Stack[Parent]() + parent_stack.push(item=Child1()) + parent_stack.push(item=Child2()) + parent_stack.push(item=Parent()) + + child_1_stack = Stack[Child1]() + child_1_stack.push(item=Child1()) + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + child_1_stack.push(item=Child2()) + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + child_1_stack.push(item=Parent()) diff --git a/tests/decorators/pedantic/test_pedantic.py b/tests/decorators/pedantic/test_pedantic.py new file mode 100644 index 00000000..273cfde7 --- /dev/null +++ b/tests/decorators/pedantic/test_pedantic.py @@ -0,0 +1,2819 @@ +import os.path +import types +import typing +from dataclasses import dataclass +from datetime import datetime, date +from functools import wraps, partial +from io import BytesIO, StringIO +from typing import List, Tuple, Callable, Any, Optional, Union, Dict, Set, FrozenSet, NewType, TypeVar, Sequence, \ + AbstractSet, Iterator, NamedTuple, Collection, Type, Generator, Generic, BinaryIO, TextIO, Iterable, Container, \ + NoReturn, ClassVar, Literal +from enum import Enum, IntEnum + +import pytest + +from pedantic import pedantic_class +from pedantic.exceptions import PedanticTypeCheckException, PedanticException, PedanticCallWithArgsException, \ + PedanticTypeVarMismatchException +from pedantic.decorators.fn_deco_pedantic import pedantic + +TEST_FILE = 'test.txt' + + +class Parent: + pass + + +class Child(Parent): + def method(self, a: int): + pass + + +def test_no_kwargs(): + @pedantic + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + calc(42, 40, 38) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + calc(42, m=40, i=38) + calc(n=42, m=40, i=38) + + +def test_nested_type_hints_1(): + @pedantic + def calc(n: int) -> List[List[float]]: + return [0.0 * n] + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42) + + +def test_nested_type_hints_1_corrected(): + @pedantic + def calc(n: int) -> List[List[float]]: + return [[0.0 * n]] + + calc(n=42) + + +def test_nested_type_hints_2(): + """Problem here: int != float""" + @pedantic + def calc(n: int) -> List[Tuple[float, str]]: + return [(n, str(n))] + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42) + + +def test_nested_type_hints_2_corrected(): + @pedantic + def calc(n: int) -> List[Tuple[int, str]]: + return [(n, str(n))] + + @pedantic + def calc_2(n: float) -> List[Tuple[float, str]]: + return [(n, str(n))] + + calc(n=42) + calc_2(n=42.0) + + +def test_nested_type_hints_3(): + """Problem here: inner function actually returns Tuple[int, str]""" + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: + @pedantic + def f(x: int, y: float) -> Tuple[float, str]: + return n * x, str(y) + return f + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42)(x=3, y=3.14) + + +def test_nested_type_hints_3_corrected(): + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[int, str]]: + @pedantic + def f(x: int, y: float) -> Tuple[int, str]: + return n * x, str(y) + + return f + + calc(n=42)(x=3, y=3.14) + + +def test_nested_type_hints_4(): + """Problem here: return type is actually float""" + @pedantic + def calc(n: List[List[float]]) -> int: + return n[0][0] + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=[[42.0]]) + + +def test_nested_type_hints_corrected(): + @pedantic + def calc(n: List[List[float]]) -> int: + return int(n[0][0]) + + calc(n=[[42.0]]) + + +def test_nested_type_hints_5(): + """Problem here: Tuple[float, str] != Tuple[float, float]""" + + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: + @pedantic + def f(x: int, y: float) -> Tuple[float, float]: + return n * float(x), y + return f + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42) + + +def test_nested_type_hints_5_corrected(): + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[float, float]]: + @pedantic + def f(x: int, y: float) -> Tuple[float, float]: + return n * float(x), y + return f + + calc(n=42) + + +def test_missing_type_hint_1(): + """Problem here: type hint for n missed""" + @pedantic + def calc(n) -> float: + return 42.0 * n + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42) + + +def test_missing_type_hint_1_corrected(): + @pedantic + def calc(n: int) -> float: + return 42.0 * n + + calc(n=42) + + +def test_missing_type_hint_2(): + """Problem here: Return type annotation missed""" + @pedantic + def calc(n: int): + return 'Hi' + str(n) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42) + + +def test_missing_type_hint_2_corrected(): + @pedantic + def calc(n: int) -> str: + return 'Hi' + str(n) + + calc(n=42) + + +def test_missing_type_hint_3(): + """Problem here: type hint for i missed""" + @pedantic + def calc(n: int, m: int, i) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=38) + + +def test_missing_type_hint_3_corrected(): + @pedantic + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + calc(n=42, m=40, i=38) + + +def test_all_ok_2(): + @pedantic + def calc(n: int, m: int, i: int) -> str: + return str(n + m + i) + + calc(n=42, m=40, i=38) + + +def test_all_ok_3(): + @pedantic + def calc(n: int, m: int, i: int) -> None: + str(n + m + i) + + calc(n=42, m=40, i=38) + + +def test_all_ok_4(): + @pedantic + def calc(n: int) -> List[List[int]]: + return [[n]] + + calc(n=42) + + +def test_all_ok_5(): + @pedantic + def calc(n: int) -> List[Tuple[float, str]]: + return [(float(n), str(n))] + + calc(n=42) + + +def test_all_ok_6(): + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: + @pedantic + def f(x: int, y: float) -> Tuple[float, str]: + return n * float(x), str(y) + return f + + calc(n=42)(x=72, y=3.14) + + +def test_all_ok_7(): + @pedantic + def calc(n: List[List[float]]) -> Any: + return n[0][0] + + calc(n=[[42.0]]) + + +def test_all_ok_8(): + @pedantic + def calc(n: int) -> Callable[[int, float], Tuple[float, str]]: + @pedantic + def f(x: int, y: float) -> Tuple[float, str]: + return n * float(x), str(y) + + return f + + calc(n=42)(x=3, y=3.14) + + +def test_wrong_type_hint_1(): + """Problem here: str != int""" + @pedantic + def calc(n: int, m: int, i: int) -> str: + return n + m + i + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_1_corrected(): + @pedantic + def calc(n: int, m: int, i: int) -> str: + return str(n + m + i) + + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_2(): + """Problem here: str != int""" + @pedantic + def calc(n: int, m: int, i: str) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_2_corrected(): + @pedantic + def calc(n: int, m: int, i: str) -> int: + return n + m + int(i) + + calc(n=42, m=40, i='38') + + +def test_wrong_type_hint_3(): + """Problem here: None != int""" + @pedantic + def calc(n: int, m: int, i: int) -> None: + return n + m + i + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_corrected(): + @pedantic + def calc(n: int, m: int, i: int) -> None: + print(n + m + i) + + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_4(): + """Problem here: None != int""" + @pedantic + def calc(n: int, m: int, i: int) -> int: + print(n + m + i) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=38) + + +def test_wrong_type_hint_4_corrected(): + @pedantic + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + calc(n=42, m=40, i=38) + + +def test_none_1(): + """Problem here: None is not accepted""" + @pedantic + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=None) + + +def test_none_2(): + @pedantic + def calc(n: int, m: int, i: Optional[int]) -> int: + return n + m + i if i is not None else n + m + + calc(n=42, m=40, i=None) + + +def test_none_3(): + @pedantic + def calc(n: int, m: int, i: Union[int, None]) -> int: + return n + m + i if i is not None else n + m + + calc(n=42, m=40, i=None) + + +def test_none_4(): + """Problem here: function may return None""" + @pedantic + def calc(n: int, m: int, i: Union[int, None]) -> int: + return n + m + i if i is not None else None + + calc(n=42, m=40, i=42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(n=42, m=40, i=None) + + +def test_none_5(): + @pedantic + def calc(n: int, m: int, i: Union[int, None]) -> Optional[int]: + return n + m + i if i is not None else None + + calc(n=42, m=40, i=None) + + +def test_inheritance_1(): + class MyClassA: + pass + + class MyClassB(MyClassA): + pass + + @pedantic + def calc(a: MyClassA) -> str: + return str(a) + + calc(a=MyClassA()) + calc(a=MyClassB()) + + +def test_inheritance_2(): + """Problem here: A is not a subtype of B""" + class MyClassA: + pass + + class MyClassB(MyClassA): + pass + + @pedantic + def calc(a: MyClassB) -> str: + return str(a) + + calc(a=MyClassB()) + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(a=MyClassA()) + + +def test_instance_method_1(): + class MyClassA: + @pedantic + def calc(self, i: int) -> str: + return str(i) + + a = MyClassA() + a.calc(i=42) + + +def test_instance_method_2(): + """Problem here: 'i' has no type annotation""" + class MyClassA: + @pedantic + def calc(self, i) -> str: + return str(i) + + a = MyClassA() + with pytest.raises(expected_exception=PedanticTypeCheckException): + a.calc(i=42) + + +def test_instance_method_2_corrected(): + class MyClassA: + @pedantic + def calc(self, i: int) -> str: + return str(i) + + a = MyClassA() + a.calc(i=42) + + +def test_instance_method_int_is_not_float(): + class MyClassA: + @pedantic + def calc(self, i: float) -> str: + return str(i) + + a = MyClassA() + with pytest.raises(expected_exception=PedanticTypeCheckException): + a.calc(i=42) + + +def test_instance_method_3_corrected(): + class MyClassA: + @pedantic + def calc(self, i: float) -> str: + return str(i) + + a = MyClassA() + a.calc(i=42.0) + + +def test_instance_method_no_kwargs(): + class MyClassA: + @pedantic + def calc(self, i: int) -> str: + return str(i) + + a = MyClassA() + with pytest.raises(expected_exception=PedanticCallWithArgsException): + a.calc(42) + + +def test_instance_method_5(): + """Problem here: instance methods is not called with kwargs""" + class MyClassA: + @pedantic + def calc(self, i: int) -> str: + return str(i) + + a = MyClassA() + a.calc(i=42) + + +def test_lambda_1(): + @pedantic + def calc(i: float) -> Callable[[float], str]: + return lambda x: str(x * i) + + calc(i=42.0)(10.0) + + +def test_lambda_3(): + @pedantic + def calc(i: float) -> Callable[[float], str]: + def res(x: float) -> str: + return str(x * i) + return res + + calc(i=42.0)(10.0) + + +def test_lambda_int_is_not_float(): + @pedantic + def calc(i: float) -> Callable[[float], str]: + def res(x: int) -> str: + return str(x * i) + return res + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=42.0)(x=10) + + +def test_lambda_4_almost_corrected(): + """Problem here: float != str""" + @pedantic + def calc(i: float) -> Callable[[float], str]: + @pedantic + def res(x: int) -> str: + return str(x * i) + return res + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=42.0)(x=10) + + +def test_lambda_4_almost_corrected_2(): + @pedantic + def calc(i: float) -> Callable[[int], str]: + @pedantic + def res(x: int) -> str: + return str(x * i) + return res + + calc(i=42.0)(x=10) + + +def test_lambda_5(): + """Problem here: float != int""" + @pedantic + def calc(i: float) -> Callable[[float], str]: + @pedantic + def res(x: float) -> str: + return str(x * i) + return res + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=42.0)(x=10) + + +def test_lambda_corrected(): + @pedantic + def calc(i: float) -> Callable[[float], str]: + @pedantic + def res(x: float) -> str: + return str(x * i) + + return res + + calc(i=42.0)(x=10.0) + + +def test_tuple_without_type_args(): + @pedantic + def calc(i: Tuple) -> str: + return str(i) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=(42.0, 43, 'hi')) + + +def test_tuple_without_args_corrected(): + @pedantic + def calc(i: Tuple[Any, ...]) -> str: + return str(i) + + calc(i=(42.0, 43, 'hi')) + + +def test_callable_without_type_args(): + @pedantic + def calc(i: Callable) -> str: + return str(i(' you')) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=lambda x: (42.0, 43, 'hi', x)) + + +def test_callable_without_args_correct_with_lambdas(): + @pedantic + def calc(i: Callable[[Any], Tuple[Any, ...]]) -> str: + return str(i(x=' you')) + + calc(i=lambda x: (42.0, 43, 'hi', x)) + + +def test_callable_without_args_corrected(): + @pedantic + def calc(i: Callable[[Any], Tuple[Any, ...]]) -> str: + return str(i(x=' you')) + + @pedantic + def arg(x: Any) -> Tuple[Any, ...]: + return 42.0, 43, 'hi', x + calc(i=arg) + + +def test_list_without_args(): + @pedantic + def calc(i: List) -> Any: + return [i] + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(i=[42.0, 43, 'hi']) + + +def test_list_without_args_corrected(): + @pedantic + def calc(i: List[Any]) -> List[List[Any]]: + return [i] + + calc(i=[42.0, 43, 'hi']) + + +def test_ellipsis_in_callable_1(): + @pedantic + def calc(i: Callable[..., int]) -> int: + return i() + + @pedantic + def call() -> int: + return 42 + + calc(i=call) + + +def test_ellipsis_in_callable_2(): + @pedantic + def calc(i: Callable[..., int]) -> int: + return i(x=3.14, y=5) + + @pedantic + def call(x: float, y: int) -> int: + return 42 + + calc(i=call) + + +def test_ellipsis_in_callable_3(): + """Problem here: call to "call" misses one argument""" + @pedantic + def calc(i: Callable[..., int]) -> int: + return i(x=3.14) + + @pedantic + def call(x: float, y: int) -> int: + return 42 + + with pytest.raises(expected_exception=PedanticException): + calc(i=call) + + +def test_optional_args_1(): + @pedantic + def calc(a: int, b: int = 42) -> int: + return a + b + + calc(a=2) + + +def test_optional_args_2(): + @pedantic + def calc(a: int = 3, b: int = 42, c: float = 5.0) -> float: + return a + b + c + + calc() + calc(a=1) + calc(b=1) + calc(c=1.0) + calc(a=1, b=1) + calc(a=1, c=1.0) + calc(b=1, c=1.0) + calc(a=1, b=1, c=1.0) + + +def test_optional_args_3(): + """Problem here: optional argument c: 5 is not a float""" + @pedantic + def calc(a: int = 3, b: int = 42, c: float = 5) -> float: + return a + b + c + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc() + + +def test_optional_args_3_corrected(): + @pedantic + def calc(a: int = 3, b: int = 42, c: float = 5.0) -> float: + return a + b + c + + calc() + + +def test_optional_args_4(): + class MyClass: + @pedantic + def foo(self, a: int, b: Optional[int] = 1) -> int: + return a + b + + my_class = MyClass() + my_class.foo(a=10) + + +def test_optional_args_5(): + @pedantic + def calc(d: Optional[Dict[int, int]] = None) -> Optional[int]: + if d is None: + return None + return sum(d.keys()) + + calc(d=None) + calc() + calc(d={42: 3}) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(d={42: 3.14}) + + +def test_optional_args_6(): + """"Problem here: str != int""" + @pedantic + def calc(d: int = 42) -> int: + return int(d) + + calc(d=99999) + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(d='999999') + + +def test_enum_1(): + """Problem here: Type hint for 'a' should be MyEnum instead of MyEnum.GAMMA""" + class MyEnum(Enum): + ALPHA = 'startEvent' + BETA = 'task' + GAMMA = 'sequenceFlow' + + class MyClass: + @pedantic + def operation(self, a: MyEnum.GAMMA) -> None: + print(a) + + m = MyClass() + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.operation(a=MyEnum.GAMMA) + + +def test_enum_1_corrected(): + class MyEnum(Enum): + ALPHA = 'startEvent' + BETA = 'task' + GAMMA = 'sequenceFlow' + + @pedantic + def operation(a: MyEnum) -> None: + print(a) + + operation(a=MyEnum.GAMMA) + + +def test_sloppy_types_dict(): + @pedantic + def operation(d: dict) -> int: + return len(d.keys()) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d={1: 1, 2: 2}) + + +def test_sloppy_types_dict_almost_corrected_no_type_args(): + @pedantic + def operation(d: Dict) -> int: + return len(d.keys()) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d={1: 1, 2: 2}) + + +def test_sloppy_types_dict_corrected(): + @pedantic + def operation(d: Dict[int, int]) -> int: + return len(d.keys()) + + operation(d={1: 1, 2: 2}) + + +def test_sloppy_types_list(): + @pedantic + def operation(d: list) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=[1, 2, 3, 4]) + + +def test_sloppy_types_list_almost_corrected_no_type_args(): + @pedantic + def operation(d: List) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=[1, 2, 3, 4]) + + +def test_sloppy_types_list_corrected(): + @pedantic + def operation(d: List[int]) -> int: + return len(d) + + operation(d=[1, 2, 3, 4]) + + +def test_sloppy_types_tuple(): + @pedantic + def operation(d: tuple) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=(1, 2, 3)) + + +def test_sloppy_types_tuple_almost_corrected_no_type_args(): + @pedantic + def operation(d: Tuple) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=(1, 2, 3)) + + +def test_sloppy_types_tuple_corrected(): + @pedantic + def operation(d: Tuple[int, int, int]) -> int: + return len(d) + + operation(d=(1, 2, 3)) + + +def test_sloppy_types_set(): + @pedantic + def operation(d: set) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d={1, 2, 3}) + + +def test_sloppy_types_set_almost_corrected_to_type_args(): + @pedantic + def operation(d: Set) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d={1, 2, 3}) + + +def test_sloppy_types_set_corrected(): + @pedantic + def operation(d: Set[int]) -> int: + return len(d) + + operation(d={1, 2, 3}) + + +def test_sloppy_types_frozenset(): + @pedantic + def operation(d: frozenset) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=frozenset({1, 2, 3})) + + +def test_sloppy_types_frozenset_almost_corrected_no_type_args(): + @pedantic + def operation(d: FrozenSet) -> int: + return len(d) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + operation(d=frozenset({1, 2, 3})) + + +def test_sloppy_types_frozenset_corrected(): + @pedantic + def operation(d: FrozenSet[int]) -> int: + return len(d) + + operation(d=frozenset({1, 2, 3})) + + +def test_type_list_but_got_tuple(): + @pedantic + def calc(ls: List[Any]) -> int: + return len(ls) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + calc(ls=(1, 2, 3)) + + +def test_type_list_corrected(): + @pedantic + def calc(ls: Tuple[Any, ...]) -> int: + return len(ls) + + calc(ls=(1, 2, 3)) + + +def test_any(): + @pedantic + def calc(ls: List[Any]) -> Dict[int, Any]: + return {i: ls[i] for i in range(0, len(ls))} + + calc(ls=[1, 2, 3]) + calc(ls=[1.11, 2.0, 3.0]) + calc(ls=['1', '2', '3']) + calc(ls=[10.5, '2', (3, 4, 5)]) + + +def test_aliases(): + Vector = List[float] + + @pedantic + def scale(scalar: float, vector: Vector) -> Vector: + return [scalar * num for num in vector] + + scale(scalar=2.0, vector=[1.0, -4.2, 5.4]) + + +def test_new_type(): + UserId = NewType('UserId', int) + + @pedantic + def get_user_name(user_id: UserId) -> str: + return str(user_id) + + some_id = UserId(524313) + get_user_name(user_id=some_id) + + # the following would be desirable but impossible to check at runtime: + # with pytest.raises(expected_exception=AssertionError): + # get_user_name(user_id=-1) + + +def test_list_of_new_type(): + UserId = NewType('UserId', int) + + @pedantic + def get_user_name(user_ids: List[UserId]) -> str: + return str(user_ids) + + get_user_name(user_ids=[UserId(524313), UserId(42)]) + with pytest.raises(expected_exception=PedanticTypeCheckException): + get_user_name(user_ids=[UserId(524313), UserId(42), 430.0]) + + +def test_callable_no_args(): + @pedantic + def f(g: Callable[[], str]) -> str: + return g() + + @pedantic + def greetings() -> str: + return 'hello world' + + f(g=greetings) + + +def test_type_var(): + T = TypeVar('T') + + @pedantic + def first(ls: List[T]) -> T: + return ls[0] + + first(ls=[1, 2, 3]) + + +def test_type_var_wrong(): + T = TypeVar('T') + + @pedantic + def first(ls: List[T]) -> T: + return str(ls[0]) + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + first(ls=[1, 2, 3]) + + +def test_type_var_wrong_sequence(): + T = TypeVar('T') + + @pedantic + def first(ls: Sequence[T]) -> T: + return str(ls[0]) + + with pytest.raises(expected_exception=PedanticTypeVarMismatchException): + first(ls=[1, 2, 3]) + + +def test_double_pedantic(): + @pedantic + @pedantic + def f(x: int, y: float) -> Tuple[float, str]: + return float(x), str(y) + + f(x=5, y=3.14) + with pytest.raises(expected_exception=PedanticTypeCheckException): + f(x=5.0, y=3.14) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + f(5, 3.14) + + +def test_args_kwargs(): + @pedantic + def some_method(a: int = 0, b: float = 0.0) -> float: + return a * b + + @pedantic + def wrapper_method(*args: Union[int, float], **kwargs: Union[int, float]) -> float: + return some_method(*args, **kwargs) + + some_method() + with pytest.raises(expected_exception=PedanticCallWithArgsException): + some_method(3, 3.0) + some_method(a=3, b=3.0) + wrapper_method() + with pytest.raises(expected_exception=PedanticCallWithArgsException): + wrapper_method(3, 3.0) + wrapper_method(a=3, b=3.0) + + +def test_args_kwargs_no_type_hint(): + @pedantic + def method_no_type_hint(*args, **kwargs) -> None: + print(args) + print(kwargs) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + method_no_type_hint(a=3, b=3.0) + with pytest.raises(expected_exception=PedanticTypeCheckException): + method_no_type_hint() + + +def test_args_kwargs_wrong_type_hint(): + """See: https://www.python.org/dev/peps/pep-0484/#arbitrary-argument-lists-and-default-argument-values""" + @pedantic + def wrapper_method(*args: str, **kwargs: str) -> None: + print(args) + print(kwargs) + + wrapper_method() + wrapper_method('hi', 'you', ':)') + wrapper_method(a='hi', b='you', c=':)') + with pytest.raises(expected_exception=PedanticTypeCheckException): + wrapper_method('hi', 'you', ':)', 7) + with pytest.raises(expected_exception=PedanticTypeCheckException): + wrapper_method(3, 3.0) + with pytest.raises(expected_exception=PedanticTypeCheckException): + wrapper_method(a=3, b=3.0) + + +def test_additional_kwargs(): + @pedantic + def some_method(a: int, b: float = 0.0, **kwargs: int) -> float: + return sum([a, b]) + + some_method(a=5) + some_method(a=5, b=0.1) + some_method(a=5, b=0.1, c=4) + some_method(a=5, b=0.1, c=4, d=5, e=6) + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_method(a=5, b=0.1, c=4, d=5.0, e=6) + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_method(a=5.0, b=0.1, c=4, d=5, e=6) + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_method(a=5, b=0, c=4, d=5, e=6) + + +def test_args_kwargs_different_types(): + @pedantic + def foo(*args: str, **kwds: int) -> None: + print(args) + print(kwds) + + foo('a', 'b', 'c') + foo(x=1, y=2) + foo('', z=0) + + +def test_pedantic_on_class(): + with pytest.raises(expected_exception=PedanticTypeCheckException): + @pedantic + class MyClass: + pass + MyClass() + + +def test_is_subtype_tuple(): + with pytest.raises(expected_exception=PedanticTypeCheckException): + @pedantic + def foo() -> Callable[[Tuple[float, str]], Tuple[int]]: + def bar(a: Tuple[float]) -> Tuple[int]: + return len(a[1]) + int(a[0]), + return bar + foo() + + +def test_is_subtype_tuple_corrected(): + @pedantic + def foo() -> Callable[[Tuple[float, str]], Tuple[int]]: + def bar(a: Tuple[float, str]) -> Tuple[int]: + return len(a[1]) + int(a[0]), + return bar + foo() + + +def test_forward_ref(): + class Conversation: + pass + + @pedantic + def get_conversations() -> List['Conversation']: + return [Conversation(), Conversation()] + + get_conversations() + + +def test_alternative_list_type_hint(): + @pedantic + def _is_digit_in_int(digit: [int], num: int) -> bool: + num_str = str(num) + for i in num_str: + if int(i) == digit: + return True + return False + + with pytest.raises(expected_exception=PedanticTypeCheckException): + _is_digit_in_int(digit=4, num=42) + + +def test_callable_with_union_return(): + class MyClass: + pass + + @pedantic + def admin_required(func: Callable[..., Union[str, MyClass]]) -> Callable[..., Union[str, MyClass]]: + @wraps(func) + def decorated_function(*args, **kwargs): + return func(*args, **kwargs) + return decorated_function + + @admin_required + @pedantic + def get_server_info() -> str: + return 'info' + + get_server_info() + + +def test_pedantic(): + @pedantic + def foo(a: int, b: str) -> str: + return 'abc' + + assert foo(a=4, b='abc') == 'abc' + + +def test_pedantic_always(): + @pedantic + def foo(a: int, b: str) -> str: + return 'abc' + + assert foo(a=4, b='abc') == 'abc' + + +def test_pedantic_arguments_fail(): + @pedantic + def foo(a: int, b: str) -> str: + return 'abc' + + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo(a=4, b=5) + + +def test_pedantic_return_type_fail(): + @pedantic + def foo(a: int, b: str) -> str: + return 6 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo(a=4, b='abc') + + +def test_return_type_none(): + @pedantic + def foo() -> None: + return 'a' + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo() + + +def test_marco(): + @pedantic_class + class A: + def __init__(self, val: int) -> None: + self.val = val + + def __eq__(self, other: 'A') -> bool: # other: A and all subclasses + return self.val == other.val + + @pedantic_class + class B(A): + def __init__(self, val: int) -> None: + super().__init__(val=val) + + @pedantic_class + class C(A): + def __init__(self, val: int) -> None: + super().__init__(val=val) + + a = A(val=42) + b = B(val=42) + c = C(val=42) + + assert a == b # works + assert a == c # works + assert b == c # error + + +def test_date_datetime(): + @pedantic + def foo(a: datetime, b: date) -> None: + pass + + foo(a=datetime(1995, 2, 5), b=date(1987, 8, 7)) + foo(a=datetime(1995, 2, 5), b=datetime(1987, 8, 7)) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo(a=date(1995, 2, 5), b=date(1987, 8, 7)) + + +def test_any_type(): + @pedantic + def foo(a: Any) -> None: + pass + + foo(a='aa') + + +def test_callable_exact_arg_count(): + @pedantic + def foo(a: Callable[[int, str], int]) -> None: + pass + + def some_callable(x: int, y: str) -> int: + pass + + foo(a=some_callable) + + +def test_callable_bad_type(): + @pedantic + def foo(a: Callable[..., int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_callable_too_few_arguments(): + @pedantic + def foo(a: Callable[[int, str], int]) -> None: + pass + + def some_callable(x: int) -> int: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=some_callable) + + +def test_callable_mandatory_kwonlyargs(): + @pedantic + def foo(a: Callable[[int, str], int]) -> None: + pass + + def some_callable(x: int, y: str, *, z: float, bar: str) -> int: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=some_callable) + + +def test_callable_class(): + """ + Test that passing a class as a callable does not count the "self" argument "a"gainst the + ones declared in the Callable specification. + + """ + @pedantic + def foo(a: Callable[[int, str], Any]) -> None: + pass + + class SomeClass: + def __init__(self, x: int, y: str): + pass + + foo(a=SomeClass) + + +def test_callable_plain(): + @pedantic + def foo(a: Callable[..., Any]) -> None: + pass + + def callback(a): + pass + + foo(a=callback) + + +def test_callable_bound_method(): + @pedantic + def foo(callback: Callable[[int], Any]) -> None: + pass + + foo(callback=Child().method) + + +def test_callable_defaults(): + """ + Test that a callable having "too many" arguments don't raise an error if the extra + arguments have default values. + + """ + @pedantic + def foo(callback: Callable[[int, str], Any]) -> None: + pass + + def some_callable(x: int, y: str, z: float = 1.2) -> int: + pass + + foo(callback=some_callable) + + +def test_callable_builtin(): + @pedantic + def foo(callback: types.BuiltinFunctionType) -> None: + pass + + foo(callback=[].append) + + +def test_dict_bad_type(): + @pedantic + def foo(a: Dict[str, int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_dict_bad_key_type(): + @pedantic + def foo(a: Dict[str, int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a={1: 2}) + + +def test_dict_bad_value_type(): + @pedantic + def foo(a: Dict[str, int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a={'x': 'a'}) + + +def test_list_bad_type(): + @pedantic + def foo(a: List[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_list_bad_element(): + @pedantic + def foo(a: List[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=[1, 2, 'bb']) + + +def test_sequence_bad_type(): + @pedantic + def foo(a: Sequence[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_sequence_bad_element(): + @pedantic + def foo(a: Sequence[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=[1, 2, 'bb']) + + +def test_abstractset_custom_type(): + T = TypeVar('T') + + @pedantic_class + class DummySet(AbstractSet[T]): + def __contains__(self, x: object) -> bool: + return x == 1 + + def __len__(self) -> T: + return 1 + + def __iter__(self) -> Iterator[T]: + yield 1 + + @pedantic + def foo(a: AbstractSet[int]) -> None: + pass + + foo(a=DummySet[int]()) + + +def test_abstractset_bad_type(): + @pedantic + def foo(a: AbstractSet[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_set_bad_type(): + @pedantic + def foo(a: Set[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_abstractset_bad_element(): + @pedantic + def foo(a: AbstractSet[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a={1, 2, 'bb'}) + + +def test_set_bad_element(): + @pedantic + def foo(a: Set[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a={1, 2, 'bb'}) + + +def test_tuple_bad_type(): + @pedantic + def foo(a: Tuple[int]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=5) + + +def test_tuple_too_many_elements(): + @pedantic + def foo(a: Tuple[int, str]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=(1, 'aa', 2)) + + +def test_tuple_too_few_elements(): + @pedantic + def foo(a: Tuple[int, str]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=(1,)) + + +def test_tuple_bad_element(): + @pedantic + def foo(a: Tuple[int, str]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=(1, 2)) + + +def test_tuple_ellipsis_bad_element(): + @pedantic + def foo(a: Tuple[int, ...]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=(1, 2, 'blah')) + + +def test_namedtuple(): + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + + @pedantic + def foo(bar: Employee) -> None: + print(bar) + + foo(bar=Employee('bob', 1)) + + +def test_namedtuple_key_mismatch(): + Employee1 = NamedTuple('Employee', [('name', str), ('id', int)]) + Employee2 = NamedTuple('Employee', [('firstname', str), ('id', int)]) + + @pedantic + def foo(bar: Employee1) -> None: + print(bar) + + with pytest.raises(PedanticTypeCheckException): + foo(bar=Employee2('bob', 1)) + + +def test_namedtuple_type_mismatch(): + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + + @pedantic + def foo(bar: Employee) -> None: + print(bar) + + with pytest.raises(PedanticTypeCheckException): + foo(bar=('bob', 1)) + + +def test_namedtuple_huge_type_mismatch(): + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + + @pedantic + def foo(bar: int) -> None: + print(bar) + + with pytest.raises(PedanticTypeCheckException): + foo(bar=foo(bar=Employee('bob', 1))) + + +def test_namedtuple_wrong_field_type(): + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + + @pedantic + def foo(bar: Employee) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(bar=Employee(2, 1)) + + +def test_union(): + @pedantic + def foo(a: Union[str, int]) -> None: + pass + + for value in [6, 'xa']: + foo(a=value) + + +def test_union_new_syntax(): + @pedantic + def foo(a: str | int) -> None: + pass + + for value in [6, 'xa']: + foo(a=value) + + with pytest.raises(PedanticTypeCheckException): + foo(a=1.7) + + +def test_union_typing_type(): + @pedantic + def foo(a: Union[str, Collection]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=1) + + +def test_union_fail(): + @pedantic + def foo(a: Union[str, int]) -> None: + pass + + for value in [5.6, b'xa']: + with pytest.raises(PedanticTypeCheckException): + foo(a=value) + + +def test_type_var_constraints(): + T = TypeVar('T', int, str) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + for values in [ + {'a': 6, 'b': 7}, + {'a': 'aa', 'b': "bb"}, + ]: + foo(**values) + + +def test_type_var_constraints_fail_typing_type(): + T = TypeVar('T', int, Collection) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a='aa', b='bb') + + +def test_typevar_constraints_fail(): + T = TypeVar('T', int, str) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=2.5, b='aa') + + +def test_typevar_bound(): + T = TypeVar('T', bound=Parent) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + foo(a=Child(), b=Child()) + + +def test_type_var_bound_fail(): + T = TypeVar('T', bound=Child) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=Parent(), b=Parent()) + + +def test_type_var_invariant_fail(): + T = TypeVar('T', int, str) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=2, b=3.6) + + +def test_type_var_covariant(): + T = TypeVar('T', covariant=True) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + foo(a=Parent(), b=Child()) + + +def test_type_var_covariant_fail(): + T = TypeVar('T', covariant=True) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeVarMismatchException): + foo(a=Child(), b=Parent()) + + +def test_type_var_contravariant(): + T = TypeVar('T', contravariant=True) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + foo(a=Child(), b=Parent()) + + +def test_type_var_contravariant_fail(): + T = TypeVar('T', contravariant=True) + + @pedantic + def foo(a: T, b: T) -> None: + pass + + with pytest.raises(PedanticTypeVarMismatchException): + foo(a=Parent(), b=Child()) + + +def test_class_bad_subclass(): + @pedantic + def foo(a: Type[Child]) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=Parent) + + +def test_class_any(): + @pedantic + def foo(a: Type[Any]) -> None: + pass + + foo(a=str) + + +def test_wrapped_function(): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + @pedantic + @decorator + def foo(a: 'Child') -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=Parent()) + + +def test_mismatching_default_type(): + @pedantic + def foo(a: str = 1) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo() + + +def test_implicit_default_none(): + """ + Test that if the default value is ``None``, a ``None`` argument can be passed. + + """ + @pedantic + def foo(a: Optional[str] = None) -> None: + pass + + foo() + + +def test_generator_simple(): + """Test that argument type checking works in a generator function too.""" + @pedantic + def generate(a: int) -> Generator[int, int, None]: + yield a + yield a + 1 + + gen = generate(a=1) + next(gen) + + +def test_wrapped_generator_no_return_type_annotation(): + """Test that return type checking works in a generator function too.""" + @pedantic + def generate(a: int) -> Generator[int, int, None]: + yield a + yield a + 1 + + gen = generate(a=1) + next(gen) + + +def test_varargs(): + @pedantic + def foo(*args: int) -> None: + pass + + foo(1, 2) + + +def test_varargs_fail(): + @pedantic + def foo(*args: int) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(1, 'a') + + +def test_kwargs(): + @pedantic + def foo(**kwargs: int) -> None: + pass + + foo(a=1, b=2) + + +def test_kwargs_fail(): + @pedantic + def foo(**kwargs: int) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=1, b='a') + + +def test_generic(): + T_Foo = TypeVar('T_Foo') + + class FooGeneric(Generic[T_Foo]): + pass + + @pedantic + def foo(a: FooGeneric[str]) -> None: + print(a) + + foo(a=FooGeneric[str]()) + + +def test_newtype(): + myint = NewType("myint", int) + + @pedantic + def foo(a: myint) -> int: + return 42 + + assert foo(a=1) == 42 + + with pytest.raises(PedanticTypeCheckException): + foo(a="a") + + +def test_collection(): + @pedantic + def foo(a: Collection) -> None: + pass + + with pytest.raises(PedanticTypeCheckException): + foo(a=True) + + +def test_binary_io(): + @pedantic + def foo(a: BinaryIO) -> None: + print(a) + + foo(a=BytesIO()) + + +def test_text_io(): + @pedantic + def foo(a: TextIO) -> None: + print(a) + + foo(a=StringIO()) + + +def test_binary_io_fail(): + @pedantic + def foo(a: TextIO) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo(a=BytesIO()) + + +def test_text_io_fail(): + @pedantic + def foo(a: BinaryIO) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo(a=StringIO()) + + +def test_binary_io_real_file(): + @pedantic + def foo(a: BinaryIO) -> None: + print(a) + + with open(file=TEST_FILE, mode='wb') as f: + foo(a=f) + + os.remove(TEST_FILE) + + +def test_text_io_real_file(): + @pedantic + def foo(a: TextIO) -> None: + print(a) + + with open(file=TEST_FILE, mode='w') as f: + foo(a=f) + + os.remove(TEST_FILE) + + +def test_pedantic_return_type_var_fail(): + T = TypeVar('T', int, float) + + @pedantic + def foo(a: T, b: T) -> T: + return 'a' + + with pytest.raises(PedanticTypeCheckException): + foo(a=4, b=2) + + +def test_callable(): + @pedantic + def foo_1(a: Callable[..., int]) -> None: + print(a) + + @pedantic + def foo_2(a: Callable) -> None: + print(a) + + def some_callable() -> int: + return 4 + + foo_1(a=some_callable) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a=some_callable) + + +def test_list(): + @pedantic + def foo_1(a: List[int]) -> None: + print(a) + + @pedantic + def foo_2(a: List) -> None: + print(a) + + @pedantic + def foo_3(a: list) -> None: + print(a) + + @pedantic + def foo_4(a: list[int]) -> None: + print(a) + + foo_1(a=[1, 2]) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a=[1, 2]) + + with pytest.raises(PedanticTypeCheckException): + foo_3(a=[1, 2]) + + foo_4(a=[1, 2]) + + +def test_dict(): + @pedantic + def foo_1(a: Dict[str, int]) -> None: + print(a) + + @pedantic + def foo_2(a: Dict) -> None: + print(a) + + @pedantic + def foo_3(a: dict) -> None: + print(a) + + @pedantic + def foo_4(a: dict[str, int]) -> None: + print(a) + + foo_1(a={'x': 2}) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a={'x': 2}) + + with pytest.raises(PedanticTypeCheckException): + foo_3(a={'x': 2}) + + foo_4(a={'x': 2}) + + +def test_sequence(): + @pedantic + def foo(a: Sequence[str]) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + foo(a=value) + + +def test_sequence_no_type_args(): + @pedantic + def foo(a: Sequence) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + with pytest.raises(PedanticTypeCheckException): + foo(a=value) + + +def test_iterable(): + @pedantic + def foo(a: Iterable[str]) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + foo(a=value) + + +def test_iterable_no_type_args(): + @pedantic + def foo(a: Iterable) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + with pytest.raises(PedanticTypeCheckException): + foo(a=value) + + +def test_container(): + @pedantic + def foo(a: Container[str]) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + foo(a=value) + + +def test_container_no_type_args(): + @pedantic + def foo(a: Container) -> None: + print(a) + + for value in [('a', 'b'), ['a', 'b'], 'abc']: + with pytest.raises(PedanticTypeCheckException): + foo(a=value) + + +def test_set(): + @pedantic + def foo_1(a: AbstractSet[int]) -> None: + print(a) + + @pedantic + def foo_2(a: Set[int]) -> None: + print(a) + + for value in [set(), {6}]: + foo_1(a=value) + foo_2(a=value) + + +def test_set_no_type_args(): + @pedantic + def foo_1(a: AbstractSet) -> None: + print(a) + + @pedantic + def foo_2(a: Set) -> None: + print(a) + + @pedantic + def foo_3(a: set) -> None: + print(a) + + for value in [set(), {6}]: + with pytest.raises(PedanticTypeCheckException): + foo_1(a=value) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a=value) + + with pytest.raises(PedanticTypeCheckException): + foo_3(a=value) + + +def test_tuple(): + @pedantic + def foo_1(a: Tuple[int, int]) -> None: + print(a) + + @pedantic + def foo_2(a: Tuple[int, ...]) -> None: + print(a) + + foo_1(a=(1, 2)) + foo_2(a=(1, 2)) + + +def test_tuple_no_type_args(): + @pedantic + def foo_1(a: Tuple) -> None: + print(a) + + @pedantic + def foo_2(a: tuple) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo_1(a=(1, 2)) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a=(1, 2)) + + +def test_empty_tuple(): + @pedantic + def foo(a: Tuple[()]) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo(a=()) + + +def test_class(): + @pedantic + def foo_1(a: Type[Parent]) -> None: + print(a) + + @pedantic + def foo_2(a: Type[TypeVar('UnboundType')]) -> None: + print(a) + + @pedantic + def foo_3(a: Type[TypeVar('BoundType', bound=Parent)]) -> None: + print(a) + + foo_1(a=Child) + foo_2(a=Child) + foo_3(a=Child) + + +def test_class_no_type_vars(): + @pedantic + def foo_1(a: Type) -> None: + print(a) + + @pedantic + def foo_2(a: type) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo_1(a=Child) + + with pytest.raises(PedanticTypeCheckException): + foo_2(a=Child) + + +def test_class_not_a_class(): + @pedantic + def foo(a: Type[Parent]) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo(a=1) + + +def test_complex(): + @pedantic + def foo(a: complex) -> None: + print(a) + + foo(a=complex(1, 5)) + + with pytest.raises(PedanticTypeCheckException): + foo(a=1.0) + + +def test_float(): + @pedantic + def foo(a: float) -> None: + print(a) + + foo(a=1.5) + + with pytest.raises(PedanticTypeCheckException): + foo(a=1) + + +def test_coroutine_correct_return_type(): + @pedantic + async def foo() -> str: + return 'foo' + + coro = foo() + + with pytest.raises(StopIteration): + coro.send(None) + + +def test_coroutine_wrong_return_type(): + @pedantic + async def foo() -> str: + return 1 + + coro = foo() + + with pytest.raises(PedanticTypeCheckException): + coro.send(None) + + +def test_bytearray_bytes(): + @pedantic + def foo(x: bytearray) -> None: + pass + + foo(x=bytearray([1])) + + +def test_class_decorator(): + @pedantic_class + class Foo: + @staticmethod + def staticmethod() -> int: + return 'foo' + + @classmethod + def classmethod(cls) -> int: + return 'foo' + + def method(self) -> int: + return 'foo' + + with pytest.raises(PedanticTypeCheckException): + Foo.staticmethod() + + with pytest.raises(PedanticTypeCheckException): + Foo.classmethod() + + with pytest.raises(PedanticTypeCheckException): + Foo().method() + + +def test_generator(): + @pedantic + def genfunc() -> Generator[int, str, List[str]]: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + gen = genfunc() + + with pytest.raises(StopIteration): + value = next(gen) + while True: + value = gen.send(str(value)) + assert isinstance(value, int) + + +def test_generator_no_type_args(): + @pedantic + def genfunc() -> Generator: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + with pytest.raises(PedanticTypeCheckException): + genfunc() + + +def test_iterator(): + @pedantic + def genfunc() -> Iterator[int]: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + gen = genfunc() + + with pytest.raises(PedanticTypeCheckException): + value = next(gen) + while True: + value = gen.send(str(value)) + assert isinstance(value, int) + + +def test_iterator_no_type_args(): + @pedantic + def genfunc() -> Iterator: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + with pytest.raises(PedanticTypeCheckException): + genfunc() + + +def test_iterable_advanced(): + @pedantic + def genfunc() -> Iterable[int]: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + gen = genfunc() + + with pytest.raises(PedanticTypeCheckException): + value = next(gen) + while True: + value = gen.send(str(value)) + assert isinstance(value, int) + + +def test_iterable_advanced_no_type_args(): + @pedantic + def genfunc() -> Iterable: + val1 = yield 2 + val2 = yield 3 + val3 = yield 4 + return [val1, val2, val3] + + with pytest.raises(PedanticTypeCheckException): + genfunc() + + +def test_generator_bad_yield(): + @pedantic + def genfunc_1() -> Generator[int, str, None]: + yield 'foo' + + @pedantic + def genfunc_2() -> Iterable[int]: + yield 'foo' + + @pedantic + def genfunc_3() -> Iterator[int]: + yield 'foo' + + gen = genfunc_1() + + with pytest.raises(PedanticTypeCheckException): + next(gen) + + gen = genfunc_2() + + with pytest.raises(PedanticTypeCheckException): + next(gen) + + gen = genfunc_3() + + with pytest.raises(PedanticTypeCheckException): + next(gen) + + +def test_generator_bad_send(): + @pedantic + def genfunc() -> Generator[int, str, None]: + yield 1 + yield 2 + + gen = genfunc() + next(gen) + + with pytest.raises(PedanticTypeCheckException): + gen.send(2) + + +def test_generator_bad_return(): + @pedantic + def genfunc() -> Generator[int, str, str]: + yield 1 + return 6 + + gen = genfunc() + next(gen) + + with pytest.raises(PedanticTypeCheckException): + gen.send('foo') + + +def test_return_generator(): + @pedantic + def genfunc() -> Generator[int, None, None]: + yield 1 + + @pedantic + def foo() -> Generator[int, None, None]: + return genfunc() + + foo() + + +def test_local_class(): + @pedantic_class + class LocalClass: + class Inner: + pass + + def create_inner(self) -> 'Inner': + return self.Inner() + + retval = LocalClass().create_inner() + assert isinstance(retval, LocalClass.Inner) + + +def test_local_class_async(): + @pedantic_class + class LocalClass: + class Inner: + pass + + async def create_inner(self) -> 'Inner': + return self.Inner() + + coro = LocalClass().create_inner() + + with pytest.raises(StopIteration): + coro.send(None) + + +def test_callable_nonmember(): + class CallableClass: + def __call__(self): + pass + + @pedantic_class + class LocalClass: + some_callable = CallableClass() + + +def test_inherited_class_method(): + @pedantic_class + class Parent: + @classmethod + def foo(cls, x: str) -> str: + return cls.__name__ + + @pedantic_class + class Child(Parent): + pass + + assert Child.foo(x='bar') == 'Parent' + + with pytest.raises(PedanticTypeCheckException): + Child.foo(x=1) + + +def test_type_var_forward_ref_bound(): + TBound = TypeVar('TBound', bound='Parent') + + @pedantic + def func(x: TBound) -> None: + pass + + func(x=Parent()) + + with pytest.raises(PedanticTypeCheckException): + func(x='foo') + + +def test_noreturn(): + @pedantic + def foo() -> NoReturn: + pass + + @pedantic + def bar() -> NoReturn: + raise ZeroDivisionError('bar') + + with pytest.raises(expected_exception=ZeroDivisionError): + bar() + + with pytest.raises(PedanticTypeCheckException): + foo() + + +def test_literal(): + @pedantic + def foo(a: Literal[1, True, 'x', b'y', 404]) -> None: + print(a) + + foo(a=404) + foo(a=True) + foo(a='x') + + with pytest.raises(PedanticTypeCheckException): + foo(a=4) + + +def test_literal_union(): + @pedantic + def foo(a: Union[str, Literal[1, 6, 8]]) -> None: + print(a) + + foo(a=6) + + with pytest.raises(PedanticTypeCheckException): + foo(a=4) + + +def test_literal_illegal_value(): + @pedantic + def foo(a: Literal[1, 1.1]) -> None: + print(a) + + with pytest.raises(PedanticTypeCheckException): + foo(a=4) + + +def test_enum(): + with pytest.raises(PedanticTypeCheckException): + @pedantic_class + class MyEnum(Enum): + A = 'a' + + +def test_enum_aggregate(): + T = TypeVar('T', bound=IntEnum) + + @pedantic_class + class EnumAggregate(Generic[T]): + enum: ClassVar[Type[T]] + + def __init__(self, value: Union[int, str, List[T]]) -> None: + assert len(self.enum) < 10 + + if value == '': + raise ValueError(f'Parameter "value" cannot be empty!') + + if isinstance(value, list): + self._value = ''.join([str(x.value) for x in value]) + else: + self._value = str(value) + + self._value = ''.join(sorted(self._value)) # sort characters in string + self.to_list() # check if is valid + + def __contains__(self, item: T) -> bool: + return item in self.to_list() + + def __eq__(self, other: Union['EnumAggregate', str]) -> bool: + if isinstance(other, str): + return self._value == other + + return self._value == other._value + + def __str__(self) -> str: + return self._value + + def to_list(self) -> List[T]: + return [self.enum(int(character)) for character in self._value] + + @property + def value(self) -> str: + return self._value + + @classmethod + def all(cls) -> str: + return ''.join([str(x.value) for x in cls.enum]) + + class Gender(IntEnum): + MALE = 1 + FEMALE = 2 + DIVERS = 3 + + @pedantic_class + class Genders(EnumAggregate[Gender]): + enum = Gender + + Genders(value=12) + + with pytest.raises(PedanticTypeCheckException): + Genders(value=Child()) + + +def test_primitive_list_dict_tuple(): + @pedantic + def f(x: list[dict[int, tuple[float, str]]]) -> list[Any]: + return x + + f(x=[{3: (3.24, 'hi')}]) + + for value in [ + [{3, (3, 'hi')}], + [{3: (3, 'hi')}], + [{3: (3.24, 3)}], + [{3: (3.24, 'hi')}, {0}], + ]: + with pytest.raises(PedanticTypeCheckException): + f(x=value) + + +def test_dataclass_protocol(): + class IsDataclass(typing.Protocol): + __dataclass_fields__: ClassVar[Dict] + + @dataclass + class Foo: + v: int + + @pedantic + def foo(x: IsDataclass) -> IsDataclass: + return x + + foo(x=Foo(v=42)) + + +def test_dataclass_protocol_in_type(): + class IsDataclass(typing.Protocol): + __dataclass_fields__: ClassVar[Dict] + + @dataclass + class Foo: + v: int + + @pedantic + def foo(x: type[IsDataclass]) -> IsDataclass: + return x + + assert foo(x=Foo) == Foo + + +def test_dataclass_protocol_in_type_with_union(): + class IsDataclass(typing.Protocol): + __dataclass_fields__: ClassVar[Dict] + + @dataclass + class Foo: + v: int + + @pedantic + def foo(x: type[None | bool | IsDataclass]) -> IsDataclass: + return x + + assert foo(x=Foo) == Foo + + +def test_partial_function(): + @pedantic + def f(a: int, b: int) -> int: + return a + b + + g = pedantic(partial(f, a=1)) + + assert f(a=2, b=3) == 5 + assert g(b=3) == 4 + + with pytest.raises(PedanticTypeCheckException): + f(a='2', b=3) + + with pytest.raises(PedanticTypeCheckException): + g(b='2') + + +def test_typing_never(): + from typing import Never + + @pedantic + def never_call_me(arg: Never) -> None: + pass + + @pedantic + def foo() -> Never: + pass + + @pedantic + def bar() -> Never: + raise ZeroDivisionError('bar') + + with pytest.raises(expected_exception=ZeroDivisionError): + bar() + + with pytest.raises(PedanticTypeCheckException): + foo() + + with pytest.raises(expected_exception=PedanticTypeCheckException) as exc: + never_call_me(arg='42') + + +def test_literal_string(): + from typing import LiteralString + + @pedantic + def foo(s: LiteralString) -> None: + pass + + foo(s='Hi') + foo(s=2 * 'Hi') + + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo(s=3) + + +def test_self_type(): + from typing import Self + + class Bar: + pass + + @pedantic_class + class Foo: + def f(self) -> Self: + return self + + @staticmethod + def g() -> Self: + return Foo() + + @classmethod + def h(cls) -> Self: + return cls() + + def f_2(self) -> Self: + return Bar() + + @staticmethod + def g_2() -> Self: + return Bar() + + @classmethod + def h_2(cls) -> Self: + return Bar() + + f = Foo() + assert f.f() == f + f.g() + f.h() + Foo.g() + Foo.h() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + f.f_2() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + f.g_2() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + f.h_2() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo.g_2() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo.h_2() + + +def test_using_self_type_annotation_outside_class(): + from typing import Self + + @pedantic + def f() -> Self: + return 'hi' + + with pytest.raises(expected_exception=PedanticTypeCheckException): + f() + + +def test_type_var_tuple(): + from typing import TypeVarTuple, Generic + + Ts = TypeVarTuple('Ts') + + @pedantic_class + class Array(Generic[*Ts]): + def __init__(self, *args: *Ts) -> None: + self._values = args + + @pedantic + def add_dimension(a: Array[*Ts], value: int) -> Array[int, *Ts]: + return Array[int, *Ts](value, *a._values) + + array = Array[int, float](42, 3.4) + array_2 = Array[bool, int, float, str](True, 4, 3.4, 'hi') + extended_array = add_dimension(a=array, value=42) + assert extended_array._values == (42, 42, 3.4) + + # this is too complicated at the moment + # with pytest.raises(expected_exception=PedanticTypeCheckException): + # Array[int, float](4.2, 3.4) diff --git a/tests/decorators/pedantic/test_pedantic_async.py b/tests/decorators/pedantic/test_pedantic_async.py new file mode 100644 index 00000000..aa9b9221 --- /dev/null +++ b/tests/decorators/pedantic/test_pedantic_async.py @@ -0,0 +1,65 @@ +import asyncio + +import pytest + +from pedantic.decorators.class_decorators import pedantic_class +from pedantic.exceptions import PedanticTypeCheckException +from pedantic.decorators.fn_deco_pedantic import pedantic + + +@pytest.mark.asyncio +async def test_coroutine_correct_return_type(): + @pedantic + async def foo() -> str: + await asyncio.sleep(0) + return 'foo' + + await foo() + + +@pytest.mark.asyncio +async def test_coroutine_wrong_return_type(): + @pedantic + async def foo() -> str: + await asyncio.sleep(0) + return 1 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + await foo() + + +@pytest.mark.asyncio +async def test_coroutine_wrong_argument_type(): + @pedantic + async def foo(x: int) -> int: + await asyncio.sleep(0) + return 1 + x + + with pytest.raises(expected_exception=PedanticTypeCheckException): + await foo(x=4.5) + + +@pytest.mark.asyncio +async def test_static_async(): + @pedantic_class + class Foo: + @staticmethod + async def staticmethod() -> int: + await asyncio.sleep(0) + return 'foo' + + @classmethod + async def classmethod(cls) -> int: + await asyncio.sleep(0) + return 'foo' + + async def method(self) -> int: + await asyncio.sleep(0) + return 'foo' + + with pytest.raises(expected_exception=PedanticTypeCheckException): + await Foo.staticmethod() + with pytest.raises(expected_exception=PedanticTypeCheckException): + await Foo.classmethod() + with pytest.raises(expected_exception=PedanticTypeCheckException): + await Foo().method() diff --git a/tests/decorators/pedantic/test_pedantic_class.py b/tests/decorators/pedantic/test_pedantic_class.py new file mode 100644 index 00000000..c63bf9c4 --- /dev/null +++ b/tests/decorators/pedantic/test_pedantic_class.py @@ -0,0 +1,565 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import IntEnum +from typing import Any, Optional, Callable, Union, Dict, List + +import pytest + +from pedantic.env_var_logic import disable_pedantic +from pedantic import overrides +from pedantic.decorators.class_decorators import pedantic_class +from pedantic.exceptions import PedanticOverrideException, PedanticTypeCheckException, \ + PedanticCallWithArgsException + + +def test_constructor(): + @pedantic_class + class MyClass: + def __init__(self, a: int) -> None: + self.a = a + + MyClass(a=42) + + +def test_constructor_with_list(): + class Foo(IntEnum): + A = 1 + B = 2 + + @pedantic_class + class MyClass: + def __init__(self, b: int, a: List[Foo]) -> None: + self.a = a + self.b = b + + MyClass(b=42, a=[Foo.A, Foo.B]) + + +def test_constructor_param_without_type_hint(): + @pedantic_class + class MyClass: + def __init__(self, a) -> None: + self.a = a + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass(a=42) + + +def test_constructor_without_return_type(): + @pedantic_class + class MyClass: + def __init__(self, a: int): + self.a = a + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass(a=42) + + +def test_constructor_wrong_return_type(): + @pedantic_class + class MyClass: + def __init__(self, a: int) -> int: + self.a = a + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass(a=42) + + +def test_constructor_must_be_called_with_kwargs(): + @pedantic_class + class MyClass: + def __init__(self, a: int) -> None: + self.a = a + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + MyClass(42) + + +def test_multiple_methods(): + @pedantic_class + class MyClass: + def __init__(self, a: int) -> None: + self.a = a + + def calc(self, b: int) -> int: + return self.a - b + + def print(self, s: str) -> None: + print(f'{self.a} and {s}') + + m = MyClass(a=5) + m.calc(b=42) + m.print(s='Hi') + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.calc(45) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.calc(b=45.0) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.print('Hi') + + +def test_multiple_methods_with_missing_and_wrong_type_hints(): + @pedantic_class + class MyClass: + def __init__(self, a: int) -> None: + self.a = a + + def calc(self, b: int) -> float: + return self.a - b + + def dream(self, b) -> int: + return self.a * b + + def print(self, s: str): + print(f'{self.a} and {s}') + + m = MyClass(a=5) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.calc(b=42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.print(s='Hi') + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.dream(b=2) + + +def test_type_annotation_string(): + @pedantic_class + class MyClass: + def __init__(self, s: str) -> None: + self.s = s + + @staticmethod + def generator() -> 'MyClass': + return MyClass(s='generated') + + MyClass.generator() + + +def test_typo_in_type_annotation_string(): + @pedantic_class + class MyClass: + def __init__(self, s: str) -> None: + self.s = s + + @staticmethod + def generator() -> 'MyClas': + return MyClass(s='generated') + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.generator() + + +def test_overriding_contains(): + @pedantic_class + class MyClass(list): + def __contains__(self, item: int) -> bool: + return True + + m = MyClass() + print(42 in m) + with pytest.raises(expected_exception=PedanticTypeCheckException): + print('something' in m) + + +def test_type_annotation_string_typo(): + @pedantic_class + class MyClass: + def compare(self, other: 'MyClas') -> bool: + return self == other + + def fixed_compare(self, other: 'MyClass') -> bool: + return self == other + + m = MyClass() + m.fixed_compare(other=m) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.compare(other=m) + + +def test_docstring_not_required(): + @pedantic_class + class Foo: + def __init__(self, a: int) -> None: + self.a = a + + def bunk(self) -> int: + ''' + Function with correct docstring. Yes, single-quoted docstrings are allowed too. + Returns: + int: 42 + ''' + return self.a + + foo = Foo(a=10) + foo.bunk() + + +def test_overrides(): + @pedantic_class + class Abstract: + def func(self, b: str) -> str: + pass + + def bunk(self) -> int: + pass + + @pedantic_class + class Foo(Abstract): + def __init__(self, a: int) -> None: + self.a = a + + @overrides(Abstract) + def func(self, b: str) -> str: + return b + + @overrides(Abstract) + def bunk(self) -> int: + return 42 + + f = Foo(a=42) + f.func(b='Hi') + f.bunk() + + +def test_overrides_abc(): + @pedantic_class + class Abstract(ABC): + @abstractmethod + def func(self, b: str) -> str: + pass + + @abstractmethod + def bunk(self) -> int: + pass + + @pedantic_class + class Foo(Abstract): + def __init__(self, a: int) -> None: + self.a = a + + @overrides(Abstract) + def func(self, b: str) -> str: + return b + + @overrides(Abstract) + def bunk(self) -> int: + return 42 + + f = Foo(a=42) + f.func(b='Hi') + f.bunk() + + +def test_overrides_with_type_errors_and_call_by_args3(): + @pedantic_class + class Abstract: + def func(self, b: str) -> str: + pass + + def bunk(self) -> int: + pass + + @pedantic_class + class Foo(Abstract): + def __init__(self, a: int) -> None: + self.a = a + + @overrides(Abstract) + def func(self, b: str) -> str: + return b + + @overrides(Abstract) + def bunk(self) -> int: + return self.a + + f = Foo(a=42) + f.func(b='Hi') + f.bunk() + with pytest.raises(expected_exception=PedanticCallWithArgsException): + f.func('Hi') + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo(a=3.1415) + f.a = 3.145 + with pytest.raises(expected_exception=PedanticTypeCheckException): + f.bunk() + + +def test_overrides_goes_wrong(): + @pedantic_class + class Parent: + def func(self, b: str) -> str: + return b + b + b + + def bunk(self) -> int: + return 42 + + with pytest.raises(expected_exception=PedanticOverrideException): + @pedantic_class + class Foo(Parent): + def __init__(self, a: int) -> None: + self.a = a + + @overrides(Parent) + def funcy(self, b: str) -> str: + return b + + @overrides(Parent) + def bunk(self) -> int: + return self.a + + f = Foo(a=40002) + f.func(b='Hi') + f.bunk() + + p = Parent() + p.func(b='Hi') + p.bunk() + + +def test_static_method_with_sloppy_type_annotation(): + @pedantic_class + class MyStaticClass: + @staticmethod + def double_func(a: int) -> int: + x, y = MyStaticClass.static_bar() + return x + + @staticmethod + def static_bar() -> (int, int): # this is wrong. Correct would be Tuple[int, int] + return 0, 1 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + print(MyStaticClass.double_func(a=0)) + + +def test_property(): + @pedantic_class + class MyClass(object): + def __init__(self, some_arg: Any) -> None: + self._some_attribute = some_arg + + @property + def some_attribute(self) -> int: + return self._some_attribute + + @some_attribute.setter + def some_attribute(self, value: str) -> None: + self._some_attribute = value + + def calc(self, value: float) -> float: + return 2 * value + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + MyClass(42) + + m = MyClass(some_arg=42) + assert m.some_attribute == 42 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_attribute = 100 + + assert m.some_attribute == 42 + m.some_attribute = '100' + assert m._some_attribute == '100' + m.calc(value=42.0) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + print(m.some_attribute) + + +def test_property_getter_and_setter_misses_type_hints(): + @pedantic_class + class MyClass(object): + def __init__(self, some_arg: int) -> None: + self._some_attribute = some_arg + + @property + def some_attribute(self): + return self._some_attribute + + @some_attribute.setter + def some_attribute(self, value: int): + self._some_attribute = value + + def calc(self, value: float) -> float: + return 2 * value + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + MyClass(42) + + m = MyClass(some_arg=42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_attribute = 100 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + print(m.some_attribute) + m.calc(value=42.0) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.calc(value=42) + + +def test_default_constructor(): + @pedantic_class + class MyClass: + def fun(self) -> int: + return 42 + m = MyClass() + m.fun() + + +def test_optional_callable(): + @pedantic_class + class SemanticSimilarity: + def __init__(self, post_processing: bool = True, val: Optional[Callable[[float], float]] = None) -> None: + if post_processing is None: + self.post_processing = val + else: + self.post_processing = lambda x: x + + SemanticSimilarity() + + +def test_optional_lambda(): + @pedantic_class + class SemanticSimilarity: + def __init__(self, val: Callable[[float], float] = lambda x: x) -> None: + self.post_processing = val + + SemanticSimilarity() + + +def test_class_method_type_annotation_missing(): + @pedantic_class + class MyClass: + @classmethod + def do(cls): + print('i did something') + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.do() + + +def test_class_method_type_annotation(): + @pedantic_class + class MyClass: + @classmethod + def do(cls) -> None: + print('i did something') + + @classmethod + def calc(cls, x: Union[int, float]) -> int: + return x * x + + MyClass.do() + MyClass.calc(x=5) + m = MyClass() + m.do() + + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.calc(5) + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.calc(x=5.1) + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.calc('hi') + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.calc(5) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.calc(x=5.1) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.calc('hi') + + +def test_dataclass_inside(): + """Pedantic cannot be used on dataclasses.""" + + with pytest.raises(expected_exception=PedanticTypeCheckException): + @pedantic_class + @dataclass + class MyClass: + name: str + unit_price: float + quantity_on_hand: int = 0 + + +def test_dataclass_outside(): + """Pedantic cannot check the constructor of dataclasses""" + + @dataclass + @pedantic_class + class MyClass: + name: str + unit_price: float + quantity_on_hand: int = 0 + + def total_cost(self) -> int: + return self.unit_price * self.quantity_on_hand + + MyClass(name='name', unit_price=5.1) + a = MyClass(name='name', unit_price=5.0, quantity_on_hand=42) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + a.total_cost() + + +def test_class_decorator_static_class_method(): + @pedantic_class + class Foo: + @staticmethod + def staticmethod() -> int: + return 'foo' + + @classmethod + def classmethod(cls) -> int: + return 'foo' + + def method(self) -> int: + return 'foo' + + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo.staticmethod() + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo.classmethod() + with pytest.raises(expected_exception=PedanticTypeCheckException): + Foo().method() + + +def test_pedantic_class_disable_pedantic(): + disable_pedantic() + + @pedantic_class + class MyClass: + def __init__(self, pw, **kwargs): + self._validate_str_len(new_values=kwargs) + + @staticmethod + def _validate_str_len(new_values: Dict[str, Any]) -> None: + return 42 + + def method(pw, **kwargs): + MyClass._validate_str_len(new_values=kwargs) + + MyClass._validate_str_len(None) + MyClass._validate_str_len(new_values={1: 1, 2: 2}) + MyClass(name='hi', age=12, pw='123') + + +def test_disable_pedantic_2(): + """ https://github.com/LostInDarkMath/pedantic-python-decorators/issues/37 """ + + disable_pedantic() + + @pedantic_class + class Foo: + def __init__(self) -> None: + self._value = 42 + + def do(self) -> None: + print(self.bar(value=self._value)) + + @staticmethod + def bar(value: int) -> int: + return value + 75 + + f = Foo() + f.do() diff --git a/tests/decorators/pedantic/test_pedantic_class_docstring.py b/tests/decorators/pedantic/test_pedantic_class_docstring.py new file mode 100644 index 00000000..7d81a7da --- /dev/null +++ b/tests/decorators/pedantic/test_pedantic_class_docstring.py @@ -0,0 +1,87 @@ +import pytest + +from pedantic import pedantic_class_require_docstring +from pedantic.exceptions import PedanticDocstringException + + +def test_require_docstring(): + @pedantic_class_require_docstring + class MyClass: + def __init__(self, s: str) -> None: + """Constructor + + Args: + s (str): name + """ + self.s = s + + def double(self, b: int) -> str: + """some method + + Args: + b (int): magic number + + Returns: + str: cool stuff + + """ + return self.s + str(b) + + @staticmethod + def generator() -> 'MyClass': + """Static + + Returns: + MyClass: instance + """ + return MyClass(s='generated') + + m = MyClass.generator() + m.double(b=42) + + +def test_typo_docstring(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_class_require_docstring + class MyClass: + def __init__(self, s: str) -> None: + """Constructor + + Args: + s (str): name + """ + self.s = s + + @staticmethod + def generator() -> 'MyClass': + """Static + + Returns: + MyClas: instance + """ + return MyClass(s='generated') + + +def test_wrong_docstring(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_class_require_docstring + class MyClass: + def __init__(self, s: str) -> None: + """Constructor + + Args: + s (str): name + """ + self.s = s + + def double(self, b: int) -> str: + """some method + + Args: + b (float): magic number + + Returns: + str: cool stuff + + """ + return self.s + str(b) diff --git a/tests/decorators/test_async_context_manager.py b/tests/decorators/test_async_context_manager.py new file mode 100644 index 00000000..bd0f167b --- /dev/null +++ b/tests/decorators/test_async_context_manager.py @@ -0,0 +1,84 @@ +import pytest + +from pedantic.decorators import safe_async_contextmanager + + +@pytest.mark.asyncio +async def test_safe_context_manager_no_exception(): + before = False + after = False + + @safe_async_contextmanager + async def foo(): + nonlocal before, after + before = True + yield 42 + after = True + + assert before is False + assert after is False + + async with foo() as f: + assert before is True + assert after is False + assert f == 42 + + assert before is True + assert after is True + + +@pytest.mark.asyncio +async def test_safe_context_manager_with_exception(): + before = False + after = False + + @safe_async_contextmanager + async def foo(): + nonlocal before, after + before = True + yield 42 + after = True + + assert before is False + assert after is False + + with pytest.raises(expected_exception=ValueError): + async with foo() as f: + assert before is True + assert after is False + assert f == 42 + raise ValueError('oh no') + + assert before is True + assert after is True + + +@pytest.mark.asyncio +async def test_safe_context_manager_with_args_kwargs(): + @safe_async_contextmanager + async def foo(a, b): + yield a, b + + async with foo(42, b=43) as f: + assert f == (42, 43) + + +def test_safe_context_manager_async(): + with pytest.raises(expected_exception=AssertionError) as err: + @safe_async_contextmanager + def foo(a, b): + yield a, b + + expected = 'foo is not an async generator. So you need to use "safe_contextmanager" instead.' + assert err.value.args[0] == expected + + +@pytest.mark.asyncio +async def test_safe_context_manager_non_generator(): + with pytest.raises(expected_exception=AssertionError) as err: + @safe_async_contextmanager + async def foo(a, b): + return a, b + + expected = 'foo is not a generator.' + assert err.value.args[0] == expected diff --git a/tests/decorators/test_class_decorators.py b/tests/decorators/test_class_decorators.py new file mode 100644 index 00000000..08162bcd --- /dev/null +++ b/tests/decorators/test_class_decorators.py @@ -0,0 +1,35 @@ +from pedantic.decorators.class_decorators import trace_class, timer_class + + +def test_trace_class(): + @trace_class + class MyClass: + def __init__(self, s: str) -> None: + self.s = s + + def double(self, b: int) -> str: + return self.s + str(b) + + @staticmethod + def generator() -> 'MyClass': + return MyClass(s='generated') + + m = MyClass.generator() + m.double(b=42) + + +def test_timer_class(): + @timer_class + class MyClass: + def __init__(self, s: str) -> None: + self.s = s + + def double(self, b: int) -> str: + return self.s + str(b) + + @staticmethod + def generator() -> 'MyClass': + return MyClass(s='generated') + + m = MyClass.generator() + m.double(b=42) diff --git a/tests/decorators/test_combination_of_decorators.py b/tests/decorators/test_combination_of_decorators.py new file mode 100644 index 00000000..3768b81c --- /dev/null +++ b/tests/decorators/test_combination_of_decorators.py @@ -0,0 +1,174 @@ +from abc import ABC, abstractmethod + +import pytest + +from pedantic.decorators.class_decorators import pedantic_class, for_all_methods +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.validators import Min +from pedantic.exceptions import PedanticException, PedanticTypeCheckException, PedanticCallWithArgsException +from pedantic.decorators.fn_deco_pedantic import pedantic +from pedantic import overrides +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, Parameter, ReturnAs + + +def test_pedantic_overrides(): + class MyClass(ABC): + @pedantic + @abstractmethod + def op(self, a: int) -> None: + pass + + class Child(MyClass): + a = 0 + + @pedantic + @overrides(MyClass) + def op(self, a: int) -> None: + self.a = a + + c = Child() + c.op(a=42) + + +def test_pedantic_below_validate(): + @validate( + Parameter(name='x', validators=[Min(0)]), + return_as=ReturnAs.KWARGS_WITH_NONE, + ) + @pedantic + def some_calculation(x: int) -> int: + return x + + some_calculation(x=42) + some_calculation(42) + + with pytest.raises(expected_exception=ParameterException): + some_calculation(x=-1) + with pytest.raises(expected_exception=ParameterException): + some_calculation(x=-42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_calculation(x=1.0) + + +def test_pedantic_above_validate(): + @pedantic + @validate( + Parameter(name='x', validators=[Min(0)]), + ) + def some_calculation(x: int) -> int: + return x + + some_calculation(x=42) + + with pytest.raises(expected_exception=ParameterException): + some_calculation(x=-1) + with pytest.raises(expected_exception=ParameterException): + some_calculation(x=-42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_calculation(x=1.0) + with pytest.raises(expected_exception=PedanticException): + some_calculation(42) + + +def test_pedantic_above_validate_on_instance_method(): + class MyClass: + @pedantic + @validate( + Parameter(name='x', validators=[Min(0)]), + ) + def some_calculation(self, x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-1) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_calculation(x=1.0) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.some_calculation(42) + + +def test_pedantic_below_validate_on_instance_method(): + class MyClass: + @validate( + Parameter(name='x', validators=[Min(0)]), + ) + @pedantic + def some_calculation(self, x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + m.some_calculation(42) + + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-1) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_calculation(x=1.0) + + +def test_pedantic_class_with_validate_instance_method(): + @pedantic_class + class MyClass: + @validate( + Parameter(name='x', validators=[Min(0)]), + ) + def some_calculation(self, x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-1) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(x=-42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_calculation(x=1.0) + with pytest.raises(expected_exception=PedanticCallWithArgsException): + m.some_calculation(42) + + +def test_pedantic_class_static_method_1(): + @pedantic_class + class MyClass: + @staticmethod + def some_calculation(x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + MyClass.some_calculation(x=45) + + +def test_pedantic_class_static_method_2(): + """Never do this, but it works""" + @for_all_methods(staticmethod) + @pedantic_class + class MyClass: + def some_calculation(x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.some_calculation(x=42.0) + MyClass.some_calculation(x=45) + with pytest.raises(expected_exception=PedanticTypeCheckException): + MyClass.some_calculation(x=45.0) + + +def test_pedantic_static_method_1(): + class MyClass: + @staticmethod + @pedantic + def some_calculation(x: int) -> int: + return x + + m = MyClass() + m.some_calculation(x=42) + MyClass.some_calculation(x=45) diff --git a/tests/decorators/test_context_manager.py b/tests/decorators/test_context_manager.py new file mode 100644 index 00000000..c4a0c4b1 --- /dev/null +++ b/tests/decorators/test_context_manager.py @@ -0,0 +1,80 @@ +import pytest + +from pedantic.decorators import safe_contextmanager + + +def test_safe_context_manager_no_exception(): + before = False + after = False + + @safe_contextmanager + def foo(): + nonlocal before, after + before = True + yield 42 + after = True + + assert before is False + assert after is False + + with foo() as f: + assert before is True + assert after is False + assert f == 42 + + assert before is True + assert after is True + + +def test_safe_context_manager_with_exception(): + before = False + after = False + + @safe_contextmanager + def foo(): + nonlocal before, after + before = True + yield 42 + after = True + + assert before is False + assert after is False + + with pytest.raises(expected_exception=ValueError): + with foo() as f: + assert before is True + assert after is False + assert f == 42 + raise ValueError('oh no') + + assert before is True + assert after is True + + +def test_safe_context_manager_with_args_kwargs(): + @safe_contextmanager + def foo(a, b): + yield a, b + + with foo(42, b=43) as f: + assert f == (42, 43) + + +def test_safe_context_manager_async(): + with pytest.raises(expected_exception=AssertionError) as err: + @safe_contextmanager + async def foo(a, b): + yield a, b + + expected = 'foo is async. So you need to use "safe_async_contextmanager" instead.' + assert err.value.args[0] == expected + + +def test_safe_context_manager_non_generator(): + with pytest.raises(expected_exception=AssertionError) as err: + @safe_contextmanager + def foo(a, b): + return a, b + + expected = 'foo is not a generator.' + assert err.value.args[0] == expected diff --git a/tests/decorators/test_deprecated.py b/tests/decorators/test_deprecated.py new file mode 100644 index 00000000..d5a4fd5c --- /dev/null +++ b/tests/decorators/test_deprecated.py @@ -0,0 +1,30 @@ +import pytest + +from pedantic import deprecated + + +def test_deprecated_no_args(): + @deprecated + def old_method(i: int) -> str: return str(i) + + with pytest.warns(DeprecationWarning, match="deprecated"): + old_method(42) + + +def test_deprecated_with_args(): + @deprecated(message="my deprecation message") + def old_method(i: int) -> str: + return str(i) + + with pytest.warns(DeprecationWarning, match="my deprecation message"): + old_method(42) + + +@pytest.mark.asyncio +async def test_deprecated_async(): + @deprecated + async def old_method(i: int) -> str: + return str(i) + + with pytest.warns(DeprecationWarning, match="deprecated"): + await old_method(42) diff --git a/tests/decorators/test_docstring.py b/tests/decorators/test_docstring.py new file mode 100644 index 00000000..34fc2a4c --- /dev/null +++ b/tests/decorators/test_docstring.py @@ -0,0 +1,500 @@ +from typing import List, Optional + +import pytest + +from pedantic import is_enabled +from pedantic.exceptions import PedanticTypeCheckException, PedanticDocstringException +from pedantic.decorators.fn_deco_pedantic import pedantic_require_docstring, pedantic +from pedantic.decorators.class_decorators import pedantic_class_require_docstring, pedantic_class + + +def test_no_docstring(): + assert is_enabled() + + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + +def test_one_line_doc_string_missing_arguments_and_return(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(n: int, m: int, i: int) -> int: + """Returns the sum of the three args.""" + return n + m + i + +def test_one_line_doc_string_corrected(): + @pedantic_require_docstring + def calc(n: int, m: int, i: int) -> int: + """Returns the sum of the three args. + + Args: + n (int): something + m (int): something + i (int): something + + Returns: + int: bar + """ + return n + m + i + + calc(n=42, m=40, i=38) + + +def test_list_vs_typing_list(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + list: a list of strings representing the header columns + """ + return [file_loc, str(print_cols)] + + +def test_google_docstring_2(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + List[str]: a list of strings representing the header columns + """ + + return [file_loc, str(print_cols)] + + calc(file_loc='Hi', print_cols=False) + + +def test_google_docstring_3(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + List[str]: a list of strings representing the header columns + """ + + return [file_loc, str(print_cols)] + + calc(file_loc='Hi', print_cols=False) + + +def test_more_parameter_documented_than_the_function_takes(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + amount (int): THIS ARGUMENT IS NOT TAKEN BY THE FUNCTION + + Returns: + List[str]: a list of strings representing the header columns + """ + return [file_loc, str(print_cols)] + + +def test_google_docstring_corrected(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool, amount: int) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + amount (int): now it is + + Returns: + List[str]: a list of strings representing the header columns + """ + + return [file_loc, str(print_cols), str(amount)] + + calc(file_loc='Hi', print_cols=False, amount=42) + + +def test_no_args_keyword_before_documented_arguments(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> list: + """Gets and prints the spreadsheet's header columns + + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + list: a list of strings representing the header columns + """ + return [file_loc, str(print_cols)] + + +def test_google_no_return_keyword(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> list: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + list: a list of strings representing the header columns + """ + return [file_loc, str(print_cols)] + + +def test_keep_it_simple(): + @pedantic_require_docstring + def calc() -> None: + """Gets and prints the spreadsheet's header columns""" + pass + + calc() + + +def test_docstring_misses_argument(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(name: str) -> None: + """Gets and prints the spreadsheet's header columns""" + print('hi ' + name) + + +def test_keep_it_simple_2_corrected(): + @pedantic_require_docstring + def calc(name: str) -> None: + """Gets and prints the spreadsheet's header columns + + Args: + name (str): the name + """ + print('hi ' + name) + + calc(name='maria') + + +def test_undocumented_arg(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool, number: int) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + List[str]: a list of strings representing the header columns + """ + return [file_loc, str(print_cols), str(number)] + + +def test_undocumented_arg_corrected(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool, number: int) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + number (int): magic number + + Returns: + List[str]: a list of strings representing the header columns + """ + + return [file_loc, str(print_cols), str(number)] + + calc(file_loc='Hi', print_cols=False, number=42) + + +def test_restructured_text_style_doctsring_cannot_be_parsed_yet(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + :param file_loc: The file location of the spreadsheet + :type file_loc: str + :param print_cols: A flag used to print the columns to the console + :type print_cols: bool + :returns: a list of strings representing the header column + :rtype: List[str] + """ + return [file_loc, str(print_cols)] + + +def test_return_nothing_but_document_return_value(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool): + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + list: a list of strings representing the header columns + """ + print([file_loc, str(print_cols)]) + + +def test_return_nothing_but_document_return_value_2(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> None: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + list: a list of strings representing the header columns + """ + print([file_loc, str(print_cols)]) + + +def test_return_value_1_corrected(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> None: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + """ + + a = [file_loc, str(print_cols)] + calc(file_loc='Hi', print_cols=False) + + +def test_return_value_is_not_documented(): + with pytest.raises(expected_exception=PedanticDocstringException): + # the error message here is actually wrong due to the behavior of the docstring-parser package + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console (default is False) + + Returns: + """ + return [file_loc, str(print_cols)] + + +def test_return_value_2_corrected(): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + + Returns: + List[str]: results + """ + + return [file_loc, str(print_cols)] + + calc(file_loc='Hi', print_cols=False) + + +def test_return_value_is_not_documented_3(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc(file_loc: str, print_cols: bool) -> List[str]: + """Gets and prints the spreadsheet's header columns + + Args: + file_loc (str): The file location of the spreadsheet + print_cols (bool): A flag used to print the columns to the console + (default is False) + """ + return [file_loc, str(print_cols)] + + +def test_wrong_format_1(): + with pytest.raises(expected_exception=PedanticDocstringException): + class MyText: + text = 'hi' + + @pedantic_require_docstring + def __contains__(self, substring: str) -> bool: + """ + Checks if contains substring. + Overriding __contains__ build in functions allows to use the 'in' operator blah readability + + Example: + my_text = MyText('abc') + if 'ab' in my_text -> true + :param: substring: substring + :return: True if substring is stored, False otherwise. + """ + return substring in self.text + + +def test_undocumented_arg_3(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic + def calc(a: int, b: float, c: str) -> str: + """Returns some cool string + + Args: + a (int): something + b (float): something + + Returns: + str: something + """ + return str(a) + str(b) + c + + calc(a=42, b=3.14, c='hi') + + +def test_pedantic_1_corrected(): + @pedantic + def calc(a: int, b: float, c: str) -> str: + """Returns some cool string + + Args: + a (int): something + b (float): something + c (str): something + + Returns: + str: something + """ + return str(a) + str(b) + c + + calc(a=42, b=3.14, c='hi') + + +def test_documented_none_as_return_type(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def calc() -> None: + """some cool stuff + + Returns: + None: the evil void + """ + pass + + +def test_exception_in_docstring_parser(): + @pedantic_class + class Foo: + def func(self, b: str) -> str: + """ + Function with docstring syntax error below. + Args: + b (str): + simple string + Returns: + str: simple string + """ + return b + + +def test_user_class(): + class BPMNEnum: + attr = 'BPMNEnum' + + class BPMNElement: + attr = 'BPMNElement' + + @pedantic_class_require_docstring + class MyClass: + def make_element(self, element_type: BPMNEnum, + src_tgt_elements: Optional[List[BPMNElement]] = None) -> List[BPMNElement]: + """ + Searches all element_types in XML-DOM and returns corresponding + BPMN-Objects. + Args: + element_type(BPMNEnum): abc + src_tgt_elements (Optional[List[BPMNElement]]): abc + + Returns: + List[BPMNElement]: abc + """ + element_type.attr = '42' + return src_tgt_elements + + m = MyClass() + m.make_element(element_type=BPMNEnum(), src_tgt_elements=[BPMNElement()]) + with pytest.raises(expected_exception=PedanticTypeCheckException): + m.make_element(element_type=BPMNElement(), src_tgt_elements=[BPMNEnum()]) + + +def test_user_class_with_typing(): + class BPMNEnum: + attr = 'BPMNEnum' + + class BPMNElement: + attr = 'BPMNElement' + + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_class_require_docstring + class MyClass: + + def make_element(self, element_type: BPMNEnum, + src_tgt_elements: Optional[List[BPMNElement]] = None) -> List[BPMNElement]: + """ + Searches all element_types in XML-DOM and returns corresponding + BPMN-Objects. + Args: + element_type(BPMNEnum): abc + src_tgt_elements (typing.Optional[List[BPMNElement]]): abc + + Returns: + List[BPMNElement]: abc + """ + element_type.attr = '42' + return src_tgt_elements + + +def test_pedantic_args(): + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic(require_docstring=True) + def no_docstrings() -> None: + pass + + with pytest.raises(expected_exception=PedanticDocstringException): + @pedantic_require_docstring + def no_docstrings() -> None: + pass diff --git a/tests/decorators/test_frozen_dataclass.py b/tests/decorators/test_frozen_dataclass.py new file mode 100644 index 00000000..0d9b892a --- /dev/null +++ b/tests/decorators/test_frozen_dataclass.py @@ -0,0 +1,384 @@ +from abc import ABC +from dataclasses import dataclass, FrozenInstanceError +from typing import List, Dict, Set, Tuple, Awaitable, Callable, Generic, TypeVar, Optional + +import pytest + +from pedantic.decorators.cls_deco_frozen_dataclass import frozen_dataclass, frozen_type_safe_dataclass +from pedantic.exceptions import PedanticTypeCheckException + + +@frozen_dataclass +class Foo: + a: int + b: str + c: bool + + +@frozen_type_safe_dataclass +class B: + v: Set[int] + + +@frozen_type_safe_dataclass +class A: + foo: List[int] + bar: Dict[str, str] + values: Tuple[B, B] + + +def test_equals_and_hash(): + a = Foo(a=6, b='hi', c=True) + b = Foo(a=6, b='hi', c=True) + c = Foo(a=7, b='hi', c=True) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + +def test_copy_with(): + foo = Foo(a=6, b='hi', c=True) + + copy_1 = foo.copy_with() + assert foo == copy_1 + + copy_2 = foo.copy_with(a=42) + assert foo != copy_2 + assert copy_2.a == 42 + assert foo.b == copy_2.b + assert foo.c == copy_2.c + + copy_3 = foo.copy_with(b='Hello') + assert foo != copy_3 + assert foo.a == copy_3.a + assert copy_3.b == 'Hello' + assert copy_3.c == foo.c + + copy_4 = foo.copy_with(c=False) + assert foo != copy_4 + assert foo.a == copy_4.a + assert foo.b == copy_4.b + assert copy_4.c is False + + copy_5 = foo.copy_with(a=676676, b='new', c=False) + assert foo != copy_5 + assert copy_5.a == 676676 + assert copy_5.b == 'new' + assert copy_5.c is False + +def test_validate_types(): + foo = Foo(a=6, b='hi', c=True) + foo.validate_types() + + bar = Foo(a=6.6, b='hi', c=True) + + with pytest.raises(expected_exception=PedanticTypeCheckException) as err: + bar.validate_types() + + expected = 'In dataclass "Foo" in field "a": Type hint is incorrect: Argument 6.6 of type ' \ + 'does not match expected type .' + assert err.value.args[0] == expected + + +def test_frozen_dataclass_above_dataclass(): + # This is the same behavior like + # >>> @dataclass(frozen=True) + # ... @dataclass + # ... class C: + # ... foo: int + + @frozen_dataclass + @dataclass + class A: + foo: int + + with pytest.raises(expected_exception=TypeError): + A() + + with pytest.raises(expected_exception=FrozenInstanceError): + A(foo=3) + + +def test_frozen_dataclass_below_dataclass(): + @dataclass + @frozen_dataclass + class A: + foo: int + + with pytest.raises(expected_exception=TypeError): + A() + + a = A(foo=3) + + with pytest.raises(expected_exception=FrozenInstanceError): + a.foo = 4 + + b = a.copy_with(foo=4) + assert b.foo == 4 + + +def test_frozen_typesafe_dataclass_with_post_init(): + b = 3 + + @frozen_dataclass(type_safe=True) + class A: + foo: int + + def __post_init__(self) -> None: + nonlocal b + b = 33 + + with pytest.raises(expected_exception=PedanticTypeCheckException) as err: + A(foo=42.7) + + assert err.value.args[0] == ('In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type' + ' does not match expected type .') + + # we check that the __post_init__ method is executed + assert b == 33 + + +def test_frozen_typesafe_dataclass_without_post_init(): + @frozen_dataclass(type_safe=True) + class A: + foo: int + + with pytest.raises(expected_exception=PedanticTypeCheckException) as err: + A(foo=42.7) + + assert err.value.args[0] == ('In dataclass "A" in field "foo": Type hint is incorrect: Argument 42.7 of type ' + ' does not match expected type .') + + +def test_frozen_dataclass_with_empty_braces(): + @frozen_dataclass() + class A: + foo: int + + a = A(foo=42) + assert a.foo == 42 + + +def test_frozen_dataclass_no_braces(): + @frozen_dataclass + class A: + foo: int + + a = A(foo=42) + assert a.foo == 42 + + +def test_frozen_dataclass_order(): + @frozen_dataclass(order=True) + class A: + foo: int + bar: int + + a = A(foo=42, bar=43) + b = A(foo=42, bar=42) + c = A(foo=41, bar=44) + d = A(foo=44, bar=0) + assert b < a + assert c < b + assert a < d + + +def test_frozen_type_safe_dataclass_copy_with_check(): + @frozen_type_safe_dataclass + class A: + foo: int + bar: bool + + a = A(foo=42, bar=False) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + a.copy_with(foo=1.1) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + a.copy_with(bar=11) + + a.copy_with(foo=11, bar=True) + + +def test_copy_with_is_shallow(): + a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6}))) + shallow = a.copy_with() + + # manipulation + shallow.bar['hello'] = 'pedantic' + shallow.foo.append(3) + + assert a.foo == [1, 2, 3] + assert shallow.foo == [1, 2, 3] + assert a.bar['hello'] == 'pedantic' + assert shallow.bar['hello'] == 'pedantic' + + +def test_copy_with_and_deep_copy_with(): + a = A(foo=[1, 2], bar={'hello': 'world'}, values=(B(v={4, 5}), B(v={6}))) + deep = a.deep_copy_with() + + # manipulation + deep.bar['hello'] = 'pedantic' + deep.foo.append(3) + + assert a.foo == [1, 2] + assert deep.foo == [1, 2, 3] + assert a.bar['hello'] == 'world' + assert deep.bar['hello'] == 'pedantic' + + +def test_frozen_dataclass_inheritance_override_post_init(): + i = 1 + + @frozen_type_safe_dataclass + class A: + bar: int + + def __post_init__(self): + nonlocal i + i += 1 + print('hello a') + + @frozen_type_safe_dataclass + class B(A): + foo: int + + def __post_init__(self): + nonlocal i + i *= 10 + print('hello b') + + A(bar=3) + assert i == 2 + + b = B(bar=3, foo=42) + assert i == 20 # post init of A was not called + assert b.bar == 3 + assert b.foo == 42 + + a = b.copy_with() + assert a == b + assert i == 200 + + +def test_frozen_dataclass_inheritance_not_override_post_init(): + i = 1 + + @frozen_type_safe_dataclass + class A: + bar: int + + def __post_init__(self): + nonlocal i + i += 1 + print('hello a') + + @frozen_type_safe_dataclass + class B(A): + foo: int + + A(bar=3) + assert i == 2 + + b = B(bar=3, foo=42) + assert i == 3 # post init of A was called + assert b.bar == 3 + assert b.foo == 42 + + a = b.copy_with() + assert a == b + assert i == 4 + + +def test_type_safe_frozen_dataclass_with_awaitable(): + @frozen_type_safe_dataclass + class A: + f: Callable[..., Awaitable[int]] + + async def _cb() -> int: + return 42 + + async def _cb_2() -> str: + return '42' + + A(f=_cb) + with pytest.raises(expected_exception=PedanticTypeCheckException): + A(f=_cb_2) + + +def test_type_safe_frozen_dataclass_with_forward_ref(): + T = TypeVar('T') + + class State(Generic[T], ABC): + pass + + class StateMachine(Generic[T], ABC): + pass + + @frozen_type_safe_dataclass + class StateChangeResult: + new_state: Optional['MachineState'] + + class MachineState(State['MachineStateMachine']): + pass + + class OfflineMachineState(MachineState): + pass + + class OnlineMachineState: + pass + + class MachineStateMachine(StateMachine[MachineState]): + pass + + s = StateChangeResult(new_state=OfflineMachineState()) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + StateChangeResult(new_state=OnlineMachineState()) + + s.validate_types() + + +def test_forward_ref_to_itself(): + """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """ + + @frozen_type_safe_dataclass + class Comment: + replies: List['Comment'] + + comment = Comment(replies=[Comment(replies=[])]) + comment.copy_with(replies=[Comment(replies=[])]) + comment.validate_types() + + +def test_forward_ref_to_itself_while_class_not_in_scope(): + """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/72 """ + + def _scope(): + @frozen_type_safe_dataclass + class Comment: + replies: List['Comment'] + + def _make(replies=None): + return Comment(replies=replies or []) + + return _make + + make = _scope() + + comment = make(replies=[make(replies=[])]) + comment.copy_with(replies=[make(replies=[])]) + comment.validate_types() + + +def test_slots_work_with_equals(): + @frozen_dataclass(slots=True) + class Foo: + a: int + + o = Foo(a=0) + assert o == o.copy_with() diff --git a/tests/decorators/test_in_subprocess.py b/tests/decorators/test_in_subprocess.py new file mode 100644 index 00000000..a8e9a8cd --- /dev/null +++ b/tests/decorators/test_in_subprocess.py @@ -0,0 +1,172 @@ +import asyncio +import time +from typing import NoReturn + +import pytest +from multiprocess import Pipe + +from pedantic import in_subprocess +from pedantic.decorators.fn_deco_in_subprocess import _inner, SubprocessError + + +@pytest.mark.asyncio +async def test_in_subprocess_simple(): + @in_subprocess + def f() -> int: + return 42 + + assert await f() == 42 + + +@pytest.mark.asyncio +async def test_in_subprocess_custom_object(): + class Foo: + def __init__(self, v) -> None: + self._value = v + + @in_subprocess + def f() -> Foo: + return Foo(v=42) + + assert (await f())._value == 42 + + +@pytest.mark.asyncio +async def test_in_subprocess_simple_async(): + @in_subprocess + async def f() -> int: + return 42 + + assert await f() == 42 + + +@pytest.mark.asyncio +async def test_in_subprocess_no_args(): + @in_subprocess + def f() -> int: + time.sleep(0.1) + return 42 + + async def t() -> None: + for _ in range(6): + await asyncio.sleep(0.01) + nonlocal counter + counter += 1 + + counter = 0 + task = asyncio.create_task(t()) + assert await f() == 42 + assert counter >= 5 + await task + + +@pytest.mark.asyncio +async def test_in_subprocess_no_args_no_return(): + @in_subprocess + def f() -> None: + time.sleep(0.1) + + assert await f() is None + + +@pytest.mark.asyncio +async def test_in_subprocess_exception(): + @in_subprocess + def f() -> NoReturn: + raise RuntimeError('foo') + + with pytest.raises(expected_exception=RuntimeError): + await f() + + +@pytest.mark.asyncio +async def test_not_in_subprocess_blocks(): + async def f() -> int: + time.sleep(0.1) + return 42 + + async def t() -> None: + for _ in range(6): + await asyncio.sleep(0.05) + nonlocal counter + counter += 1 + + counter = 0 + task = asyncio.create_task(t()) + assert await f() == 42 + assert counter == 0 + await task + + +@pytest.mark.asyncio +async def test_in_subprocess_with_arguments(): + @in_subprocess + def f(a: int, b: int) -> int: + return a + b + + assert await f(4, 5) == 9 + assert await f(a=4, b=5) == 9 + + +def test_inner_function_sync(): + """ Needed for line coverage""" + + rx, tx = Pipe(duplex=False) + _inner(tx, lambda x: 1 / x, x=42) + assert rx.recv() == 1 / 42 + + _inner(tx, lambda x: 1 / x, x=0) + ex = rx.recv() + assert isinstance(ex, SubprocessError) + + +def test_inner_function_async(): + """ Needed for line coverage""" + + async def foo(x): + return 1/x + + rx, tx = Pipe(duplex=False) + _inner(tx, foo, x=42) + assert rx.recv() == 1 / 42 + + _inner(tx, foo, x=0) + ex = rx.recv() + assert isinstance(ex, SubprocessError) + + +@pytest.mark.asyncio +async def test_in_subprocess_instance_method(): + class Foo: + async def pos_args(self) -> int: + return await self.f(4, 5) + + async def kw_args(self) -> int: + return await self.f(a=4, b=5) + + @in_subprocess + def f(self, a: int, b: int) -> int: + return a + b + + foo = Foo() + assert await foo.pos_args() == 9 + assert await foo.kw_args() == 9 + + +@pytest.mark.asyncio +async def test_in_subprocess_static_method(): + class Foo: + async def pos_args(self) -> int: + return await self.f(4, 5) + + async def kw_args(self) -> int: + return await self.f(a=4, b=5) + + @staticmethod + @in_subprocess + def f(a: int, b: int) -> int: + return a + b + + foo = Foo() + assert await foo.pos_args() == 9 + assert await foo.kw_args() == 9 diff --git a/tests/decorators/test_mock.py b/tests/decorators/test_mock.py new file mode 100644 index 00000000..07c78546 --- /dev/null +++ b/tests/decorators/test_mock.py @@ -0,0 +1,10 @@ +from pedantic import mock + + +def test_mock() -> None: + @mock(return_value=42) + def my_function(a, b, c): + return a + b + c + + assert my_function(1, 2, 3) == 42 + assert my_function(100, 200, 300) == 42 diff --git a/tests/decorators/test_overrides.py b/tests/decorators/test_overrides.py new file mode 100644 index 00000000..fb234870 --- /dev/null +++ b/tests/decorators/test_overrides.py @@ -0,0 +1,97 @@ +import asyncio +from abc import abstractmethod, ABC + +import pytest + +from pedantic import overrides +from pedantic.exceptions import PedanticOverrideException + + +def test_overrides_parent_has_no_such_method(): + class MyClassA: + pass + + with pytest.raises(expected_exception=PedanticOverrideException): + class MyClassB(MyClassA): + @overrides(MyClassA) + def operation(self): pass + + +def test_overrides_all_good(): + class MyClassA: + def operation(self): pass + + class MyClassB(MyClassA): + @overrides(MyClassA) + def operation(self): + return 42 + + b = MyClassB() + b.operation() + + +def test_overrides_static_method(): + class MyClassA: + @staticmethod + def operation(): pass + + class MyClassB(MyClassA): + @staticmethod + @overrides(MyClassA) + def operation(): + return 42 + + b = MyClassB() + assert b.operation() == 42 + assert MyClassB.operation() == 42 + + +def test_overrides_below_property(): + class MyClassA(ABC): + @property + @abstractmethod + def operation(self): pass + + class MyClassB(MyClassA): + @property + @overrides(MyClassA) # Note: it does not work the other way around + def operation(self): + return 43 + + b = MyClassB() + assert b.operation == 43 + + +def test_overrides_function(): + class MyClassA: + pass + + with pytest.raises(expected_exception=PedanticOverrideException): + @overrides(MyClassA) + def operation(): return 42 + + +@pytest.mark.asyncio +async def test_overrides_async_instance_method() -> None: + class MyClassA: + async def operation(self): pass + + class MyClassB(MyClassA): + @overrides(MyClassA) + async def operation(self): + await asyncio.sleep(0) + return 42 + + b = MyClassB() + await b.operation() + + +@pytest.mark.asyncio +async def test_overrides_parent_has_no_such_method_async(): + class MyClassA: + pass + + with pytest.raises(expected_exception=PedanticOverrideException): + class MyClassB(MyClassA): + @overrides(MyClassA) + async def operation(self): return 42 diff --git a/tests/decorators/test_rename_kwargs.py b/tests/decorators/test_rename_kwargs.py new file mode 100644 index 00000000..3f5e9911 --- /dev/null +++ b/tests/decorators/test_rename_kwargs.py @@ -0,0 +1,15 @@ +from pedantic import rename_kwargs, Rename + + +def test_rename_kwargs(): + @rename_kwargs( + Rename(from_='x', to='a'), + Rename(from_='y', to='b'), + ) + def operation(a: int, b: int) -> int: + return a + b + + operation(4, 5) + operation(a=4, b=5) + operation(x=4, y=5) + operation(x=4, b=5) diff --git a/tests/decorators/test_require_kwargs.py b/tests/decorators/test_require_kwargs.py new file mode 100644 index 00000000..8b2feb09 --- /dev/null +++ b/tests/decorators/test_require_kwargs.py @@ -0,0 +1,29 @@ +import pytest + +from pedantic.exceptions import PedanticCallWithArgsException +from pedantic import require_kwargs + + +def test_kwargs(): + @require_kwargs + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + calc(n=1, m=2, i=3) + + +def test_no_kwargs_1(): + @require_kwargs + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + calc(1, m=2, i=3) + +def test_no_kwargs_2(): + @require_kwargs + def calc(n: int, m: int, i: int) -> int: + return n + m + i + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + calc(1, 2, 3) diff --git a/tests/decorators/test_retry_decorator.py b/tests/decorators/test_retry_decorator.py new file mode 100644 index 00000000..eda7bd54 --- /dev/null +++ b/tests/decorators/test_retry_decorator.py @@ -0,0 +1,89 @@ +import pytest + +from pedantic import retry + + +def test_retry_positive_no_args(): + count = 0 + + @retry(attempts=5) + def foo(): + nonlocal count + count += 1 + + foo() + assert count == 1 + + +def test_retry_positive_args_and_kwargs(): + count = 0 + + @retry(attempts=5) + def foo(x, y): + nonlocal count + count += x + y + + foo(12, y=42) + assert count == 54 + + +def test_retry_positive_no_args_fails_every_time(): + count = 0 + + @retry(attempts=5) + def foo(): + nonlocal count + count += 1 + raise ValueError('foo') + + with pytest.raises(ValueError): + foo() + + assert count == 5 + + +def test_retry_positive_no_args_fails_different_exception_type(): + count = 0 + + @retry(attempts=5, exceptions=AssertionError) + def foo(): + nonlocal count + count += 1 + raise ValueError('foo') + + with pytest.raises(ValueError): + foo() + + assert count == 1 + + +def test_retry_fails_same_exception_type(): + count = 0 + + @retry(attempts=5, exceptions=AssertionError) + def foo(): + nonlocal count + count += 1 + raise AssertionError('foo') + + with pytest.raises(AssertionError): + foo() + + assert count == 5 + + +def test_retry_positive_no_args_fails_on_first_times(): + count = 0 + + @retry(attempts=5) + def foo() -> int: + nonlocal count + count += 1 + + if count < 3: + raise ValueError('foo') + + return count + + assert foo() == 3 + assert count == 3 diff --git a/tests/decorators/test_retry_func.py b/tests/decorators/test_retry_func.py new file mode 100644 index 00000000..a42b2438 --- /dev/null +++ b/tests/decorators/test_retry_func.py @@ -0,0 +1,82 @@ +import pytest + +from pedantic.decorators.fn_deco_retry import retry_func + +def test_retry_positive_no_args(): + count = 0 + + def foo(): + nonlocal count + count += 1 + + retry_func(func=foo, attempts=5) + assert count == 1 + + +def test_retry_positive_args_and_kwargs(): + count = 0 + + def foo(x, y): + nonlocal count + count += x + y + + retry_func(foo, 12, attempts=5, y=42) + assert count == 54 + + +def test_retry_positive_no_args_fails_every_time(): + count = 0 + + def foo(): + nonlocal count + count += 1 + raise ValueError('foo') + + with pytest.raises(ValueError): + retry_func(func=foo, attempts=5) + + assert count == 5 + + +def test_retry_positive_no_args_fails_different_exception_type(): + count = 0 + + def foo(): + nonlocal count + count += 1 + raise ValueError('foo') + + with pytest.raises(ValueError): + retry_func(func=foo, attempts=5, exceptions=AssertionError) + + assert count == 1 + + +def test_retry_fails_same_exception_type(): + count = 0 + + def foo(): + nonlocal count + count += 1 + raise AssertionError('foo') + + with pytest.raises(AssertionError): + retry_func(func=foo, attempts=5, exceptions=AssertionError) + + assert count == 5 + + +def test_retry_positive_no_args_fails_on_first_times(): + count = 0 + + def foo() -> int: + nonlocal count + count += 1 + + if count < 3: + raise ValueError('foo') + + return count + + assert retry_func(func=foo, attempts=5) == 3 + assert count == 3 diff --git a/tests/decorators/test_small_method_decorators.py b/tests/decorators/test_small_method_decorators.py new file mode 100644 index 00000000..fd810067 --- /dev/null +++ b/tests/decorators/test_small_method_decorators.py @@ -0,0 +1,184 @@ +import asyncio +import warnings + +import pytest + +from pedantic import timer, count_calls, trace, trace_if_returns, does_same_as_function, deprecated, \ + unimplemented, mock, require_kwargs +from pedantic.exceptions import NotImplementedException, PedanticCallWithArgsException + + +def test_unimplemented(): + @unimplemented + def dirt(i: int) -> str: + return str(i) + + with pytest.raises(expected_exception=NotImplementedException): + dirt(42) + + +def test_timer(): + @timer + def operation(i: int) -> str: + return str(i) + + operation(42) + + +def test_count_calls(): + @count_calls + def operation(i: int) -> str: + return str(i) + + operation(42) + + +def test_trace(): + def some_method(x, y): + return x + y + + traced_method = trace(some_method) + assert some_method(42, 99) == traced_method(42, 99) + + +def test_trace_if_returns(): + def some_method(x, y): + return x + y + + traced_method = trace_if_returns(100)(some_method) + assert some_method(42, 99) == traced_method(42, 99) + assert some_method(42, 58) == traced_method(42, 58) + + +def test_does_same_as_function(): + def some_method(x, y, z): + return x * (y + z) + + @does_same_as_function(some_method) + def other_method(x, y, z): + return x * y + x * z + + other_method(1, 2, 3) + other_method(4, 5, 6) + + +def test_does_same_as_function_wrong(): + def some_method(x, y, z): + return x * (y + z) + + @does_same_as_function(some_method) + def other_method(x, y, z): + return x * y + z + + other_method(0, 2, 0) + with pytest.raises(expected_exception=AssertionError): + other_method(4, 5, 6) + + +@pytest.mark.asyncio +async def test_count_calls_async(): + @count_calls + async def operation(i: int) -> str: + await asyncio.sleep(0) + return str(i) + + res = await operation(42) + assert res == '42' + + +@pytest.mark.asyncio +async def test_trace_async(): + async def some_method(x, y): + await asyncio.sleep(0) + return x + y + + traced_method = trace(some_method) + assert await some_method(42, 99) == await traced_method(42, 99) + + +@pytest.mark.asyncio +async def test_trace_if_returns_async(): + async def some_method(x, y): + await asyncio.sleep(0) + return x + y + + traced_method = trace_if_returns(100)(some_method) + assert await some_method(42, 99) == await traced_method(42, 99) + assert await some_method(42, 58), await traced_method(42, 58) + + +@pytest.mark.asyncio +async def test_timer_async(): + @timer + async def operation(i: int) -> str: + await asyncio.sleep(0.05) + return str(i) + + await operation(42) + + +@pytest.mark.asyncio +async def test_does_same_as_function_async(): + async def some_method(x, y, z): + await asyncio.sleep(0) + return x * (y + z) + + @does_same_as_function(some_method) + async def other_method(x, y, z): + await asyncio.sleep(0) + return x * y + x * z + + await other_method(1, 2, 3) + await other_method(4, 5, 6) + + +@pytest.mark.asyncio +async def test_does_same_as_function_async_and_sync(): + def some_method(x, y, z): + return x * (y + z) + + @does_same_as_function(some_method) + async def other_method(x, y, z): + await asyncio.sleep(0) + return x * y + x * z + + await other_method(1, 2, 3) + await other_method(4, 5, 6) + + +@pytest.mark.asyncio +async def test_does_same_as_function_wrong(): + async def some_method(x, y, z): + await asyncio.sleep(0) + return x * (y + z) + + @does_same_as_function(some_method) + async def other_method(x, y, z): + await asyncio.sleep(0) + return x * y + z + + await other_method(0, 2, 0) + + with pytest.raises(expected_exception=AssertionError): + await other_method(4, 5, 6) + + +@pytest.mark.asyncio +async def test_mock_async(): + @mock(return_value=42) + async def my_function(a, b, c): return a + b + c + + assert await my_function(1, 2, 3) == 42 + assert await my_function(100, 200, 300) == 42 + + +@pytest.mark.asyncio +async def test_require_kwargs(): + @require_kwargs + async def calc(n: int, m: int, i: int) -> int: + return n + m + i + + await calc(n=1, m=2, i=3) + + with pytest.raises(expected_exception=PedanticCallWithArgsException): + await calc(1, m=2, i=3) diff --git a/tests/decorators/validate/__init__.py b/tests/decorators/validate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/decorators/validate/test_convert_value.py b/tests/decorators/validate/test_convert_value.py new file mode 100644 index 00000000..c5ebfa88 --- /dev/null +++ b/tests/decorators/validate/test_convert_value.py @@ -0,0 +1,56 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ConversionError +from pedantic.decorators.fn_deco_validate.convert_value import convert_value + + +def test_convert_to_bool(): + for value in [True, 1, '1', ' 1 ', ' tRuE ', 'TRUE']: + assert convert_value(value=value, target_type=bool) is True + + for value in [False, 0, '0', ' 0 ', ' fAlSe ', 'FALSE']: + assert convert_value(value=value, target_type=bool) is False + + for value in ['alse', 0.1, '0.2', ' 0000 ', 'Talse', 'Frue', 42]: + with pytest.raises(expected_exception=ConversionError): + convert_value(value=value, target_type=bool) + + +def test_convert_to_int(): + for value in range(-4, 4): + assert convert_value(value=value, target_type=int) == value + + assert convert_value(value='42', target_type=int) == 42 + assert convert_value(value=' 0000 ', target_type=int) == 0 + + for value in ['alse', 'Talse', 'Frue', 0.2, '0.2']: + with pytest.raises(expected_exception=ConversionError): + convert_value(value=value, target_type=int) + + +def test_convert_to_float(): + for value in range(-4, 4): + assert convert_value(value=value, target_type=float) == value + + assert convert_value(value=0.2, target_type=float) == 0.2 + assert convert_value(value='0.2', target_type=float) == 0.2 + assert convert_value(value='42', target_type=float) == 42 + assert convert_value(value=' 0000 ', target_type=float) == 0 + + for value in ['alse', 'Talse', 'Frue']: + with pytest.raises(expected_exception=ConversionError): + convert_value(value=value, target_type=float) + + +def test_convert_to_list(): + for value in [[], [1], ['1', ' 1 '], [' tRuE ', 'TRUE']]: + assert convert_value(value=value, target_type=list) == value + + assert convert_value(value='1,2,3', target_type=list) == ['1', '2', '3'] + + +def test_convert_to_dict(): + for value in [{}, {1: 2}, {'1': ' 1 '}, {1: ' tRuE ', 2: 'TRUE'}]: + assert convert_value(value=value, target_type=dict) == value + + assert convert_value(value='1:1,2:4,3:7', target_type=dict) == {'1': '1', '2': '4', '3': '7'} diff --git a/tests/decorators/validate/test_datetime_isoformat.py b/tests/decorators/validate/test_datetime_isoformat.py new file mode 100644 index 00000000..d0935d08 --- /dev/null +++ b/tests/decorators/validate/test_datetime_isoformat.py @@ -0,0 +1,23 @@ +from datetime import datetime, timedelta + +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import DatetimeIsoFormat + + +def test_validator_datetime_iso_format(): + @validate(Parameter(name='x', validators=[DatetimeIsoFormat()])) + def foo(x): + return x + + now = datetime.now() + assert abs(now - foo(now.isoformat()) )< timedelta(milliseconds=1) + + with pytest.raises(expected_exception=ParameterException): + foo('12.12.2020') + + with pytest.raises(expected_exception=ParameterException): + foo('invalid') diff --git a/tests/decorators/validate/test_flask_parameters.py b/tests/decorators/validate/test_flask_parameters.py new file mode 100644 index 00000000..b8573469 --- /dev/null +++ b/tests/decorators/validate/test_flask_parameters.py @@ -0,0 +1,628 @@ +import json +from dataclasses import dataclass +from typing import List, Dict, Any + +import pytest +from flask import Flask, Response, jsonify + +from pedantic import Email, overrides +from pedantic.decorators.fn_deco_validate.exceptions import ValidateException, TooManyArguments, \ + ParameterException, ExceptionDictKey, InvalidHeader +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, ReturnAs +from pedantic.decorators.fn_deco_validate.parameters.flask_parameters import FlaskJsonParameter, FlaskFormParameter, \ + FlaskHeaderParameter, FlaskGetParameter, FlaskPathParameter, Deserializable, GenericFlaskDeserializer +from pedantic.decorators.fn_deco_validate.validators import NotEmpty, Min + +JSON = 'application/json' +OK = 200 +INVALID = 422 +TOO_MANY_ARGS = 400 + + +def test_validator_flask_json(): + app = Flask(__name__) + + @app.route('/') + @validate( + FlaskJsonParameter(name='key', validators=[NotEmpty()]), + ) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.route('/required') + @validate( + FlaskJsonParameter(name='required', required=True), + FlaskJsonParameter(name='not_required', required=False), + FlaskJsonParameter(name='not_required_with_default', required=False, default=42), + ) + def required_params(required, not_required, not_required_with_default) -> Response: + return jsonify({ + 'required': required, + 'not_required': not_required, + 'not_required_with_default': not_required_with_default, + }) + + @app.route('/types') + @validate( + FlaskJsonParameter(name='bool_param', value_type=bool), + FlaskJsonParameter(name='int_param', value_type=int), + FlaskJsonParameter(name='float_param', value_type=float), + FlaskJsonParameter(name='str_param', value_type=str), + FlaskJsonParameter(name='list_param', value_type=list), + FlaskJsonParameter(name='dict_param', value_type=dict), + ) + def different_types( + bool_param, + int_param, + float_param, + str_param, + list_param, + dict_param, + ) -> Response: + return jsonify({ + 'bool_param': bool_param, + 'int_param': int_param, + 'float_param': float_param, + 'str_param': str_param, + 'list_param': list_param, + 'dict_param': dict_param, + }) + + @app.route('/args') + @validate( + FlaskJsonParameter(name='a', validators=[NotEmpty()]), + FlaskJsonParameter(name='b', validators=[NotEmpty()]), + return_as=ReturnAs.ARGS, + ) + def names_do_not_need_to_match(my_key: str, another: str) -> Response: + return jsonify({ + 'my_key': my_key, + 'another': another, + }) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + @app.errorhandler(TooManyArguments) + def handle_validation_error(exception: TooManyArguments) -> Response: + print(str(exception)) + response = jsonify(str(exception)) + response.status_code = TOO_MANY_ARGS + return response + + with app.test_client() as client: + res = client.get('/', data=json.dumps({'key': ' hello world '}), content_type=JSON) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/', data=json.dumps({'key': ' '}), content_type=JSON) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: 'NotEmpty', + ExceptionDictKey.VALUE: ' ', + ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + data = { + 'key': ' hello world ', + 'required': '1', + } + res = client.get('/', data=json.dumps(data), content_type=JSON) + assert res.status_code == TOO_MANY_ARGS + assert res.json == "Got unexpected arguments: ['required']" + + +def test_validator_flask_form_data(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='key', validators=[NotEmpty()])) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/', data={'key': ' hello world '}) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/', data={'key': ' '}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: 'NotEmpty', + ExceptionDictKey.VALUE: ' ', + ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + +def test_validator_flask_header(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskHeaderParameter(name='key', validators=[NotEmpty()])) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(InvalidHeader) + def handle_validation_error(exception: InvalidHeader) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/', headers={'key': ' hello world '}, data={}) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/', headers={'key': ' '}, data={}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALUE: ' ', + ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', + ExceptionDictKey.PARAMETER: 'key', + ExceptionDictKey.VALIDATOR: 'NotEmpty', + } + assert res.json == expected + + +def test_validator_flask_header_optional_parameter(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskHeaderParameter(name='key', validators=[NotEmpty()], required=False)) + def hello_world(key: str = None) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get('/', headers={'key': ' hello world '}, data={}) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/', headers={}, data={}) + assert res.status_code == OK + assert res.json is None + + +def test_validator_flask_get(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskGetParameter(name='key', value_type=str, validators=[NotEmpty()])) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/?key=hello_world', data={}) + assert res.status_code == OK + assert res.json == 'hello_world' + + res = client.get('/?key=hello world', data={}) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/?key= ', data={}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: 'NotEmpty', + ExceptionDictKey.VALUE: ' ', + ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + +def test_validator_flask_get_multiple_values_for_same_key(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskGetParameter(name='key', value_type=list, validators=[NotEmpty()])) + def hello_world(key: List[str]) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get('/?key=hello&key=world', data={}) + assert res.status_code == OK + assert res.json == ['hello', 'world'] + + +def test_validator_flask_path(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskPathParameter(name='key', validators=[NotEmpty()])) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/hello_world', data={}) + assert res.status_code == OK + assert res.json == 'hello_world' + + res = client.get('/hello world', data={}) + assert res.status_code == OK + assert res.json == 'hello world' + + res = client.get('/ ', data={}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: 'NotEmpty', + ExceptionDictKey.VALUE: ' ', + ExceptionDictKey.MESSAGE: 'Got empty String which is invalid.', + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + +def test_invalid_value_type(): + app = Flask(__name__) + + with pytest.raises(expected_exception=AssertionError): + @app.route('/') + @validate(FlaskFormParameter(name='key', value_type=tuple)) + def hello_world(key: str) -> Response: + return jsonify(key) + + +def test_wrong_name(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='k', value_type=str)) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ValidateException) + def handle_validation_error(exception: ValidateException) -> Response: + print(str(exception)) + response = jsonify(str(exception)) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get(data={'key': 'k'}) + assert res.status_code == INVALID + assert 'Value for parameter k is required.' in res.json + + +def test_default_value(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='key', value_type=str, default='42')) + def hello_world(key: str) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get(data={}) + assert res.status_code == OK + assert res.json == '42' + + +def test_not_required_allows_none_kwargs_without_none(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='key', value_type=str, required=False), + return_as=ReturnAs.KWARGS_WITHOUT_NONE) + def hello_world(key: str = 'it works') -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get(data={}) + assert res.status_code == OK + assert res.json == 'it works' + + +def test_not_required_allows_none_kwargs_with_none(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='key', value_type=str, required=False, default=None), + return_as=ReturnAs.KWARGS_WITH_NONE) + def hello_world(key: str) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get(data={'key': None}) + assert res.status_code == OK + assert res.json is None + + +def test_not_required_with_default(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskFormParameter(name='key', value_type=str, required=False, default='42')) + def hello_world(key: str) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get(data={}) + assert res.status_code == OK + assert res.json == '42' + + +def test_validator_flask_path_type_conversion(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskPathParameter(name='key', value_type=int, validators=[Min(42)])) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/42', data={}) + assert res.status_code == OK + assert res.json == 42 + + res = client.get('/42f', data={}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: None, + ExceptionDictKey.VALUE: '42f', + ExceptionDictKey.MESSAGE: "Value 42f cannot be converted to .", + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + +def test_validator_flask_json_parameter_does_not_get_json(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskJsonParameter(name='key')) + def hello_world(key: str) -> Response: + return jsonify(key) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + print(str(exception)) + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get('/', data={}) + assert res.status_code == INVALID + expected = { + ExceptionDictKey.VALIDATOR: None, + ExceptionDictKey.VALUE: 'None', + ExceptionDictKey.MESSAGE: 'Value for parameter key is required.', + ExceptionDictKey.PARAMETER: 'key', + } + assert res.json == expected + + +def test_validator_flask_json_parameter_does_not_get_json_but_default(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskJsonParameter(name='key', default='42')) + def hello_world(key: str) -> Response: + return jsonify(key) + + with app.test_client() as client: + res = client.get('/', data={}) + assert res.status_code == OK + assert res.json == '42' + + +def test_too_many_arguments(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskJsonParameter(name='k', value_type=str)) + def hello_world(**kwargs) -> Response: + return jsonify(str(kwargs)) + + @app.errorhandler(TooManyArguments) + def handle_validation_error(exception: TooManyArguments) -> Response: + print(str(exception)) + response = jsonify(str(exception)) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get(data=json.dumps({'k': 'k', 'a': 1}), content_type=JSON) + assert res.status_code == INVALID + assert res.json == "Got unexpected arguments: ['a']" + + +def test_exception_for_required_parameter(): + app = Flask(__name__) + key = 'PASSWORD' + + @app.route('/') + @validate(FlaskJsonParameter(name=key, value_type=str)) + def hello_world(**kwargs) -> Response: + return jsonify(str(kwargs)) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + reason = 'required' if 'required' in exception.message else 'invalid' + response = jsonify({exception.parameter_name: [{'KEY': reason}]}) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get(data=json.dumps({}), content_type=JSON) + assert res.status_code == INVALID + assert res.json == {key: [{'KEY': 'required'}]} + + +def test_async_endpoints(): + app = Flask(__name__) + + @app.route('/') + @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)])) + async def hello_world(k) -> Response: + return jsonify(str(k)) + + @app.route('/foo/') + @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)]), return_as=ReturnAs.ARGS) + async def return_args(k) -> Response: + return jsonify(str(k)) + + @app.route('/bar/') + @validate(FlaskPathParameter(name='k', value_type=int, validators=[Min(42)]), + return_as=ReturnAs.KWARGS_WITH_NONE) + async def return_kwargs_with_none(k) -> Response: + return jsonify(str(k)) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + res = client.get(path=f'/45') + assert res.status_code == OK + + res = client.get(path=f'/foo/45') + assert res.status_code == OK + + res = client.get(path=f'/bar/45') + assert res.status_code == OK + + res = client.get(path=f'/41') + assert res.status_code == INVALID + expected = { + ExceptionDictKey.MESSAGE: 'smaller then allowed: 41 is not >= 42', + ExceptionDictKey.PARAMETER: 'k', + ExceptionDictKey.VALIDATOR: 'Min', + ExceptionDictKey.VALUE: '41', + } + assert res.json == expected + + +def test_json_parameter_with_default_value(): + app = Flask(__name__) + + @app.route('/testing/message/', methods=['POST']) + @validate( + FlaskPathParameter(name='email', value_type=str, validators=[Email()]), + FlaskJsonParameter(name='content', value_type=str, default='this is a fake message', required=False), + ) + def testing_send_system_message(email: str, content: str) -> Response: + return jsonify({'email': email, 'content': content}) + + with app.test_client() as client: + res = client.post(path=f'/testing/message/adam@eva.de') + assert res.status_code == OK + expected = { + 'email': 'adam@eva.de', + 'content': 'this is a fake message', + } + assert res.json == expected + + res = client.post(path=f'/testing/message/adam@eva.de', json={'content': 'hello world'}) + assert res.status_code == OK + expected = { + 'email': 'adam@eva.de', + 'content': 'hello world', + } + assert res.json == expected + + +def test_generic_deserializer(): + @dataclass(frozen=True) + class User(Deserializable): + firstname: str + lastname: str + age: int + + @staticmethod + @overrides(Deserializable) + def from_json(data: Dict[str, Any]) -> 'User': + return User( + firstname=data['firstname'], + lastname=data['lastname'], + age=Min(value=18).validate_param(value=int(data['age']), parameter_name='age'), + ) + + app = Flask(__name__) + + @app.route('/foo', methods=['POST']) + @validate( + GenericFlaskDeserializer(name='user', cls=User, catch_exception=True), + ) + def the_generic_approach(user: User) -> Response: + return jsonify({'name': user.firstname}) + + @app.route('/bar', methods=['POST']) + @validate( + GenericFlaskDeserializer(name='user', cls=User, catch_exception=False), + ) + def do_not_catch(user: User) -> Response: + return jsonify({'name': user.firstname}) + + @app.errorhandler(ParameterException) + def handle_validation_error(exception: ParameterException) -> Response: + response = jsonify(exception.to_dict) + response.status_code = INVALID + return response + + with app.test_client() as client: + data = { + 'firstname': 'Albert', + 'lastname': 'Einstein', + 'age': '56', + } + res = client.post(path=f'/foo', json=data) + assert res.status_code == 200 + assert res.json == {'name': 'Albert'} + + data.pop('age') + res = client.post(path=f'/foo', json=data) + assert res.status_code == 422 + + res = client.post(path=f'/bar', json=data) + assert res.status_code == 500 + + data['age'] = '16' + res = client.post(path=f'/foo', json=data) + assert res.status_code == 422 + expected = { + ExceptionDictKey.MESSAGE: 'smaller then allowed: 16 is not >= 18', + ExceptionDictKey.PARAMETER: 'age', + ExceptionDictKey.VALIDATOR: 'Min', + ExceptionDictKey.VALUE: '16', + } + assert res.json == expected diff --git a/tests/decorators/validate/test_parameter_environment_variable.py b/tests/decorators/validate/test_parameter_environment_variable.py new file mode 100644 index 00000000..c79b16ff --- /dev/null +++ b/tests/decorators/validate/test_parameter_environment_variable.py @@ -0,0 +1,87 @@ +import os + +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import EnvironmentVariableParameter + + +def test_parameter_environment_variable_str(): + @validate(EnvironmentVariableParameter(name='foo', value_type=str)) + def bar(foo): + return foo + + os.environ['foo'] = '42' + assert bar() == '42' + + +def test_parameter_environment_variable_int(): + @validate(EnvironmentVariableParameter(name='foo', value_type=int)) + def bar(foo): + return foo + + os.environ['foo'] = '42' + assert bar() == 42 + + +def test_parameter_environment_variable_float(): + @validate(EnvironmentVariableParameter(name='foo', value_type=float)) + def bar(foo): + return foo + + os.environ['foo'] = '42.7' + assert bar() == 42.7 + + +def test_parameter_environment_variable_bool(): + @validate(EnvironmentVariableParameter(name='foo', value_type=bool)) + def bar(foo): + return foo + + for value in ['true', 'True', 'TRUE']: + os.environ['foo'] = value + assert bar() is True + + for value in ['false', 'False', 'FALSE']: + os.environ['foo'] = value + assert bar() is False + + for value in ['invalid', 'frue', 'talse']: + os.environ['foo'] = value + + with pytest.raises(expected_exception=ParameterException): + bar() + +def test_parameter_environment_variable_not_set(): + @validate(EnvironmentVariableParameter(name='foo')) + def bar(foo): + return foo + + with pytest.raises(expected_exception=ParameterException): + bar() + + +def test_invalid_value_type(): + with pytest.raises(expected_exception=AssertionError): + @validate(EnvironmentVariableParameter(name='foo', value_type=dict)) + def bar(foo): + return foo + + +def test_parameter_environment_variable_different_name(): + @validate(EnvironmentVariableParameter(name='foo', env_var_name='fuu', value_type=str)) + def bar(foo): + return foo + + os.environ['fuu'] = '42' + assert bar() == '42' + + +def test_two_parameters(): + @validate(EnvironmentVariableParameter(name='a'), strict=False) + def foo(a: float, b: int) -> str: + return f'{a} and {b}' + + os.environ['a'] = '42' + assert foo(b=42) == '42 and 42' diff --git a/tests/decorators/validate/test_validate.py b/tests/decorators/validate/test_validate.py new file mode 100644 index 00000000..ba7ebe86 --- /dev/null +++ b/tests/decorators/validate/test_validate.py @@ -0,0 +1,453 @@ +import os +from datetime import datetime +from typing import Optional, Any + +import pytest + +from pedantic import DateTimeUnixTimestamp +from pedantic.decorators.fn_deco_validate.exceptions import ValidateException, ParameterException, \ +ValidatorException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate, ReturnAs +from pedantic.decorators.fn_deco_validate.parameters import Parameter, EnvironmentVariableParameter +from pedantic.decorators.fn_deco_validate.validators import MaxLength, Min, Max, Email, Validator + + +def test_single_validator(): + validator = MaxLength(3) + converted_value = validator.validate(value='hed') + assert 'hed' == converted_value + + with pytest.raises(expected_exception=ValidatorException) as ex: + validator.validate(value='hello world') + + expected_error_msg = 'MaxLength: hello world is too long with length 11. Value: hello world' + assert str(ex.value) == expected_error_msg + + +def test_single_parameter(): + parameter = Parameter(name='x', validators=[MaxLength(3)]) + converted_value = parameter.validate(value='hed') + assert 'hed' == converted_value + + with pytest.raises(expected_exception=ParameterException): + parameter.validate(value='hello world') + + +def test_multiple_parameters(): + @validate( + Parameter(name='a', validators=[Min(3)]), + Parameter(name='b', validators=[Max(3)]), + Parameter(name='c', validators=[Max(43)]), + ) + def bar(a, b, c): + return a + b + c + + assert bar(3, 3, 5) == 11 + assert bar(a=3, b=3, c=5) == 11 + + +def test_validate_args(): + @validate( + Parameter(name='a', validators=[Min(42, include_boundary=False)]), + Parameter(name='b', validators=[Min(42, include_boundary=False)]), + Parameter(name='c', validators=[Min(42, include_boundary=False)]), + ) + def some_calculation(a, b, c): + return a + b + c + + some_calculation(43, 45, 50) + with pytest.raises(expected_exception=ParameterException): + some_calculation(30, 40, 50) + with pytest.raises(expected_exception=ParameterException): + some_calculation(c=30, a=40, b=50) + + +def test_validate_instance_method(): + class MyClass: + @validate( + Parameter(name='x', validators=[Min(1)]), + ) + def some_calculation(self, x: int) -> int: + return x + + @validate( + Parameter(name='x', validators=[Min(1)]), + return_as=ReturnAs.KWARGS_WITHOUT_NONE, + ) + def some_calculation_2(self, x: int) -> int: + return x + + m = MyClass() + m.some_calculation(1) + m.some_calculation(42) + + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(0) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(-42) + + m.some_calculation_2(1) + m.some_calculation_2(42) + + with pytest.raises(expected_exception=ParameterException): + m.some_calculation_2(0) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation_2(-42) + + +def test_validate_static_method(): + """ The @staticmethod decorator have to be ABOVE the @validate decorator. """ + + class MyClass: + @staticmethod + @validate( + Parameter(name='x', validators=[Min(1)]), + ) + def some_calculation(x: int) -> int: + return x + + m = MyClass() + m.some_calculation(1) + m.some_calculation(42) + + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(0) + with pytest.raises(expected_exception=ParameterException): + m.some_calculation(-42) + + +def test_less_parameter_than_arguments(): + @validate( + Parameter(name='b'), + strict=False, + ) + def some_calculation(a, b, c): + return a + b + c + + some_calculation(43, 0, -50) + + with pytest.raises(expected_exception=ValidateException): + some_calculation(30, None, 50) + + +def test_empty_parameter_kwargs_with_none(): + @validate( + Parameter(name='a', required=False), + Parameter(name='b', required=True), + Parameter(name='c', required=False), + return_as=ReturnAs.KWARGS_WITH_NONE + ) + def some_calculation(a, b, c): + return str(a) + str(b) + str(c) + + assert some_calculation(43, 0, -50) == '430-50' + assert some_calculation(None, 0, None) == 'None0None' + + +def test_empty_parameter_kwargs_without_none(): + @validate( + Parameter(name='a', required=False), + Parameter(name='b', required=True), + Parameter(name='c', required=False), + return_as=ReturnAs.KWARGS_WITHOUT_NONE + ) + def some_calculation(a: Optional[int] = 1, b: Optional[int] = 2, c: Optional[int] = 3): + return str(a) + str(b) + str(c) + + assert some_calculation(43, 0, -50) == '430-50' + assert some_calculation(None, 0, None) == '103' + + +def test_required(): + @validate( + Parameter(name='a', required=True), + Parameter(name='b', required=True), + Parameter(name='c', required=True), + ) + def some_calculation(a, b, c): + return a + b + c + + assert some_calculation(43, 0, -50) == -7 + + with pytest.raises(expected_exception=ValidateException): + some_calculation(30, None, 50) + + +def test_call_with_args(): + @validate( + Parameter(name='x', validators=[Min(1)]), + ) + def some_calculation(x: int) -> int: + return x + + assert some_calculation(42) == 42 + + +def test_external_parameter_accepts_value_when_given(): + @validate(EnvironmentVariableParameter(name='foo')) + def bar(foo): + return foo + + assert bar('42') == '42' + assert bar(foo='42') == '42' + + +def test_external_parameter_ignores_value_when_given(): + @validate(EnvironmentVariableParameter(name='foo'), ignore_input=True) + def bar(foo): + return foo + + os.environ['foo'] = '1' + + assert bar('42') == '1' + assert bar(foo='42') == '1' + + +def test_external_parameter_mixed_with_normal_parameter(): + @validate( + EnvironmentVariableParameter(name='foo'), + Parameter(name='footer'), + return_as=ReturnAs.KWARGS_WITHOUT_NONE, + ) + def bar(foo, footer): + return foo, footer + + assert bar('42', 3) == ('42', 3) + + os.environ['foo'] = '42' + assert bar(footer=3) == ('42', 3) + + +def test_too_many_arguments(): + @validate( + Parameter(name='x'), + ) + def bar(x): + return x + + assert bar(42) == 42 + + with pytest.raises(expected_exception=ValidateException): + bar(42, 43) + + +def test_unexpected_parameter_strict(): + @validate(Parameter(name='y')) + def bar(x): + return x + + with pytest.raises(expected_exception=ValidateException): + bar(42) + with pytest.raises(expected_exception=ValidateException): + bar(x=42) + + +def test_unexpected_parameter_not_strict(): + @validate(Parameter(name='y'), strict=False) + def bar(x): + return x + + with pytest.raises(expected_exception=ValidateException): + bar(42) + + with pytest.raises(expected_exception=ValidateException): + bar(x=42) + + +def test_unexpected_parameter_not_strict_external(): + @validate(EnvironmentVariableParameter(name='foo')) + def bar(x): + return x + + with pytest.raises(expected_exception=ValidateException): + bar(42) + + with pytest.raises(expected_exception=ValidateException): + bar(x=42) + + +def test_return_as_simple(): + @validate(Parameter(name='x'), return_as=ReturnAs.ARGS) + def bar(x): + return x + + assert bar(42) == 42 + assert bar(x=42) == 42 + + +def test_return_as_args(): + @validate(Parameter(name='x'), return_as=ReturnAs.ARGS) + def bar(*args, **kwargs): + return args, kwargs + + assert bar(42) == ((42,), {}) + assert bar(x=42) == ((42,), {}) + + +def test_return_as_kwargs_with_none(): + @validate(Parameter(name='x'), return_as=ReturnAs.KWARGS_WITH_NONE) + def bar(*args, **kwargs): + return args, kwargs + + assert bar(42) == ((), {'x': 42}) + assert bar(x=42) == ((), {'x': 42}) + + +def test_return_as_kwargs_without_none(): + @validate(Parameter(name='x'), return_as=ReturnAs.KWARGS_WITHOUT_NONE) + def bar(*args, **kwargs): + return args, kwargs + + assert bar(42) == ((), {'x': 42}) + assert bar(x=42) == ((), {'x': 42}) + + +def test_return_as_args_advanced(): + @validate( + Parameter(name='a'), + Parameter(name='b'), + Parameter(name='c'), + return_as=ReturnAs.ARGS, + ) + def bar(a, b, *args, **kwargs): + return a, b, args, kwargs + + bar(a=1, b=3, c=42) + bar(1, 3, 4) + bar(1, 3, c=4) + +def test_return_as_args_advanced_different_order(): + @validate( + Parameter(name='c'), + Parameter(name='a'), + Parameter(name='b'), + return_as=ReturnAs.ARGS, + ) + def bar(a, b, *args, **kwargs): + return a, b, args, kwargs + + assert bar(a=1, b=3, c=42) == (1, 3, (42,), {}) + assert bar(1, 3, 42) == (1, 3, (42,), {}) + assert bar(1, 3, c=42) == (42, 1, (3,), {}) + + +def test_return_multiple_args(): + @validate( + Parameter(name='c'), + Parameter(name='a'), + Parameter(name='b'), + return_as=ReturnAs.KWARGS_WITH_NONE, + ) + def bar(*args, **kwargs): + return args, kwargs + + assert bar(a=1, b=3, c=42) == ((), {'a': 1, 'b': 3, 'c': 42}) + assert bar(1, 3, 42) == ((), {'a': 3, 'b': 42, 'c': 1}) + assert bar(1, 3, c=42) == ((), {'a': 1, 'b': 3, 'c': 42}) + + +def test_none_is_not_validated_if_not_required_kwargs_with_none(): + @validate(Parameter(name='a', validators=[Email()], required=False), return_as=ReturnAs.KWARGS_WITH_NONE) + def bar(a: Optional[str]): + return a + + assert bar(a=None) is None + assert bar(None) is None + + with pytest.raises(expected_exception=ParameterException): + bar('no_email') + + +def test_none_is_not_validated_if_not_required_kwargs_without_none(): + @validate(Parameter(name='a', validators=[Email()], required=False), return_as=ReturnAs.KWARGS_WITHOUT_NONE) + def bar(a: Optional[str] = None): + return a + + assert bar(a=None) is None + assert bar(None) is None + + with pytest.raises(expected_exception=ParameterException): + bar('no_email') + + +def test_allow_renaming_of_parameter_of_custom_validator(): + class MyCustomValidator(Validator): + def validate(self, i_renamed_this_arg: Any) -> Any: + return i_renamed_this_arg + + @validate(Parameter(name='a', validators=[MyCustomValidator()])) + def bar(a: int): + return a + + assert bar(42) == 42 + assert bar(a=42) == 42 + + +def test_none_is_removed_for_not_required_parameter(): + @validate(Parameter(name='a', required=False)) + def bar(a: int = 42): + return a + + assert bar() == 42 + assert bar(a=2) == 2 + assert bar(2) == 2 + + +def test_default_value_is_not_validated_internal_parameter(): + t = datetime(year=2021, month=11, day=24) + unix_timestamp = (t - datetime(year=1970, month=1, day=1)).total_seconds() + + @validate(Parameter(name='a', required=False, default=t, validators=[DateTimeUnixTimestamp()])) + def bar(a: datetime) -> datetime: + return a + + assert bar(a=unix_timestamp) == t + assert bar() == t + + +def test_no_default_value(): + @validate(Parameter(name='a', required=False)) + def bar(a: datetime) -> datetime: + return a + + with pytest.raises(expected_exception=ValidateException): + bar() + + +def test_default_value_is_not_validated_external_parameter(): + t = datetime(year=2021, month=11, day=24) + + if 'a' in os.environ: + del os.environ['a'] + + @validate(EnvironmentVariableParameter(name='a', default=t, validators=[DateTimeUnixTimestamp()], required=False)) + def bar(a: datetime) -> datetime: + return a + + assert bar() == t + + +@pytest.mark.asyncio +async def test_async_instance_method(): + class Foo: + @validate(Parameter(name='k', value_type=int, validators=[Min(42)]), + return_as=ReturnAs.KWARGS_WITHOUT_NONE) + async def bar(self, k): + return k + + @validate(Parameter(name='k', value_type=int, validators=[Min(42)]), return_as=ReturnAs.ARGS) + async def bar_2(self, k): + return k + + f = Foo() + res = await f.bar(k=42) + assert res == 42 + + with pytest.raises(expected_exception=ParameterException): + await f.bar(k=41) + + res = await f.bar_2(k=42) + assert res == 42 + + with pytest.raises(expected_exception=ParameterException): + await f.bar_2(k=41) diff --git a/tests/decorators/validate/test_validator_composite.py b/tests/decorators/validate/test_validator_composite.py new file mode 100644 index 00000000..b80c5dae --- /dev/null +++ b/tests/decorators/validate/test_validator_composite.py @@ -0,0 +1,22 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import Composite, Max, Min + + +def test_validator_composite(): + @validate(Parameter(name='x', validators=[Composite([Min(3), Max(5)])])) + def foo(x): + return x + + assert foo(3) == 3 + assert foo(4) == 4 + assert foo(5) == 5 + + with pytest.raises(expected_exception=ParameterException): + foo(5.0001) + + with pytest.raises(expected_exception=ParameterException): + foo(2.9999) diff --git a/tests/decorators/validate/test_validator_datetime_unix_timestamp.py b/tests/decorators/validate/test_validator_datetime_unix_timestamp.py new file mode 100644 index 00000000..eac55138 --- /dev/null +++ b/tests/decorators/validate/test_validator_datetime_unix_timestamp.py @@ -0,0 +1,31 @@ +from datetime import datetime + +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import DateTimeUnixTimestamp + + +def test_validator_datetime_unix_timestamp(): + @validate(Parameter(name='x', validators=[DateTimeUnixTimestamp()])) + def foo(x): + return x + + now = datetime.now() + unix_timestamp = (now - datetime(year=1970, month=1, day=1)).total_seconds() + assert foo(unix_timestamp) == now + assert foo(str(unix_timestamp)) == now + + with pytest.raises(expected_exception=ParameterException): + foo('12.12.2020') + + with pytest.raises(expected_exception=ParameterException): + foo('invalid') + + with pytest.raises(expected_exception=ParameterException): + foo({'a': 1}) + + with pytest.raises(expected_exception=ParameterException): + foo(unix_timestamp * 1000) diff --git a/tests/decorators/validate/test_validator_email.py b/tests/decorators/validate/test_validator_email.py new file mode 100644 index 00000000..3d68ba2d --- /dev/null +++ b/tests/decorators/validate/test_validator_email.py @@ -0,0 +1,28 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import Email + + +def test_validator_email(): + @validate(Parameter(name='x', validators=[Email()])) + def foo(x): + return x + + for value in ['fred@web.de', 'genial@gmail.com', 'test@test.co.uk']: + assert foo(value) == value + + for value in ['fred', 'fred@web', 'fred@w@eb.de', 'fred@@web.de', 'invalid@invalid']: + with pytest.raises(expected_exception=ParameterException): + foo(value) + + +def test_validator_email_converts_to_lower_case(): + @validate(Parameter(name='x', validators=[Email(post_processor=lambda x: x.lower())])) + def foo(x): + return x + + for value in ['Fred@Web.de', 'GENIAL@GMAIL.com', 'test@test.CO.UK']: + assert foo(value) == value.lower() diff --git a/tests/decorators/validate/test_validator_for_each.py b/tests/decorators/validate/test_validator_for_each.py new file mode 100644 index 00000000..082fd6a3 --- /dev/null +++ b/tests/decorators/validate/test_validator_for_each.py @@ -0,0 +1,30 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import ForEach, Min, Max + + +def test_validator_for_each_single_child(): + @validate(Parameter(name='x', validators=[ForEach(Min(3))])) + def foo(x): + return x + + assert foo([3, 4, 5]) == [3, 4, 5] + + for value in [42, [3, 2, 5]]: + with pytest.raises(expected_exception=ParameterException): + foo(value) + + +def test_validator_for_each_multiple_children(): + @validate(Parameter(name='x', validators=[ForEach([Min(3), Max(4)])])) + def foo(x): + return x + + assert foo([3, 4]) == [3, 4] + + for value in [42, [3, 2, 5]]: + with pytest.raises(expected_exception=ParameterException): + foo(value) diff --git a/tests/decorators/validate/test_validator_is_enum.py b/tests/decorators/validate/test_validator_is_enum.py new file mode 100644 index 00000000..646f5843 --- /dev/null +++ b/tests/decorators/validate/test_validator_is_enum.py @@ -0,0 +1,78 @@ +from enum import Enum, IntEnum + +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import IsEnum + + +class MyEnum(Enum): + RED = 'RED' + BLUE = 'BLUE' + + +class MyIntEnum(IntEnum): + RED = 1 + BLUE = 2 + + +def test_validator_is_enum_convert_true(): + @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=True)])) + def foo(x): + return x + + assert foo('RED') == MyEnum.RED + assert foo('BLUE') == MyEnum.BLUE + + for value in ['fred', 1, 'GREEN']: + with pytest.raises(expected_exception=ParameterException): + foo(value) + + +def test_validator_is_enum_int_enum_convert_true(): + @validate(Parameter(name='x', validators=[IsEnum(MyIntEnum, convert=True)])) + def foo(x): + return x + + assert foo('1') == MyIntEnum.RED + assert foo('2') == MyIntEnum.BLUE + assert foo(1) == MyIntEnum.RED + assert foo(2) == MyIntEnum.BLUE + + for value in ['fred', 3, 'GREEN']: + with pytest.raises(expected_exception=ParameterException): + foo(value) + + +def test_validator_is_enum_convert_false(): + @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) + def foo(x): + return x + + assert foo('RED') == 'RED' + assert foo('BLUE') == 'BLUE' + + for value in ['fred', 1, 'GREEN']: + with pytest.raises(expected_exception=ParameterException): + foo(value) + + +def test_validator_is_enum_to_upper_case(): + @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False)])) + def foo(x): + return x + + assert foo('red') == 'RED' + assert foo('blue') == 'BLUE' + assert foo('bLUe') == 'BLUE' + + +def test_validator_is_enum_to_upper_case_disabled(): + @validate(Parameter(name='x', validators=[IsEnum(MyEnum, convert=False, to_upper_case=False)])) + def foo(x): print(x) + + for value in ['red', 'blue', 'Red', 'bLUe']: + with pytest.raises(expected_exception=ParameterException): + foo(value) diff --git a/tests/decorators/validate/test_validator_is_uuid.py b/tests/decorators/validate/test_validator_is_uuid.py new file mode 100644 index 00000000..fe865c4f --- /dev/null +++ b/tests/decorators/validate/test_validator_is_uuid.py @@ -0,0 +1,35 @@ +from uuid import uuid1, uuid3, uuid4, uuid5 + +import pytest + +from pedantic import ForEach +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import IsUuid + + +def test_validator_is_uuid(): + @validate(Parameter(name='x', validators=[IsUuid()], required=False)) + def foo(x): + return x + + for id_ in [str(uuid1()), str(uuid3(uuid1(), 'b')), str(uuid4()), str(uuid5(uuid1(), 'b'))]: + assert foo(id_) == id_ + + for no_id in ['invalid', 12]: + with pytest.raises(expected_exception=ParameterException): + foo(no_id) + + +def test_validator_is_uuid_with_for_each_and_none_value(): + @validate(Parameter(name='x', validators=[ForEach(IsUuid())])) + def foo(x): + return x + + uuid = str(uuid1()) + assert foo([]) == [] + assert foo([uuid]) == [uuid] + + with pytest.raises(expected_exception=ParameterException): + foo([None]) diff --git a/tests/decorators/validate/test_validator_match_pattern.py b/tests/decorators/validate/test_validator_match_pattern.py new file mode 100644 index 00000000..99a79c73 --- /dev/null +++ b/tests/decorators/validate/test_validator_match_pattern.py @@ -0,0 +1,21 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import MatchPattern + + +def test_validator_match_pattern(): + pattern = r'^(([01]\d|2[0-3]):([0-5]\d)|24:00)$' + + @validate(Parameter(name='x', validators=[MatchPattern(pattern)])) + def foo(x): + return x + + for value in ['00:00', '02:45', '14:59', '23:59', '24:00']: + assert foo(value) == value + + for value in ['00:70', '24:01', '30:00', 'invalid']: + with pytest.raises(expected_exception=ParameterException): + foo(value) diff --git a/tests/decorators/validate/test_validator_max.py b/tests/decorators/validate/test_validator_max.py new file mode 100644 index 00000000..82cad490 --- /dev/null +++ b/tests/decorators/validate/test_validator_max.py @@ -0,0 +1,35 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators.max import Max + + +def test_validator_max_length_include_boundary_true(): + @validate(Parameter(name='x', validators=[Max(3, include_boundary=True)])) + def foo(x): + return x + + assert foo(3) == 3 + assert foo(2) == 2 + + with pytest.raises(expected_exception=ParameterException): + foo(4) + + with pytest.raises(expected_exception=ParameterException): + foo(3.001) + +def test_validator_max_length_include_boundary_false(): + @validate(Parameter(name='x', validators=[Max(3, include_boundary=False)])) + def foo(x): + return x + + assert foo(2.9999) == 2.9999 + assert foo(2) == 2 + + with pytest.raises(expected_exception=ParameterException): + foo(4) + + with pytest.raises(expected_exception=ParameterException): + foo(3) diff --git a/tests/decorators/validate/test_validator_max_length.py b/tests/decorators/validate/test_validator_max_length.py new file mode 100644 index 00000000..bd7f2a4d --- /dev/null +++ b/tests/decorators/validate/test_validator_max_length.py @@ -0,0 +1,31 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import MaxLength + + +def test_validator_max_length(): + @validate(Parameter(name='x', validators=[MaxLength(3)])) + def foo(x): + return x + + assert foo('hi') == 'hi' + assert foo('hi!') == 'hi!' + assert foo([1, 2, 3]) == [1, 2, 3] + + with pytest.raises(expected_exception=ParameterException) as err: + foo('hi!!') + + assert err.value.message == 'hi!! is too long with length 4.' + + with pytest.raises(expected_exception=ParameterException) as err: + foo([1, 2, 3, 4]) + + assert err.value.message == '[1, 2, 3, 4] is too long with length 4.' + + with pytest.raises(expected_exception=ParameterException) as err: + foo(42) + + assert err.value.message == '42 has no length.' diff --git a/tests/decorators/validate/test_validator_min.py b/tests/decorators/validate/test_validator_min.py new file mode 100644 index 00000000..056692cd --- /dev/null +++ b/tests/decorators/validate/test_validator_min.py @@ -0,0 +1,36 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import Min + + +def test_validator_min_length_include_boundary_true(): + @validate(Parameter(name='x', validators=[Min(3, include_boundary=True)])) + def foo(x): + return x + + assert foo(3) == 3 + assert foo(4) == 4 + + with pytest.raises(expected_exception=ParameterException): + foo(2) + + with pytest.raises(expected_exception=ParameterException): + foo(2.9999) + + +def test_validator_min_length_include_boundary_false(): + @validate(Parameter(name='x', validators=[Min(3, include_boundary=False)])) + def foo(x): + return x + + assert foo(3.0001) == 3.0001 + assert foo(4) == 4 + + with pytest.raises(expected_exception=ParameterException): + foo(2) + + with pytest.raises(expected_exception=ParameterException): + foo(3) diff --git a/tests/decorators/validate/test_validator_min_length.py b/tests/decorators/validate/test_validator_min_length.py new file mode 100644 index 00000000..7c9eac79 --- /dev/null +++ b/tests/decorators/validate/test_validator_min_length.py @@ -0,0 +1,31 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import MinLength + + +def test_validator_min_length(): + @validate(Parameter(name='x', validators=[MinLength(3)])) + def foo(x): + return x + + assert foo('hi!') == 'hi!' + assert foo('hello') == 'hello' + assert foo([1, 2, 3]) == [1, 2, 3] + + with pytest.raises(expected_exception=ParameterException) as err: + foo('hi') + + assert err.value.message == 'hi is too short with length 2.' + + with pytest.raises(expected_exception=ParameterException) as err: + foo([1, 2]) + + assert err.value.message == '[1, 2] is too short with length 2.' + + with pytest.raises(expected_exception=ParameterException) as err: + foo(42) + + assert err.value.message == '42 has no length.' diff --git a/tests/decorators/validate/test_validator_not_empty.py b/tests/decorators/validate/test_validator_not_empty.py new file mode 100644 index 00000000..2f5b8c0a --- /dev/null +++ b/tests/decorators/validate/test_validator_not_empty.py @@ -0,0 +1,20 @@ +import pytest + +from pedantic.decorators.fn_deco_validate.exceptions import ParameterException +from pedantic.decorators.fn_deco_validate.fn_deco_validate import validate +from pedantic.decorators.fn_deco_validate.parameters import Parameter +from pedantic.decorators.fn_deco_validate.validators import NotEmpty + + +def test_validator_not_empty(): + @validate(Parameter(name='x', validators=[NotEmpty()])) + def foo(x): + return x + + assert foo('hi') == 'hi' + assert foo(' hi ') == 'hi' + assert foo([1]) == [1] + + for value in ['', ' ', [], {}, set()]: + with pytest.raises(expected_exception=ParameterException): + foo(value) diff --git a/tests/mixins/__init__.py b/tests/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/mixins/test_generic_mixin.py b/tests/mixins/test_generic_mixin.py new file mode 100644 index 00000000..b1c44f2f --- /dev/null +++ b/tests/mixins/test_generic_mixin.py @@ -0,0 +1,170 @@ +from typing import TypeVar, Generic, List, Type + +import pytest + +from pedantic import GenericMixin + +A = TypeVar('A') +E = TypeVar('E') +S = TypeVar('S') +T = TypeVar('T') +U = TypeVar('U') + + +def test_single_type_var(): + class Foo(Generic[T], GenericMixin): + value: T + + foo = Foo[str]() + assert foo.type_var == str + assert foo.type_vars == {T: str} + + invalid = Foo() + + with pytest.raises(expected_exception=AssertionError) as err: + invalid.type_var + + assert f'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.value.args[0] + + +def test_multiple_type_vars(): + class Foo(Generic[T, U], GenericMixin): + value: T + values: List[U] + + foo = Foo[str, int]() + + with pytest.raises(expected_exception=AssertionError) as err: + foo.type_var + + assert 'You have multiple type parameters. Please use "type_vars" instead of "type_var".' in err.value.args[0] + + assert foo.type_vars == {T: str, U: int} + + invalid = Foo() + + with pytest.raises(expected_exception=AssertionError) as err: + invalid.type_var + + assert 'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.value.args[0] + + +def test_non_generic_class(): + class Foo(GenericMixin): + value: int + + invalid = Foo() + + with pytest.raises(expected_exception=AssertionError) as err: + invalid.type_var + + assert err.value.args[0] == 'Foo is not a generic class. To make it generic, declare it like: class Foo(Generic[T], GenericMixin):...' + + +def test_call_type_var_in_constructor(): + class Foo(Generic[T], GenericMixin): + def __init__(self) -> None: + self.x = self.type_var() + + with pytest.raises(expected_exception=AssertionError) as err: + Foo[str]() + + assert 'make sure that you do not call this in the __init__() method' in err.value.args[0] + + +def test_subclass_set_type_variable(): + class Gen(Generic[T], GenericMixin): + def __init__(self, value: T) -> None: + self.value = value + + def get_type(self) -> dict[TypeVar, Type]: + return self.type_vars + + class MyClass(Gen[int]): + pass + + bar = Gen[int](value=4) + assert bar.get_type() == {T: int} + + foo = MyClass(value=4) + assert foo.get_type() == {T: int} + + +def test_subclass_with_multiple_parents(): + class Gen(Generic[T], GenericMixin): + def __init__(self, value: T) -> None: + self.value = value + + def get_type(self) -> dict[TypeVar, Type]: + return self.type_vars + + class MyMixin: + value = 42 + + class MyClass(MyMixin, Gen[int]): + pass + + bar = Gen[int](value=4) + assert bar.get_type() == {T: int} + + foo = MyClass(value=4) + assert foo.get_type() == {T: int} + + +def test_resolved_type_var_inheritance(): + class Foo(Generic[T]): ... + + class Bar(Foo[int], Generic[U], GenericMixin): ... + + bar = Bar[str]() + assert bar.type_vars == {T: int, U: str} + +def test_resolved_type_var_inheritance_2(): + class Foo(Generic[T], GenericMixin): ... + + class Bar(Foo[int], Generic[U]): ... + + bar = Bar[str]() + assert bar.type_vars == {T: int, U: str} + +def test_very_complex_inheritance(): + class Foo(Generic[E], GenericMixin): ... + class Bar(Foo[int], Generic[S]): ... + class Baz(Foo[int]): ... + class Deep(Baz): ... + class Deeper(Baz, Generic[T]): ... + + foo = Foo[str]() + actual = foo.type_vars + assert actual == {E: str} + + bar = Bar[str]() + actual = bar.type_vars + assert actual == {E: int, S: str} + + baz = Baz() + actual = baz.type_vars + assert actual == {E: int} + + deep = Deep() + actual = deep.type_vars + assert actual == {E: int} + + deeper = Deeper[bool]() + actual = deeper.type_vars + assert actual == {E: int, T: bool} + + with pytest.raises(expected_exception=AssertionError) as err: + Foo().type_vars + + assert 'You need to instantiate this class with type parameters! Example: Foo[int]()' in err.value.args[0] + + +def test_substitution_lookup_hits(): + class Base(Generic[A], GenericMixin): ... + class Mid(Base[A], Generic[A]): ... + class Final(Mid[int]): ... + + obj = Final() + actual = obj.type_vars + assert actual == {A: int} diff --git a/tests/mixins/test_with_decorated_methods.py b/tests/mixins/test_with_decorated_methods.py new file mode 100644 index 00000000..27af82ba --- /dev/null +++ b/tests/mixins/test_with_decorated_methods.py @@ -0,0 +1,127 @@ +from functools import wraps +from typing import TypeVar, Generic, NoReturn + +from pedantic import DecoratorType, create_decorator, WithDecoratedMethods + +T = TypeVar('T') + +class Decorators(DecoratorType): + FOO = '_foo' + BAR = '_bar' + + +foo = create_decorator(decorator_type=Decorators.FOO) +bar = create_decorator(decorator_type=Decorators.BAR) + + +def test_no_decorated_methods(): + class MyClass(WithDecoratedMethods[Decorators]): + pass + + instance = MyClass() + assert instance.get_decorated_functions() == {Decorators.FOO: {}, Decorators.BAR: {}} + + +def test_class_with_bad_property(): + class MyClass(WithDecoratedMethods[Decorators]): + @property + def bad(self) -> NoReturn: + raise RuntimeError('bad man') + + instance = MyClass() + assert instance.get_decorated_functions() == {Decorators.FOO: {}, Decorators.BAR: {}} + + +def test_with_decorated_methods_sync(): + class MyClass(WithDecoratedMethods[Decorators]): + @foo(42) + def m1(self) -> None: + print('bar') + + @foo(value=43) + def m2(self) -> None: + print('bar') + + @bar(value=44) + def m3(self) -> None: + print('bar') + + instance = MyClass() + expected = { + Decorators.FOO: { + instance.m1: 42, + instance.m2: 43, + }, + Decorators.BAR: { + instance.m3: 44, + } + } + assert instance.get_decorated_functions() == expected + + +def test_with_decorated_methods_async(): + class MyClass(WithDecoratedMethods[Decorators]): + @foo(42) + async def m1(self) -> None: + print('bar') + + @foo(value=43) + async def m2(self) -> None: + print('bar') + + @bar(value=44) + async def m3(self) -> None: + print('bar') + + instance = MyClass() + expected = { + Decorators.FOO: { + instance.m1: 42, + instance.m2: 43, + }, + Decorators.BAR: { + instance.m3: 44, + } + } + assert instance.get_decorated_functions() == expected + + +def test_with_custom_transformation(): + def my_transformation(f, decorator_type, value): + assert decorator_type == Decorators.BAR + assert value == 42 + + @wraps(f) + def wrapper(*args, **kwargs): + f(*args, **kwargs) + return 4422 # we add a return value + + return wrapper + + my_decorator = create_decorator(decorator_type=Decorators.BAR, transformation=my_transformation) + + class MyClass(WithDecoratedMethods[Decorators]): + @my_decorator(42) + def m1(self) -> int: + return 1 + + instance = MyClass() + expected = { + Decorators.BAR: { + instance.m1: 42, + }, + Decorators.FOO: {}, + } + assert instance.get_decorated_functions() == expected + + assert instance.m1() == 4422 # check that transformation was applied + + +def test_with_decorated_methods_can_have_generic_child_class(): + class MyClass(Generic[T], WithDecoratedMethods[Decorators]): + @foo(42) + def m1(self) -> None: ... + + instance = MyClass[int]() + actual = instance.get_decorated_functions() + assert actual == {Decorators.FOO: {instance.m1: 42}, Decorators.BAR: {}} diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/models/test_decorated_function.py b/tests/models/test_decorated_function.py new file mode 100644 index 00000000..969a50fc --- /dev/null +++ b/tests/models/test_decorated_function.py @@ -0,0 +1,142 @@ +from pedantic.models.decorated_function import DecoratedFunction + + +def test_static_method(): + def f_1(): pass + + deco_f = DecoratedFunction(f_1) + assert deco_f.is_static_method is False + + class MyClass: + def f_1(self): pass + + @staticmethod + def f_2(): pass + + @classmethod + def f_3(cls): pass + + deco_f_1 = DecoratedFunction(MyClass.f_1) + deco_f_2 = DecoratedFunction(MyClass.f_2) + deco_f_3 = DecoratedFunction(MyClass.f_3) + + assert deco_f_1.is_static_method is False + assert deco_f_2.is_static_method is True + assert deco_f_3.is_static_method is False + + +def test_function_wants_args(): + def f_1(*args, **kwargs): pass + + def f_2(a, b, *args, **kwargs): pass + + def f_3(a, b, *args): pass + + def f_4(*args): pass + + def f_5(): pass + + assert DecoratedFunction(f_1).wants_args is True + assert DecoratedFunction(f_2).wants_args is True + assert DecoratedFunction(f_3).wants_args is True + assert DecoratedFunction(f_4).wants_args is True + assert DecoratedFunction(f_5).wants_args is False + + class MyClass: + def f(self): pass + + @staticmethod + def g(): pass + + assert DecoratedFunction(MyClass.f).wants_args is False + assert DecoratedFunction(MyClass.g).wants_args is False + + +def test_is_property_setter(): + def f_1(): pass + + assert DecoratedFunction(f_1).is_property_setter is False + + class MyClass: + _h = 42 + + def f_1(self): pass + + @staticmethod + def f_2(): pass + + assert DecoratedFunction(MyClass.f_1).is_property_setter is False + assert DecoratedFunction(MyClass.f_2).is_property_setter is False + + +def test_wants_kwargs(): + def f_1(*args, **kwargs): pass + + def f_2(a, b, *args, **kwargs): pass + + def f_3(a, b, *args): pass + + def f_4(*args): pass + + def f_5(): pass + + def f_6(a, b, c): pass + + assert DecoratedFunction(f_1).should_have_kwargs is False + assert DecoratedFunction(f_2).should_have_kwargs is False + assert DecoratedFunction(f_3).should_have_kwargs is False + assert DecoratedFunction(f_4).should_have_kwargs is False + assert DecoratedFunction(f_5).should_have_kwargs is True + assert DecoratedFunction(f_6).should_have_kwargs is True + + class A: + def f(self): pass + + @staticmethod + def g(): pass + + def __compare__(self, other): pass + + assert DecoratedFunction(A.f).should_have_kwargs is True + assert DecoratedFunction(A.g).should_have_kwargs is True + assert DecoratedFunction(A.__compare__).should_have_kwargs is False + + +def test_instance_method(): + def h(): pass + + assert DecoratedFunction(h).is_instance_method is False + + class A: + def f(self): pass + + @staticmethod + def g(): pass + + assert DecoratedFunction(A.f).is_instance_method is True + assert DecoratedFunction(A.g).is_instance_method is False + + +def test_num_decorators(): + def decorator(f): + return f + + def f_1(): pass + + @decorator + def f_2(): pass + + @decorator + @decorator + def f_3(): pass + + @decorator + @decorator + @decorator + def f_4(): + pass + + assert DecoratedFunction(f_1).num_of_decorators == 0 + assert DecoratedFunction(f_2).num_of_decorators == 1 + assert DecoratedFunction(f_3).num_of_decorators == 2 + assert DecoratedFunction(f_4).num_of_decorators == 3 diff --git a/tests/models/test_generator_wrapper.py b/tests/models/test_generator_wrapper.py new file mode 100644 index 00000000..76ec6774 --- /dev/null +++ b/tests/models/test_generator_wrapper.py @@ -0,0 +1,33 @@ +from typing import Iterator + +import pytest + +from pedantic.models import GeneratorWrapper + + +def test_generator_wrapper(): + def gen_func() -> Iterator[int]: + num = 0 + + while num < 100: + yield num + num += 1 + + generator = gen_func() + + g = GeneratorWrapper( + wrapped=generator, + expected_type=Iterator[int], + err_msg='msg', + type_vars={}, + ) + + print(sum([x for x in g])) + + with pytest.raises(expected_exception=Exception): + g.throw(Exception('error')) + + with pytest.raises(expected_exception=AttributeError): + g.invalid + + g.close() diff --git a/tests/test_environment_variables.py b/tests/test_environment_variables.py new file mode 100644 index 00000000..5166427c --- /dev/null +++ b/tests/test_environment_variables.py @@ -0,0 +1,35 @@ +import pytest + +from pedantic.exceptions import PedanticTypeCheckException +from pedantic.env_var_logic import enable_pedantic, disable_pedantic, is_enabled +from pedantic.decorators.fn_deco_pedantic import pedantic + + +def test_pedantic_enabled(): + enable_pedantic() + + @pedantic + def some_method(): + return 42 + + with pytest.raises(expected_exception=PedanticTypeCheckException): + some_method() + + +def test_pedantic_disabled(): + disable_pedantic() + + @pedantic + def some_method(): + return 42 + + some_method() + + +def test_enable_disable(): + enable_pedantic() + assert is_enabled() is True + disable_pedantic() + assert is_enabled() is False + enable_pedantic() + assert is_enabled() is True diff --git a/tests/test_the_tests.py b/tests/test_the_tests.py new file mode 100644 index 00000000..2335f578 --- /dev/null +++ b/tests/test_the_tests.py @@ -0,0 +1,19 @@ +from pathlib import Path + +def test_all_test_files_follow_pytest_naming_convention(): + tests_dir = Path(__file__).parent + invalid_files = [] + + for path in tests_dir.rglob("*.py"): + name = path.name + + if name in {"__init__.py", "conftest.py"}: + continue + + if not name.startswith("test_"): + invalid_files.append(path.relative_to(tests_dir)) + + assert not invalid_files, ( + "Found Python files in tests/ that do not follow pytest naming conventions:\n" + + "\n".join(str(p) for p in invalid_files) + ) diff --git a/tests/type_checking_logic/__init__.py b/tests/type_checking_logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/type_checking_logic/test_assert_value_matches_type.py b/tests/type_checking_logic/test_assert_value_matches_type.py new file mode 100644 index 00000000..30240ff9 --- /dev/null +++ b/tests/type_checking_logic/test_assert_value_matches_type.py @@ -0,0 +1,216 @@ +from abc import ABC +from dataclasses import dataclass +from datetime import datetime +from typing import Callable, Awaitable, Coroutine, Any, Union, Optional, Generic, TypeVar, Tuple + +import pytest + +from pedantic.exceptions import PedanticTypeCheckException +from pedantic.type_checking_logic.check_types import assert_value_matches_type + + +@dataclass +class Foo: + value: int + + +def test_callable(): + def _cb(foo: Foo) -> str: + return str(foo.value) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., str], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., int], + err='', + type_vars={}, + ) + +def test_callable_return_type_none(): + def _cb(foo: Foo) -> None: + return print(foo) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., None], + err='', + type_vars={}, + ) + +def test_callable_awaitable(): + async def _cb(foo: Foo) -> str: + return str(foo.value) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[str]], + err='', + type_vars={}, + ) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[Any]], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[int]], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., str], + err='', + type_vars={}, + ) + + +def test_callable_coroutine(): + async def _cb(foo: Foo) -> str: + return str(foo.value) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Coroutine[None, None, str]], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., Coroutine[None, None, int]], + err='', + type_vars={}, + ) + + +def test_callable_awaitable_with_none_return_type(): + async def _cb(foo: Foo) -> None: + print(foo) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[None]], + err='', + type_vars={}, + ) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[Any]], + err='', + type_vars={}, + ) + + +def test_callable_with_old_union_type_hint(): + async def _cb(_: str) -> Union[int, None]: + return 42 + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[Union[int, None]]], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[int]], + err='', + type_vars={}, + ) + + +def test_callable_with_new_union_type_hint(): + async def _cb(_: str) -> int | None: + return 42 + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[int | None]], + err='', + type_vars={}, + ) + + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[Any]], + err='', + type_vars={}, + ) + + with pytest.raises(expected_exception=PedanticTypeCheckException): + assert_value_matches_type( + value=_cb, + type_=Callable[..., Awaitable[int]], + err='', + type_vars={}, + ) + + +def test_forward_ref_inheritance(): + T = TypeVar('T') + + class State(Generic[T], ABC): + pass + + class StateMachine(Generic[T], ABC): + pass + + class MachineState(State['MachineStateMachine']): + pass + + class OfflineMachineState(MachineState): + pass + + class MachineStateMachine(StateMachine[MachineState]): + pass + + assert_value_matches_type( + value=OfflineMachineState(), + type_=Optional['MachineState'], + err='', + type_vars={}, + context=locals(), + ) + + +def test_tuple_with_ellipsis(): + """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/75 """ + + assert_value_matches_type( + value=(1, 2.0, 'hello'), + type_=Tuple[Any, ...], + err='', + type_vars={}, + context=locals(), + ) + + +def test_union_of_callable(): + """ Regression test for https://github.com/LostInDarkMath/pedantic-python-decorators/issues/74 """ + + assert_value_matches_type( + value=datetime.now(), + type_=Union[datetime, Callable[[], datetime]], + err='', + type_vars={}, + context=locals(), + ) diff --git a/tests/type_checking_logic/test_resolve_forward_ref.py b/tests/type_checking_logic/test_resolve_forward_ref.py new file mode 100644 index 00000000..003a8879 --- /dev/null +++ b/tests/type_checking_logic/test_resolve_forward_ref.py @@ -0,0 +1,28 @@ +from typing import List, Optional, Union + +import pytest + +from pedantic.type_checking_logic.resolve_forward_ref import resolve_forward_ref + + +def test_resolve_forward_ref_primitive_types(): + assert resolve_forward_ref(type_='int') == int + assert resolve_forward_ref(type_='float') == float + assert resolve_forward_ref(type_='str') == str + assert resolve_forward_ref(type_='bool') == bool + +def test_resolve_forward_ref_typing_types(): + assert resolve_forward_ref(type_='List[int]') == List[int] + assert resolve_forward_ref(type_='Optional[List[Union[str, float]]]') == Optional[List[Union[str, float]]] + +def test_unresolvable_type(): + with pytest.raises(NameError): + resolve_forward_ref(type_='Invalid') + +def test_resolve_forward_ref_custom_class(): + class Foo: + pass + + context = locals() + assert resolve_forward_ref(type_='Foo', context=context) == Foo + assert resolve_forward_ref(type_='Optional[Foo]', context=context) == Optional[Foo] diff --git a/type_hint_parser_dependency_graph.graphml b/type_hint_parser_dependency_graph.graphml deleted file mode 100644 index 81202670..00000000 --- a/type_hint_parser_dependency_graph.graphml +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - _is_instance - - - - - - - - - - - _get_name - - - - - - - - - - - _is_generic - - - - - - - - - - - _is_subtype - - - - - - - - - - - _instancecheck_callable - - - - - - - - - - - _has_required_type_arguments - - - - - - - - - - - _get_type_arguments - - - - - - - - - - - _get_class_of_type_annotation - - - - - - - - - - - _instancecheck_union - - - - - - - - - - - _instancecheck_iterable - - - - - - - - - - - _instancecheck_mapping - - - - - - - - - - - _instancecheck_itemsview - - - - - - - - - - - _instancecheck_tuple - - - - - - - - - - - _is_type_newtype - - - - - - - - - - - _get_base_generic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -