From 6814a7336c46a3dca0378b21783bdf8e76d25c66 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 25 Jul 2021 15:23:51 +0100 Subject: [PATCH 1/3] Hoist common variable for additional re-use --- jedi/inference/gradual/annotation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index eadcfc2e..b4caeb5e 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -202,6 +202,7 @@ def infer_return_types(function, arguments): Infers the type of a function's return value, according to type annotations. """ + context = function.get_default_param_context() all_annotations = py__annotations__(function.tree_node) annotation = all_annotations.get("return", None) if annotation is None: @@ -217,11 +218,10 @@ def infer_return_types(function, arguments): return NO_VALUES return _infer_annotation_string( - function.get_default_param_context(), + context, match.group(1).strip() ).execute_annotation() - context = function.get_default_param_context() unknown_type_vars = find_unknown_type_vars(context, annotation) annotation_values = infer_annotation(context, annotation) if not unknown_type_vars: From 599a1c3ee1b6e21f552ec72d2d542363e2b88b17 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 25 Jul 2021 15:29:30 +0100 Subject: [PATCH 2/3] Handle generics appearing within quoted return annotations This ensures that these quoted likely forwards references in return type annotations behave like their non-quoted equivalents. I suspect there may be other places which will need similar adjustments, which may mean that we should push the conversion a layer closer to the parsing (perhaps in `py__annotations__`?). One case I know that this doesn't solve (but which likely needs similar adjustment) is generics in return types of comment-style annotations. They're less likely and may not be worth supporting since all supported Python versions can use the in-syntax spelling for annotations at this point. --- jedi/inference/gradual/annotation.py | 12 ++++++++++++ test/completion/pep0484_generic_passthroughs.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index b4caeb5e..9f53a267 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -222,6 +222,18 @@ def infer_return_types(function, arguments): match.group(1).strip() ).execute_annotation() + elif annotation.type == 'string': + annotation = _get_forward_reference_node( + context, + context.inference_state.compiled_subprocess.safe_literal_eval( + annotation.value, + ), + ) + # The forward reference tree has an additional root node ('eval_input') + # that we don't want. Extract the node we do want, that is equivalent to + # the nodes returned by `py__annotations__` for a non-quoted annotation. + annotation = annotation.children[0] + unknown_type_vars = find_unknown_type_vars(context, annotation) annotation_values = infer_annotation(context, annotation) if not unknown_type_vars: diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py index 49b133a8..e250b1a4 100644 --- a/test/completion/pep0484_generic_passthroughs.py +++ b/test/completion/pep0484_generic_passthroughs.py @@ -58,6 +58,12 @@ def typed_bound_generic_passthrough(x: TList) -> TList: return x +# Forward references are more likely with custom types, however this aims to +# test just the handling of the quoted type rather than any other part of the +# machinery. +def typed_quoted_generic_passthrough(x: T) -> 'List[T]': + return [x] + for a in untyped_passthrough(untyped_list_str): #? str() @@ -146,6 +152,16 @@ for q in typed_bound_generic_passthrough(typed_list_str): q +for r in typed_quoted_generic_passthrough("something"): + #? str() + r + +for s in typed_quoted_generic_passthrough(42): + #? int() + s + + + class CustomList(List): def get_first(self): return self[0] From 78a95f475129c8e00b734893d4b6780b4c421f59 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 25 Jul 2021 15:54:51 +0100 Subject: [PATCH 3/3] Handle generics appearing within any quoted annotations This hoists the solution added for return-type annotations to also apply for input annotations so they work too. --- jedi/inference/gradual/annotation.py | 43 +++++++++++++------ .../pep0484_generic_passthroughs.py | 17 ++++++-- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 9f53a267..81780010 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -196,6 +196,32 @@ def py__annotations__(funcdef): return dct +def resolve_forward_references(context, all_annotations): + def resolve(node): + if node is None or node.type != 'string': + return node + + node = _get_forward_reference_node( + context, + context.inference_state.compiled_subprocess.safe_literal_eval( + node.value, + ), + ) + + if node is None: + # There was a string, but it's not a valid annotation + return None + + # The forward reference tree has an additional root node ('eval_input') + # that we don't want. Extract the node we do want, that is equivalent to + # the nodes returned by `py__annotations__` for a non-quoted node. + node = node.children[0] + + return node + + return {name: resolve(node) for name, node in all_annotations.items()} + + @inference_state_method_cache() def infer_return_types(function, arguments): """ @@ -203,7 +229,10 @@ def infer_return_types(function, arguments): according to type annotations. """ context = function.get_default_param_context() - all_annotations = py__annotations__(function.tree_node) + all_annotations = resolve_forward_references( + context, + py__annotations__(function.tree_node), + ) annotation = all_annotations.get("return", None) if annotation is None: # If there is no Python 3-type annotation, look for an annotation @@ -222,18 +251,6 @@ def infer_return_types(function, arguments): match.group(1).strip() ).execute_annotation() - elif annotation.type == 'string': - annotation = _get_forward_reference_node( - context, - context.inference_state.compiled_subprocess.safe_literal_eval( - annotation.value, - ), - ) - # The forward reference tree has an additional root node ('eval_input') - # that we don't want. Extract the node we do want, that is equivalent to - # the nodes returned by `py__annotations__` for a non-quoted annotation. - annotation = annotation.children[0] - unknown_type_vars = find_unknown_type_vars(context, annotation) annotation_values = infer_annotation(context, annotation) if not unknown_type_vars: diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py index e250b1a4..7c7b8820 100644 --- a/test/completion/pep0484_generic_passthroughs.py +++ b/test/completion/pep0484_generic_passthroughs.py @@ -61,9 +61,13 @@ def typed_bound_generic_passthrough(x: TList) -> TList: # Forward references are more likely with custom types, however this aims to # test just the handling of the quoted type rather than any other part of the # machinery. -def typed_quoted_generic_passthrough(x: T) -> 'List[T]': +def typed_quoted_return_generic_passthrough(x: T) -> 'List[T]': return [x] +def typed_quoted_input_generic_passthrough(x: 'Tuple[T]') -> T: + x + return x[0] + for a in untyped_passthrough(untyped_list_str): #? str() @@ -152,15 +156,22 @@ for q in typed_bound_generic_passthrough(typed_list_str): q -for r in typed_quoted_generic_passthrough("something"): +for r in typed_quoted_return_generic_passthrough("something"): #? str() r -for s in typed_quoted_generic_passthrough(42): +for s in typed_quoted_return_generic_passthrough(42): #? int() s +#? str() +typed_quoted_input_generic_passthrough(("something",)) + +#? int() +typed_quoted_input_generic_passthrough((42,)) + + class CustomList(List): def get_first(self):