From 886279fb6d0d26a7e89e7e0db5239d93bb566374 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 28 Jul 2023 22:35:07 +0200 Subject: [PATCH] Try to use the return annotations of properties, if available, fixes #1933 --- jedi/inference/compiled/access.py | 2 +- jedi/inference/compiled/value.py | 9 +++++- test/test_api/test_interpreter.py | 51 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index 4a685bf1..9aa0d71d 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -354,7 +354,7 @@ class DirectObjectAccess: if is_get_descriptor and type(attr) not in ALLOWED_DESCRIPTOR_ACCESS: if isinstance(attr, property): if hasattr(attr.fget, '__annotations__'): - a = DirectObjectAccess(self._inference_state, attr) + a = DirectObjectAccess(self._inference_state, attr.fget) return True, True, a.get_return_annotation() # In case of descriptors that have get methods we cannot return # it's value, because that would mean code execution. diff --git a/jedi/inference/compiled/value.py b/jedi/inference/compiled/value.py index d03f1743..2d5323ad 100644 --- a/jedi/inference/compiled/value.py +++ b/jedi/inference/compiled/value.py @@ -51,7 +51,6 @@ class CompiledValue(Value): def py__call__(self, arguments): return_annotation = self.access_handle.get_return_annotation() if return_annotation is not None: - # TODO the return annotation may also be a string. return create_from_access_path( self.inference_state, return_annotation @@ -453,6 +452,14 @@ class CompiledValueFilter(AbstractFilter): has_attribute, is_descriptor, property_return_annotation = allowed_getattr_callback( name, ) + if property_return_annotation is not None: + values = create_from_access_path( + self._inference_state, + property_return_annotation + ).execute_annotation() + if values: + return [CompiledValueName(v, name) for v in values] + if check_has_attribute and not has_attribute: return [] diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 63392405..3182b506 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -764,6 +764,7 @@ def test_keyword_param_completion(code, expected): @pytest.mark.parametrize('class_is_findable', [False, True]) def test_avoid_descriptor_executions_if_not_necessary(class_is_findable): counter = 0 + class AvoidDescriptor(object): @property def prop(self): @@ -779,3 +780,53 @@ def test_avoid_descriptor_executions_if_not_necessary(class_is_findable): assert counter == 0 _assert_interpreter_complete('b.prop.pro', namespace, expected, check_type=True) assert counter == 1 + + +class Hello: + its_me = 1 + + +@pytest.mark.parametrize('class_is_findable', [False, True]) +def test_try_to_use_return_annotation_for_property(class_is_findable): + class WithProperties(object): + @property + def with_annotation1(self) -> str: + raise BaseException + + @property + def with_annotation2(self) -> 'str': + raise BaseException + + @property + def with_annotation3(self) -> Hello: + raise BaseException + + @property + def with_annotation4(self) -> 'Hello': + raise BaseException + + @property + def with_annotation_garbage1(self) -> 'asldjflksjdfljdslkjfsl': # noqa + return Hello() + + @property + def with_annotation_garbage2(self) -> 'sdf$@@$5*+8': # noqa + return Hello() + + @property + def without_annotation(self): + return "" + + if not class_is_findable: + WithProperties.__name__ = "something_somewhere" + Hello.__name__ = "something_somewhere_else" + + namespace = {'p': WithProperties()} + _assert_interpreter_complete('p.without_annotation.upp', namespace, ['upper']) + _assert_interpreter_complete('p.with_annotation1.upp', namespace, ['upper']) + _assert_interpreter_complete('p.with_annotation2.upp', namespace, ['upper']) + _assert_interpreter_complete('p.with_annotation3.its', namespace, ['its_me']) + _assert_interpreter_complete('p.with_annotation4.its', namespace, ['its_me']) + # This is a fallback, if the annotations don't help + _assert_interpreter_complete('p.with_annotation_garbage1.its', namespace, ['its_me']) + _assert_interpreter_complete('p.with_annotation_garbage2.its', namespace, ['its_me'])