From eff6bc9d71885f07169efdcc4d8984dfa37aec04 Mon Sep 17 00:00:00 2001 From: Willi Sontopski <32729196+LostInDarkMath@users.noreply.github.com> Date: Sat, 13 Jun 2026 10:43:32 +0200 Subject: [PATCH] improve line coverage --- .coveragerc | 6 +---- CHANGELOG.md | 5 ++++ Taskfile.yml | 5 ++-- pedantic/models/decorated_function.py | 2 +- pedantic/type_checking_logic/check_types.py | 17 +++++------- pyproject.toml | 2 +- tests/decorators/pedantic/test_pedantic.py | 29 +++++++++++++++++++++ tests/models/test_decorated_function.py | 8 ++++++ 8 files changed, 55 insertions(+), 19 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8695c6d8..e0b2c1ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,12 +1,8 @@ -[run] -omit = - *pedantic/tests* - */home/travis/virtualenv* - [report] exclude_lines = # Don't complain if non-runnable code isn't run: pragma: no cover except ImportError raise ImportError + raise RuntimeError if __name__ == .__main__.: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e302b6c..f2b5879c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +## Pedantic 3.0.1 +- improve line coverage +- make task `check-changelog` work even before making a commit +- cleanup `.coveragerc` + ## Pedantic 3.0.0 - removed decorator `@count_calls` - removed decorator `@does_same_as_function` diff --git a/Taskfile.yml b/Taskfile.yml index b924a211..641bbaeb 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -101,8 +101,9 @@ tasks: # Fetch latest master if we're in a repo with a remote git fetch origin master >/dev/null 2>&1 || true - if git diff --name-only "$BASE"...HEAD | grep -qx "CHANGELOG.md"; then - echo "✅ CHANGELOG.md was updated." + if git diff --name-only "$BASE"...HEAD | grep -qx "CHANGELOG.md" || + git diff --name-only HEAD | grep -qx "CHANGELOG.md"; then + echo "✅ CHANGELOG.md was updated." else echo "❌ CHANGELOG.md was NOT updated." echo "" diff --git a/pedantic/models/decorated_function.py b/pedantic/models/decorated_function.py index 88c1ded3..2315efa4 100644 --- a/pedantic/models/decorated_function.py +++ b/pedantic/models/decorated_function.py @@ -26,7 +26,7 @@ def __init__(self, func: Callable[..., Any]) -> None: # noqa: D107 self._func = func if not callable(func): - raise PedanticTypeCheckException(f'{self.full_name} should be a method or function.') + raise PedanticTypeCheckException(f'{func} should be a method or function') self._full_arg_spec = inspect.getfullargspec(func) self._signature = inspect.signature(func) diff --git a/pedantic/type_checking_logic/check_types.py b/pedantic/type_checking_logic/check_types.py index 932a6cfb..07edd6b6 100644 --- a/pedantic/type_checking_logic/check_types.py +++ b/pedantic/type_checking_logic/check_types.py @@ -223,8 +223,6 @@ def _is_instance(obj: Any, type_: Any, type_vars: Dict[TypeVar_, Any], context: validator = _ORIGIN_TYPE_CHECKERS[base] return validator(obj, type_args, type_vars, context) - if base.__base__ != typing.Generic: - raise RuntimeError(f'Unknown base: {base}') return isinstance(obj, base) if _is_forward_ref(type_=type_): @@ -249,13 +247,13 @@ def _is_instance(obj: Any, type_: Any, type_vars: Dict[TypeVar_, Any], context: if type_ in {list, set, dict, frozenset, tuple, type}: raise PedanticTypeCheckException('Missing type arguments') - try: - return isinstance(obj, type_) - except TypeError: - if isinstance(type_, _ProtocolMeta): - return True # we do not check this + if isinstance(type_, _ProtocolMeta): + if getattr(type_, '_is_runtime_protocol', False): + return isinstance(obj, type_) + + return True # since this Protocol not has the @runtime_cheable decorator - raise + return isinstance(obj, type_) def _is_forward_ref(type_: Any) -> bool: @@ -713,8 +711,7 @@ def _instancecheck_generator( generator: typing.Generator, type_args: Tuple, _: Dict[TypeVar_, Any], __: Dict[str, Any] | None = None, ) -> bool: from pedantic.models import GeneratorWrapper # noqa: PLC0415 must be local due to circular imports - if not isinstance(generator, GeneratorWrapper): - raise TypeError(generator) + assert isinstance(generator, GeneratorWrapper) # noqa: S101 return (generator.yield_type == type_args[0] and generator.send_type == type_args[1] and generator.return_type == type_args[2]) diff --git a/pyproject.toml b/pyproject.toml index 1f6a0573..1bfe2772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pedantic" -version = "3.0.0" +version = "3.0.1" description = "Some useful Python decorators for cleaner software development." readme = "README.md" requires-python = ">=3.11,<4.0" diff --git a/tests/decorators/pedantic/test_pedantic.py b/tests/decorators/pedantic/test_pedantic.py index 1a081e86..3eb311de 100644 --- a/tests/decorators/pedantic/test_pedantic.py +++ b/tests/decorators/pedantic/test_pedantic.py @@ -39,6 +39,7 @@ TypeVar, TypeVarTuple, Union, + runtime_checkable, ) import pytest @@ -2588,12 +2589,40 @@ class IsDataclass(Protocol): class Foo: v: int + class NoDataClass: + def __init__(self, v: int) -> None: + self.v = v + + @pedantic + def foo(x: IsDataclass) -> IsDataclass: + return x + + foo(x=Foo(v=42)) + foo(x=NoDataClass(v=42)) # raises no error since IsDataclass is not runtime checkable + + +def test_dataclass_runtime_checkable_protocol(): + @runtime_checkable + class IsDataclass(Protocol): + __dataclass_fields__: ClassVar[Dict] + + @dataclass + class Foo: + v: int + + class NoDataClass: + def __init__(self, v: int) -> None: + self.v = v + @pedantic def foo(x: IsDataclass) -> IsDataclass: return x foo(x=Foo(v=42)) + with pytest.raises(expected_exception=PedanticTypeCheckException): + foo(x=NoDataClass(v=42)) + def test_dataclass_protocol_in_type(): class IsDataclass(Protocol): diff --git a/tests/models/test_decorated_function.py b/tests/models/test_decorated_function.py index 969a50fc..2bcd45ed 100644 --- a/tests/models/test_decorated_function.py +++ b/tests/models/test_decorated_function.py @@ -1,3 +1,6 @@ +import pytest + +from pedantic.exceptions import PedanticTypeCheckException from pedantic.models.decorated_function import DecoratedFunction @@ -140,3 +143,8 @@ def f_4(): assert DecoratedFunction(f_2).num_of_decorators == 1 assert DecoratedFunction(f_3).num_of_decorators == 2 assert DecoratedFunction(f_4).num_of_decorators == 3 + + +def test_wrap_obj_which_is_not_callable(): + with pytest.raises(expected_exception=PedanticTypeCheckException, match='42 should be a method or function'): + DecoratedFunction(func=42)