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

This commit is contained in:
Dave Halter
2020-04-01 00:54:25 +02:00
6 changed files with 213 additions and 188 deletions

View File

@@ -10,6 +10,43 @@ class BaseValue(object):
return value
value = value.parent_context
def infer_type_vars(self, value_set, is_class_value=False):
"""
When the current instance represents a type annotation, this method
tries to find information about undefined type vars and returns a dict
from type var name to value set.
This is for example important to understand what `iter([1])` returns.
According to typeshed, `iter` returns an `Iterator[_T]`:
def iter(iterable: Iterable[_T]) -> Iterator[_T]: ...
This functions would generate `int` for `_T` in this case, because it
unpacks the `Iterable`.
Parameters
----------
`self`: 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.
"""
return {}
class BaseValueSet(object):
def __init__(self, iterable):

View File

@@ -438,6 +438,18 @@ class ValueSet(BaseValueSet):
s = 'Optional[%s]' % s
return s
def infer_type_vars(self, value_set, is_class_value=False):
# Circular
from jedi.inference.gradual.annotation import merge_type_var_dicts
type_var_dict = {}
for value in self._set:
merge_type_var_dicts(
type_var_dict,
value.infer_type_vars(value_set, is_class_value),
)
return type_var_dict
NO_VALUES = ValueSet([])

View File

@@ -14,7 +14,6 @@ from jedi.inference.cache import inference_state_method_cache
from jedi.inference.base_value import ValueSet, NO_VALUES
from jedi.inference.gradual.base import DefineGenericBase, GenericClass
from jedi.inference.gradual.generics import TupleGenericManager
from jedi.inference.gradual.typing import TypingClassValueWithIndex
from jedi.inference.gradual.type_var import TypeVar
from jedi.inference.helpers import is_string
from jedi.inference.compiled import builtin_from_name
@@ -269,11 +268,10 @@ def infer_type_vars_for_execution(function, arguments, annotation_dict):
elif kind is Parameter.VAR_KEYWORD:
# TODO _dict_values is not public.
actual_value_set = actual_value_set.try_merge('_dict_values')
for ann in annotation_value_set:
_merge_type_var_dicts(
annotation_variable_results,
_infer_type_vars(ann, actual_value_set),
)
merge_type_var_dicts(
annotation_variable_results,
annotation_value_set.infer_type_vars(actual_value_set),
)
return annotation_variable_results
@@ -302,15 +300,14 @@ def infer_type_vars_for_callable(arguments, lazy_params):
callable_param_values = lazy_callable_param.infer()
# Infer unknown type var
actual_value_set = lazy_value.infer()
for v in callable_param_values:
_merge_type_var_dicts(
annotation_variable_results,
_infer_type_vars(v, actual_value_set),
)
merge_type_var_dicts(
annotation_variable_results,
callable_param_values.infer_type_vars(actual_value_set),
)
return annotation_variable_results
def _merge_type_var_dicts(base_dict, new_dict):
def merge_type_var_dicts(base_dict, new_dict):
for type_var_name, values in new_dict.items():
if values:
try:
@@ -319,195 +316,59 @@ def _merge_type_var_dicts(base_dict, new_dict):
base_dict[type_var_name] = values
def _infer_type_vars(annotation_value, value_set, is_class_value=False):
def merge_pairwise_generics(annotation_value, annotated_argument_class):
"""
This function tries to find information about undefined type vars and
returns a dict from type var name to value set.
Match up the generic parameters from the given argument class to the
target annotation.
This is for example important to understand what `iter([1])` returns.
According to typeshed, `iter` returns an `Iterator[_T]`:
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.
def iter(iterable: Iterable[_T]) -> Iterator[_T]: ...
For example, given the following code:
This functions would generate `int` for `_T` in this case, because it
unpacks the `Iterable`.
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 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.
`annotation_value`: represents the annotation to infer the concrete
parameter types of.
`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.
`annotated_argument_class`: represents the annotated class of the
argument being passed to the object annotated by `annotation_value`.
"""
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.
if not isinstance(annotated_argument_class, DefineGenericBase):
return type_var_dict
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.
annotation_generics = annotation_value.get_generics()
actual_generics = annotated_argument_class.get_generics()
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_name: value_set.py__class__()}
return {annotation_name: value_set}
elif isinstance(annotation_value, TypingClassValueWithIndex):
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,
_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:
for nested_annotation_value in given[1]:
_merge_type_var_dicts(
type_var_dict,
_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):
if annotation_name == 'Iterable' and not is_class_value:
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.merge_types_of_iterate(),
),
)
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
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
for annotation_generics_set, actual_generic_set in zip(annotation_generics, actual_generics):
merge_type_var_dicts(
type_var_dict,
annotation_generics_set.infer_type_vars(
actual_generic_set,
# This is a note to ourselves that we have already
# converted the instance representation to its class.
is_class_value=True,
),
)
return type_var_dict

View File

@@ -200,6 +200,46 @@ class GenericClass(ClassMixin, DefineGenericBase):
return True
return self._class_value.is_sub_class_of(class_value)
def infer_type_vars(self, value_set, is_class_value=False):
# Circular
from jedi.inference.gradual.annotation import merge_pairwise_generics, merge_type_var_dicts
annotation_name = self.py__name__()
type_var_dict = {}
if annotation_name == 'Iterable' and not is_class_value:
annotation_generics = self.get_generics()
if annotation_generics:
return annotation_generics[0].infer_type_vars(
value_set.merge_types_of_iterate(),
)
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 within
# TypingClassValueWithIndex).
continue
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_type_var_dicts(
type_var_dict,
merge_pairwise_generics(self, parent_class),
)
break
return type_var_dict
class _LazyGenericBaseClass(object):
def __init__(self, class_value, lazy_base_class):

View File

@@ -107,5 +107,11 @@ class TypeVar(BaseTypingValue):
def execute_annotation(self):
return self._get_classes().execute_annotation()
def infer_type_vars(self, value_set, is_class_value=False):
annotation_name = self.py__name__()
if not is_class_value:
return {annotation_name: value_set.py__class__()}
return {annotation_name: value_set}
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.py__name__())

View File

@@ -184,7 +184,44 @@ class _TypingClassMixin(ClassMixin):
class TypingClassValueWithIndex(_TypingClassMixin, TypingValueWithIndex):
pass
def infer_type_vars(self, value_set, is_class_value=False):
# Circular
from jedi.inference.gradual.annotation import merge_pairwise_generics, merge_type_var_dicts
type_var_dict = {}
annotation_generics = self.get_generics()
if not annotation_generics:
return type_var_dict
annotation_name = self.py__name__()
if annotation_name == 'Type':
if is_class_value:
for element in value_set:
element_name = element.py__name__()
if annotation_name == element_name:
merge_type_var_dicts(
type_var_dict,
merge_pairwise_generics(self, element),
)
else:
return annotation_generics[0].infer_type_vars(
value_set,
is_class_value=True,
)
elif annotation_name == 'Callable':
if len(annotation_generics) == 2:
return annotation_generics[1].infer_type_vars(
value_set.execute_annotation(),
)
elif annotation_name == 'Tuple':
tuple_annotation, = self.execute_annotation()
return tuple_annotation.infer_type_vars(value_set, is_class_value)
return type_var_dict
class ProxyTypingClassValue(_TypingClassMixin, ProxyTypingValue):
@@ -281,6 +318,38 @@ class Tuple(BaseTypingValueWithGenerics):
.py__getattribute__('tuple').execute_annotation()
return tuple_
def infer_type_vars(self, value_set, is_class_value=False):
# Circular
from jedi.inference.gradual.annotation import merge_pairwise_generics, merge_type_var_dicts
from jedi.inference.gradual.base import GenericClass
if self._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.
return self.get_generics()[0].infer_type_vars(
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.
type_var_dict = {}
for element in value_set:
py_class = element.get_annotated_class_object()
if not isinstance(py_class, GenericClass):
py_class = element
merge_type_var_dicts(
type_var_dict,
merge_pairwise_generics(self, py_class),
)
return type_var_dict
class Generic(BaseTypingValueWithGenerics):
pass