From 556202aee844515f3461e1f839e0dcedac5bbf19 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Wed, 23 Apr 2025 18:37:38 +0200 Subject: [PATCH 01/28] annotations tests --- pyproject.toml | 1 + src/contractme/__init__.py | 1 + src/contractme/contracting.py | 4 ++++ tests/test_contracting.py | 42 ++++++++++++++++++++++++++++++++++- uv.lock | 15 ++++++++++++- 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3bbb68f..2c25520 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = [ ] requires-python = ">=3.13" dependencies = [ + "annotated-types>=0.7.0", "pyright>=1.1.397", ] diff --git a/src/contractme/__init__.py b/src/contractme/__init__.py index c08fb73..d0aaad1 100644 --- a/src/contractme/__init__.py +++ b/src/contractme/__init__.py @@ -7,3 +7,4 @@ check_postconditions = contracting.check_postconditions precondition = contracting.precondition postcondition = contracting.postcondition +annotations = contracting.annotations diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index e26da7a..667a5d3 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -314,4 +314,8 @@ def postcondition(conditional, message=None): return condition_decorator(False, conditional, message) +def annotations(f, message=None): + return f + + never_returns = postcondition(lambda: False, "should never return") diff --git a/tests/test_contracting.py b/tests/test_contracting.py index b0d7986..a4b9061 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -1,8 +1,10 @@ import pytest import contractme from epycs.subprocess import cmd -from contractme import precondition, postcondition +from contractme import precondition, postcondition, annotations import warnings +from typing import Annotated +from annotated_types import Ge @pytest.fixture @@ -233,3 +235,41 @@ def test_cannot_have_precondition_of_a_constructor(): @precondition(lambda self: True) def __init__(self): # pragma: no cover pass + + +Age = Annotated[int, Ge(1)] + + +def test_annotated_types_ok(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + assert birthday(1) == 2 + + +def test_annotated_types_precondition(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + with pytest.raises(AssertionError): + birthday(0) + + +def test_annotated_types_postcondition(): + @annotations + def antibirthday(age: Age) -> Age: + return age - 1 + + with pytest.raises(AssertionError): + antibirthday(1) + + +def test_annotated_types_int(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + with pytest.raises(AssertionError): + birthday(1.0) diff --git a/uv.lock b/uv.lock index a9f36ed..4a45ace 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 1 requires-python = ">=3.13" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -16,6 +25,7 @@ name = "contractme" version = "1.1.0" source = { editable = "." } dependencies = [ + { name = "annotated-types" }, { name = "pyright" }, ] @@ -27,7 +37,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "pyright", specifier = ">=1.1.397" }] +requires-dist = [ + { name = "annotated-types", specifier = ">=0.7.0" }, + { name = "pyright", specifier = ">=1.1.397" }, +] [package.metadata.requires-dev] dev = [ -- GitLab From c50599735863e9567305ffb72c90994662259391 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Wed, 23 Apr 2025 19:23:23 +0200 Subject: [PATCH 02/28] partial pass, more test --- src/contractme/contracting.py | 53 +++++++++++++++++++++++++++-------- tests/test_contracting.py | 11 +++++++- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 667a5d3..62dfdfe 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -1,7 +1,7 @@ import inspect import copy from warnings import warn -from typing import Callable +from typing import Callable, get_type_hints import functools @@ -162,14 +162,25 @@ def get_kw_for_conditional_before(cond_type, conditional, in_kw): assert callable(conditional) out_kw = {} argspec = inspect.getfullargspec(conditional) - for nm in argspec.args: - if nm == "result": - assert cond_type == POST, f"only postconditions can use {nm!r}" - elif nm == "old": - assert cond_type == POST, f"only postconditions can use {nm!r}" - out_kw[nm] = OldHolder(in_kw) - else: - out_kw[nm] = in_kw[nm] + + if argspec.varargs: + raise NotImplemented("*args in conditions") + + if argspec.varkw is not None: + if argspec.args: + raise NotImplemented("mixing args and **kw in conditions") + + out_kw = in_kw + else: + for nm in argspec.args: + if nm == "result": + assert cond_type == POST, f"only postconditions can use {nm!r}" + elif nm == "old": + assert cond_type == POST, f"only postconditions can use {nm!r}" + out_kw[nm] = OldHolder(in_kw) + else: + out_kw[nm] = in_kw[nm] + assert_argspec_and_kw_match(argspec, out_kw, ("result",)) return out_kw @@ -178,9 +189,10 @@ def get_kw_for_postcondition_after(result, conditional, in_kw): assert callable(conditional) out_kw = {} argspec = inspect.getfullargspec(conditional) - if "result" in argspec.args: + must_have_result = "result" in argspec.args + if must_have_result: out_kw["result"] = result - assert ("result" in out_kw) == ("result" in argspec.args) + assert ("result" in out_kw) == must_have_result return out_kw @@ -315,6 +327,25 @@ def postcondition(conditional, message=None): def annotations(f, message=None): + th = get_type_hints(f) + def check_type(n, t): + msg = f"{n} should be instance of {t}" + + if n != 'return': + def chk(**kw): + return isinstance(kw[n], t) + cond_f = precondition + else: + def chk(result): + return isinstance(result, t) + cond_f = postcondition + + cond = cond_f(chk, msg) + return cond + + for n, t in th.items(): + f = check_type(n, t)(f) + return f diff --git a/tests/test_contracting.py b/tests/test_contracting.py index a4b9061..35016f0 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -266,10 +266,19 @@ def test_annotated_types_postcondition(): antibirthday(1) -def test_annotated_types_int(): +def test_annotated_types_precondition_int(): @annotations def birthday(age: Age) -> Age: return age + 1 with pytest.raises(AssertionError): birthday(1.0) + + +def test_annotated_types_postcondition_int(): + @annotations + def birthday(age: Age) -> Age: + return age + 1.0 + + with pytest.raises(AssertionError): + birthday(1) -- GitLab From 34c76ad290464e031a7c8e0009a04f51c4cfaafb Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 09:35:19 +0200 Subject: [PATCH 03/28] wip annotations support --- src/contractme/contracting.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 62dfdfe..b54611b 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -1,8 +1,9 @@ import inspect import copy from warnings import warn -from typing import Callable, get_type_hints +from typing import Callable, get_type_hints, get_annotations import functools +import annotated_types def show_source(f: Callable) -> str: @@ -326,18 +327,42 @@ def postcondition(conditional, message=None): return condition_decorator(False, conditional, message) +def get_all_constraints_failures(val, constraints): + failing = [] + for constraint in constraints: + match constraint: + case annotated_types.Gt(gt=n): + if not (val > n): + failing.add(constraint) + case annotated_types.Lt(lt=n): + if not (val < n): + failing.add(constraint) + case annotated_types.Ge(ge=n): + if not (val >= n): + failing.add(constraint) + case annotated_types.Le(le=n): + if not (val <= n): + failing.add(constraint) + case _: + raise NotImplemented(constraint) + return failing + + def annotations(f, message=None): th = get_type_hints(f) + an = get_annotations(f) def check_type(n, t): - msg = f"{n} should be instance of {t}" + meta = an[n] + msg = f"{n} should be instance of {t} under constraints {meta}" if n != 'return': def chk(**kw): - return isinstance(kw[n], t) + v = kw[n] + return isinstance(v, t) and not get_all_constraints_failures(v, meta) cond_f = precondition else: def chk(result): - return isinstance(result, t) + return isinstance(result, t) and not get_all_constraints_failures(result, meta) cond_f = postcondition cond = cond_f(chk, msg) -- GitLab From 2ffc71a12ebbb9d4ef79610aad8d295b165a13a5 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 14:00:36 +0200 Subject: [PATCH 04/28] tests for all Lt...Gt --- src/contractme/contracting.py | 13 ++++----- tests/test_contracting.py | 50 ++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index b54611b..271cc88 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -1,7 +1,8 @@ import inspect import copy from warnings import warn -from typing import Callable, get_type_hints, get_annotations +from typing import Callable, get_type_hints +from inspect import get_annotations import functools import annotated_types @@ -333,16 +334,16 @@ def get_all_constraints_failures(val, constraints): match constraint: case annotated_types.Gt(gt=n): if not (val > n): - failing.add(constraint) + failing.append(constraint) case annotated_types.Lt(lt=n): if not (val < n): - failing.add(constraint) + failing.append(constraint) case annotated_types.Ge(ge=n): if not (val >= n): - failing.add(constraint) + failing.append(constraint) case annotated_types.Le(le=n): if not (val <= n): - failing.add(constraint) + failing.append(constraint) case _: raise NotImplemented(constraint) return failing @@ -352,7 +353,7 @@ def annotations(f, message=None): th = get_type_hints(f) an = get_annotations(f) def check_type(n, t): - meta = an[n] + meta = an[n].__metadata__ msg = f"{n} should be instance of {t} under constraints {meta}" if n != 'return': diff --git a/tests/test_contracting.py b/tests/test_contracting.py index 35016f0..e5ce736 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -4,7 +4,7 @@ from epycs.subprocess import cmd from contractme import precondition, postcondition, annotations import warnings from typing import Annotated -from annotated_types import Ge +from annotated_types import Lt, Le, Gt, Ge @pytest.fixture @@ -282,3 +282,51 @@ def test_annotated_types_postcondition_int(): with pytest.raises(AssertionError): birthday(1) + +def test_annotated_types_Lt(): + Lt0 = Annotated[int, Lt(0)] + + @annotations + def f(lt0: Lt0): + pass + + f(-1) + + with pytest.raises(AssertionError): + f(0) + +def test_annotated_types_Le(): + Le0 = Annotated[int, Le(0)] + + @annotations + def f(le0: Le0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(1) + +def test_annotated_types_Ge(): + Ge0 = Annotated[int, Ge(0)] + + @annotations + def f(ge0: Ge0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(-1) + +def test_annotated_types_Gt(): + Gt0 = Annotated[int, Gt(0)] + + @annotations + def f(gt0: Gt0): + pass + + f(1) + + with pytest.raises(AssertionError): + f(0) -- GitLab From a055a8b47a1f2db552743bc2588e7ca154388a38 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:10:01 +0200 Subject: [PATCH 05/28] enrich test: checking special cases --- src/contractme/contracting.py | 11 ++++-- tests/test_contracting.py | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 271cc88..e287284 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -352,18 +352,25 @@ def get_all_constraints_failures(val, constraints): def annotations(f, message=None): th = get_type_hints(f) an = get_annotations(f) + def check_type(n, t): meta = an[n].__metadata__ msg = f"{n} should be instance of {t} under constraints {meta}" - if n != 'return': + if n != "return": + def chk(**kw): v = kw[n] return isinstance(v, t) and not get_all_constraints_failures(v, meta) + cond_f = precondition else: + def chk(result): - return isinstance(result, t) and not get_all_constraints_failures(result, meta) + return isinstance(result, t) and not get_all_constraints_failures( + result, meta + ) + cond_f = postcondition cond = cond_f(chk, msg) diff --git a/tests/test_contracting.py b/tests/test_contracting.py index e5ce736..2449283 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -283,6 +283,7 @@ def test_annotated_types_postcondition_int(): with pytest.raises(AssertionError): birthday(1) + def test_annotated_types_Lt(): Lt0 = Annotated[int, Lt(0)] @@ -295,6 +296,7 @@ def test_annotated_types_Lt(): with pytest.raises(AssertionError): f(0) + def test_annotated_types_Le(): Le0 = Annotated[int, Le(0)] @@ -307,6 +309,7 @@ def test_annotated_types_Le(): with pytest.raises(AssertionError): f(1) + def test_annotated_types_Ge(): Ge0 = Annotated[int, Ge(0)] @@ -319,6 +322,7 @@ def test_annotated_types_Ge(): with pytest.raises(AssertionError): f(-1) + def test_annotated_types_Gt(): Gt0 = Annotated[int, Gt(0)] @@ -330,3 +334,63 @@ def test_annotated_types_Gt(): with pytest.raises(AssertionError): f(0) + + +def test_annotated_types_Gt_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __gt__(self, other): + return self.val > other.val + + Gt0 = Annotated[MyInt, Gt(MyInt(0))] + + @annotations + def f_Gt0(gt0: Gt0): + pass + + f_Gt0(MyInt(1)) + + with pytest.raises(AssertionError): + f_Gt0(MyInt(0)) + + +def test_annotated_types_Lt_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __gt__(self, other): + return self.val > other.val + + Lt0 = Annotated[MyInt, Lt(MyInt(0))] + + @annotations + def f_Lt0(gt0: Lt0): + pass + + f_Lt0(MyInt(-1)) + + with pytest.raises(AssertionError): + f_Lt0(MyInt(0)) + + +def test_annotated_types_Ge_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __ge__(self, other): + return self.val >= other.val + + Ge0 = Annotated[MyInt, Ge(MyInt(0))] + + @annotations + def f_Ge0(gt0: Ge0): + pass + + f_Ge0(MyInt(0)) + + with pytest.raises(AssertionError): + f_Ge0(MyInt(-1)) -- GitLab From ebfa551607c611c6173d98e72d2280cce479c23b Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:11:51 +0200 Subject: [PATCH 06/28] annotation tests separated, check not Annotated --- src/contractme/contracting.py | 46 +++++---- tests/test_annotations.py | 173 ++++++++++++++++++++++++++++++++++ tests/test_contracting.py | 163 +------------------------------- 3 files changed, 200 insertions(+), 182 deletions(-) create mode 100644 tests/test_annotations.py diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index e287284..fe9d77d 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -329,24 +329,27 @@ def postcondition(conditional, message=None): def get_all_constraints_failures(val, constraints): - failing = [] - for constraint in constraints: - match constraint: - case annotated_types.Gt(gt=n): - if not (val > n): - failing.append(constraint) - case annotated_types.Lt(lt=n): - if not (val < n): - failing.append(constraint) - case annotated_types.Ge(ge=n): - if not (val >= n): - failing.append(constraint) - case annotated_types.Le(le=n): - if not (val <= n): - failing.append(constraint) - case _: - raise NotImplemented(constraint) - return failing + if not constraints: + return [] + else: + failing = [] + for constraint in constraints: + match constraint: + case annotated_types.Gt(gt=n): + if not (val > n): + failing.append(constraint) + case annotated_types.Lt(lt=n): + if not (val < n): + failing.append(constraint) + case annotated_types.Ge(ge=n): + if not (val >= n): + failing.append(constraint) + case annotated_types.Le(le=n): + if not (val <= n): + failing.append(constraint) + case _: + raise NotImplemented(constraint) + return failing def annotations(f, message=None): @@ -354,8 +357,11 @@ def annotations(f, message=None): an = get_annotations(f) def check_type(n, t): - meta = an[n].__metadata__ - msg = f"{n} should be instance of {t} under constraints {meta}" + meta = getattr(an[n], "__metadata__", tuple()) + if meta: + msg = f"{n} should be instance of {t} under constraints {meta}" + else: + msg = f"{n} should be instance of {t}" if n != "return": diff --git a/tests/test_annotations.py b/tests/test_annotations.py new file mode 100644 index 0000000..2b6db82 --- /dev/null +++ b/tests/test_annotations.py @@ -0,0 +1,173 @@ +import pytest +from contractme import annotations +from typing import Annotated +from annotated_types import Lt, Le, Gt, Ge + + +def test_int(): + @annotations + def birthday(age: int) -> int: + return age + 1 + + assert birthday(1) == 2 + with pytest.raises(AssertionError): + birthday(1.0) + + +Age = Annotated[int, Ge(1)] + + +def test_annotated_types_ok(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + assert birthday(1) == 2 + + +def test_annotated_types_precondition(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + with pytest.raises(AssertionError): + birthday(0) + + +def test_annotated_types_postcondition(): + @annotations + def antibirthday(age: Age) -> Age: + return age - 1 + + with pytest.raises(AssertionError): + antibirthday(1) + + +def test_annotated_types_precondition_int(): + @annotations + def birthday(age: Age) -> Age: + return age + 1 + + with pytest.raises(AssertionError): + birthday(1.0) + + +def test_annotated_types_postcondition_int(): + @annotations + def birthday(age: Age) -> Age: + return age + 1.0 + + with pytest.raises(AssertionError): + birthday(1) + + +def test_annotated_types_Lt(): + Lt0 = Annotated[int, Lt(0)] + + @annotations + def f(lt0: Lt0): + pass + + f(-1) + + with pytest.raises(AssertionError): + f(0) + + +def test_annotated_types_Le(): + Le0 = Annotated[int, Le(0)] + + @annotations + def f(le0: Le0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(1) + + +def test_annotated_types_Ge(): + Ge0 = Annotated[int, Ge(0)] + + @annotations + def f(ge0: Ge0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(-1) + + +def test_annotated_types_Gt(): + Gt0 = Annotated[int, Gt(0)] + + @annotations + def f(gt0: Gt0): + pass + + f(1) + + with pytest.raises(AssertionError): + f(0) + + +def test_annotated_types_Gt_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __gt__(self, other): + return self.val > other.val + + Gt0 = Annotated[MyInt, Gt(MyInt(0))] + + @annotations + def f_Gt0(gt0: Gt0): + pass + + f_Gt0(MyInt(1)) + + with pytest.raises(AssertionError): + f_Gt0(MyInt(0)) + + +def test_annotated_types_Lt_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __gt__(self, other): + return self.val > other.val + + Lt0 = Annotated[MyInt, Lt(MyInt(0))] + + @annotations + def f_Lt0(gt0: Lt0): + pass + + f_Lt0(MyInt(-1)) + + with pytest.raises(AssertionError): + f_Lt0(MyInt(0)) + + +def test_annotated_types_Ge_custom(): + class MyInt: + def __init__(self, val): + self.val = val + + def __ge__(self, other): + return self.val >= other.val + + Ge0 = Annotated[MyInt, Ge(MyInt(0))] + + @annotations + def f_Ge0(gt0: Ge0): + pass + + f_Ge0(MyInt(0)) + + with pytest.raises(AssertionError): + f_Ge0(MyInt(-1)) diff --git a/tests/test_contracting.py b/tests/test_contracting.py index 2449283..b0d7986 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -1,10 +1,8 @@ import pytest import contractme from epycs.subprocess import cmd -from contractme import precondition, postcondition, annotations +from contractme import precondition, postcondition import warnings -from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge @pytest.fixture @@ -235,162 +233,3 @@ def test_cannot_have_precondition_of_a_constructor(): @precondition(lambda self: True) def __init__(self): # pragma: no cover pass - - -Age = Annotated[int, Ge(1)] - - -def test_annotated_types_ok(): - @annotations - def birthday(age: Age) -> Age: - return age + 1 - - assert birthday(1) == 2 - - -def test_annotated_types_precondition(): - @annotations - def birthday(age: Age) -> Age: - return age + 1 - - with pytest.raises(AssertionError): - birthday(0) - - -def test_annotated_types_postcondition(): - @annotations - def antibirthday(age: Age) -> Age: - return age - 1 - - with pytest.raises(AssertionError): - antibirthday(1) - - -def test_annotated_types_precondition_int(): - @annotations - def birthday(age: Age) -> Age: - return age + 1 - - with pytest.raises(AssertionError): - birthday(1.0) - - -def test_annotated_types_postcondition_int(): - @annotations - def birthday(age: Age) -> Age: - return age + 1.0 - - with pytest.raises(AssertionError): - birthday(1) - - -def test_annotated_types_Lt(): - Lt0 = Annotated[int, Lt(0)] - - @annotations - def f(lt0: Lt0): - pass - - f(-1) - - with pytest.raises(AssertionError): - f(0) - - -def test_annotated_types_Le(): - Le0 = Annotated[int, Le(0)] - - @annotations - def f(le0: Le0): - pass - - f(0) - - with pytest.raises(AssertionError): - f(1) - - -def test_annotated_types_Ge(): - Ge0 = Annotated[int, Ge(0)] - - @annotations - def f(ge0: Ge0): - pass - - f(0) - - with pytest.raises(AssertionError): - f(-1) - - -def test_annotated_types_Gt(): - Gt0 = Annotated[int, Gt(0)] - - @annotations - def f(gt0: Gt0): - pass - - f(1) - - with pytest.raises(AssertionError): - f(0) - - -def test_annotated_types_Gt_custom(): - class MyInt: - def __init__(self, val): - self.val = val - - def __gt__(self, other): - return self.val > other.val - - Gt0 = Annotated[MyInt, Gt(MyInt(0))] - - @annotations - def f_Gt0(gt0: Gt0): - pass - - f_Gt0(MyInt(1)) - - with pytest.raises(AssertionError): - f_Gt0(MyInt(0)) - - -def test_annotated_types_Lt_custom(): - class MyInt: - def __init__(self, val): - self.val = val - - def __gt__(self, other): - return self.val > other.val - - Lt0 = Annotated[MyInt, Lt(MyInt(0))] - - @annotations - def f_Lt0(gt0: Lt0): - pass - - f_Lt0(MyInt(-1)) - - with pytest.raises(AssertionError): - f_Lt0(MyInt(0)) - - -def test_annotated_types_Ge_custom(): - class MyInt: - def __init__(self, val): - self.val = val - - def __ge__(self, other): - return self.val >= other.val - - Ge0 = Annotated[MyInt, Ge(MyInt(0))] - - @annotations - def f_Ge0(gt0: Ge0): - pass - - f_Ge0(MyInt(0)) - - with pytest.raises(AssertionError): - f_Ge0(MyInt(-1)) -- GitLab From 3475abc54141c5f9bfbfe532494097157847372b Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:35:21 +0200 Subject: [PATCH 07/28] support functools partial constraint --- src/contractme/contracting.py | 29 +++++++++++++++++++++++++---- tests/test_annotations.py | 15 +++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index fe9d77d..4120d72 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -4,6 +4,7 @@ from warnings import warn from typing import Callable, get_type_hints from inspect import get_annotations import functools +import operator import annotated_types @@ -166,11 +167,11 @@ def get_kw_for_conditional_before(cond_type, conditional, in_kw): argspec = inspect.getfullargspec(conditional) if argspec.varargs: - raise NotImplemented("*args in conditions") + raise NotImplementedError("*args in conditions") if argspec.varkw is not None: if argspec.args: - raise NotImplemented("mixing args and **kw in conditions") + raise NotImplementedError("mixing args and **kw in conditions") out_kw = in_kw else: @@ -328,12 +329,32 @@ def postcondition(conditional, message=None): return condition_decorator(False, conditional, message) +def normalize_constraint(constraint): + if isinstance(constraint, functools.partial): + # https://github.com/annotated-types/annotated-types?tab=readme-ov-file#gt-ge-lt-le + match constraint.func, constraint.args: + case operator.lt, (n,): + n = annotated_types.Lt(n) + case operator.le, (n,): + n = annotated_types.Le(n) + case operator.gt, (n,): + n = annotated_types.Gt(n) + case operator.ge, (n,): + n = annotated_types.Ge(n) + case _: + raise NotImplementedError(constraint) + return n + else: + return constraint + + def get_all_constraints_failures(val, constraints): if not constraints: return [] else: failing = [] - for constraint in constraints: + for constraint_denorm in constraints: + constraint = normalize_constraint(constraint_denorm) match constraint: case annotated_types.Gt(gt=n): if not (val > n): @@ -348,7 +369,7 @@ def get_all_constraints_failures(val, constraints): if not (val <= n): failing.append(constraint) case _: - raise NotImplemented(constraint) + raise NotImplementedError(constraint) return failing diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 2b6db82..0f0b840 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -2,6 +2,8 @@ import pytest from contractme import annotations from typing import Annotated from annotated_types import Lt, Le, Gt, Ge +import functools +import operator def test_int(): @@ -113,6 +115,19 @@ def test_annotated_types_Gt(): f(0) +def test_annotated_types_functools_partial_Gt(): + Gt0 = Annotated[int, functools.partial(operator.gt, 0)] + + @annotations + def f(gt0: Gt0): + pass + + f(1) + + with pytest.raises(AssertionError): + f(0) + + def test_annotated_types_Gt_custom(): class MyInt: def __init__(self, val): -- GitLab From cccb14473f5ba628c32245411be374044290a9f4 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:45:01 +0200 Subject: [PATCH 08/28] add support for intervals --- src/contractme/contracting.py | 31 +++++++++++++++++++------------ tests/test_annotations.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 4120d72..3edfba5 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -356,20 +356,27 @@ def get_all_constraints_failures(val, constraints): for constraint_denorm in constraints: constraint = normalize_constraint(constraint_denorm) match constraint: - case annotated_types.Gt(gt=n): - if not (val > n): - failing.append(constraint) - case annotated_types.Lt(lt=n): - if not (val < n): - failing.append(constraint) - case annotated_types.Ge(ge=n): - if not (val >= n): - failing.append(constraint) - case annotated_types.Le(le=n): - if not (val <= n): - failing.append(constraint) + case annotated_types.Gt(gt=gt): + check = val > gt + case annotated_types.Lt(lt=lt): + check = val < lt + case annotated_types.Ge(ge=ge): + check = val >= ge + case annotated_types.Le(le=le): + check = val <= le + case annotated_types.Interval(gt=gt, lt=lt, ge=None, le=None): + check = gt < val < lt + case annotated_types.Interval(ge=ge, lt=lt, gt=None, le=None): + check = ge <= val < lt + case annotated_types.Interval(gt=gt, le=le, ge=None, lt=None): + check = gt < val <= le + case annotated_types.Interval(ge=ge, le=le, gt=None, lt=None): + check = ge <= val <= le case _: raise NotImplementedError(constraint) + + if not check: + failing.append(constraint) return failing diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 0f0b840..b309f18 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,7 +1,7 @@ import pytest from contractme import annotations from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge +from annotated_types import Lt, Le, Gt, Ge, Interval import functools import operator @@ -186,3 +186,34 @@ def test_annotated_types_Ge_custom(): with pytest.raises(AssertionError): f_Ge0(MyInt(-1)) + + +def test_annotated_types_Interval_gt_lt(): + Gt0Lt2 = Annotated[int, Interval(gt=0, lt=2)] + + @annotations + def f(_: Gt0Lt2): + pass + + f(1) + + with pytest.raises(AssertionError): + f(0) + with pytest.raises(AssertionError): + f(2) + + +def test_annotated_types_Interval_ge_le(): + Ge0Le2 = Annotated[int, Interval(ge=0, le=2)] + + @annotations + def f(_: Ge0Le2): + pass + + f(0) + f(2) + + with pytest.raises(AssertionError): + f(-1) + with pytest.raises(AssertionError): + f(3) -- GitLab From ca480494147baa2bfa4fb8c04eb36a6621780809 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:53:19 +0200 Subject: [PATCH 09/28] Add MultipleOf, doc a bit --- README.md | 20 ++++++++++++++++++++ src/contractme/contracting.py | 2 ++ tests/test_annotations.py | 20 +++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d0777e..31a6d4d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,26 @@ def append_count(l: list[int], n: int): l.append(n) ``` +## Using annotations + +```python +@annotations +def incr(v : int) -> int: + return v + 1 +``` + +Supports annotations and [PEP-593](https://peps.python.org/pep-0593/) +using the [annotated-types](https://pypi.org/project/annotated-types/) library. +**Note:** `annodated_types.MultipleOf` follows the Python semantics. + +```python +Even = Annotated[int, MultipleOf(2)] + +@annotations +def square(v : Even) -> Even + return v * v +``` + # Test `uv run pytest` diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 3edfba5..44aa2b6 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -372,6 +372,8 @@ def get_all_constraints_failures(val, constraints): check = gt < val <= le case annotated_types.Interval(ge=ge, le=le, gt=None, lt=None): check = ge <= val <= le + case annotated_types.MultipleOf(multiple_of=multiple_of): + check = val % multiple_of == 0 case _: raise NotImplementedError(constraint) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index b309f18..7067217 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,7 +1,7 @@ import pytest from contractme import annotations from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge, Interval +from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf import functools import operator @@ -217,3 +217,21 @@ def test_annotated_types_Interval_ge_le(): f(-1) with pytest.raises(AssertionError): f(3) + + +def test_annotated_types_MultipleOf(): + Mult3 = Annotated[int, MultipleOf(3)] + + @annotations + def f(_: Mult3): + pass + + f(-3) + f(0) + f(3) + f(6) + + with pytest.raises(AssertionError): + f(-1) + with pytest.raises(AssertionError): + f(1) -- GitLab From 064cbc58003549065f9338c25e724425b6207f42 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 17:54:36 +0200 Subject: [PATCH 10/28] simplified annotations tests names --- tests/test_annotations.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 7067217..3187ebd 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -19,7 +19,7 @@ def test_int(): Age = Annotated[int, Ge(1)] -def test_annotated_types_ok(): +def test_ok(): @annotations def birthday(age: Age) -> Age: return age + 1 @@ -27,7 +27,7 @@ def test_annotated_types_ok(): assert birthday(1) == 2 -def test_annotated_types_precondition(): +def test_precondition(): @annotations def birthday(age: Age) -> Age: return age + 1 @@ -36,7 +36,7 @@ def test_annotated_types_precondition(): birthday(0) -def test_annotated_types_postcondition(): +def test_postcondition(): @annotations def antibirthday(age: Age) -> Age: return age - 1 @@ -45,7 +45,7 @@ def test_annotated_types_postcondition(): antibirthday(1) -def test_annotated_types_precondition_int(): +def test_precondition_int(): @annotations def birthday(age: Age) -> Age: return age + 1 @@ -54,7 +54,7 @@ def test_annotated_types_precondition_int(): birthday(1.0) -def test_annotated_types_postcondition_int(): +def test_postcondition_int(): @annotations def birthday(age: Age) -> Age: return age + 1.0 @@ -63,7 +63,7 @@ def test_annotated_types_postcondition_int(): birthday(1) -def test_annotated_types_Lt(): +def test_Lt(): Lt0 = Annotated[int, Lt(0)] @annotations @@ -76,7 +76,7 @@ def test_annotated_types_Lt(): f(0) -def test_annotated_types_Le(): +def test_Le(): Le0 = Annotated[int, Le(0)] @annotations @@ -89,7 +89,7 @@ def test_annotated_types_Le(): f(1) -def test_annotated_types_Ge(): +def test_Ge(): Ge0 = Annotated[int, Ge(0)] @annotations @@ -102,7 +102,7 @@ def test_annotated_types_Ge(): f(-1) -def test_annotated_types_Gt(): +def test_Gt(): Gt0 = Annotated[int, Gt(0)] @annotations @@ -115,7 +115,7 @@ def test_annotated_types_Gt(): f(0) -def test_annotated_types_functools_partial_Gt(): +def test_functools_partial_Gt(): Gt0 = Annotated[int, functools.partial(operator.gt, 0)] @annotations @@ -128,7 +128,7 @@ def test_annotated_types_functools_partial_Gt(): f(0) -def test_annotated_types_Gt_custom(): +def test_Gt_custom(): class MyInt: def __init__(self, val): self.val = val @@ -148,7 +148,7 @@ def test_annotated_types_Gt_custom(): f_Gt0(MyInt(0)) -def test_annotated_types_Lt_custom(): +def test_Lt_custom(): class MyInt: def __init__(self, val): self.val = val @@ -168,7 +168,7 @@ def test_annotated_types_Lt_custom(): f_Lt0(MyInt(0)) -def test_annotated_types_Ge_custom(): +def test_Ge_custom(): class MyInt: def __init__(self, val): self.val = val @@ -188,7 +188,7 @@ def test_annotated_types_Ge_custom(): f_Ge0(MyInt(-1)) -def test_annotated_types_Interval_gt_lt(): +def test_Interval_gt_lt(): Gt0Lt2 = Annotated[int, Interval(gt=0, lt=2)] @annotations @@ -203,7 +203,7 @@ def test_annotated_types_Interval_gt_lt(): f(2) -def test_annotated_types_Interval_ge_le(): +def test_Interval_ge_le(): Ge0Le2 = Annotated[int, Interval(ge=0, le=2)] @annotations @@ -219,7 +219,7 @@ def test_annotated_types_Interval_ge_le(): f(3) -def test_annotated_types_MultipleOf(): +def test_MultipleOf(): Mult3 = Annotated[int, MultipleOf(3)] @annotations -- GitLab From cda253e125129bebf51f07f298d853bd3c05e005 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 18:03:30 +0200 Subject: [PATCH 11/28] support for half-specified Interval --- src/contractme/contracting.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 44aa2b6..a9ba4a6 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -356,13 +356,21 @@ def get_all_constraints_failures(val, constraints): for constraint_denorm in constraints: constraint = normalize_constraint(constraint_denorm) match constraint: - case annotated_types.Gt(gt=gt): + case annotated_types.Gt(gt=gt) | annotated_types.Interval( + gt=gt, lt=None, ge=None, le=None + ): check = val > gt - case annotated_types.Lt(lt=lt): + case annotated_types.Lt(lt=lt) | annotated_types.Interval( + lt=lt, gt=None, ge=None, le=None + ): check = val < lt - case annotated_types.Ge(ge=ge): + case annotated_types.Ge(ge=ge) | annotated_types.Interval( + ge=ge, lt=None, gt=None, le=None + ): check = val >= ge - case annotated_types.Le(le=le): + case annotated_types.Le(le=le) | annotated_types.Interval( + le=le, lt=None, ge=None, gt=None + ): check = val <= le case annotated_types.Interval(gt=gt, lt=lt, ge=None, le=None): check = gt < val < lt -- GitLab From 1e219e35437d75e0e1eaf127a0a21871d90267f2 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 18:03:38 +0200 Subject: [PATCH 12/28] add support for len --- src/contractme/contracting.py | 10 ++++++ tests/test_annotations.py | 58 ++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index a9ba4a6..51cc088 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -382,6 +382,16 @@ def get_all_constraints_failures(val, constraints): check = ge <= val <= le case annotated_types.MultipleOf(multiple_of=multiple_of): check = val % multiple_of == 0 + case annotated_types.MinLen( + min_length=min_length + ) | annotated_types.Len(min_length=min_length, max_length=None): + check = min_length <= len(val) + case annotated_types.MaxLen( + max_length=max_length + ) | annotated_types.Len(max_length=max_length, min_length=None): + check = len(val) <= max_length + case annotated_types.Len(min_length=min_length, max_length=max_length): + check = min_length <= len(val) <= max_length case _: raise NotImplementedError(constraint) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 3187ebd..e003c7a 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,7 +1,7 @@ import pytest from contractme import annotations from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf +from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf, MinLen, MaxLen, Len import functools import operator @@ -188,6 +188,20 @@ def test_Ge_custom(): f_Ge0(MyInt(-1)) +def test_Interval_only_gt(): + Gt0Lt2 = Annotated[int, Interval(gt=0)] + + @annotations + def f(_: Gt0Lt2): + pass + + f(1) + f(1000) + + with pytest.raises(AssertionError): + f(0) + + def test_Interval_gt_lt(): Gt0Lt2 = Annotated[int, Interval(gt=0, lt=2)] @@ -235,3 +249,45 @@ def test_MultipleOf(): f(-1) with pytest.raises(AssertionError): f(1) + + +def test_MinLen(): + NotEmpty = Annotated[str, MinLen(1)] + + @annotations + def f(_: NotEmpty): + pass + + f("a") + with pytest.raises(AssertionError): + f("") + + +def test_MaxLen(): + SmallStr = Annotated[str, MaxLen(3)] + + @annotations + def f(_: SmallStr): + pass + + f("") + f("aaa") + + with pytest.raises(AssertionError): + f("aaaa") + + +def test_Len(): + BigramOrTrigram = Annotated[str, Len(2, 3)] + + @annotations + def f(_: BigramOrTrigram): + pass + + f("aa") + f("aaa") + + with pytest.raises(AssertionError): + f("") + with pytest.raises(AssertionError): + f("aaaa") -- GitLab From 53917fb8511656ccf6140500937e7bb3623ca5e8 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 18:08:15 +0200 Subject: [PATCH 13/28] unknown annotations cause no issues --- README.md | 2 ++ src/contractme/contracting.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31a6d4d..fed850a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ def incr(v : int) -> int: Supports annotations and [PEP-593](https://peps.python.org/pep-0593/) using the [annotated-types](https://pypi.org/project/annotated-types/) library. **Note:** `annodated_types.MultipleOf` follows the Python semantics. +**Note 2:** Following an open-world reasoning, any unknown annotation is considered +to be correct, so it won't cause a check failure. ```python Even = Annotated[int, MultipleOf(2)] diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 51cc088..a13925d 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -393,7 +393,7 @@ def get_all_constraints_failures(val, constraints): case annotated_types.Len(min_length=min_length, max_length=max_length): check = min_length <= len(val) <= max_length case _: - raise NotImplementedError(constraint) + check = True if not check: failing.append(constraint) -- GitLab From 04cd5117799e0c5fad661b8fd3fedc92b03ffeb6 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 18:38:46 +0200 Subject: [PATCH 14/28] Timezone --- src/contractme/contracting.py | 11 +++++++ tests/test_annotations.py | 56 ++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index a13925d..9e6f822 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -6,6 +6,8 @@ from inspect import get_annotations import functools import operator import annotated_types +import datetime +import zoneinfo def show_source(f: Callable) -> str: @@ -392,6 +394,15 @@ def get_all_constraints_failures(val, constraints): check = len(val) <= max_length case annotated_types.Len(min_length=min_length, max_length=max_length): check = min_length <= len(val) <= max_length + case annotated_types.Timezone(tz=None): + check = val.tzinfo is None + case annotated_types.Timezone(tz=tz): + if tz is Ellipsis: + check = val.tzinfo is not None + elif isinstance(tz, datetime.tzinfo): + check = val.tzinfo == tz + else: + check = val.tzinfo == zoneinfo.ZoneInfo(tz) case _: check = True diff --git a/tests/test_annotations.py b/tests/test_annotations.py index e003c7a..0fe8200 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,9 +1,11 @@ import pytest from contractme import annotations from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf, MinLen, MaxLen, Len +from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf, MinLen, MaxLen, Len, Timezone import functools import operator +from datetime import datetime, timezone +from zoneinfo import ZoneInfo def test_int(): @@ -291,3 +293,55 @@ def test_Len(): f("") with pytest.raises(AssertionError): f("aaaa") + + +def test_Timezone_datetime_Ellipsis(): + TzAware = Annotated[datetime, Timezone(...)] + + @annotations + def f(_: TzAware): + pass + + f(datetime.now(tz=ZoneInfo("Europe/Paris"))) + + with pytest.raises(AssertionError): + f(datetime.now(tz=None)) + + +def test_Timezone_datetime_None(): + NaiveDatetime = Annotated[datetime, Timezone(None)] + + @annotations + def f(_: NaiveDatetime): + pass + + f(datetime.now(tz=None)) + + with pytest.raises(AssertionError): + f(datetime.now(timezone.utc)) + + +def test_Timezone_datetime_str(): + AtParisTz = Annotated[datetime, Timezone("Europe/Paris")] + + @annotations + def f(_: AtParisTz): + pass + + f(datetime.now(tz=ZoneInfo("Europe/Paris"))) + + with pytest.raises(AssertionError): + f(datetime.now(timezone.utc)) + + +def test_Timezone_datetime_ZoneInfo(): + AtParisTz = Annotated[datetime, Timezone(ZoneInfo("Europe/Paris"))] + + @annotations + def f(_: AtParisTz): + pass + + f(datetime.now(tz=ZoneInfo("Europe/Paris"))) + + with pytest.raises(AssertionError): + f(datetime.now(timezone.utc)) -- GitLab From bf9b9fdb903ebd5ecb2502a10bf7ee30e69c0d9a Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 18:46:53 +0200 Subject: [PATCH 15/28] black --- tests/test_annotations.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 0fe8200..94799a7 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,7 +1,18 @@ import pytest from contractme import annotations from typing import Annotated -from annotated_types import Lt, Le, Gt, Ge, Interval, MultipleOf, MinLen, MaxLen, Len, Timezone +from annotated_types import ( + Lt, + Le, + Gt, + Ge, + Interval, + MultipleOf, + MinLen, + MaxLen, + Len, + Timezone, +) import functools import operator from datetime import datetime, timezone -- GitLab From 1b0bc6500f94d62b3e1024b55db22394aeb59c30 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:01:49 +0200 Subject: [PATCH 16/28] TypeVar annotations --- src/contractme/contracting.py | 9 +++++++-- tests/test_annotations.py | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 9e6f822..2fa24db 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -1,7 +1,7 @@ import inspect import copy from warnings import warn -from typing import Callable, get_type_hints +from typing import Callable, get_type_hints, TypeVar from inspect import get_annotations import functools import operator @@ -441,7 +441,12 @@ def annotations(f, message=None): cond = cond_f(chk, msg) return cond - for n, t in th.items(): + for n, t_or_typevar in th.items(): + + if isinstance(t_or_typevar, TypeVar): + t = t_or_typevar.__bound__ + else: + t = t_or_typevar f = check_type(n, t)(f) return f diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 94799a7..fea579a 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,6 +1,6 @@ import pytest from contractme import annotations -from typing import Annotated +from typing import Annotated, TypeVar from annotated_types import ( Lt, Le, @@ -29,6 +29,18 @@ def test_int(): birthday(1.0) +def test_bound_typevar(): + ActuallyAnInt = TypeVar("ActuallyAnInt", bound=int) + + @annotations + def birthday(age: ActuallyAnInt) -> ActuallyAnInt: + return age + 1 + + assert birthday(1) == 2 + with pytest.raises(AssertionError): + birthday(1.0) + + Age = Annotated[int, Ge(1)] -- GitLab From 05a6d7dece3f44ba1b46633419bf353f79bc54d1 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:04:12 +0200 Subject: [PATCH 17/28] add Predicate --- src/contractme/contracting.py | 2 ++ tests/test_annotations.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 2fa24db..204ef5d 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -403,6 +403,8 @@ def get_all_constraints_failures(val, constraints): check = val.tzinfo == tz else: check = val.tzinfo == zoneinfo.ZoneInfo(tz) + case annotated_types.Predicate(func=func): + check = func(val) case _: check = True diff --git a/tests/test_annotations.py b/tests/test_annotations.py index fea579a..360a7de 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -12,6 +12,8 @@ from annotated_types import ( MaxLen, Len, Timezone, + # predicates + LowerCase, ) import functools import operator @@ -368,3 +370,23 @@ def test_Timezone_datetime_ZoneInfo(): with pytest.raises(AssertionError): f(datetime.now(timezone.utc)) + + +def test_predicate_input(): + @annotations + def f(_: LowerCase): + pass + + f("aaaa") + with pytest.raises(AssertionError): + f("aaaA") + + +def test_predicate_return(): + @annotations + def f(a: str) -> LowerCase: + return a + + f("aaaa") + with pytest.raises(AssertionError): + f("aaaA") -- GitLab From 3b15e886e6150de97613e10bf47b7eb2deeb1359 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:10:21 +0200 Subject: [PATCH 18/28] coverage for not implemented --- src/contractme/contracting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 204ef5d..d7b7ed4 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -168,11 +168,11 @@ def get_kw_for_conditional_before(cond_type, conditional, in_kw): out_kw = {} argspec = inspect.getfullargspec(conditional) - if argspec.varargs: + if argspec.varargs: # pragma: no cover raise NotImplementedError("*args in conditions") if argspec.varkw is not None: - if argspec.args: + if argspec.args: # pragma: no cover raise NotImplementedError("mixing args and **kw in conditions") out_kw = in_kw -- GitLab From 5370b87c6ceed689fd0527bd983ac646bdb8301a Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:23:15 +0200 Subject: [PATCH 19/28] 100% coverage, extend coverage to tests --- .gitlab-ci.yml | 2 +- src/contractme/contracting.py | 2 +- tests/test_annotations.py | 116 ++++++++++++++++++++++++++++------ tests/test_contracting.py | 2 +- 4 files changed, 101 insertions(+), 21 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d760d92..aee46f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ variables: tests: image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER script: - - uv run pytest --cov=contractme --cov-report term-missing --cov-report xml:obj/cov.xml --junitxml=obj/tests_report.xml + - uv run pytest --cov --cov-report term-missing --cov-report xml:obj/cov.xml --junitxml=obj/tests_report.xml - uvx black --check . - uv run pyright . --outputjson > obj/pyright.json || true after_script: diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index d7b7ed4..bf68609 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -343,7 +343,7 @@ def normalize_constraint(constraint): n = annotated_types.Gt(n) case operator.ge, (n,): n = annotated_types.Ge(n) - case _: + case _: # pragma: no cover raise NotImplementedError(constraint) return n else: diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 360a7de..e75484d 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -12,6 +12,7 @@ from annotated_types import ( MaxLen, Len, Timezone, + doc, # predicates LowerCase, ) @@ -23,7 +24,17 @@ from zoneinfo import ZoneInfo def test_int(): @annotations - def birthday(age: int) -> int: + def birthday(age: int): + return age + 1 + + assert birthday(1) == 2 + with pytest.raises(AssertionError): + birthday(1.0) + + +def test_return_int(): + @annotations + def birthday(age) -> int: return age + 1 assert birthday(1) == 2 @@ -46,22 +57,19 @@ def test_bound_typevar(): Age = Annotated[int, Ge(1)] -def test_ok(): +def test_precondition(): @annotations def birthday(age: Age) -> Age: return age + 1 assert birthday(1) == 2 - -def test_precondition(): - @annotations - def birthday(age: Age) -> Age: - return age + 1 - with pytest.raises(AssertionError): birthday(0) + with pytest.raises(AssertionError): + birthday(1.0) + def test_postcondition(): @annotations @@ -72,18 +80,9 @@ def test_postcondition(): antibirthday(1) -def test_precondition_int(): - @annotations - def birthday(age: Age) -> Age: - return age + 1 - - with pytest.raises(AssertionError): - birthday(1.0) - - def test_postcondition_int(): @annotations - def birthday(age: Age) -> Age: + def birthday(age) -> Age: return age + 1.0 with pytest.raises(AssertionError): @@ -142,6 +141,45 @@ def test_Gt(): f(0) +def test_functools_partial_Lt(): + Lt0 = Annotated[int, functools.partial(operator.lt, 0)] + + @annotations + def f(_: Lt0): + pass + + f(-1) + + with pytest.raises(AssertionError): + f(0) + + +def test_functools_partial_Le(): + Le0 = Annotated[int, functools.partial(operator.le, 0)] + + @annotations + def f(_: Le0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(1) + + +def test_functools_partial_Ge(): + Ge0 = Annotated[int, functools.partial(operator.ge, 0)] + + @annotations + def f(_: Ge0): + pass + + f(0) + + with pytest.raises(AssertionError): + f(-1) + + def test_functools_partial_Gt(): Gt0 = Annotated[int, functools.partial(operator.gt, 0)] @@ -244,6 +282,38 @@ def test_Interval_gt_lt(): f(2) +def test_Interval_ge_lt(): + Ge0Lt2 = Annotated[int, Interval(ge=0, lt=2)] + + @annotations + def f(_: Ge0Lt2): + pass + + f(0) + f(1) + + with pytest.raises(AssertionError): + f(-1) + with pytest.raises(AssertionError): + f(2) + + +def test_Interval_gt_le(): + Gt0Le2 = Annotated[int, Interval(gt=0, le=2)] + + @annotations + def f(_: Gt0Le2): + pass + + f(1) + f(2) + + with pytest.raises(AssertionError): + f(0) + with pytest.raises(AssertionError): + f(3) + + def test_Interval_ge_le(): Ge0Le2 = Annotated[int, Interval(ge=0, le=2)] @@ -390,3 +460,13 @@ def test_predicate_return(): f("aaaa") with pytest.raises(AssertionError): f("aaaA") + + +def test_Doc(): + Documented = Annotated[int, doc("Well, it's a integer")] + + @annotations + def f(a: Documented) -> Documented: + return a + + assert f(1) == 1 diff --git a/tests/test_contracting.py b/tests/test_contracting.py index b0d7986..e536992 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -161,7 +161,7 @@ def test_with_global(): def test_call_precondition_with_no_arguments(): @precondition(lambda: 1 != 0) - def f(a): + def f(a): # pragma: no cover pass with pytest.raises(TypeError, match=r"missing .* required .* argument"): -- GitLab From 73c8083bf039f9206dc9d33fa93be4167ca59ce0 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:23:28 +0200 Subject: [PATCH 20/28] black --- src/contractme/contracting.py | 2 +- tests/test_contracting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index bf68609..6559840 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -343,7 +343,7 @@ def normalize_constraint(constraint): n = annotated_types.Gt(n) case operator.ge, (n,): n = annotated_types.Ge(n) - case _: # pragma: no cover + case _: # pragma: no cover raise NotImplementedError(constraint) return n else: diff --git a/tests/test_contracting.py b/tests/test_contracting.py index e536992..0ddeedb 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -161,7 +161,7 @@ def test_with_global(): def test_call_precondition_with_no_arguments(): @precondition(lambda: 1 != 0) - def f(a): # pragma: no cover + def f(a): # pragma: no cover pass with pytest.raises(TypeError, match=r"missing .* required .* argument"): -- GitLab From ae2c971faf58f7b0839efcad68795f07a869fab6 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:42:53 +0200 Subject: [PATCH 21/28] pyright for dev, and updated --- pyproject.toml | 2 +- uv.lock | 90 ++++++++++++++++++++++++-------------------------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c25520..368e8a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ authors = [ requires-python = ">=3.13" dependencies = [ "annotated-types>=0.7.0", - "pyright>=1.1.397", ] [build-system] @@ -19,6 +18,7 @@ build-backend = "hatchling.build" [dependency-groups] dev = [ "epycs>=1.4.0", + "pyright>=1.1.400", "pytest>=8.3.5", "pytest-cov>=6.0.0", ] diff --git a/uv.lock b/uv.lock index 4a45ace..2dc9f76 100644 --- a/uv.lock +++ b/uv.lock @@ -26,74 +26,72 @@ version = "1.1.0" source = { editable = "." } dependencies = [ { name = "annotated-types" }, - { name = "pyright" }, ] [package.dev-dependencies] dev = [ { name = "epycs" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, ] [package.metadata] -requires-dist = [ - { name = "annotated-types", specifier = ">=0.7.0" }, - { name = "pyright", specifier = ">=1.1.397" }, -] +requires-dist = [{ name = "annotated-types", specifier = ">=0.7.0" }] [package.metadata.requires-dev] dev = [ { name = "epycs", specifier = ">=1.4.0" }, + { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cov", specifier = ">=6.0.0" }, ] [[package]] name = "coverage" -version = "7.6.12" +version = "7.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, - { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, - { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, - { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, - { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, - { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, - { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, - { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, - { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, - { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, - { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, - { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, - { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, - { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, - { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, - { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, - { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, - { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, - { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, - { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, - { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, ] [[package]] name = "epycs" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/3a/10561dd1f2082530e70caf42788d46a6eab6f858c805c5555653ff3d673f/epycs-1.4.0.tar.gz", hash = "sha256:d20ca14a769f0cb8b5889607f7dc1d25abf7b5bc3b7cd4acad6e15ae06e17225", size = 9450 } +sdist = { url = "https://files.pythonhosted.org/packages/17/21/6925a3cc48df2650d4351d17d05faa105de2d8bf0e210c50b83bdae81db2/epycs-1.5.0.tar.gz", hash = "sha256:4eaf4493c242327f5988663ff24e719c02f1051414de1ed92b8a0efb3926693d", size = 9835 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/5f/df7f11cde338f96be4f05ba0e048b051a8995d03404de54ff70a14e3ef83/epycs-1.4.0-py3-none-any.whl", hash = "sha256:2be6ad40c6473207059ffafc2a87722d9ca6b95a4bd841fd048a86157744699b", size = 10065 }, + { url = "https://files.pythonhosted.org/packages/50/01/aa81bef60e1aa2ce606ba7c86f7119ed6e1e3bf30a9e8b33a507681847c8/epycs-1.5.0-py3-none-any.whl", hash = "sha256:79a99b197eb251f80fd0a42bb54404e170c030e0df3698402f4037ac07ce9878", size = 10452 }, ] [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] @@ -107,11 +105,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] @@ -125,15 +123,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.397" +version = "1.1.400" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/23/cefa10c9cb198e0858ed0b9233371d62bca880337f628e58f50dfdfb12f0/pyright-1.1.397.tar.gz", hash = "sha256:07530fd65a449e4b0b28dceef14be0d8e0995a7a5b1bb2f3f897c3e548451ce3", size = 3818998 } +sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/b5/98ec41e1e0ad5576ecd42c90ec363560f7b389a441722ea3c7207682dec7/pyright-1.1.397-py3-none-any.whl", hash = "sha256:2e93fba776e714a82b085d68f8345b01f91ba43e1ab9d513e79b70fc85906257", size = 5693631 }, + { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460 }, ] [[package]] @@ -153,22 +151,22 @@ wheels = [ [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, ] -- GitLab From 2026df6f4f898dfe373278fb5d14280966a8c30f Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:48:06 +0200 Subject: [PATCH 22/28] add vacuous None interval --- src/contractme/contracting.py | 3 +++ tests/test_annotations.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 6559840..b645469 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -358,6 +358,9 @@ def get_all_constraints_failures(val, constraints): for constraint_denorm in constraints: constraint = normalize_constraint(constraint_denorm) match constraint: + case annotated_types.Interval(gt=None, lt=None, ge=None, le=None): + # null range + check = True case annotated_types.Gt(gt=gt) | annotated_types.Interval( gt=gt, lt=None, ge=None, le=None ): diff --git a/tests/test_annotations.py b/tests/test_annotations.py index e75484d..8bee41b 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -253,6 +253,16 @@ def test_Ge_custom(): f_Ge0(MyInt(-1)) +def test_vacuous_Interval(): + Vacuous = Annotated[int, Interval()] + + @annotations + def f(_: Vacuous): + pass + + f(0) + + def test_Interval_only_gt(): Gt0Lt2 = Annotated[int, Interval(gt=0)] -- GitLab From e6fa2b41adb7afc04d1aa207c067e1354cbf4133 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 19:48:13 +0200 Subject: [PATCH 23/28] simplify tz handling --- src/contractme/contracting.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index b645469..4cba8f8 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -396,16 +396,19 @@ def get_all_constraints_failures(val, constraints): ) | annotated_types.Len(max_length=max_length, min_length=None): check = len(val) <= max_length case annotated_types.Len(min_length=min_length, max_length=max_length): + assert min_length is not None and max_length is not None check = min_length <= len(val) <= max_length - case annotated_types.Timezone(tz=None): - check = val.tzinfo is None case annotated_types.Timezone(tz=tz): - if tz is Ellipsis: + if tz is None: + check = val.tzinfo is None + elif tz is Ellipsis: check = val.tzinfo is not None elif isinstance(tz, datetime.tzinfo): check = val.tzinfo == tz - else: + elif isinstance(tz, str): check = val.tzinfo == zoneinfo.ZoneInfo(tz) + else: # pragma: no cover + raise NotImplementedError(f"tz being {tz.__class__.__name__}") case annotated_types.Predicate(func=func): check = func(val) case _: -- GitLab From 2113ef8e792afa72b125b8991d0c693150ea7653 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 22:22:23 +0200 Subject: [PATCH 24/28] can have kwargs as postcondition --- src/contractme/contracting.py | 2 +- tests/test_contracting.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 4cba8f8..d315d26 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -194,7 +194,7 @@ def get_kw_for_postcondition_after(result, conditional, in_kw): assert callable(conditional) out_kw = {} argspec = inspect.getfullargspec(conditional) - must_have_result = "result" in argspec.args + must_have_result = "result" in argspec.args or argspec.varkw is not None if must_have_result: out_kw["result"] = result assert ("result" in out_kw) == must_have_result diff --git a/tests/test_contracting.py b/tests/test_contracting.py index 0ddeedb..638c9f5 100644 --- a/tests/test_contracting.py +++ b/tests/test_contracting.py @@ -233,3 +233,23 @@ def test_cannot_have_precondition_of_a_constructor(): @precondition(lambda self: True) def __init__(self): # pragma: no cover pass + + +def test_can_have_kwarg_precondition(): + @precondition(lambda **kw: kw["a"] > 0) + def f(a): + return a + + f(1) + with pytest.raises(AssertionError): + f(0) + + +def test_can_have_kwarg_postcondition(): + @postcondition(lambda **kw: kw["result"] > 0) + def f(a): + return a + + f(1) + with pytest.raises(AssertionError): + f(0) -- GitLab From 105fd19383be371fe97f37b224123a5b0606e580 Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 22:23:08 +0200 Subject: [PATCH 25/28] simplify annotation on return type (pyright) --- src/contractme/contracting.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index d315d26..2a65974 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -430,27 +430,21 @@ def annotations(f, message=None): else: msg = f"{n} should be instance of {t}" - if n != "return": - - def chk(**kw): - v = kw[n] - return isinstance(v, t) and not get_all_constraints_failures(v, meta) - - cond_f = precondition + if n == "return": + n2 = "result" + cond_f = postcondition else: + n2 = n + cond_f = precondition - def chk(result): - return isinstance(result, t) and not get_all_constraints_failures( - result, meta - ) - - cond_f = postcondition + def chk(**kw): + v = kw[n2] + return isinstance(v, t) and not get_all_constraints_failures(v, meta) cond = cond_f(chk, msg) return cond for n, t_or_typevar in th.items(): - if isinstance(t_or_typevar, TypeVar): t = t_or_typevar.__bound__ else: -- GitLab From da06c2ade23fbbc6553f245d0adcef72367d3cbc Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 23:19:23 +0200 Subject: [PATCH 26/28] fix pyright issues, and generic test --- tests/test_annotations.py | 174 ++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 100 deletions(-) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 8bee41b..4e58c42 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,6 +1,6 @@ import pytest from contractme import annotations -from typing import Annotated, TypeVar +from typing import Annotated, TypeVar, TypeAlias from annotated_types import ( Lt, Le, @@ -12,7 +12,8 @@ from annotated_types import ( MaxLen, Len, Timezone, - doc, + doc, # type: ignore + # for some reason this triggers pyright? # predicates LowerCase, ) @@ -29,7 +30,8 @@ def test_int(): assert birthday(1) == 2 with pytest.raises(AssertionError): - birthday(1.0) + birthday(1.0) # type: ignore + # mistyped on purpose def test_return_int(): @@ -43,15 +45,14 @@ def test_return_int(): def test_bound_typevar(): - ActuallyAnInt = TypeVar("ActuallyAnInt", bound=int) - @annotations - def birthday(age: ActuallyAnInt) -> ActuallyAnInt: - return age + 1 + def add[I: int](age: I, added: I) -> int: + return int(age + added) - assert birthday(1) == 2 + assert add(1, 1) == 2 with pytest.raises(AssertionError): - birthday(1.0) + add(1.0, 1.0) # type: ignore + # mistyped on purpose Age = Annotated[int, Ge(1)] @@ -68,7 +69,8 @@ def test_precondition(): birthday(0) with pytest.raises(AssertionError): - birthday(1.0) + birthday(1.0) # type: ignore + # mistyped on purpose def test_postcondition(): @@ -89,9 +91,8 @@ def test_postcondition_int(): birthday(1) +Lt0: TypeAlias = Annotated[int, Lt(0)] def test_Lt(): - Lt0 = Annotated[int, Lt(0)] - @annotations def f(lt0: Lt0): pass @@ -102,9 +103,8 @@ def test_Lt(): f(0) +Le0: TypeAlias = Annotated[int, Le(0)] def test_Le(): - Le0 = Annotated[int, Le(0)] - @annotations def f(le0: Le0): pass @@ -115,9 +115,8 @@ def test_Le(): f(1) +Ge0: TypeAlias = Annotated[int, Ge(0)] def test_Ge(): - Ge0 = Annotated[int, Ge(0)] - @annotations def f(ge0: Ge0): pass @@ -128,9 +127,8 @@ def test_Ge(): f(-1) +Gt0: TypeAlias = Annotated[int, Gt(0)] def test_Gt(): - Gt0 = Annotated[int, Gt(0)] - @annotations def f(gt0: Gt0): pass @@ -141,11 +139,10 @@ def test_Gt(): f(0) +Lt0_partial: TypeAlias = Annotated[int, functools.partial(operator.lt, 0)] def test_functools_partial_Lt(): - Lt0 = Annotated[int, functools.partial(operator.lt, 0)] - @annotations - def f(_: Lt0): + def f(_: Lt0_partial): pass f(-1) @@ -153,12 +150,12 @@ def test_functools_partial_Lt(): with pytest.raises(AssertionError): f(0) +Le0_partial: TypeAlias = Annotated[int, functools.partial(operator.le, 0)] -def test_functools_partial_Le(): - Le0 = Annotated[int, functools.partial(operator.le, 0)] +def test_functools_partial_Le(): @annotations - def f(_: Le0): + def f(_: Le0_partial): pass f(0) @@ -167,11 +164,10 @@ def test_functools_partial_Le(): f(1) +Ge0_partial: TypeAlias = Annotated[int, functools.partial(operator.ge, 0)] def test_functools_partial_Ge(): - Ge0 = Annotated[int, functools.partial(operator.ge, 0)] - @annotations - def f(_: Ge0): + def f(_: Ge0_partial): pass f(0) @@ -179,12 +175,10 @@ def test_functools_partial_Ge(): with pytest.raises(AssertionError): f(-1) - +Gt0_partial: TypeAlias = Annotated[int, functools.partial(operator.gt, 0)] def test_functools_partial_Gt(): - Gt0 = Annotated[int, functools.partial(operator.gt, 0)] - @annotations - def f(gt0: Gt0): + def f(gt0: Gt0_partial): pass f(1) @@ -192,70 +186,65 @@ def test_functools_partial_Gt(): with pytest.raises(AssertionError): f(0) +class MyInt_gt: + def __init__(self, val): + self.val = val -def test_Gt_custom(): - class MyInt: - def __init__(self, val): - self.val = val - - def __gt__(self, other): - return self.val > other.val + def __gt__(self, other: "MyInt_gt"): + return self.val > other.val - Gt0 = Annotated[MyInt, Gt(MyInt(0))] +Gt0_MyInt: TypeAlias = Annotated[MyInt_gt, Gt(MyInt_gt(0))] +def test_Gt_custom(): @annotations - def f_Gt0(gt0: Gt0): + def f_Gt0(gt0: Gt0_MyInt): pass - f_Gt0(MyInt(1)) + f_Gt0(MyInt_gt(1)) with pytest.raises(AssertionError): - f_Gt0(MyInt(0)) + f_Gt0(MyInt_gt(0)) +class MyInt_lt: + def __init__(self, val): + self.val = val -def test_Lt_custom(): - class MyInt: - def __init__(self, val): - self.val = val - - def __gt__(self, other): - return self.val > other.val + def __lt__(self, other: "MyInt_lt"): + return self.val < other.val - Lt0 = Annotated[MyInt, Lt(MyInt(0))] +Lt0_MyInt: TypeAlias = Annotated[MyInt_lt, Lt(MyInt_lt(0))] +def test_Lt_custom(): @annotations - def f_Lt0(gt0: Lt0): + def f_Lt0(lt0: Lt0_MyInt): pass - f_Lt0(MyInt(-1)) + f_Lt0(MyInt_lt(-1)) with pytest.raises(AssertionError): - f_Lt0(MyInt(0)) - - -def test_Ge_custom(): - class MyInt: - def __init__(self, val): - self.val = val + f_Lt0(MyInt_lt(0)) - def __ge__(self, other): - return self.val >= other.val +class MyInt_ge: + def __init__(self, val): + self.val = val - Ge0 = Annotated[MyInt, Ge(MyInt(0))] + def __ge__(self, other: "MyInt_ge"): + return self.val >= other.val +Ge0_MyInt: TypeAlias = Annotated[MyInt_ge, Ge(MyInt_ge(0))] +def test_Ge_custom(): @annotations - def f_Ge0(gt0: Ge0): + def f_Ge0(ge0: Ge0_MyInt): pass - f_Ge0(MyInt(0)) + f_Ge0(MyInt_ge(0)) with pytest.raises(AssertionError): - f_Ge0(MyInt(-1)) + f_Ge0(MyInt_ge(-1)) +Vacuous: TypeAlias = Annotated[int, Interval()] def test_vacuous_Interval(): - Vacuous = Annotated[int, Interval()] - @annotations def f(_: Vacuous): pass @@ -263,11 +252,10 @@ def test_vacuous_Interval(): f(0) +Gt0_Interval: TypeAlias = Annotated[int, Interval(gt=0)] def test_Interval_only_gt(): - Gt0Lt2 = Annotated[int, Interval(gt=0)] - @annotations - def f(_: Gt0Lt2): + def f(_: Gt0_Interval): pass f(1) @@ -277,9 +265,8 @@ def test_Interval_only_gt(): f(0) +Gt0Lt2: TypeAlias = Annotated[int, Interval(gt=0, lt=2)] def test_Interval_gt_lt(): - Gt0Lt2 = Annotated[int, Interval(gt=0, lt=2)] - @annotations def f(_: Gt0Lt2): pass @@ -292,9 +279,8 @@ def test_Interval_gt_lt(): f(2) +Ge0Lt2: TypeAlias = Annotated[int, Interval(ge=0, lt=2)] def test_Interval_ge_lt(): - Ge0Lt2 = Annotated[int, Interval(ge=0, lt=2)] - @annotations def f(_: Ge0Lt2): pass @@ -308,9 +294,8 @@ def test_Interval_ge_lt(): f(2) +Gt0Le2: TypeAlias = Annotated[int, Interval(gt=0, le=2)] def test_Interval_gt_le(): - Gt0Le2 = Annotated[int, Interval(gt=0, le=2)] - @annotations def f(_: Gt0Le2): pass @@ -324,9 +309,8 @@ def test_Interval_gt_le(): f(3) +Ge0Le2: TypeAlias = Annotated[int, Interval(ge=0, le=2)] def test_Interval_ge_le(): - Ge0Le2 = Annotated[int, Interval(ge=0, le=2)] - @annotations def f(_: Ge0Le2): pass @@ -340,9 +324,8 @@ def test_Interval_ge_le(): f(3) +Mult3: TypeAlias = Annotated[int, MultipleOf(3)] def test_MultipleOf(): - Mult3 = Annotated[int, MultipleOf(3)] - @annotations def f(_: Mult3): pass @@ -357,10 +340,8 @@ def test_MultipleOf(): with pytest.raises(AssertionError): f(1) - +NotEmpty: TypeAlias = Annotated[str, MinLen(1)] def test_MinLen(): - NotEmpty = Annotated[str, MinLen(1)] - @annotations def f(_: NotEmpty): pass @@ -370,9 +351,8 @@ def test_MinLen(): f("") +SmallStr: TypeAlias = Annotated[str, MaxLen(3)] def test_MaxLen(): - SmallStr = Annotated[str, MaxLen(3)] - @annotations def f(_: SmallStr): pass @@ -384,9 +364,8 @@ def test_MaxLen(): f("aaaa") +BigramOrTrigram: TypeAlias = Annotated[str, Len(2, 3)] def test_Len(): - BigramOrTrigram = Annotated[str, Len(2, 3)] - @annotations def f(_: BigramOrTrigram): pass @@ -400,11 +379,10 @@ def test_Len(): f("aaaa") +TzAware_Ellipsis: TypeAlias = Annotated[datetime, Timezone(...)] def test_Timezone_datetime_Ellipsis(): - TzAware = Annotated[datetime, Timezone(...)] - @annotations - def f(_: TzAware): + def f(_: TzAware_Ellipsis): pass f(datetime.now(tz=ZoneInfo("Europe/Paris"))) @@ -413,9 +391,8 @@ def test_Timezone_datetime_Ellipsis(): f(datetime.now(tz=None)) +NaiveDatetime: TypeAlias = Annotated[datetime, Timezone(None)] def test_Timezone_datetime_None(): - NaiveDatetime = Annotated[datetime, Timezone(None)] - @annotations def f(_: NaiveDatetime): pass @@ -426,11 +403,10 @@ def test_Timezone_datetime_None(): f(datetime.now(timezone.utc)) +AtParisTz_str: TypeAlias = Annotated[datetime, Timezone("Europe/Paris")] def test_Timezone_datetime_str(): - AtParisTz = Annotated[datetime, Timezone("Europe/Paris")] - @annotations - def f(_: AtParisTz): + def f(_: AtParisTz_str): pass f(datetime.now(tz=ZoneInfo("Europe/Paris"))) @@ -439,11 +415,10 @@ def test_Timezone_datetime_str(): f(datetime.now(timezone.utc)) +AtParisTz_ZoneInfo: TypeAlias = Annotated[datetime, Timezone(ZoneInfo("Europe/Paris"))] def test_Timezone_datetime_ZoneInfo(): - AtParisTz = Annotated[datetime, Timezone(ZoneInfo("Europe/Paris"))] - @annotations - def f(_: AtParisTz): + def f(_: AtParisTz_ZoneInfo): pass f(datetime.now(tz=ZoneInfo("Europe/Paris"))) @@ -472,9 +447,8 @@ def test_predicate_return(): f("aaaA") +Documented: TypeAlias = Annotated[int, doc("Well, it's a integer")] def test_Doc(): - Documented = Annotated[int, doc("Well, it's a integer")] - @annotations def f(a: Documented) -> Documented: return a -- GitLab From d166a47fce77da2ae5922f995005289c2d2c4e5c Mon Sep 17 00:00:00 2001 From: Leo Germond Date: Thu, 24 Apr 2025 23:19:53 +0200 Subject: [PATCH 27/28] black --- tests/test_annotations.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index 4e58c42..ad81c6b 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -92,6 +92,8 @@ def test_postcondition_int(): Lt0: TypeAlias = Annotated[int, Lt(0)] + + def test_Lt(): @annotations def f(lt0: Lt0): @@ -104,6 +106,8 @@ def test_Lt(): Le0: TypeAlias = Annotated[int, Le(0)] + + def test_Le(): @annotations def f(le0: Le0): @@ -116,6 +120,8 @@ def test_Le(): Ge0: TypeAlias = Annotated[int, Ge(0)] + + def test_Ge(): @annotations def f(ge0: Ge0): @@ -128,6 +134,8 @@ def test_Ge(): Gt0: TypeAlias = Annotated[int, Gt(0)] + + def test_Gt(): @annotations def f(gt0: Gt0): @@ -140,6 +148,8 @@ def test_Gt(): Lt0_partial: TypeAlias = Annotated[int, functools.partial(operator.lt, 0)] + + def test_functools_partial_Lt(): @annotations def f(_: Lt0_partial): @@ -150,6 +160,7 @@ def test_functools_partial_Lt(): with pytest.raises(AssertionError): f(0) + Le0_partial: TypeAlias = Annotated[int, functools.partial(operator.le, 0)] @@ -165,6 +176,8 @@ def test_functools_partial_Le(): Ge0_partial: TypeAlias = Annotated[int, functools.partial(operator.ge, 0)] + + def test_functools_partial_Ge(): @annotations def f(_: Ge0_partial): @@ -175,7 +188,10 @@ def test_functools_partial_Ge(): with pytest.raises(AssertionError): f(-1) + Gt0_partial: TypeAlias = Annotated[int, functools.partial(operator.gt, 0)] + + def test_functools_partial_Gt(): @annotations def f(gt0: Gt0_partial): @@ -186,6 +202,7 @@ def test_functools_partial_Gt(): with pytest.raises(AssertionError): f(0) + class MyInt_gt: def __init__(self, val): self.val = val @@ -193,8 +210,10 @@ class MyInt_gt: def __gt__(self, other: "MyInt_gt"): return self.val > other.val + Gt0_MyInt: TypeAlias = Annotated[MyInt_gt, Gt(MyInt_gt(0))] + def test_Gt_custom(): @annotations def f_Gt0(gt0: Gt0_MyInt): @@ -205,6 +224,7 @@ def test_Gt_custom(): with pytest.raises(AssertionError): f_Gt0(MyInt_gt(0)) + class MyInt_lt: def __init__(self, val): self.val = val @@ -214,6 +234,8 @@ class MyInt_lt: Lt0_MyInt: TypeAlias = Annotated[MyInt_lt, Lt(MyInt_lt(0))] + + def test_Lt_custom(): @annotations def f_Lt0(lt0: Lt0_MyInt): @@ -224,6 +246,7 @@ def test_Lt_custom(): with pytest.raises(AssertionError): f_Lt0(MyInt_lt(0)) + class MyInt_ge: def __init__(self, val): self.val = val @@ -231,7 +254,10 @@ class MyInt_ge: def __ge__(self, other: "MyInt_ge"): return self.val >= other.val + Ge0_MyInt: TypeAlias = Annotated[MyInt_ge, Ge(MyInt_ge(0))] + + def test_Ge_custom(): @annotations def f_Ge0(ge0: Ge0_MyInt): @@ -244,6 +270,8 @@ def test_Ge_custom(): Vacuous: TypeAlias = Annotated[int, Interval()] + + def test_vacuous_Interval(): @annotations def f(_: Vacuous): @@ -253,6 +281,8 @@ def test_vacuous_Interval(): Gt0_Interval: TypeAlias = Annotated[int, Interval(gt=0)] + + def test_Interval_only_gt(): @annotations def f(_: Gt0_Interval): @@ -266,6 +296,8 @@ def test_Interval_only_gt(): Gt0Lt2: TypeAlias = Annotated[int, Interval(gt=0, lt=2)] + + def test_Interval_gt_lt(): @annotations def f(_: Gt0Lt2): @@ -280,6 +312,8 @@ def test_Interval_gt_lt(): Ge0Lt2: TypeAlias = Annotated[int, Interval(ge=0, lt=2)] + + def test_Interval_ge_lt(): @annotations def f(_: Ge0Lt2): @@ -295,6 +329,8 @@ def test_Interval_ge_lt(): Gt0Le2: TypeAlias = Annotated[int, Interval(gt=0, le=2)] + + def test_Interval_gt_le(): @annotations def f(_: Gt0Le2): @@ -310,6 +346,8 @@ def test_Interval_gt_le(): Ge0Le2: TypeAlias = Annotated[int, Interval(ge=0, le=2)] + + def test_Interval_ge_le(): @annotations def f(_: Ge0Le2): @@ -325,6 +363,8 @@ def test_Interval_ge_le(): Mult3: TypeAlias = Annotated[int, MultipleOf(3)] + + def test_MultipleOf(): @annotations def f(_: Mult3): @@ -340,7 +380,10 @@ def test_MultipleOf(): with pytest.raises(AssertionError): f(1) + NotEmpty: TypeAlias = Annotated[str, MinLen(1)] + + def test_MinLen(): @annotations def f(_: NotEmpty): @@ -352,6 +395,8 @@ def test_MinLen(): SmallStr: TypeAlias = Annotated[str, MaxLen(3)] + + def test_MaxLen(): @annotations def f(_: SmallStr): @@ -365,6 +410,8 @@ def test_MaxLen(): BigramOrTrigram: TypeAlias = Annotated[str, Len(2, 3)] + + def test_Len(): @annotations def f(_: BigramOrTrigram): @@ -380,6 +427,8 @@ def test_Len(): TzAware_Ellipsis: TypeAlias = Annotated[datetime, Timezone(...)] + + def test_Timezone_datetime_Ellipsis(): @annotations def f(_: TzAware_Ellipsis): @@ -392,6 +441,8 @@ def test_Timezone_datetime_Ellipsis(): NaiveDatetime: TypeAlias = Annotated[datetime, Timezone(None)] + + def test_Timezone_datetime_None(): @annotations def f(_: NaiveDatetime): @@ -404,6 +455,8 @@ def test_Timezone_datetime_None(): AtParisTz_str: TypeAlias = Annotated[datetime, Timezone("Europe/Paris")] + + def test_Timezone_datetime_str(): @annotations def f(_: AtParisTz_str): @@ -416,6 +469,8 @@ def test_Timezone_datetime_str(): AtParisTz_ZoneInfo: TypeAlias = Annotated[datetime, Timezone(ZoneInfo("Europe/Paris"))] + + def test_Timezone_datetime_ZoneInfo(): @annotations def f(_: AtParisTz_ZoneInfo): @@ -448,6 +503,8 @@ def test_predicate_return(): Documented: TypeAlias = Annotated[int, doc("Well, it's a integer")] + + def test_Doc(): @annotations def f(a: Documented) -> Documented: -- GitLab From d51b924b17639661bc9bdc19c6070d9dbe65fbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Germond?= Date: Sun, 27 Apr 2025 18:57:04 +0200 Subject: [PATCH 28/28] rename @annotations -> @annotated --- README.md | 4 +- src/contractme/__init__.py | 2 +- src/contractme/contracting.py | 2 +- tests/test_annotations.py | 70 +++++++++++++++++------------------ 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fed850a..f840db7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ def append_count(l: list[int], n: int): ## Using annotations ```python -@annotations +@annotated def incr(v : int) -> int: return v + 1 ``` @@ -45,7 +45,7 @@ to be correct, so it won't cause a check failure. ```python Even = Annotated[int, MultipleOf(2)] -@annotations +@annotated def square(v : Even) -> Even return v * v ``` diff --git a/src/contractme/__init__.py b/src/contractme/__init__.py index d0aaad1..dc47ebe 100644 --- a/src/contractme/__init__.py +++ b/src/contractme/__init__.py @@ -7,4 +7,4 @@ check_postconditions = contracting.check_postconditions precondition = contracting.precondition postcondition = contracting.postcondition -annotations = contracting.annotations +annotated = contracting.annotated diff --git a/src/contractme/contracting.py b/src/contractme/contracting.py index 2a65974..b1f8197 100644 --- a/src/contractme/contracting.py +++ b/src/contractme/contracting.py @@ -419,7 +419,7 @@ def get_all_constraints_failures(val, constraints): return failing -def annotations(f, message=None): +def annotated(f, message=None): th = get_type_hints(f) an = get_annotations(f) diff --git a/tests/test_annotations.py b/tests/test_annotations.py index ad81c6b..c06437c 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -1,5 +1,5 @@ import pytest -from contractme import annotations +from contractme import annotated from typing import Annotated, TypeVar, TypeAlias from annotated_types import ( Lt, @@ -24,7 +24,7 @@ from zoneinfo import ZoneInfo def test_int(): - @annotations + @annotated def birthday(age: int): return age + 1 @@ -35,7 +35,7 @@ def test_int(): def test_return_int(): - @annotations + @annotated def birthday(age) -> int: return age + 1 @@ -45,7 +45,7 @@ def test_return_int(): def test_bound_typevar(): - @annotations + @annotated def add[I: int](age: I, added: I) -> int: return int(age + added) @@ -59,7 +59,7 @@ Age = Annotated[int, Ge(1)] def test_precondition(): - @annotations + @annotated def birthday(age: Age) -> Age: return age + 1 @@ -74,7 +74,7 @@ def test_precondition(): def test_postcondition(): - @annotations + @annotated def antibirthday(age: Age) -> Age: return age - 1 @@ -83,7 +83,7 @@ def test_postcondition(): def test_postcondition_int(): - @annotations + @annotated def birthday(age) -> Age: return age + 1.0 @@ -95,7 +95,7 @@ Lt0: TypeAlias = Annotated[int, Lt(0)] def test_Lt(): - @annotations + @annotated def f(lt0: Lt0): pass @@ -109,7 +109,7 @@ Le0: TypeAlias = Annotated[int, Le(0)] def test_Le(): - @annotations + @annotated def f(le0: Le0): pass @@ -123,7 +123,7 @@ Ge0: TypeAlias = Annotated[int, Ge(0)] def test_Ge(): - @annotations + @annotated def f(ge0: Ge0): pass @@ -137,7 +137,7 @@ Gt0: TypeAlias = Annotated[int, Gt(0)] def test_Gt(): - @annotations + @annotated def f(gt0: Gt0): pass @@ -151,7 +151,7 @@ Lt0_partial: TypeAlias = Annotated[int, functools.partial(operator.lt, 0)] def test_functools_partial_Lt(): - @annotations + @annotated def f(_: Lt0_partial): pass @@ -165,7 +165,7 @@ Le0_partial: TypeAlias = Annotated[int, functools.partial(operator.le, 0)] def test_functools_partial_Le(): - @annotations + @annotated def f(_: Le0_partial): pass @@ -179,7 +179,7 @@ Ge0_partial: TypeAlias = Annotated[int, functools.partial(operator.ge, 0)] def test_functools_partial_Ge(): - @annotations + @annotated def f(_: Ge0_partial): pass @@ -193,7 +193,7 @@ Gt0_partial: TypeAlias = Annotated[int, functools.partial(operator.gt, 0)] def test_functools_partial_Gt(): - @annotations + @annotated def f(gt0: Gt0_partial): pass @@ -215,7 +215,7 @@ Gt0_MyInt: TypeAlias = Annotated[MyInt_gt, Gt(MyInt_gt(0))] def test_Gt_custom(): - @annotations + @annotated def f_Gt0(gt0: Gt0_MyInt): pass @@ -237,7 +237,7 @@ Lt0_MyInt: TypeAlias = Annotated[MyInt_lt, Lt(MyInt_lt(0))] def test_Lt_custom(): - @annotations + @annotated def f_Lt0(lt0: Lt0_MyInt): pass @@ -259,7 +259,7 @@ Ge0_MyInt: TypeAlias = Annotated[MyInt_ge, Ge(MyInt_ge(0))] def test_Ge_custom(): - @annotations + @annotated def f_Ge0(ge0: Ge0_MyInt): pass @@ -273,7 +273,7 @@ Vacuous: TypeAlias = Annotated[int, Interval()] def test_vacuous_Interval(): - @annotations + @annotated def f(_: Vacuous): pass @@ -284,7 +284,7 @@ Gt0_Interval: TypeAlias = Annotated[int, Interval(gt=0)] def test_Interval_only_gt(): - @annotations + @annotated def f(_: Gt0_Interval): pass @@ -299,7 +299,7 @@ Gt0Lt2: TypeAlias = Annotated[int, Interval(gt=0, lt=2)] def test_Interval_gt_lt(): - @annotations + @annotated def f(_: Gt0Lt2): pass @@ -315,7 +315,7 @@ Ge0Lt2: TypeAlias = Annotated[int, Interval(ge=0, lt=2)] def test_Interval_ge_lt(): - @annotations + @annotated def f(_: Ge0Lt2): pass @@ -332,7 +332,7 @@ Gt0Le2: TypeAlias = Annotated[int, Interval(gt=0, le=2)] def test_Interval_gt_le(): - @annotations + @annotated def f(_: Gt0Le2): pass @@ -349,7 +349,7 @@ Ge0Le2: TypeAlias = Annotated[int, Interval(ge=0, le=2)] def test_Interval_ge_le(): - @annotations + @annotated def f(_: Ge0Le2): pass @@ -366,7 +366,7 @@ Mult3: TypeAlias = Annotated[int, MultipleOf(3)] def test_MultipleOf(): - @annotations + @annotated def f(_: Mult3): pass @@ -385,7 +385,7 @@ NotEmpty: TypeAlias = Annotated[str, MinLen(1)] def test_MinLen(): - @annotations + @annotated def f(_: NotEmpty): pass @@ -398,7 +398,7 @@ SmallStr: TypeAlias = Annotated[str, MaxLen(3)] def test_MaxLen(): - @annotations + @annotated def f(_: SmallStr): pass @@ -413,7 +413,7 @@ BigramOrTrigram: TypeAlias = Annotated[str, Len(2, 3)] def test_Len(): - @annotations + @annotated def f(_: BigramOrTrigram): pass @@ -430,7 +430,7 @@ TzAware_Ellipsis: TypeAlias = Annotated[datetime, Timezone(...)] def test_Timezone_datetime_Ellipsis(): - @annotations + @annotated def f(_: TzAware_Ellipsis): pass @@ -444,7 +444,7 @@ NaiveDatetime: TypeAlias = Annotated[datetime, Timezone(None)] def test_Timezone_datetime_None(): - @annotations + @annotated def f(_: NaiveDatetime): pass @@ -458,7 +458,7 @@ AtParisTz_str: TypeAlias = Annotated[datetime, Timezone("Europe/Paris")] def test_Timezone_datetime_str(): - @annotations + @annotated def f(_: AtParisTz_str): pass @@ -472,7 +472,7 @@ AtParisTz_ZoneInfo: TypeAlias = Annotated[datetime, Timezone(ZoneInfo("Europe/Pa def test_Timezone_datetime_ZoneInfo(): - @annotations + @annotated def f(_: AtParisTz_ZoneInfo): pass @@ -483,7 +483,7 @@ def test_Timezone_datetime_ZoneInfo(): def test_predicate_input(): - @annotations + @annotated def f(_: LowerCase): pass @@ -493,7 +493,7 @@ def test_predicate_input(): def test_predicate_return(): - @annotations + @annotated def f(a: str) -> LowerCase: return a @@ -506,7 +506,7 @@ Documented: TypeAlias = Annotated[int, doc("Well, it's a integer")] def test_Doc(): - @annotations + @annotated def f(a: Documented) -> Documented: return a -- GitLab