From 0374c82dbea3516b7eb8d9d28b4706774fe78a4e Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Fri, 4 Nov 2022 16:22:58 +0000 Subject: [PATCH 1/2] tools/exekall: Support PEP 673 Self return annotation FEATURE Recognize PEP 673 Self return annotation on classmethods as being a factory class method. --- tools/exekall/exekall/engine.py | 14 +++++++-- tools/exekall/exekall/tests/suite.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index cfd893bf9..964059691 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -2824,10 +2824,20 @@ class Operator: """ ``True`` if the callable is a factory ``classmethod``, i.e. a classmethod that returns objects of the class it is defined in (or of a - subclass of it). + subclass of it), or has a return annotation of typing.Self. """ callable_ = self._unwrapped_unbound - return self.is_cls_method and issubclass(callable_.__self__, self.value_type) + try: + from typing import Self + except ImportError: + returns_self = False + else: + returns_self = (self.annotations.get('return') == Self) + + return self.is_cls_method and ( + returns_self or + issubclass(callable_.__self__, self.value_type) + ) def make_expr_val_iter(self, expr, param_map): """ diff --git a/tools/exekall/exekall/tests/suite.py b/tools/exekall/exekall/tests/suite.py index c57be5e43..044fc50ed 100644 --- a/tools/exekall/exekall/tests/suite.py +++ b/tools/exekall/exekall/tests/suite.py @@ -658,3 +658,47 @@ class AssociatedTypesTestCase(NoExcepTestCase): } # no tags used EXPR_VAL_ID = EXPR_ID + + +try: + from typing import Self +except ImportError: + test_typing_self = False +else: + test_typing_self = True + +class HavingFactoryMethod: + @classmethod + def factory_basic(cls) -> 'HavingFactoryMethod': + return cls() + + if test_typing_self: + @classmethod + def factory_self(cls) -> typing.Self: + return cls() + +class HavingFactoryMethodDerived(HavingFactoryMethod): + pass + +def consume_HavingFactoryMethodDerived(x: HavingFactoryMethodDerived) -> Final: + return Final() + +class BasicFactoryTestCase(NoExcepTestCase): + CALLABLES = {HavingFactoryMethodDerived.factory_basic, consume_HavingFactoryMethodDerived} + EXPR_ID = { + (('qual', True),): 'exekall.tests.suite.HavingFactoryMethodDerived.factory_basic:exekall.tests.suite.consume_HavingFactoryMethodDerived', + (('qual', False),): 'HavingFactoryMethodDerived.factory_basic:consume_HavingFactoryMethodDerived', + } + # no tags used + EXPR_VAL_ID = EXPR_ID + + +if test_typing_self: + class SelfFactoryTestCase(NoExcepTestCase): + CALLABLES = {HavingFactoryMethodDerived.factory_self, consume_HavingFactoryMethodDerived} + EXPR_ID = { + (('qual', True),): 'exekall.tests.suite.HavingFactoryMethodDerived.factory_self:exekall.tests.suite.consume_HavingFactoryMethodDerived', + (('qual', False),): 'HavingFactoryMethodDerived.factory_self:consume_HavingFactoryMethodDerived', + } + # no tags used + EXPR_VAL_ID = EXPR_ID -- GitLab From 8b6a8a4b4859aaf9b88bd48f8a5d5f4d33d8e4df Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Mon, 7 Nov 2022 11:12:57 +0000 Subject: [PATCH 2/2] tools/exekall: Allow class without __init__ FIX Do not error if a class does not define __init__. --- tools/exekall/exekall/engine.py | 47 ++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tools/exekall/exekall/engine.py b/tools/exekall/exekall/engine.py index 964059691..49f8b508b 100644 --- a/tools/exekall/exekall/engine.py +++ b/tools/exekall/exekall/engine.py @@ -2408,20 +2408,24 @@ class Operator: # This has functionally no use but provides a massive speedup by # skipping all the callables that have arguments without default values # but no annotation - code = sig_f.__code__ - non_default_args = ( - code.co_argcount + - code.co_kwonlyargcount - - # Discount the parameters that have a default value, as they don't - # necessarily need an annotation to be useful - len(sig_f.__defaults__ or tuple()) - ) - if non_default_args and not has_annotations(sig_f): - raise AnnotationError( - 'Missing annotation for operator "{op}"'.format( - op=self.name, - ) + try: + code = sig_f.__code__ + except AttributeError: + pass + else: + non_default_args = ( + code.co_argcount + + code.co_kwonlyargcount - + # Discount the parameters that have a default value, as they don't + # necessarily need an annotation to be useful + len(sig_f.__defaults__ or tuple()) ) + if non_default_args and not has_annotations(sig_f): + raise AnnotationError( + 'Missing annotation for operator "{op}"'.format( + op=self.name, + ) + ) signature = inspect.signature(sig_f) annotations = { @@ -2490,6 +2494,14 @@ class Operator: # the original annotation, we replace the annotation by the # subclass That allows implementing factory classmethods # easily. + try: + self_cls = self.resolved_callable.__self__ + except AttributeError: + if inspect.isclass(self.callable_): + self_cls = self.callable_ + else: + raise TypeError(f'Could not determine the return type of the factory method {self.callable_.__qualname__}') + self.annotations['return'] = self.resolved_callable.__self__ # Refresh the prototype self.prototype = self._get_prototype() @@ -2516,7 +2528,11 @@ class Operator: ) return {name: obj} - globals_ = self.resolved_callable.__globals__ or {} + try: + globals_ = self.resolved_callable.__globals__ or {} + except AttributeError: + globals_ = {} + if isinstance(self.callable_, UnboundMethod): globals_ = { **globals_, @@ -2998,7 +3014,8 @@ class Operator: if ( param not in annotation_map and param not in extra_ignored_param and - param not in self.ignored_param + param not in self.ignored_param and + param_spec.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) ): # If some parameters are annotated but not all, we raise a # slightly different exception to allow better reporting -- GitLab