From 2ea3e9ca15b27b2c09a87bb0e390d2868c3ecab2 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Thu, 10 Feb 2022 10:13:40 +0200 Subject: [PATCH 01/12] feat: Add typing for function_decorator # Conflicts: # tests/test_typing.py --- src/decopatch/main.py | 58 +++++++++++++++++++++++++++++++++++++------ tests/test_typing.py | 40 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 tests/test_typing.py diff --git a/src/decopatch/main.py b/src/decopatch/main.py index 68200d1..c6e0a1a 100644 --- a/src/decopatch/main.py +++ b/src/decopatch/main.py @@ -1,21 +1,63 @@ -from makefun import with_signature, add_signature_parameters +from makefun import add_signature_parameters, with_signature + +from decopatch.utils_calls import (call_in_appropriate_mode, + no_parenthesis_usage, + with_parenthesis_usage) +from decopatch.utils_disambiguation import ( + DecoratorUsageInfo, FirstArgDisambiguation, can_arg_be_a_decorator_target, + create_single_arg_callable_or_class_disambiguator, disambiguate_call) from decopatch.utils_modes import SignatureInfo, make_decorator_spec -from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \ - DecoratorUsageInfo, can_arg_be_a_decorator_target -from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode try: # python 3.3+ - from inspect import signature, Parameter + from inspect import Parameter, signature except ImportError: - from funcsigs import signature, Parameter + from funcsigs import Parameter, signature try: # python 3.5+ - from typing import Callable, Any, Optional + from typing import Any, Callable, Optional +except ImportError: + pass + +try: # Python 3.9 + from typing import Any, Callable, Protocol, TypeVar, Union, overload + try: # Python 3.10 + from typing import ParamSpec + except ImportError: + from typing_extensions import ParamSpec + + P = ParamSpec("P") + F = TypeVar("F", bound=Callable) + + class _Decorator(Protocol[P]): + + @overload + def __call__(self, func: F) -> F: + ... + + @overload + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[F], F]: + ... + + @overload + def function_decorator( + enable_stack_introspection: Callable[P, Any], + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + flat_mode_decorated_name: Optional[str] = ..., + ) -> _Decorator[P]: + ... + + @overload + def function_decorator( + enable_stack_introspection: bool = ..., + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + flat_mode_decorated_name: Optional[str] = ..., + ) -> Callable[[Callable[P, Any]], _Decorator[P]]: + ... except ImportError: pass -def function_decorator(enable_stack_introspection=False, # type: bool +def function_decorator(enable_stack_introspection=False, # type: Union[Callable, bool] custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] flat_mode_decorated_name=None, # type: Optional[str] ): diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 0000000..1936fb2 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,40 @@ +# This is test file for typing, +# No automatic testing is used at the moment. Just use your type checker and see if it works. +from decopatch import DECORATED, function_decorator + + +@function_decorator +def decorator(scope: str = "test", f=DECORATED): + pass + +# Ok, should reveal coorect type for `enable_stack_introspection` +@function_decorator(enable_stack_introspection=True) +def decorator_with_params(scope: str = "test", f=DECORATED): + pass + + +# Error, invalid argument +@function_decorator(invalid_param=True) +def decorator_wint_invalid_param(): + pass + +# Ok +@decorator +def decorated_flat(): + pass + +# Error, Literal[2] is incompatible with str +@decorator(scope=2) +def decorated_with_invalid_options(): + pass + +# Ok, should reveal coorect type for `scope` +@decorator(scope="success") +def decorated_with_valid_options(): + pass + + +# Ok, should reveal coorect type for `scope` +@decorator_with_params(scope="success") +def decorated_with_valid_options_v2(): + pass From 4c31ae5d8df85d75aa125361ee357f6d5f7bc4ad Mon Sep 17 00:00:00 2001 From: Serhii Tereshchenko Date: Fri, 11 Feb 2022 17:19:27 +0200 Subject: [PATCH 02/12] Update tests/test_typing.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sylvain MariƩ --- tests/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 1936fb2..391c850 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -28,7 +28,7 @@ def decorated_flat(): def decorated_with_invalid_options(): pass -# Ok, should reveal coorect type for `scope` +# Ok, should reveal correct type for `scope` @decorator(scope="success") def decorated_with_valid_options(): pass From 7c1ca2532ca68cd4c7780bdb01e6f4f447d24317 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 17:19:04 +0200 Subject: [PATCH 03/12] review: Remove try/except cases for old python --- src/decopatch/main.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/decopatch/main.py b/src/decopatch/main.py index c6e0a1a..5fea6ca 100644 --- a/src/decopatch/main.py +++ b/src/decopatch/main.py @@ -1,3 +1,6 @@ +from inspect import Parameter +from typing import Any, Callable, Optional + from makefun import add_signature_parameters, with_signature from decopatch.utils_calls import (call_in_appropriate_mode, @@ -8,18 +11,8 @@ create_single_arg_callable_or_class_disambiguator, disambiguate_call) from decopatch.utils_modes import SignatureInfo, make_decorator_spec -try: # python 3.3+ - from inspect import Parameter, signature -except ImportError: - from funcsigs import Parameter, signature - -try: # python 3.5+ - from typing import Any, Callable, Optional -except ImportError: - pass - try: # Python 3.9 - from typing import Any, Callable, Protocol, TypeVar, Union, overload + from typing import Protocol, TypeVar, Union, overload try: # Python 3.10 from typing import ParamSpec except ImportError: From 08ecfd4272433804b8341cabb6de86c2aa09bc87 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 17:20:32 +0200 Subject: [PATCH 04/12] chore: Fix typos --- tests/test_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 391c850..10bad81 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -7,7 +7,7 @@ def decorator(scope: str = "test", f=DECORATED): pass -# Ok, should reveal coorect type for `enable_stack_introspection` +# Ok, should reveal correct type for `enable_stack_introspection` @function_decorator(enable_stack_introspection=True) def decorator_with_params(scope: str = "test", f=DECORATED): pass @@ -34,7 +34,7 @@ def decorated_with_valid_options(): pass -# Ok, should reveal coorect type for `scope` +# Ok, should reveal correct type for `scope` @decorator_with_params(scope="success") def decorated_with_valid_options_v2(): pass From d0e97bf560047a4effce48c1d4fb5a6cccdc9717 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 18:01:03 +0200 Subject: [PATCH 05/12] docs: Add comments --- src/decopatch/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/decopatch/main.py b/src/decopatch/main.py index 5fea6ca..4e29c3b 100644 --- a/src/decopatch/main.py +++ b/src/decopatch/main.py @@ -22,13 +22,19 @@ F = TypeVar("F", bound=Callable) class _Decorator(Protocol[P]): + """ + This is callable Protocol, to distinguish between cases where + created decorator is called as `@decorator` or `@decorator()` + """ @overload def __call__(self, func: F) -> F: + # decorator is called without parenthesis: @decorator ... @overload def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[F], F]: + # decorator is called with options or parenthesis: @decorator(some_option=...) ... @overload @@ -37,6 +43,7 @@ def function_decorator( custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., flat_mode_decorated_name: Optional[str] = ..., ) -> _Decorator[P]: + # @function_decorator is called without options or parenthesis ... @overload @@ -45,6 +52,7 @@ def function_decorator( custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., flat_mode_decorated_name: Optional[str] = ..., ) -> Callable[[Callable[P, Any]], _Decorator[P]]: + # @function_decorator() is called with options or parenthesis. ... except ImportError: pass From 13d8e0337b5dfb7c31f15fe08ce75349916d7233 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 18:24:51 +0200 Subject: [PATCH 06/12] chore: Rename typing cases file --- tests/{test_typing.py => typing_cases.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_typing.py => typing_cases.py} (100%) diff --git a/tests/test_typing.py b/tests/typing_cases.py similarity index 100% rename from tests/test_typing.py rename to tests/typing_cases.py From bbf24fe2aab16b1dfb47e4d2aa0939afa72ca2f1 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 18:28:45 +0200 Subject: [PATCH 07/12] revert try/except cleanup --- src/decopatch/main.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/decopatch/main.py b/src/decopatch/main.py index 4e29c3b..18fe748 100644 --- a/src/decopatch/main.py +++ b/src/decopatch/main.py @@ -1,6 +1,3 @@ -from inspect import Parameter -from typing import Any, Callable, Optional - from makefun import add_signature_parameters, with_signature from decopatch.utils_calls import (call_in_appropriate_mode, @@ -11,6 +8,16 @@ create_single_arg_callable_or_class_disambiguator, disambiguate_call) from decopatch.utils_modes import SignatureInfo, make_decorator_spec +try: + from inspect import Parameter +except ImportError: + from funcsigs import Parameter + +try: # Python 3.5+ + from typing import Any, Callable, Optional +except ImportError: + pass + try: # Python 3.9 from typing import Protocol, TypeVar, Union, overload try: # Python 3.10 From e4ddeabeb0b797563f5c817c4a5b22d6955527bd Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 19:03:52 +0200 Subject: [PATCH 08/12] fix: Use pytest in test file --- tests/typing_cases.py | 61 ++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/tests/typing_cases.py b/tests/typing_cases.py index 10bad81..218ad41 100644 --- a/tests/typing_cases.py +++ b/tests/typing_cases.py @@ -1,40 +1,47 @@ # This is test file for typing, # No automatic testing is used at the moment. Just use your type checker and see if it works. +# Pytest here is used to make sure that runtime behavir matches with type checker expecter errors. from decopatch import DECORATED, function_decorator +import pytest -@function_decorator -def decorator(scope: str = "test", f=DECORATED): - pass +@pytest.mark.xfail(raises=TypeError) +def test_invalid_parameter(): + # Error, invalid argument + @function_decorator(invalid_param=True) + def decorator_wint_invalid_param(): + pass -# Ok, should reveal correct type for `enable_stack_introspection` -@function_decorator(enable_stack_introspection=True) -def decorator_with_params(scope: str = "test", f=DECORATED): - pass +def test_normal_decorator(): + @function_decorator + def decorator(scope: str = "test", f=DECORATED): + pass -# Error, invalid argument -@function_decorator(invalid_param=True) -def decorator_wint_invalid_param(): - pass + # Ok + @decorator + def decorated_flat(): + pass -# Ok -@decorator -def decorated_flat(): - pass + with pytest.raises(TypeError): + # Error, Literal[2] is incompatible with str + @decorator(scope=2) + def decorated_with_invalid_options(): + pass -# Error, Literal[2] is incompatible with str -@decorator(scope=2) -def decorated_with_invalid_options(): - pass + # Ok, should reveal correct type for `scope` + @decorator(scope="success") + def decorated_with_valid_options(): + pass -# Ok, should reveal correct type for `scope` -@decorator(scope="success") -def decorated_with_valid_options(): - pass +def test_decorator_with_params(): + # Ok, should reveal correct type for `enable_stack_introspection` + @function_decorator(enable_stack_introspection=True) + def decorator_with_params(scope: str = "test", f=DECORATED): + pass -# Ok, should reveal correct type for `scope` -@decorator_with_params(scope="success") -def decorated_with_valid_options_v2(): - pass + # Ok, should reveal correct type for `scope` + @decorator_with_params(scope="success") + def decorated_with_valid_options_v2(): + pass From a13877f9ccd4afd60fb434d7d891770bce0ff419 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 19:04:12 +0200 Subject: [PATCH 09/12] chore: Move test file --- tests/{typing_cases.py => test_typing.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{typing_cases.py => test_typing.py} (100%) diff --git a/tests/typing_cases.py b/tests/test_typing.py similarity index 100% rename from tests/typing_cases.py rename to tests/test_typing.py From d5762b50ceb0924fcc2eab94921eb0bac5ef30b7 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Fri, 11 Feb 2022 21:15:21 +0200 Subject: [PATCH 10/12] chore: Fix tests --- tests/test_typing.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 218ad41..b7840b3 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -5,25 +5,28 @@ import pytest -@pytest.mark.xfail(raises=TypeError) def test_invalid_parameter(): - # Error, invalid argument - @function_decorator(invalid_param=True) - def decorator_wint_invalid_param(): - pass + with pytest.raises(TypeError): + # Error, invalid argument + @function_decorator(invalid_param=True) + def decorator_wint_invalid_param(fn=DECORATED): + return fn def test_normal_decorator(): @function_decorator - def decorator(scope: str = "test", f=DECORATED): - pass + def decorator(scope: str = "test", fn=DECORATED): + assert isinstance(scope, str) + return fn # Ok @decorator def decorated_flat(): pass - with pytest.raises(TypeError): + assert decorated_flat + + with pytest.raises(AssertionError): # Error, Literal[2] is incompatible with str @decorator(scope=2) def decorated_with_invalid_options(): @@ -34,14 +37,18 @@ def decorated_with_invalid_options(): def decorated_with_valid_options(): pass + assert decorated_with_valid_options -def test_decorator_with_params(): + +def test_function_decorator_with_params(): # Ok, should reveal correct type for `enable_stack_introspection` @function_decorator(enable_stack_introspection=True) - def decorator_with_params(scope: str = "test", f=DECORATED): - pass + def decorator_with_params(scope: str = "test", fn=DECORATED): + return fn # Ok, should reveal correct type for `scope` @decorator_with_params(scope="success") - def decorated_with_valid_options_v2(): + def decorated_with_valid_options(): pass + + assert decorated_with_valid_options From 69ff6483782c2158576a105427b20fff31c63004 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Sat, 12 Feb 2022 00:18:45 +0200 Subject: [PATCH 11/12] fix: Move typings to pyi --- src/decopatch/main.py | 70 ++++++---------------------------------- src/decopatch/main.pyi | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 60 deletions(-) create mode 100644 src/decopatch/main.pyi diff --git a/src/decopatch/main.py b/src/decopatch/main.py index 18fe748..68200d1 100644 --- a/src/decopatch/main.py +++ b/src/decopatch/main.py @@ -1,71 +1,21 @@ -from makefun import add_signature_parameters, with_signature - -from decopatch.utils_calls import (call_in_appropriate_mode, - no_parenthesis_usage, - with_parenthesis_usage) -from decopatch.utils_disambiguation import ( - DecoratorUsageInfo, FirstArgDisambiguation, can_arg_be_a_decorator_target, - create_single_arg_callable_or_class_disambiguator, disambiguate_call) +from makefun import with_signature, add_signature_parameters from decopatch.utils_modes import SignatureInfo, make_decorator_spec +from decopatch.utils_disambiguation import create_single_arg_callable_or_class_disambiguator, disambiguate_call, \ + DecoratorUsageInfo, can_arg_be_a_decorator_target +from decopatch.utils_calls import with_parenthesis_usage, no_parenthesis_usage, call_in_appropriate_mode -try: - from inspect import Parameter +try: # python 3.3+ + from inspect import signature, Parameter except ImportError: - from funcsigs import Parameter + from funcsigs import signature, Parameter -try: # Python 3.5+ - from typing import Any, Callable, Optional +try: # python 3.5+ + from typing import Callable, Any, Optional except ImportError: pass -try: # Python 3.9 - from typing import Protocol, TypeVar, Union, overload - try: # Python 3.10 - from typing import ParamSpec - except ImportError: - from typing_extensions import ParamSpec - P = ParamSpec("P") - F = TypeVar("F", bound=Callable) - - class _Decorator(Protocol[P]): - """ - This is callable Protocol, to distinguish between cases where - created decorator is called as `@decorator` or `@decorator()` - """ - - @overload - def __call__(self, func: F) -> F: - # decorator is called without parenthesis: @decorator - ... - - @overload - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[F], F]: - # decorator is called with options or parenthesis: @decorator(some_option=...) - ... - - @overload - def function_decorator( - enable_stack_introspection: Callable[P, Any], - custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., - flat_mode_decorated_name: Optional[str] = ..., - ) -> _Decorator[P]: - # @function_decorator is called without options or parenthesis - ... - - @overload - def function_decorator( - enable_stack_introspection: bool = ..., - custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., - flat_mode_decorated_name: Optional[str] = ..., - ) -> Callable[[Callable[P, Any]], _Decorator[P]]: - # @function_decorator() is called with options or parenthesis. - ... -except ImportError: - pass - - -def function_decorator(enable_stack_introspection=False, # type: Union[Callable, bool] +def function_decorator(enable_stack_introspection=False, # type: bool custom_disambiguator=None, # type: Callable[[Any], FirstArgDisambiguation] flat_mode_decorated_name=None, # type: Optional[str] ): diff --git a/src/decopatch/main.pyi b/src/decopatch/main.pyi new file mode 100644 index 0000000..8649211 --- /dev/null +++ b/src/decopatch/main.pyi @@ -0,0 +1,72 @@ +from typing import Any, Callable, Optional, Protocol, TypeVar, overload + +from decopatch.utils_disambiguation import FirstArgDisambiguation +from decopatch.utils_modes import SignatureInfo + +try: + from typing import ParamSpec +except ImportError: + from typing_extensions import ParamSpec + +P = ParamSpec("P") +F = TypeVar("F", bound=Callable[..., Any]) + +class _Decorator(Protocol[P]): + """ + This is callable Protocol, to distinguish between cases where + created decorator is called as `@decorator` or `@decorator()` + """ + + # decorator is called without parenthesis: @decorator + @overload + def __call__(self, func: F) -> F: ... + # decorator is called with options or parenthesis: @decorator(some_option=...) + @overload + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[F], F]: ... + +# @function_decorator is called without options or parenthesis +@overload +def function_decorator( + enable_stack_introspection: Callable[P, Any], + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + flat_mode_decorated_name: Optional[str] = ..., +) -> _Decorator[P]: ... + +# @function_decorator() is called with options or parenthesis. +@overload +def function_decorator( + enable_stack_introspection: bool = ..., + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + flat_mode_decorated_name: Optional[str] = ..., +) -> Callable[[Callable[P, Any]], _Decorator[P]]: ... +def class_decorator( + enable_stack_introspection: bool = ..., + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + flat_mode_decorated_name: Optional[str] = ..., +): ... +def decorator( + is_function_decorator: bool = ..., + is_class_decorator: bool = ..., + enable_stack_introspection: bool = ..., + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + use_signature_trick: bool = ..., + flat_mode_decorated_name: str = ..., +): ... +def create_decorator( + impl_function, + is_function_decorator: bool = ..., + is_class_decorator: bool = ..., + enable_stack_introspection: bool = ..., + custom_disambiguator: Callable[[Any], FirstArgDisambiguation] = ..., + use_signature_trick: bool = ..., + flat_mode_decorated_name: Optional[str] = ..., +): ... +def create_no_args_decorator( + decorator_function, function_for_metadata: Any | None = ... +): ... +def create_kwonly_decorator( + sig_info: SignatureInfo, decorator_function, disambiguator, function_for_metadata +): ... +def create_general_case_decorator( + sig_info: SignatureInfo, impl_function, disambiguator, function_for_metadata +): ... From ad3e2f35d56ca5282f8cf39185d81f8b9b352325 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Sat, 12 Feb 2022 00:31:28 +0200 Subject: [PATCH 12/12] fix: Fix tests for python 2.7 --- tests/test_typing.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index b7840b3..9fbb395 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,9 +1,12 @@ # This is test file for typing, # No automatic testing is used at the moment. Just use your type checker and see if it works. # Pytest here is used to make sure that runtime behavir matches with type checker expecter errors. -from decopatch import DECORATED, function_decorator +from typing import Any, Callable + import pytest +from decopatch import DECORATED, function_decorator + def test_invalid_parameter(): with pytest.raises(TypeError): @@ -15,7 +18,7 @@ def decorator_wint_invalid_param(fn=DECORATED): def test_normal_decorator(): @function_decorator - def decorator(scope: str = "test", fn=DECORATED): + def decorator(scope="test", fn=DECORATED): # type: (str, Any) -> Callable[..., Any] assert isinstance(scope, str) return fn @@ -43,7 +46,7 @@ def decorated_with_valid_options(): def test_function_decorator_with_params(): # Ok, should reveal correct type for `enable_stack_introspection` @function_decorator(enable_stack_introspection=True) - def decorator_with_params(scope: str = "test", fn=DECORATED): + def decorator_with_params(scope = "test", fn=DECORATED): # type: (str, Any) -> Callable[..., Any] return fn # Ok, should reveal correct type for `scope`