Merge branch 'improve-type-annotation-inference' of https://github.com/PeterJCLaw/jedi

This commit is contained in:
Dave Halter
2020-04-01 00:54:13 +02:00
7 changed files with 890 additions and 51 deletions

View File

@@ -331,17 +331,99 @@ 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__()
def merge_pairwise_generics(annotation_value, annotated_argument_class):
"""
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:
def values(mapping: Mapping[K, V]) -> List[V]: ...
for val in values({1: 'a'}):
val
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_value`: represents the annotation to infer the concrete
parameter types of.
`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()
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_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:
if is_class_value:
for element in value_set:
element_name = element.py__name__()
if annotation_name == element_name:
merge_pairwise_generics(annotation_value, element)
else:
for nested_annotation_value in given[0]:
_merge_type_var_dicts(
type_var_dict,
@@ -349,9 +431,10 @@ def _infer_type_vars(annotation_value, value_set, is_class_value=False):
nested_annotation_value,
value_set,
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]:
@@ -360,11 +443,41 @@ 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()
# 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
merge_pairwise_generics(annotation_value, py_class)
elif isinstance(annotation_value, GenericClass):
name = annotation_value.py__name__()
if 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]:
@@ -372,35 +485,30 @@ 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':
given = annotation_value.get_generics()
if len(given) == 2:
for value in value_set:
try:
method = value.get_mapping_item_values
except AttributeError:
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 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
key_values, value_values = method()
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,
)
)
if element.is_instance():
py_class = element.get_annotated_class_object()
else:
py_class = element
for parent_class in py_class.py__mro__():
class_name = parent_class.py__name__()
if annotation_name == class_name:
merge_pairwise_generics(annotation_value, parent_class)
break
return type_var_dict

View File

@@ -245,12 +245,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.

View File

@@ -328,6 +328,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):

View File

@@ -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

View File

@@ -0,0 +1,277 @@
# python >= 3.4
from typing import (
Callable,
Dict,
Generic,
Iterable,
List,
Mapping,
Type,
TypeVar,
Union,
)
K = TypeVar('K')
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
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]:
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
# 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]]
# 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
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
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 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]:
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
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]]
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
# Test that classes which have generic 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 classes which have generic 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
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]

View File

@@ -0,0 +1,128 @@
# python >= 3.4
from typing import Any, Iterable, List, Sequence, Tuple, TypeVar, Union
T = TypeVar('T')
U = TypeVar('U')
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]
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
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_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
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
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 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
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

View File

@@ -340,8 +340,14 @@ 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_mismatches.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(