From aa72381ed150733afb60ee9db7986572256f7475 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 29 Apr 2026 17:51:01 +0200 Subject: [PATCH] Implement Self, fixes #2023, fixes #2068 --- jedi/inference/base_value.py | 2 +- jedi/inference/compiled/value.py | 14 +++++----- jedi/inference/docstrings.py | 2 +- jedi/inference/gradual/annotation.py | 12 ++++----- jedi/inference/gradual/base.py | 4 +-- jedi/inference/gradual/generics.py | 2 +- jedi/inference/gradual/type_var.py | 6 ++--- jedi/inference/gradual/typing.py | 38 ++++++++++++++++++---------- jedi/inference/names.py | 2 +- jedi/inference/syntax_tree.py | 10 +++++--- jedi/inference/value/function.py | 4 +-- jedi/inference/value/instance.py | 2 +- jedi/inference/value/iterable.py | 4 +-- jedi/plugins/django.py | 4 +-- jedi/plugins/pytest.py | 2 +- test/completion/pep0484_typing.py | 31 ++++++++++++++++++++++- 16 files changed, 92 insertions(+), 47 deletions(-) diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 25c4181d..4ce2900e 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -59,7 +59,7 @@ class HelperValueMixin: arguments = ValuesArguments([ValueSet([value]) for value in value_list]) return self.inference_state.execute(self, arguments) - def execute_annotation(self): + def execute_annotation(self, context): return self.execute_with_values() def gather_annotation_classes(self): diff --git a/jedi/inference/compiled/value.py b/jedi/inference/compiled/value.py index 5852adec..aeeb12fd 100644 --- a/jedi/inference/compiled/value.py +++ b/jedi/inference/compiled/value.py @@ -54,7 +54,7 @@ class CompiledValue(Value): return create_from_access_path( self.inference_state, return_annotation - ).execute_annotation() + ).execute_annotation(arguments.context) try: self.access_handle.getattr_paths('__call__') @@ -241,7 +241,7 @@ class CompiledValue(Value): except TypeError: return NO_VALUES - def execute_annotation(self): + def execute_annotation(self, context): if self.access_handle.get_repr() == 'None': # None as an annotation doesn't need to be executed. return ValueSet([self]) @@ -252,7 +252,9 @@ class CompiledValue(Value): for path in args ] if name == 'Union': - return ValueSet.from_sets(arg.execute_annotation() for arg in arguments) + return ValueSet.from_sets( + arg.execute_annotation(context) + for arg in arguments) elif name: # While with_generics only exists on very specific objects, we # should probably be fine, because we control all the typing @@ -260,8 +262,8 @@ class CompiledValue(Value): return ValueSet([ v.with_generics(arguments) for v in self.inference_state.typing_module.py__getattribute__(name) - ]).execute_annotation() - return super().execute_annotation() + ]).execute_annotation(context) + return super().execute_annotation(context) def negate(self): return create_from_access_path(self.inference_state, self.access_handle.negate()) @@ -459,7 +461,7 @@ class CompiledValueFilter(AbstractFilter): values = create_from_access_path( self._inference_state, property_return_annotation - ).execute_annotation() + ).execute_annotation(None) if values: return [CompiledValueName(v, name) for v in values] diff --git a/jedi/inference/docstrings.py b/jedi/inference/docstrings.py index f1d0f31f..484e073f 100644 --- a/jedi/inference/docstrings.py +++ b/jedi/inference/docstrings.py @@ -246,7 +246,7 @@ def _execute_array_values(inference_state, array): cls = FakeTuple if array.array_type == 'tuple' else FakeList return {cls(inference_state, values)} else: - return array.execute_annotation() + return array.execute_annotation(None) @inference_state_method_cache() diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 57098276..51dc5460 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -249,12 +249,12 @@ def infer_return_types(function, arguments): return _infer_annotation_string( context, match.group(1).strip() - ).execute_annotation() + ).execute_annotation(context) unknown_type_vars = find_unknown_type_vars(context, annotation) annotation_values = infer_annotation(context, annotation) if not unknown_type_vars: - return annotation_values.execute_annotation() + return annotation_values.execute_annotation(context) type_var_dict = infer_type_vars_for_execution(function, arguments, all_annotations) @@ -262,7 +262,7 @@ def infer_return_types(function, arguments): ann.define_generics(type_var_dict) if isinstance(ann, (DefineGenericBaseClass, TypeVar)) else ValueSet({ann}) for ann in annotation_values - ).execute_annotation() + ).execute_annotation(context) def infer_type_vars_for_execution(function, arguments, annotation_dict): @@ -315,7 +315,7 @@ def infer_return_for_callable(arguments, param_values, result_values): if isinstance(v, (DefineGenericBaseClass, TypeVar)) else ValueSet({v}) for v in result_values - ).execute_annotation() + ).execute_annotation(arguments.context) def _infer_type_vars_for_callable(arguments, lazy_params): @@ -391,7 +391,7 @@ def merge_pairwise_generics(annotation_value, annotated_argument_class): for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): merge_type_var_dicts( type_var_dict, - annotation_generics_set.infer_type_vars(actual_generic_set.execute_annotation()), + annotation_generics_set.infer_type_vars(actual_generic_set.execute_annotation(None)), ) return type_var_dict @@ -438,7 +438,7 @@ def _find_type_from_comment_hint(context, node, varlist, name): return [] return _infer_annotation_string( context, match.group(1).strip(), index - ).execute_annotation() + ).execute_annotation(context) def find_unknown_type_vars(context, node): diff --git a/jedi/inference/gradual/base.py b/jedi/inference/gradual/base.py index 33bf6201..3f553d9e 100644 --- a/jedi/inference/gradual/base.py +++ b/jedi/inference/gradual/base.py @@ -306,7 +306,7 @@ class _GenericInstanceWrapper(ValueWrapper): if cls.py__name__() == 'Generator': generics = cls.get_generics() try: - return generics[2].execute_annotation() + return generics[2].execute_annotation(None) except IndexError: pass elif cls.py__name__() == 'Iterator': @@ -427,7 +427,7 @@ class BaseTypingInstance(LazyValueWrapper): return ValueName(self, self._tree_name) def _get_wrapped_value(self): - object_, = builtin_from_name(self.inference_state, 'object').execute_annotation() + object_, = builtin_from_name(self.inference_state, 'object').execute_annotation(None) return object_ def __repr__(self): diff --git a/jedi/inference/gradual/generics.py b/jedi/inference/gradual/generics.py index 4a1cd8a9..ddd010ae 100644 --- a/jedi/inference/gradual/generics.py +++ b/jedi/inference/gradual/generics.py @@ -35,7 +35,7 @@ class _AbstractGenericManager: def get_index_and_execute(self, index): try: - return self[index].execute_annotation() + return self[index].execute_annotation(None) except IndexError: debug.warning('No param #%s found for annotation %s', index, self) return NO_VALUES diff --git a/jedi/inference/gradual/type_var.py b/jedi/inference/gradual/type_var.py index c09773f1..d74c97a6 100644 --- a/jedi/inference/gradual/type_var.py +++ b/jedi/inference/gradual/type_var.py @@ -100,8 +100,8 @@ class TypeVar(BaseTypingValue): return found return ValueSet({self}) - def execute_annotation(self): - return self._get_classes().execute_annotation() + def execute_annotation(self, context): + return self._get_classes().execute_annotation(context) def infer_type_vars(self, value_set): def iterate(): @@ -123,5 +123,5 @@ class TypeWrapper(ValueWrapper): super().__init__(wrapped_value) self._original_value = original_value - def execute_annotation(self): + def execute_annotation(self, context): return ValueSet({self._original_value}) diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 30eb1ddf..febb80cc 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -83,6 +83,9 @@ class TypingModuleName(NameWrapper): elif name == 'cast': cast_fn, = self._wrapped_name.infer() yield CastFunction.create_cached(inference_state, cast_fn) + elif name == 'Self': + yield SelfClass.create_cached( + inference_state, self.parent_context, self.tree_name) elif name == 'TypedDict': # TODO doesn't even exist in typeshed/typing.py, yet. But will be # added soon. @@ -100,24 +103,24 @@ class TypingModuleFilterWrapper(FilterWrapper): class ProxyWithGenerics(BaseTypingClassWithGenerics): - def execute_annotation(self): + def execute_annotation(self, context): string_name = self._tree_name.value if string_name == 'Union': # This is kind of a special case, because we have Unions (in Jedi # ValueSets). - return self.gather_annotation_classes().execute_annotation() + return self.gather_annotation_classes().execute_annotation(context) elif string_name == 'Optional': # Optional is basically just saying it's either None or the actual # type. - return self.gather_annotation_classes().execute_annotation() \ + return self.gather_annotation_classes().execute_annotation(context) \ | ValueSet([builtin_from_name(self.inference_state, 'None')]) elif string_name == 'Type': # The type is actually already given in the index_value return self._generics_manager[0] elif string_name in IGNORE_ANNOTATION_PARTS: # For now don't do anything here, ClassVars are always used. - return self._generics_manager[0].execute_annotation() + return self._generics_manager[0].execute_annotation(context) mapped = { 'Tuple': Tuple, @@ -217,17 +220,17 @@ class TypingClassWithGenerics(ProxyWithGenerics, _TypingClassMixin): # This is basically a trick to avoid extra code: We execute the # incoming classes to be able to use the normal code for type # var inference. - value_set.execute_annotation(), + value_set.execute_annotation(None), ) elif annotation_name == 'Callable': if len(annotation_generics) == 2: return annotation_generics[1].infer_type_vars( - value_set.execute_annotation(), + value_set.execute_annotation(None), ) elif annotation_name == 'Tuple': - tuple_annotation, = self.execute_annotation() + tuple_annotation, = self.execute_annotation(None) return tuple_annotation.infer_type_vars(value_set) return type_var_dict @@ -323,7 +326,7 @@ class Tuple(BaseTypingInstance): yield LazyKnownValues(self._generics_manager.get_index_and_execute(0)) else: for v in self._generics_manager.to_tuple(): - yield LazyKnownValues(v.execute_annotation()) + yield LazyKnownValues(v.execute_annotation(None)) def py__getitem__(self, index_value_set, contextualized_node): if self._is_homogenous(): @@ -331,11 +334,11 @@ class Tuple(BaseTypingInstance): return ValueSet.from_sets( self._generics_manager.to_tuple() - ).execute_annotation() + ).execute_annotation(None) def _get_wrapped_value(self): tuple_, = self.inference_state.builtins_module \ - .py__getattribute__('tuple').execute_annotation() + .py__getattribute__('tuple').execute_annotation(None) return tuple_ @property @@ -392,11 +395,20 @@ class Protocol(BaseTypingInstance): class AnyClass(BaseTypingValue): - def execute_annotation(self): + def execute_annotation(self, context): debug.warning('Used Any - returned no results') return NO_VALUES +class SelfClass(BaseTypingValue): + def execute_annotation(self, context): + debug.warning('Used Self') + if context is not None: + # Execute the class of Self + return context.get_value().execute_annotation(None) + return NO_VALUES + + class OverloadFunction(BaseTypingValue): @repack_with_argument_clinic('func, /') def py__call__(self, func_value_set): @@ -431,7 +443,7 @@ class NewType(Value): return c def py__call__(self, arguments): - return self._type_value_set.execute_annotation() + return self._type_value_set.execute_annotation(arguments.context) @property def name(self): @@ -445,7 +457,7 @@ class NewType(Value): class CastFunction(ValueWrapper): @repack_with_argument_clinic('type, object, /') def py__call__(self, type_value_set, object_value_set): - return type_value_set.execute_annotation() + return type_value_set.execute_annotation(None) class TypedDictClass(BaseTypingValue): diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 901d1af5..53549c3d 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -466,7 +466,7 @@ class _ActualTreeParamName(BaseTreeParamName): self.function_value, self._get_param_node(), ignore_stars=ignore_stars) if execute_annotation: - values = values.execute_annotation() + values = values.execute_annotation(self.function_value.get_default_param_context()) return values def infer_default(self): diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index 780b682e..bd97c939 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -239,7 +239,7 @@ def _infer_node(context, element): return context.infer_node(element.children[0]) elif typ == 'annassign': return annotation.infer_annotation(context, element.children[1]) \ - .execute_annotation() + .execute_annotation(context) elif typ == 'yield_expr': if len(element.children) and element.children[1].type == 'yield_arg': # Implies that it's a yield from. @@ -497,7 +497,7 @@ def infer_factor(value_set, operator): b = value.py__bool__() if b is None: # Uncertainty. yield list(value.inference_state.builtins_module.py__getattribute__('bool') - .execute_annotation()).pop() + .execute_annotation(None)).pop() else: yield compiled.create_simple_object(value.inference_state, not b) else: @@ -650,7 +650,9 @@ def _infer_comparison_part(inference_state, context, left, operator, right): _bool_to_value(inference_state, False) ]) elif str_operator in ('in', 'not in'): - return inference_state.builtins_module.py__getattribute__('bool').execute_annotation() + return inference_state.builtins_module.py__getattribute__('bool').execute_annotation( + context + ) def check(obj): """Checks if a Jedi object is either a float or an int.""" @@ -719,7 +721,7 @@ def tree_name_to_values(inference_state, context, tree_name): and first.name.string_name in IGNORE_ANNOTATION_PARTS ) found_annotation = set_found_annotation - value_set |= found.execute_annotation() + value_set |= found.execute_annotation(context) if found_annotation: return value_set diff --git a/jedi/inference/value/function.py b/jedi/inference/value/function.py index 479503d1..e9aa2c68 100644 --- a/jedi/inference/value/function.py +++ b/jedi/inference/value/function.py @@ -338,7 +338,7 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin): return ValueSet( GenericClass(c, TupleGenericManager(generics)) for c in async_generator_classes - ).execute_annotation() + ).execute_annotation(None) else: async_classes = inference_state.typing_module.py__getattribute__('Coroutine') return_values = self.get_return_values() @@ -346,7 +346,7 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin): generics = (return_values.py__class__(), NO_VALUES, NO_VALUES) return ValueSet( GenericClass(c, TupleGenericManager(generics)) for c in async_classes - ).execute_annotation() + ).execute_annotation(None) else: # If there are annotations, prefer them over anything else. if self.is_generator() and not self.infer_annotations(): diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 0ea89155..75301e2a 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -506,7 +506,7 @@ class SelfName(TreeNameDefinition): from jedi.inference.gradual.annotation import infer_annotation values = infer_annotation( self.parent_context, stmt.children[1].children[1] - ).execute_annotation() + ).execute_annotation(None) if values: return values return super().infer() diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index aea79863..7d76b4bf 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -44,7 +44,7 @@ class GeneratorBase(LazyAttributeOverwrite, IterableMixin): array_type = None def _get_wrapped_value(self): - instance, = self._get_cls().execute_annotation() + instance, = self._get_cls().execute_annotation(None) return instance def _get_cls(self): @@ -214,7 +214,7 @@ class Sequence(LazyAttributeOverwrite, IterableMixin): c, = GenericClass( klass, TupleGenericManager(self._cached_generics()) - ).execute_annotation() + ).execute_annotation(None) return c def py__bool__(self): diff --git a/jedi/plugins/django.py b/jedi/plugins/django.py index c83620d7..fd879676 100644 --- a/jedi/plugins/django.py +++ b/jedi/plugins/django.py @@ -46,7 +46,7 @@ _FILTER_LIKE_METHODS = ('create', 'filter', 'exclude', 'update', 'get', def _get_deferred_attributes(inference_state): return inference_state.import_module( ('django', 'db', 'models', 'query_utils') - ).py__getattribute__('DeferredAttribute').execute_annotation() + ).py__getattribute__('DeferredAttribute').execute_annotation(None) def _infer_scalar_field(inference_state, field_name, field_tree_instance, is_instance): @@ -130,7 +130,7 @@ def _create_manager_for(cls, manager_cls='BaseManager'): for m in managers: if m.is_class_mixin(): generics_manager = TupleGenericManager((ValueSet([cls]),)) - for c in GenericClass(m, generics_manager).execute_annotation(): + for c in GenericClass(m, generics_manager).execute_annotation(None): return c return None diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index ae27da4e..f7e88d2c 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -37,7 +37,7 @@ def infer_anonymous_param(func): == ('typing', 'Generator') for v in result): return ValueSet.from_sets( - v.py__getattribute__('__next__').execute_annotation() + v.py__getattribute__('__next__').execute_annotation(None) for v in result ) return result diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index a2bdd71a..d52da23a 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -3,7 +3,7 @@ Test the typing library, with docstrings and annotations """ import typing from typing import Sequence, MutableSequence, List, Iterable, Iterator, \ - AbstractSet, Tuple, Mapping, Dict, Union, Optional, Final + AbstractSet, Tuple, Mapping, Dict, Union, Optional, Final, Self class B: pass @@ -555,3 +555,32 @@ def typed_dict_test_foo(arg: Bar): arg['an_int'] #? int() arg['another_variable'] + +# ----------------- +# Self +# ----------------- + +# From #2023, #2068 +class Builder: + def __init__(self): + self.x = 0 + self.y = 0 + + def add_x(self: Self, x: int) -> Self: + self.x = x + return self + + def add_y(self: Self, y: int) -> Self: + self.y = y + return self + + def add_not_implemented(self: Self, y: int) -> Self: + raise NotImplementedError + +b = Builder() +#? Builder() +b.add_x(2) +#? Builder() +b.add_x(2).add_y(5) +#? Builder() +b.add_x(2).add_not_implemented(5)