From 36b4b797c1f6432d9de607a6e144b3736959bf79 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 18:22:42 +0000 Subject: [PATCH 01/26] Add trailing comma --- jedi/inference/gradual/annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 1de87962..b4bb90a2 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -372,7 +372,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): type_var_dict, _infer_type_vars( nested_annotation_value, - value_set.merge_types_of_iterate() + value_set.merge_types_of_iterate(), ) ) elif name == 'Mapping': From 6efafb348e3602459b29b288f56279a22ee2b119 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 19:40:10 +0000 Subject: [PATCH 02/26] Extract the annotation name upfront We almost always need this and this simplifies the code within each branch. This also means we'll be able to the name to determine the branching. --- jedi/inference/gradual/annotation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index b4bb90a2..75e544d9 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -333,13 +333,14 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): unpacks the `Iterable`. """ type_var_dict = {} + annotation_name = annotation_value.py__name__() + if isinstance(annotation_value, TypeVar): if not is_class_value: - return {annotation_value.py__name__(): value_set.py__class__()} - return {annotation_value.py__name__(): value_set} + return {annotation_name: value_set.py__class__()} + return {annotation_name: value_set} elif isinstance(annotation_value, TypingClassValueWithIndex): - name = annotation_value.py__name__() - if name == 'Type': + if annotation_name == 'Type': given = annotation_value.get_generics() if given: for nested_annotation_value in given[0]: @@ -351,7 +352,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): is_class_value=True, ) ) - elif name == 'Callable': + elif annotation_name == 'Callable': given = annotation_value.get_generics() if len(given) == 2: for nested_annotation_value in given[1]: @@ -363,8 +364,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ) ) elif isinstance(annotation_value, GenericClass): - name = annotation_value.py__name__() - if name == 'Iterable': + if annotation_name == 'Iterable': given = annotation_value.get_generics() if given: for nested_annotation_value in given[0]: @@ -375,7 +375,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): value_set.merge_types_of_iterate(), ) ) - elif name == 'Mapping': + elif annotation_name == 'Mapping': given = annotation_value.get_generics() if len(given) == 2: for value in value_set: From 0a7820f6dee079a063336b7d1c88ec88b985ff0d Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 16 Feb 2020 23:09:28 +0000 Subject: [PATCH 03/26] Add many test cases While these definitely _ought_ to work on Python 2.7, the annotation support there is very limited and as Python 2 is deprecated it doesn't seem worth it. --- test/completion/pep0484_generic_parameters.py | 151 ++++++++++++++++++ .../pep0484_generic_passthroughs.py | 84 ++++++++++ test/test_api/test_api.py | 9 +- 3 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 test/completion/pep0484_generic_parameters.py create mode 100644 test/completion/pep0484_generic_passthroughs.py diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py new file mode 100644 index 00000000..f13fafd1 --- /dev/null +++ b/test/completion/pep0484_generic_parameters.py @@ -0,0 +1,151 @@ +# python >= 3.4 +from typing import Iterable, List, Type, TypeVar, Dict, Mapping, Generic + +K = TypeVar('K') +T = TypeVar('T') +T_co = TypeVar('T_co', covariant=True) +V = TypeVar('V') + + +list_of_ints = [42] # type: List[int] + +# Test that simple parameters are handled +def list_t_to_list_t(the_list: List[T]) -> List[T]: + return the_list + +x0 = list_t_to_list_t(list_of_ints)[0] +#? int() +x0 + +for a in list_t_to_list_t(list_of_ints): + #? int() + a + + +list_of_int_type = [int] # type: List[Type[int]] + +# Test that nested parameters are handled +def list_type_t_to_list_t(the_list: List[Type[T]]) -> List[T]: + return [x() for x in the_list] + + +x1 = list_type_t_to_list_t(list_of_int_type)[0] +#? int() +x1 + + +for b in list_type_t_to_list_t(list_of_int_type): + #? int() + b + + +mapping_int_str = {42: 'a'} # type: Dict[int, str] + +# Test that mappings (that have more than one parameter) are handled +def invert_mapping(mapping: Mapping[K, V]) -> Mapping[V, K]: + return {v: k for k, v in mapping.items()} + +#? int() +invert_mapping(mapping_int_str)['a'] + + +# Test that the right type is chosen when a mapping is passed to something with +# only a single parameter. This checks that our inheritance checking picks the +# right thing. +def first(iterable: Iterable[T]) -> T: + return next(iter(iterable)) + +#? int() +first(mapping_int_str) + + +# Test that the right type is chosen when a partially realised mapping is expected +def values(mapping: Mapping[int, T]) -> List[T]: + return list(mapping.values()) + +#? str() +values(mapping_int_str)[0] + +x2 = values(mapping_int_str)[0] +#? str() +x2 + +for b in values(mapping_int_str): + #? str() + b + + +# +# Tests that user-defined generic types are handled +# +list_ints = [42] # type: List[int] + +class CustomGeneric(Generic[T_co]): + def __init__(self, val: T_co) -> None: + self.val = val + + +# Test extraction of type from a custom generic type +def custom(x: CustomGeneric[T]) -> T: + return x.val + +custom_instance = CustomGeneric(42) # type: CustomGeneric[int] + +#? int() +custom(custom_instance) + +x3 = custom(custom_instance) +#? int() +x3 + + +# Test construction of a custom generic type +def wrap_custom(iterable: Iterable[T]) -> List[CustomGeneric[T]]: + return [CustomGeneric(x) for x in iterable] + +#? int() +wrap_custom(list_ints)[0].val + +x4 = wrap_custom(list_ints)[0] +#? int() +x4.val + +for x5 in wrap_custom(list_ints): + #? int() + x5.val + + +# Test extraction of type from a nested custom generic type +list_custom_instances = [CustomGeneric(42)] # type: List[CustomGeneric[int]] + +def unwrap_custom(iterable: Iterable[CustomGeneric[T]]) -> List[T]: + return [x.val for x in iterable] + +#? int() +unwrap_custom(list_custom_instances)[0] + +x6 = unwrap_custom(list_custom_instances)[0] +#? int() +x6 + +for x7 in unwrap_custom(list_custom_instances): + #? int() + x7 + + +# Test extraction of type from type parameer nested within a custom generic type +custom_instance_list_int = CustomGeneric([42]) # type: CustomGeneric[List[int]] + +def unwrap_custom2(instance: CustomGeneric[Iterable[T]]) -> List[T]: + return list(instance.val) + +#? int() +unwrap_custom2(custom_instance_list_int)[0] + +x8 = unwrap_custom2(custom_instance_list_int)[0] +#? int() +x8 + +for x9 in unwrap_custom2(custom_instance_list_int): + #? int() + x9 diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py new file mode 100644 index 00000000..bd13f926 --- /dev/null +++ b/test/completion/pep0484_generic_passthroughs.py @@ -0,0 +1,84 @@ +# python >= 3.4 +from typing import Any, Iterable, List, Tuple, TypeVar + +T = TypeVar('T') +TList = TypeVar('TList', bound=List[Any]) + +untyped_list_str = ['abc', 'def'] +typed_list_str = ['abc', 'def'] # type: List[str] + +untyped_tuple_str = ('abc',) +typed_tuple_str = ('abc',) # type: Tuple[str] + + +def untyped_passthrough(x): + return x + +def typed_list_generic_passthrough(x: List[T]) -> List[T]: + return x + +def typed_tuple_generic_passthrough(x: Tuple[T]) -> Tuple[T]: + return x + +def typed_iterable_generic_passthrough(x: Iterable[T]) -> Iterable[T]: + return x + +def typed_fully_generic_passthrough(x: T) -> T: + return x + +def typed_bound_generic_passthrough(x: TList) -> TList: + return x + + +for a in untyped_passthrough(untyped_list_str): + #? str() + a + +for b in untyped_passthrough(typed_list_str): + #? str() + b + + +for c in typed_list_generic_passthrough(untyped_list_str): + #? str() + c + +for d in typed_list_generic_passthrough(typed_list_str): + #? str() + d + + +for e in typed_iterable_generic_passthrough(untyped_list_str): + #? str() + e + +for f in typed_iterable_generic_passthrough(typed_list_str): + #? str() + f + + +for g in typed_tuple_generic_passthrough(untyped_tuple_str): + #? str() + g + +for h in typed_tuple_generic_passthrough(typed_tuple_str): + #? str() + h + + +for n in typed_fully_generic_passthrough(untyped_list_str): + #? str() + n + +for o in typed_fully_generic_passthrough(typed_list_str): + #? str() + o + + +for p in typed_bound_generic_passthrough(untyped_list_str): + #? str() + p + +for q in typed_bound_generic_passthrough(typed_list_str): + #? str() + q diff --git a/test/test_api/test_api.py b/test/test_api/test_api.py index 841151db..a7337cfe 100644 --- a/test/test_api/test_api.py +++ b/test/test_api/test_api.py @@ -340,8 +340,13 @@ def test_math_fuzzy_completion(Script, environment): def test_file_fuzzy_completion(Script): path = os.path.join(test_dir, 'completion') script = Script('"{}/ep08_i'.format(path)) - assert ['pep0484_basic.py"', 'pep0484_typing.py"'] \ - == [comp.name for comp in script.complete(fuzzy=True)] + expected = [ + 'pep0484_basic.py"', + 'pep0484_generic_parameters.py"', + 'pep0484_generic_passthroughs.py"', + 'pep0484_typing.py"', + ] + assert expected == [comp.name for comp in script.complete(fuzzy=True)] @pytest.mark.parametrize( From 969a8f1fd94c643f388ebd434d156466b4745482 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 19:16:35 +0000 Subject: [PATCH 04/26] First pass at extending infer_type_vars This mostly works for the new tests, but doesn't work for: - tuples (though this seems to be because they lack generic information anyway) - nested Type[T] handling (e.g: List[Type[T]]) --- jedi/inference/gradual/annotation.py | 72 ++++++++++++++++++---------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 75e544d9..51dbca40 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -364,7 +364,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ) ) elif isinstance(annotation_value, GenericClass): - if annotation_name == 'Iterable': + if annotation_name == 'Iterable' and not is_class_value: given = annotation_value.get_generics() if given: for nested_annotation_value in given[0]: @@ -375,32 +375,52 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): value_set.merge_types_of_iterate(), ) ) - elif annotation_name == 'Mapping': - given = annotation_value.get_generics() - if len(given) == 2: - for value in value_set: - try: - method = value.get_mapping_item_values - except AttributeError: - continue - key_values, value_values = method() + else: + # Note: we need to handle the MRO _in order_, so we need to extract + # the elements from the set first, then handle them, even if we put + # them back in a set afterwards. + for element in value_set: + if not hasattr(element, 'is_instance'): + continue + + if element.is_instance(): + py_class = element.py__class__() + else: + py_class = element + + # TODO: what about things like 'str', which likely aren't + # generic, but do implement 'Iterable[str]'? + if not isinstance(py_class, DefineGenericBase): + continue + + for klass in py_class.py__mro__(): + class_name = klass.py__name__() + if annotation_name == class_name: + annotation_generics = annotation_value.get_generics() + actual_generics = klass.get_generics() + + if len(annotation_generics) != len(actual_generics): + # TODO: might there be other matches elsewhere in the MRO? + # TODO: what happens if _some_ of the generics are realised at + # this point, but not all (e.g: class `Foo(Dict[str, T])`)? + break + + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): + for nested_annotation_value in annotation_generics_set: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + actual_generic_set, + # This is a note to ourselves that we + # have already converted the instance + # representation to its class. + is_class_value=True, + ), + ) + + break - for nested_annotation_value in given[0]: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - key_values, - ) - ) - for nested_annotation_value in given[1]: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - value_values, - ) - ) return type_var_dict From bc53dabce32e5d72e764800e0d6c2462097bf3c1 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 20:36:57 +0000 Subject: [PATCH 05/26] Make tuple generic parameters work --- jedi/inference/gradual/annotation.py | 29 ++++++++++++++++++++++++++++ jedi/inference/gradual/typing.py | 7 +------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 51dbca40..b4ce16e5 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -363,6 +363,35 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): value_set.execute_annotation(), ) ) + elif annotation_name == 'Tuple': + # TODO: check that this works both for fixed and variadic tuples + # (and maybe for combiantions of those). + # TODO: this logic is pretty similar to the general logic below, can + # we combine them? + + for element in value_set: + py_class = element.py__class__() + if not isinstance(py_class, GenericClass): + py_class = element + + if not isinstance(py_class, DefineGenericBase): + continue + + annotation_generics = annotation_value.get_generics() + actual_generics = py_class.get_generics() + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): + for nested_annotation_value in annotation_generics_set: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + actual_generic_set, + # This is a note to ourselves that we + # have already converted the instance + # representation to its class. + is_class_value=True, + ), + ) elif isinstance(annotation_value, GenericClass): if annotation_name == 'Iterable' and not is_class_value: given = annotation_value.get_generics() diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 3b3cdf17..03873c52 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -241,12 +241,7 @@ class Callable(BaseTypingValueWithGenerics): return infer_return_for_callable(arguments, param_values, result_values) -class Tuple(LazyValueWrapper): - def __init__(self, parent_context, name, generics_manager): - self.inference_state = parent_context.inference_state - self.parent_context = parent_context - self._generics_manager = generics_manager - +class Tuple(BaseTypingValueWithGenerics): def _is_homogenous(self): # To specify a variable-length tuple of homogeneous type, Tuple[T, ...] # is used. From c03ae0315e84585c55927fe470477bd7faa9869a Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 22:10:59 +0000 Subject: [PATCH 06/26] Make nested Type[T] annotations work --- jedi/inference/gradual/annotation.py | 37 ++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index b4ce16e5..29ad95d0 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -343,15 +343,36 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): if annotation_name == 'Type': given = annotation_value.get_generics() if given: - for nested_annotation_value in given[0]: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - value_set, - is_class_value=True, + if is_class_value: + for element in value_set: + element_name = element.py__name__() + if annotation_name == element_name: + annotation_generics = annotation_value.get_generics() + actual_generics = element.get_generics() + + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): + for nested_annotation_value in annotation_generics_set: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + actual_generic_set, + # This is a note to ourselves that we + # have already converted the instance + # representation to its class. + is_class_value=True, + ), + ) + else: + for nested_annotation_value in given[0]: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + value_set, + is_class_value=True, + ) ) - ) elif annotation_name == 'Callable': given = annotation_value.get_generics() if len(given) == 2: From e455709a31b32d4927ed4436f4f19a272db6bcbb Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 22:30:22 +0000 Subject: [PATCH 07/26] Add test case for nested generic callables --- test/completion/pep0484_generic_parameters.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index f13fafd1..98522b7a 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -1,5 +1,14 @@ # python >= 3.4 -from typing import Iterable, List, Type, TypeVar, Dict, Mapping, Generic +from typing import ( + Callable, + Dict, + Generic, + Iterable, + List, + Mapping, + Type, + TypeVar, +) K = TypeVar('K') T = TypeVar('T') @@ -39,6 +48,25 @@ for b in list_type_t_to_list_t(list_of_int_type): b +def foo(x: T) -> T: + return x + + +list_of_funcs = [foo] # type: List[Callable[[T], T]] + +def list_func_t_to_list_func_type_t(the_list: List[Callable[[T], T]]) -> List[Callable[[Type[T]], T]]: + def adapt(func: Callable[[T], T]) -> Callable[[Type[T]], T]: + def wrapper(typ: Type[T]) -> T: + return func(typ()) + return wrapper + return [adapt(x) for x in the_list] + + +for b in list_func_t_to_list_func_type_t(list_of_funcs): + #? int() + b(int) + + mapping_int_str = {42: 'a'} # type: Dict[int, str] # Test that mappings (that have more than one parameter) are handled From c15e0ef9b89712a7d948b3e02d49eab5025ed906 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 23:10:27 +0000 Subject: [PATCH 08/26] Ensure specialised types inheriting from generics work --- jedi/inference/gradual/annotation.py | 8 +-- test/completion/pep0484_generic_parameters.py | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 29ad95d0..2a9e3d73 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -438,14 +438,12 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): else: py_class = element - # TODO: what about things like 'str', which likely aren't - # generic, but do implement 'Iterable[str]'? - if not isinstance(py_class, DefineGenericBase): - continue - for klass in py_class.py__mro__(): class_name = klass.py__name__() if annotation_name == class_name: + if not isinstance(klass, DefineGenericBase): + continue + annotation_generics = annotation_value.get_generics() actual_generics = klass.get_generics() diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index 98522b7a..4f8a33b9 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -86,6 +86,14 @@ def first(iterable: Iterable[T]) -> T: #? int() first(mapping_int_str) +# Test inference of str as an iterable of str. +#? str() +first("abc") + +some_str = NotImplemented # type: str +#? str() +first(some_str) + # Test that the right type is chosen when a partially realised mapping is expected def values(mapping: Mapping[int, T]) -> List[T]: @@ -177,3 +185,57 @@ x8 for x9 in unwrap_custom2(custom_instance_list_int): #? int() x9 + + +# Test that classes which have gneeric parents but are not generic themselves +# are still inferred correctly. +class Specialised(Mapping[int, str]): + pass + + +specialised_instance = NotImplemented # type: Specialised + +#? int() +first(specialised_instance) + +#? str() +values(specialised_instance)[0] + + +# Test that unbound generics are inferred as much as possible +class CustomPartialGeneric1(Mapping[str, T]): + pass + + +custom_partial1_instance = NotImplemented # type: CustomPartialGeneric1[int] + +#? str() +first(custom_partial1_instance) + + +custom_partial1_unbound_instance = NotImplemented # type: CustomPartialGeneric1 + +#? str() +first(custom_partial1_unbound_instance) + + +class CustomPartialGeneric2(Mapping[T, str]): + pass + + +custom_partial2_instance = NotImplemented # type: CustomPartialGeneric2[int] + +#? int() +first(custom_partial2_instance) + +#? str() +values(custom_partial2_instance)[0] + + +custom_partial2_unbound_instance = NotImplemented # type: CustomPartialGeneric2 + +#? [] +first(custom_partial2_unbound_instance) + +#? str() +values(custom_partial2_unbound_instance)[0] From e55712912144ff89842f8a5b44c54d6a28ce70d4 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 23:13:31 +0000 Subject: [PATCH 09/26] Remove check which doesn't seem to be needed I'm not sure why I added this, though removing it doesn't seem to casue any issues. I suspect there might be some oddness if the type being passed in doesn't match the type expected, though them having the same number of generic paramters isn't an expecially great way to validate that. --- jedi/inference/gradual/annotation.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 2a9e3d73..7efff3cd 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -447,12 +447,6 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): annotation_generics = annotation_value.get_generics() actual_generics = klass.get_generics() - if len(annotation_generics) != len(actual_generics): - # TODO: might there be other matches elsewhere in the MRO? - # TODO: what happens if _some_ of the generics are realised at - # this point, but not all (e.g: class `Foo(Dict[str, T])`)? - break - for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): for nested_annotation_value in annotation_generics_set: _merge_type_var_dicts( From 80db4dcf56eba8b2101f03c14ed2f2edd87ff94d Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 22 Feb 2020 23:47:20 +0000 Subject: [PATCH 10/26] Add test to ensure unions work --- test/completion/pep0484_generic_parameters.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index 4f8a33b9..a5ad8f45 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -8,6 +8,7 @@ from typing import ( Mapping, Type, TypeVar, + Union, ) K = TypeVar('K') @@ -17,6 +18,7 @@ V = TypeVar('V') list_of_ints = [42] # type: List[int] +list_of_ints_and_strs = [42, 'abc'] # type: List[Union[int, str]] # Test that simple parameters are handled def list_t_to_list_t(the_list: List[T]) -> List[T]: @@ -30,6 +32,15 @@ for a in list_t_to_list_t(list_of_ints): #? int() a +# Test that unions are handled +x2 = list_t_to_list_t(list_of_ints_and_strs)[0] +#? int() str() +x2 + +for z in list_t_to_list_t(list_of_ints_and_strs): + #? int() str() + z + list_of_int_type = [int] # type: List[Type[int]] From 5e990d9206c71aef1b65f197a2e4b4087e299199 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Feb 2020 00:27:30 +0000 Subject: [PATCH 11/26] Support passing through values for non-annotated tuples --- jedi/inference/value/iterable.py | 5 +++++ .../pep0484_generic_passthroughs.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index 236ea851..d1545b63 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -329,6 +329,11 @@ class SequenceLiteralValue(Sequence): self.array_type = SequenceLiteralValue.mapping[atom.children[0]] """The builtin name of the array (list, set, tuple or dict).""" + def _get_generics(self): + if self.array_type == u'tuple': + return tuple(x.infer().py__class__() for x in self.py__iter__()) + return super(SequenceLiteralValue, self)._get_generics() + def py__simple_getitem__(self, index): """Here the index is an int/str. Raises IndexError/KeyError.""" if isinstance(index, slice): diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py index bd13f926..c8a7db7e 100644 --- a/test/completion/pep0484_generic_passthroughs.py +++ b/test/completion/pep0484_generic_passthroughs.py @@ -2,6 +2,7 @@ from typing import Any, Iterable, List, Tuple, TypeVar T = TypeVar('T') +U = TypeVar('U') TList = TypeVar('TList', bound=List[Any]) untyped_list_str = ['abc', 'def'] @@ -10,6 +11,9 @@ typed_list_str = ['abc', 'def'] # type: List[str] untyped_tuple_str = ('abc',) typed_tuple_str = ('abc',) # type: Tuple[str] +untyped_tuple_str_int = ('abc', 4) +typed_tuple_str_int = ('abc', 4) # type: Tuple[str, int] + def untyped_passthrough(x): return x @@ -20,6 +24,9 @@ def typed_list_generic_passthrough(x: List[T]) -> List[T]: def typed_tuple_generic_passthrough(x: Tuple[T]) -> Tuple[T]: return x +def typed_multi_typed_tuple_generic_passthrough(x: Tuple[T, U]) -> Tuple[U, T]: + return x[1], x[0] + def typed_iterable_generic_passthrough(x: Iterable[T]) -> Iterable[T]: return x @@ -66,6 +73,20 @@ for h in typed_tuple_generic_passthrough(typed_tuple_str): h +out_untyped = typed_multi_typed_tuple_generic_passthrough(untyped_tuple_str_int) +#? int() +out_untyped[0] +#? str() +out_untyped[1] + + +out_typed = typed_multi_typed_tuple_generic_passthrough(typed_tuple_str_int) +#? int() +out_typed[0] +#? str() +out_typed[1] + + for n in typed_fully_generic_passthrough(untyped_list_str): #? str() n From f4cbf616043d843042baa75e5523599e7c3c2954 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Feb 2020 00:53:41 +0000 Subject: [PATCH 12/26] Ensure variadic tuples (Tuple[T, ...]) behave like sequences --- jedi/inference/gradual/annotation.py | 30 ++++++++++++------- .../pep0484_generic_passthroughs.py | 25 +++++++++++++++- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 7efff3cd..44f041c5 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -385,8 +385,6 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ) ) elif annotation_name == 'Tuple': - # TODO: check that this works both for fixed and variadic tuples - # (and maybe for combiantions of those). # TODO: this logic is pretty similar to the general logic below, can # we combine them? @@ -399,20 +397,32 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): continue annotation_generics = annotation_value.get_generics() - actual_generics = py_class.get_generics() - for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): - for nested_annotation_value in annotation_generics_set: + tuple_annotation, = annotation_value.execute_annotation() + # TODO: is can we avoid using this private method? + if tuple_annotation._is_homogenous(): + for nested_annotation_value in annotation_generics[0]: _merge_type_var_dicts( type_var_dict, _infer_type_vars( nested_annotation_value, - actual_generic_set, - # This is a note to ourselves that we - # have already converted the instance - # representation to its class. - is_class_value=True, + value_set.merge_types_of_iterate(), ), ) + else: + actual_generics = py_class.get_generics() + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): + for nested_annotation_value in annotation_generics_set: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + actual_generic_set, + # This is a note to ourselves that we + # have already converted the instance + # representation to its class. + is_class_value=True, + ), + ) elif isinstance(annotation_value, GenericClass): if annotation_name == 'Iterable' and not is_class_value: given = annotation_value.get_generics() diff --git a/test/completion/pep0484_generic_passthroughs.py b/test/completion/pep0484_generic_passthroughs.py index c8a7db7e..3ccdae7a 100644 --- a/test/completion/pep0484_generic_passthroughs.py +++ b/test/completion/pep0484_generic_passthroughs.py @@ -1,5 +1,5 @@ # python >= 3.4 -from typing import Any, Iterable, List, Tuple, TypeVar +from typing import Any, Iterable, List, Sequence, Tuple, TypeVar, Union T = TypeVar('T') U = TypeVar('U') @@ -14,6 +14,9 @@ typed_tuple_str = ('abc',) # type: Tuple[str] untyped_tuple_str_int = ('abc', 4) typed_tuple_str_int = ('abc', 4) # type: Tuple[str, int] +variadic_tuple_str = ('abc',) # type: Tuple[str, ...] +variadic_tuple_str_int = ('abc', 4) # type: Tuple[Union[str, int], ...] + def untyped_passthrough(x): return x @@ -27,6 +30,9 @@ def typed_tuple_generic_passthrough(x: Tuple[T]) -> Tuple[T]: def typed_multi_typed_tuple_generic_passthrough(x: Tuple[T, U]) -> Tuple[U, T]: return x[1], x[0] +def typed_variadic_tuple_generic_passthrough(x: Tuple[T, ...]) -> Sequence[T]: + return x + def typed_iterable_generic_passthrough(x: Iterable[T]) -> Iterable[T]: return x @@ -87,6 +93,23 @@ out_typed[0] out_typed[1] +for j in typed_variadic_tuple_generic_passthrough(untyped_tuple_str_int): + #? str() int() + j + +for k in typed_variadic_tuple_generic_passthrough(typed_tuple_str_int): + #? str() int() + k + +for l in typed_variadic_tuple_generic_passthrough(variadic_tuple_str): + #? str() + l + +for m in typed_variadic_tuple_generic_passthrough(variadic_tuple_str_int): + #? str() int() + m + + for n in typed_fully_generic_passthrough(untyped_list_str): #? str() n From f1a9e681ada0509ba7c7472df1319632f5f6b0a7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Feb 2020 15:25:28 +0000 Subject: [PATCH 13/26] Ensure comprehensions and generator expressions work --- jedi/inference/gradual/annotation.py | 4 ++-- test/completion/pep0484_generic_parameters.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 44f041c5..0e8582ee 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -389,7 +389,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): # we combine them? for element in value_set: - py_class = element.py__class__() + py_class = element.get_annotated_class_object() if not isinstance(py_class, GenericClass): py_class = element @@ -444,7 +444,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): continue if element.is_instance(): - py_class = element.py__class__() + py_class = element.get_annotated_class_object() else: py_class = element diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index a5ad8f45..dca64299 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -180,6 +180,16 @@ for x7 in unwrap_custom(list_custom_instances): x7 +for xc in unwrap_custom([CustomGeneric(s) for s in 'abc']): + #? str() + xc + + +for xg in unwrap_custom(CustomGeneric(s) for s in 'abc'): + #? str() + xg + + # Test extraction of type from type parameer nested within a custom generic type custom_instance_list_int = CustomGeneric([42]) # type: CustomGeneric[List[int]] From 54e29eede19947c27f63cfa7929e23d47327b135 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 16:31:12 +0000 Subject: [PATCH 14/26] Add explanation of the parameters to `_infer_type_vars` --- jedi/inference/gradual/annotation.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 0e8582ee..31b36ae1 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -331,6 +331,27 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): This functions would generate `int` for `_T` in this case, because it unpacks the `Iterable`. + + Parameters + ---------- + + `annotation_value`: represents the annotation of the current parameter to + infer the value for. In the above example, this would initially be the + `Iterable[_T]` of the `iterable` parameter and then, when recursing, + just the `_T` generic parameter. + + `value_set`: represents the actual argument passed to the parameter we're + inferrined for, or (for recursive calls) their types. In the above + example this would first be the representation of the list `[1]` and + then, when recursing, just of `1`. + + `is_class_value`: tells us whether or not to treat the `value_set` as + representing the instances or types being passed, which is neccesary to + correctly cope with `Type[T]` annotations. When it is True, this means + that we are being called with a nested portion of an annotation and that + the `value_set` represents the types of the arguments, rather than their + actual instances. + Note: not all recursive calls will neccesarily set this to True. """ type_var_dict = {} annotation_name = annotation_value.py__name__() From 3b4fa2aa9c1a95b9fa3b909b311bfc269f4e2c2b Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 16:32:38 +0000 Subject: [PATCH 15/26] Clarify variable name --- jedi/inference/gradual/annotation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 31b36ae1..932067f5 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -469,14 +469,14 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): else: py_class = element - for klass in py_class.py__mro__(): - class_name = klass.py__name__() + for parent_class in py_class.py__mro__(): + class_name = parent_class.py__name__() if annotation_name == class_name: - if not isinstance(klass, DefineGenericBase): + if not isinstance(parent_class, DefineGenericBase): continue annotation_generics = annotation_value.get_generics() - actual_generics = klass.get_generics() + actual_generics = parent_class.get_generics() for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): for nested_annotation_value in annotation_generics_set: From 95cec459a8b3a485665a46ead4661e92de9ce5db Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 17:06:22 +0000 Subject: [PATCH 16/26] Extract nested function for common pattern This slightly simplifies the code, as well as providing a place to put an explanation of what the moved block of code does. --- jedi/inference/gradual/annotation.py | 94 ++++++++++++++++------------ 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 932067f5..9f291425 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -356,6 +356,56 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): type_var_dict = {} annotation_name = annotation_value.py__name__() + def merge_pairwise_generics(annotation_generics, actual_generics): + """ + Given iterables of the generics immediately within an annotation and + within an argument's type, match up the generic parameters with the + corrsponding actual types. + + For example, given the following code: + + def values(mapping: Mapping[K, V]) -> List[V]: ... + + for val in values({1: 'a'}): + val + + Then in this function we are given `K` & `V` and `int` & `str` in order + to determine that `K` is `int and `V` is `str`. + + Parameters + ---------- + + `annotation_generics`: an ordered collection of the immediately nested + generic parameters within the annotation being considered. In the + above example, the caller would be analysing `Mapping[K, V]` and + would give us an iterable yileding representations of the parameters + `K` and then `V`. + + `actual_generics`: an ordered collection of the immediately nested + generic parameters within the type of the argument being considered. + These need to be the parameters at the level of the type for which + the annotation generics are given, rather than of the actual type of + the parameter. + In the above example, the caller would be analysing `{1: 'a'}`. The + caller must have already determined that this is an instance of a + type which implements `Mapping` and should pass us an iterable over + representations of the type parameters to that `Mapping` + implementation (here, `int` and `str` in that order). + """ + + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): + for nested_annotation_value in annotation_generics_set: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + actual_generic_set, + # This is a note to ourselves that we have already + # converted the instance representation to its class. + is_class_value=True, + ), + ) + if isinstance(annotation_value, TypeVar): if not is_class_value: return {annotation_name: value_set.py__class__()} @@ -371,19 +421,8 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): annotation_generics = annotation_value.get_generics() actual_generics = element.get_generics() - for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): - for nested_annotation_value in annotation_generics_set: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - actual_generic_set, - # This is a note to ourselves that we - # have already converted the instance - # representation to its class. - is_class_value=True, - ), - ) + merge_pairwise_generics(annotation_generics, actual_generics) + else: for nested_annotation_value in given[0]: _merge_type_var_dicts( @@ -431,19 +470,8 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ) else: actual_generics = py_class.get_generics() - for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): - for nested_annotation_value in annotation_generics_set: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - actual_generic_set, - # This is a note to ourselves that we - # have already converted the instance - # representation to its class. - is_class_value=True, - ), - ) + merge_pairwise_generics(annotation_generics, actual_generics) + elif isinstance(annotation_value, GenericClass): if annotation_name == 'Iterable' and not is_class_value: given = annotation_value.get_generics() @@ -478,19 +506,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): annotation_generics = annotation_value.get_generics() actual_generics = parent_class.get_generics() - for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): - for nested_annotation_value in annotation_generics_set: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - actual_generic_set, - # This is a note to ourselves that we - # have already converted the instance - # representation to its class. - is_class_value=True, - ), - ) + merge_pairwise_generics(annotation_generics, actual_generics) break From 5d273f46301fca0761e4ed5a51e493ca184cc559 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 17:35:03 +0000 Subject: [PATCH 17/26] Explain these branches --- jedi/inference/gradual/annotation.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 9f291425..54e0b00f 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -460,6 +460,9 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): tuple_annotation, = annotation_value.execute_annotation() # TODO: is can we avoid using this private method? if tuple_annotation._is_homogenous(): + # The parameter annotation is of the form `Tuple[T, ...]`, + # so we treat the incoming tuple like a iterable sequence + # rather than a positional container of elements. for nested_annotation_value in annotation_generics[0]: _merge_type_var_dicts( type_var_dict, @@ -469,6 +472,11 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ), ) else: + # The parameter annotation has only explicit type parameters + # (e.g: `Tuple[T]`, `Tuple[T, U]`, `Tuple[T, U, V]`, etc.) so we + # treat the incoming values as needing to match the annotation + # exactly, just as we would for non-tuple annotations. + actual_generics = py_class.get_generics() merge_pairwise_generics(annotation_generics, actual_generics) From 96132587b75137263ef4ffd000f328e01c948c25 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 17:35:29 +0000 Subject: [PATCH 18/26] Clarify generic tuple inference This hoist a loop invariant conditional check outside the loop making it clearer and one branch more obviously similar to the general type handling. --- jedi/inference/gradual/annotation.py | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 54e0b00f..699a14f1 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -445,37 +445,37 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): ) ) elif annotation_name == 'Tuple': - # TODO: this logic is pretty similar to the general logic below, can - # we combine them? + annotation_generics = annotation_value.get_generics() + tuple_annotation, = annotation_value.execute_annotation() + # TODO: is can we avoid using this private method? + if tuple_annotation._is_homogenous(): + # The parameter annotation is of the form `Tuple[T, ...]`, + # so we treat the incoming tuple like a iterable sequence + # rather than a positional container of elements. + for nested_annotation_value in annotation_generics[0]: + _merge_type_var_dicts( + type_var_dict, + _infer_type_vars( + nested_annotation_value, + value_set.merge_types_of_iterate(), + ), + ) + else: + # The parameter annotation has only explicit type parameters + # (e.g: `Tuple[T]`, `Tuple[T, U]`, `Tuple[T, U, V]`, etc.) so we + # treat the incoming values as needing to match the annotation + # exactly, just as we would for non-tuple annotations. - for element in value_set: - py_class = element.get_annotated_class_object() - if not isinstance(py_class, GenericClass): - py_class = element + # TODO: this logic is pretty similar to the general logic below, can + # we combine them? - if not isinstance(py_class, DefineGenericBase): - continue + for element in value_set: + py_class = element.get_annotated_class_object() + if not isinstance(py_class, GenericClass): + py_class = element - annotation_generics = annotation_value.get_generics() - tuple_annotation, = annotation_value.execute_annotation() - # TODO: is can we avoid using this private method? - if tuple_annotation._is_homogenous(): - # The parameter annotation is of the form `Tuple[T, ...]`, - # so we treat the incoming tuple like a iterable sequence - # rather than a positional container of elements. - for nested_annotation_value in annotation_generics[0]: - _merge_type_var_dicts( - type_var_dict, - _infer_type_vars( - nested_annotation_value, - value_set.merge_types_of_iterate(), - ), - ) - else: - # The parameter annotation has only explicit type parameters - # (e.g: `Tuple[T]`, `Tuple[T, U]`, `Tuple[T, U, V]`, etc.) so we - # treat the incoming values as needing to match the annotation - # exactly, just as we would for non-tuple annotations. + if not isinstance(py_class, DefineGenericBase): + continue actual_generics = py_class.get_generics() merge_pairwise_generics(annotation_generics, actual_generics) From d06efd0dd14f5cf587feceaec54c293fba480d69 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 18:09:20 +0000 Subject: [PATCH 19/26] Push fetching of generics into nested function This slightly simplifies both the calling code and semantics of the nested function. --- jedi/inference/gradual/annotation.py | 56 +++++++++++++--------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 699a14f1..552325db 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -356,11 +356,14 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): type_var_dict = {} annotation_name = annotation_value.py__name__() - def merge_pairwise_generics(annotation_generics, actual_generics): + def merge_pairwise_generics(annotation_value, annotated_argument_class): """ - Given iterables of the generics immediately within an annotation and - within an argument's type, match up the generic parameters with the - corrsponding actual types. + Match up the generic parameters from the given argument class to the + target annotation. + + This walks the generic parameters immediately within the annotation and + argument's type, in order to determine the concrete values of the + annotation's parameters for the current case. For example, given the following code: @@ -369,30 +372,28 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): for val in values({1: 'a'}): val - Then in this function we are given `K` & `V` and `int` & `str` in order - to determine that `K` is `int and `V` is `str`. + Then this function should be given representations of `Mapping[K, V]` + and `Mapping[int, str]`, so that it can determine that `K` is `int and + `V` is `str`. + + Note that it is responsibility of the caller to traverse the MRO of the + argument type as needed in order to find the type matching the + annotation (in this case finding `Mapping[int, str]` as a parent of + `Dict[int, str]`). Parameters ---------- - `annotation_generics`: an ordered collection of the immediately nested - generic parameters within the annotation being considered. In the - above example, the caller would be analysing `Mapping[K, V]` and - would give us an iterable yileding representations of the parameters - `K` and then `V`. + `annotation_value`: represents the annotation to infer the concrete + parameter types of. - `actual_generics`: an ordered collection of the immediately nested - generic parameters within the type of the argument being considered. - These need to be the parameters at the level of the type for which - the annotation generics are given, rather than of the actual type of - the parameter. - In the above example, the caller would be analysing `{1: 'a'}`. The - caller must have already determined that this is an instance of a - type which implements `Mapping` and should pass us an iterable over - representations of the type parameters to that `Mapping` - implementation (here, `int` and `str` in that order). + `annotated_argument_class`: represents the annotated class of the + argument being passed to the object annotated by `annotation_value`. """ + annotation_generics = annotation_value.get_generics() + actual_generics = annotated_argument_class.get_generics() + for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics): for nested_annotation_value in annotation_generics_set: _merge_type_var_dicts( @@ -418,10 +419,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): for element in value_set: element_name = element.py__name__() if annotation_name == element_name: - annotation_generics = annotation_value.get_generics() - actual_generics = element.get_generics() - - merge_pairwise_generics(annotation_generics, actual_generics) + merge_pairwise_generics(annotation_value, element) else: for nested_annotation_value in given[0]: @@ -477,8 +475,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): if not isinstance(py_class, DefineGenericBase): continue - actual_generics = py_class.get_generics() - merge_pairwise_generics(annotation_generics, actual_generics) + merge_pairwise_generics(annotation_value, py_class) elif isinstance(annotation_value, GenericClass): if annotation_name == 'Iterable' and not is_class_value: @@ -511,10 +508,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): if not isinstance(parent_class, DefineGenericBase): continue - annotation_generics = annotation_value.get_generics() - actual_generics = parent_class.get_generics() - - merge_pairwise_generics(annotation_generics, actual_generics) + merge_pairwise_generics(annotation_value, parent_class) break From b198434694aa9dd5fabcae535e7ee356823f5eb6 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 7 Mar 2020 20:29:14 +0000 Subject: [PATCH 20/26] Remove resolved TODO The common logic this refers to has now been extracted (see 95cec459) and the remaining checks are specific to tuple handling. --- jedi/inference/gradual/annotation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 552325db..bb77473c 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -464,9 +464,6 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): # treat the incoming values as needing to match the annotation # exactly, just as we would for non-tuple annotations. - # TODO: this logic is pretty similar to the general logic below, can - # we combine them? - for element in value_set: py_class = element.get_annotated_class_object() if not isinstance(py_class, GenericClass): From da9d31218593ec1e761107aa3c1b6fc55895111b Mon Sep 17 00:00:00 2001 From: Peter Law Date: Thu, 12 Mar 2020 22:06:13 +0000 Subject: [PATCH 21/26] Remove redundant attribute check --- jedi/inference/gradual/annotation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index bb77473c..11801398 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -491,9 +491,6 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): # the elements from the set first, then handle them, even if we put # them back in a set afterwards. for element in value_set: - if not hasattr(element, 'is_instance'): - continue - if element.is_instance(): py_class = element.get_annotated_class_object() else: From 0f8e7b453e785b8825dbff3d87decaba7bcd7626 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 18 Mar 2020 21:01:41 +0000 Subject: [PATCH 22/26] Formatting --- jedi/inference/gradual/annotation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 11801398..79481a45 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -429,8 +429,9 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): nested_annotation_value, value_set, is_class_value=True, - ) + ), ) + elif annotation_name == 'Callable': given = annotation_value.get_generics() if len(given) == 2: @@ -440,8 +441,9 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): _infer_type_vars( nested_annotation_value, value_set.execute_annotation(), - ) + ), ) + elif annotation_name == 'Tuple': annotation_generics = annotation_value.get_generics() tuple_annotation, = annotation_value.execute_annotation() @@ -458,6 +460,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): value_set.merge_types_of_iterate(), ), ) + else: # The parameter annotation has only explicit type parameters # (e.g: `Tuple[T]`, `Tuple[T, U]`, `Tuple[T, U, V]`, etc.) so we @@ -484,7 +487,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): _infer_type_vars( nested_annotation_value, value_set.merge_types_of_iterate(), - ) + ), ) else: # Note: we need to handle the MRO _in order_, so we need to extract From 95b0cdcb5e91a8d592a7fa44842558110219bd6f Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 18 Mar 2020 22:15:32 +0000 Subject: [PATCH 23/26] Add test for child of specialised generic --- test/completion/pep0484_generic_parameters.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index dca64299..efc7c926 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -223,6 +223,21 @@ first(specialised_instance) values(specialised_instance)[0] +# Test that classes which have gneeric ancestry but neither they nor their +# parents are not generic are still inferred correctly. +class ChildOfSpecialised(Specialised): + pass + + +child_of_specialised_instance = NotImplemented # type: ChildOfSpecialised + +#? int() +first(child_of_specialised_instance) + +#? str() +values(child_of_specialised_instance)[0] + + # Test that unbound generics are inferred as much as possible class CustomPartialGeneric1(Mapping[str, T]): pass From 5ca69458d4d3c88f8d01cccc7bad33e916f09de7 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 22 Mar 2020 14:58:18 +0000 Subject: [PATCH 24/26] Add testing for mismatch cases This should help catch any errors in our handling of invalid cases. While some of these produce outputs which aren't correct, what we're checking here is that we don't _error_ while producing that output. Also fix a case which this showed up. --- jedi/inference/gradual/annotation.py | 5 + test/completion/pep0484_generic_mismatches.py | 320 ++++++++++++++++++ test/test_api/test_api.py | 1 + 3 files changed, 326 insertions(+) create mode 100644 test/completion/pep0484_generic_mismatches.py diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 79481a45..0d5effbd 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -494,6 +494,11 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): # the elements from the set first, then handle them, even if we put # them back in a set afterwards. for element in value_set: + if element.api_type == u'function': + # Functions & methods don't have an MRO and we're not + # expecting a Callable (those are handled separately above). + continue + if element.is_instance(): py_class = element.get_annotated_class_object() else: diff --git a/test/completion/pep0484_generic_mismatches.py b/test/completion/pep0484_generic_mismatches.py new file mode 100644 index 00000000..bd7d393a --- /dev/null +++ b/test/completion/pep0484_generic_mismatches.py @@ -0,0 +1,320 @@ +# python >= 3.4 +from typing import ( + Callable, + Dict, + Generic, + List, + Sequence, + Tuple, + Type, + TypeVar, +) + +T = TypeVar('T') + + +def foo(x: T) -> T: + return x + + +class CustomGeneric(Generic[T]): + def __init__(self, val: T) -> None: + self.val = val + + +class PlainClass(object): + pass + + +tpl = ("1", 2) +tpl_typed = ("2", 3) # type: Tuple[str, int] + +collection = {"a": 1} +collection_typed = {"a": 1} # type: Dict[str, int] + +list_of_funcs = [foo] # type: List[Callable[[T], T]] + +custom_generic = CustomGeneric(123.45) + +plain_instance = PlainClass() + + +# Test that simple parameters are handled +def list_t_to_list_t(the_list: List[T]) -> List[T]: + return the_list + +x0 = list_t_to_list_t("abc")[0] +#? +x0 + +x1 = list_t_to_list_t(foo)[0] +#? +x1 + +x2 = list_t_to_list_t(tpl)[0] +#? +x2 + +x3 = list_t_to_list_t(tpl_typed)[0] +#? +x3 + +x4 = list_t_to_list_t(collection)[0] +#? +x4 + +x5 = list_t_to_list_t(collection_typed)[0] +#? +x5 + +x6 = list_t_to_list_t(custom_generic)[0] +#? +x6 + +x7 = list_t_to_list_t(plain_instance)[0] +#? +x7 + +for a in list_t_to_list_t(12): + #? + a + + +# Test that simple parameters are handled +def list_type_t_to_list_t(the_list: List[Type[T]]) -> List[T]: + return [x() for x in the_list] + +x0 = list_type_t_to_list_t("abc")[0] +#? +x0 + +x1 = list_type_t_to_list_t(foo)[0] +#? +x1 + +x2 = list_type_t_to_list_t(tpl)[0] +#? +x2 + +x3 = list_type_t_to_list_t(tpl_typed)[0] +#? +x3 + +x4 = list_type_t_to_list_t(collection)[0] +#? +x4 + +x5 = list_type_t_to_list_t(collection_typed)[0] +#? +x5 + +x6 = list_type_t_to_list_t(custom_generic)[0] +#? +x6 + +x7 = list_type_t_to_list_t(plain_instance)[0] +#? +x7 + +for a in list_type_t_to_list_t(12): + #? + a + + +x0 = list_type_t_to_list_t(["abc"])[0] +#? +x0 + +x1 = list_type_t_to_list_t([foo])[0] +#? +x1 + +x2 = list_type_t_to_list_t([tpl])[0] +#? +x2 + +x3 = list_type_t_to_list_t([tpl_typed])[0] +#? +x3 + +x4 = list_type_t_to_list_t([collection])[0] +#? +x4 + +x5 = list_type_t_to_list_t([collection_typed])[0] +#? +x5 + +x6 = list_type_t_to_list_t([custom_generic])[0] +#? +x6 + +x7 = list_type_t_to_list_t([plain_instance])[0] +#? +x7 + +for a in list_type_t_to_list_t([12]): + #? + a + + +def list_func_t_to_list_t(the_list: List[Callable[[T], T]]) -> List[T]: + # Not actually a viable signature, but should be enough to test our handling + # of the generic parameters. + pass + + +x0 = list_func_t_to_list_t("abc")[0] +#? +x0 + +x1 = list_func_t_to_list_t(foo)[0] +#? +x1 + +x2 = list_func_t_to_list_t(tpl)[0] +#? +x2 + +x3 = list_func_t_to_list_t(tpl_typed)[0] +#? +x3 + +x4 = list_func_t_to_list_t(collection)[0] +#? +x4 + +x5 = list_func_t_to_list_t(collection_typed)[0] +#? +x5 + +x6 = list_func_t_to_list_t(custom_generic)[0] +#? +x6 + +x7 = list_func_t_to_list_t(plain_instance)[0] +#? +x7 + +for a in list_func_t_to_list_t(12): + #? + a + + +# The following are all actually wrong, however we're mainly testing here that +# we don't error when processing invalid values, rather than that we get the +# right output. + +x0 = list_func_t_to_list_t(["abc"])[0] +#? str() +x0 + +x2 = list_func_t_to_list_t([tpl])[0] +#? tuple() +x2 + +x3 = list_func_t_to_list_t([tpl_typed])[0] +#? tuple() +x3 + +x4 = list_func_t_to_list_t([collection])[0] +#? dict() +x4 + +x5 = list_func_t_to_list_t([collection_typed])[0] +#? dict() +x5 + +x6 = list_func_t_to_list_t([custom_generic])[0] +#? CustomGeneric() +x6 + +x7 = list_func_t_to_list_t([plain_instance])[0] +#? PlainClass() +x7 + +for a in list_func_t_to_list_t([12]): + #? int() + a + + +def tuple_t(tuple_in: Tuple[T]]) -> Sequence[T]: + return tuple_in + + +x0 = list_t_to_list_t("abc")[0] +#? +x0 + +x1 = list_t_to_list_t(foo)[0] +#? +x1 + +x2 = list_t_to_list_t(tpl)[0] +#? +x2 + +x3 = list_t_to_list_t(tpl_typed)[0] +#? +x3 + +x4 = list_t_to_list_t(collection)[0] +#? +x4 + +x5 = list_t_to_list_t(collection_typed)[0] +#? +x5 + +x6 = list_t_to_list_t(custom_generic)[0] +#? +x6 + +x7 = list_t_to_list_t(plain_instance)[0] +#? +x7 + +for a in list_t_to_list_t(12): + #? + a + + +def tuple_t_elipsis(tuple_in: Tuple[T, ...]]) -> Sequence[T]: + return tuple_in + + +x0 = list_t_to_list_t("abc")[0] +#? +x0 + +x1 = list_t_to_list_t(foo)[0] +#? +x1 + +x2 = list_t_to_list_t(tpl)[0] +#? +x2 + +x3 = list_t_to_list_t(tpl_typed)[0] +#? +x3 + +x4 = list_t_to_list_t(collection)[0] +#? +x4 + +x5 = list_t_to_list_t(collection_typed)[0] +#? +x5 + +x6 = list_t_to_list_t(custom_generic)[0] +#? +x6 + +x7 = list_t_to_list_t(plain_instance)[0] +#? +x7 + +for a in list_t_to_list_t(12): + #? + a diff --git a/test/test_api/test_api.py b/test/test_api/test_api.py index a7337cfe..f8dab6e6 100644 --- a/test/test_api/test_api.py +++ b/test/test_api/test_api.py @@ -342,6 +342,7 @@ def test_file_fuzzy_completion(Script): script = Script('"{}/ep08_i'.format(path)) expected = [ 'pep0484_basic.py"', + 'pep0484_generic_mismatches.py"', 'pep0484_generic_parameters.py"', 'pep0484_generic_passthroughs.py"', 'pep0484_typing.py"', From c743e5d9f33c1bda9e9ad37da7c6273470045812 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 22 Mar 2020 15:14:01 +0000 Subject: [PATCH 25/26] Push type check into helper --- jedi/inference/gradual/annotation.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/jedi/inference/gradual/annotation.py b/jedi/inference/gradual/annotation.py index 0d5effbd..1b334573 100644 --- a/jedi/inference/gradual/annotation.py +++ b/jedi/inference/gradual/annotation.py @@ -390,6 +390,8 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): `annotated_argument_class`: represents the annotated class of the argument being passed to the object annotated by `annotation_value`. """ + if not isinstance(annotated_argument_class, DefineGenericBase): + return annotation_generics = annotation_value.get_generics() actual_generics = annotated_argument_class.get_generics() @@ -472,9 +474,6 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): if not isinstance(py_class, GenericClass): py_class = element - if not isinstance(py_class, DefineGenericBase): - continue - merge_pairwise_generics(annotation_value, py_class) elif isinstance(annotation_value, GenericClass): @@ -507,11 +506,7 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False): for parent_class in py_class.py__mro__(): class_name = parent_class.py__name__() if annotation_name == class_name: - if not isinstance(parent_class, DefineGenericBase): - continue - merge_pairwise_generics(annotation_value, parent_class) - break return type_var_dict From 7e9ad9e7334fc82756951c9ed71be6b08437414b Mon Sep 17 00:00:00 2001 From: Peter Law Date: Wed, 25 Mar 2020 22:32:53 +0000 Subject: [PATCH 26/26] Fix typo --- test/completion/pep0484_generic_parameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/completion/pep0484_generic_parameters.py b/test/completion/pep0484_generic_parameters.py index efc7c926..89572d99 100644 --- a/test/completion/pep0484_generic_parameters.py +++ b/test/completion/pep0484_generic_parameters.py @@ -208,7 +208,7 @@ for x9 in unwrap_custom2(custom_instance_list_int): x9 -# Test that classes which have gneeric parents but are not generic themselves +# Test that classes which have generic parents but are not generic themselves # are still inferred correctly. class Specialised(Mapping[int, str]): pass @@ -223,7 +223,7 @@ first(specialised_instance) values(specialised_instance)[0] -# Test that classes which have gneeric ancestry but neither they nor their +# Test that classes which have generic ancestry but neither they nor their # parents are not generic are still inferred correctly. class ChildOfSpecialised(Specialised): pass