This commit is contained in:
Dave Halter
2020-03-01 01:31:17 +01:00
7 changed files with 233 additions and 11 deletions

View File

@@ -258,6 +258,7 @@ class Value(HelperValueMixin, BaseValue):
def _as_context(self):
raise NotImplementedError('Not all values need to be converted to contexts: %s', self)
@property
def name(self):
raise NotImplementedError

View File

@@ -5,8 +5,9 @@ values.
This file deals with all the typing.py cases.
"""
import itertools
from jedi import debug
from jedi.inference.compiled import builtin_from_name
from jedi.inference.compiled import builtin_from_name, create_simple_object
from jedi.inference.base_value import ValueSet, NO_VALUES, Value, \
LazyValueWrapper
from jedi.inference.lazy_value import LazyKnownValues
@@ -81,7 +82,8 @@ class TypingModuleName(NameWrapper):
elif name == 'TypedDict':
# TODO doesn't even exist in typeshed/typing.py, yet. But will be
# added soon.
pass
yield TypedDictBase.create_cached(
inference_state, self.parent_context, self.tree_name)
elif name in ('no_type_check', 'no_type_check_decorator'):
# This is not necessary, as long as we are not doing type checking.
for c in self._wrapped_name.infer(): # Fuck my life Python 2
@@ -339,3 +341,74 @@ class CastFunction(BaseTypingValue):
@repack_with_argument_clinic('type, object, /')
def py__call__(self, type_value_set, object_value_set):
return type_value_set.execute_annotation()
class TypedDictBase(BaseTypingValue):
"""
This class has no responsibilities and is just here to make sure that typed
dicts can be identified.
"""
class TypedDictClass(Value):
"""
This represents a class defined like:
class Foo(TypedDict):
bar: str
"""
def __init__(self, definition_class):
super().__init__(definition_class.inference_state, definition_class.parent_context)
self.tree_node = definition_class.tree_node
self._definition_class = definition_class
def get_filters(self, origin_scope=None):
"""
A TypedDict doesn't have attributes.
"""
o, = self.inference_state.builtins_module.py__getattribute__('object')
return o.get_filters()
@property
def name(self):
return ValueName(self, self.tree_node.name)
def py__call__(self, arguments):
return ValueSet({TypedDict(self._definition_class)})
class TypedDict(LazyValueWrapper):
"""Represents the instance version of ``TypedDictClass``."""
def __init__(self, definition_class):
self.inference_state = definition_class.inference_state
self.parent_context = definition_class.parent_context
self.tree_node = definition_class.tree_node
self._definition_class = definition_class
@property
def name(self):
return ValueName(self, self.tree_node.name)
def py__simple_getitem__(self, index):
if isinstance(index, str):
return ValueSet.from_sets(
name.infer()
for filter in self._definition_class.get_filters(is_instance=True)
for name in filter.get(index)
)
return NO_VALUES
def get_key_values(self):
filtered_values = itertools.chain.from_iterable((
f.values()
for f in self._definition_class.get_filters(is_instance=True)
))
return ValueSet({
create_simple_object(self.inference_state, v.string_name)
for v in filtered_values
})
def _get_wrapped_value(self):
d, = self.inference_state.builtins_module.py__getattribute__('dict')
result, = d.execute_with_values()
return result

View File

@@ -26,6 +26,7 @@ from jedi.inference.compiled.access import COMPARISON_OPERATORS
from jedi.inference.cache import inference_state_method_cache
from jedi.inference.gradual.stub_value import VersionInfo
from jedi.inference.gradual import annotation
from jedi.inference.gradual.typing import TypedDictClass
from jedi.inference.names import TreeNameDefinition
from jedi.inference.context import CompForContext
from jedi.inference.value.decorator import Decoratee
@@ -748,6 +749,8 @@ def _apply_decorators(context, node):
parent_context=context,
tree_node=node
)
if decoratee_value.is_typeddict():
decoratee_value = TypedDictClass(decoratee_value)
else:
decoratee_value = FunctionValue.from_context(context, node)
initial = values = ValueSet([decoratee_value])

View File

@@ -547,10 +547,10 @@ class InstanceClassFilter(AbstractFilter):
self._class_filter = class_filter
def get(self, name):
return self._convert(self._class_filter.get(name, from_instance=True))
return self._convert(self._class_filter.get(name))
def values(self):
return self._convert(self._class_filter.values(from_instance=True))
return self._convert(self._class_filter.values())
def _convert(self, names):
return [
@@ -586,7 +586,7 @@ class SelfAttributeFilter(ClassFilter):
if trailer.type == 'trailer' \
and len(trailer.parent.children) == 2 \
and trailer.children[0] == '.':
if name.is_definition() and self._access_possible(name, from_instance=True):
if name.is_definition() and self._access_possible(name):
# TODO filter non-self assignments instead of this bad
# filter.
if self._is_in_right_scope(trailer.parent.children[0], name):

View File

@@ -38,11 +38,11 @@ py__doc__() Returns the docstring for a value.
"""
from jedi import debug
from jedi._compatibility import use_metaclass
from jedi.parser_utils import get_cached_parent_scope
from jedi.parser_utils import get_cached_parent_scope, expr_is_dotted
from jedi.inference.cache import inference_state_method_cache, CachedMetaClass, \
inference_state_method_generator_cache
from jedi.inference import compiled
from jedi.inference.lazy_value import LazyKnownValues
from jedi.inference.lazy_value import LazyKnownValues, LazyTreeValue
from jedi.inference.filters import ParserTreeFilter
from jedi.inference.names import TreeNameDefinition, ValueName
from jedi.inference.arguments import unpack_arglist, ValuesArguments
@@ -104,12 +104,12 @@ class ClassFilter(ParserTreeFilter):
node = get_cached_parent_scope(self._used_names, node)
return False
def _access_possible(self, name, from_instance=False):
def _access_possible(self, name):
# Filter for ClassVar variables
# TODO this is not properly done, yet. It just checks for the string
# ClassVar in the annotation, which can be quite imprecise. If we
# wanted to do this correct, we would have to infer the ClassVar.
if not from_instance:
if not self._is_instance:
expr_stmt = name.get_definition()
if expr_stmt is not None and expr_stmt.type == 'expr_stmt':
annassign = expr_stmt.children[1]
@@ -122,9 +122,9 @@ class ClassFilter(ParserTreeFilter):
return not name.value.startswith('__') or name.value.endswith('__') \
or self._equals_origin_scope()
def _filter(self, names, from_instance=False):
def _filter(self, names):
names = super(ClassFilter, self)._filter(names)
return [name for name in names if self._access_possible(name, from_instance)]
return [name for name in names if self._access_possible(name)]
class ClassMixin(object):
@@ -270,6 +270,36 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase
self.inference_state.builtins_module.py__getattribute__('object')
)]
@inference_state_method_cache(default=False)
def is_typeddict(self):
# TODO Do a proper mro resolution. Currently we are just listing
# classes. However, it's a complicated algorithm.
from jedi.inference.gradual.typing import TypedDictBase
for lazy_cls in self.py__bases__():
if not isinstance(lazy_cls, LazyTreeValue):
return False
tree_node = lazy_cls.data
# Only resolve simple classes, stuff like Iterable[str] are more
# intensive to resolve and if generics are involved, we know it's
# not a TypedDict.
if not expr_is_dotted(tree_node):
return False
for cls in lazy_cls.infer():
if isinstance(cls, TypedDictBase):
return True
try:
method = cls.is_typeddict
except AttributeError:
# We're only dealing with simple classes, so just returning
# here should be fine. This only happens with e.g. compiled
# classes.
return False
else:
if method():
return True
return False
def py__getitem__(self, index_value_set, contextualized_node):
from jedi.inference.gradual.base import GenericClass
if not index_value_set:

View File

@@ -293,6 +293,25 @@ def cut_value_at_position(leaf, position):
return ''.join(lines)
def expr_is_dotted(node):
"""
Checks if a path looks like `name` or `name.foo.bar` and not `name()`.
"""
if node.type == 'atom':
if len(node.children) == 3 and node.children[0] == '(':
return expr_is_dotted(node.children[1])
return False
if node.type == 'atom_expr':
children = node.children
if children[0] == 'await':
return False
if not expr_is_dotted(children[0]):
return False
# Check trailers
return all(c.children[0] == '.' for c in children[1:])
return node.type == 'name'
def _function_is_x_method(method_name):
def wrapper(function_node):
"""

View File

@@ -499,3 +499,99 @@ def dynamic_annotation(x: int):
#? int()
dynamic_annotation('')
# -------------------------
# TypeDict
# -------------------------
# python >= 3.8
class Foo(typing.TypedDict):
foo: str
bar: typing.List[float]
an_int: int
#! ['foo: str']
foo
#? str()
foo
#? int()
an_int
def typed_dict_test_foo(arg: Foo):
a_string = arg['foo']
a_list_of_floats = arg['bar']
an_int = arg['an_int']
#? str()
a_string
#? list()
a_list_of_floats
#? float()
a_list_of_floats[0]
#? int()
an_int
#? ['isupper']
a_string.isuppe
#? ['pop']
a_list_of_floats.po
#? ['as_integer_ratio']
an_int.as_integer_rati
#! ['class Foo']
d: Foo
#? str()
d['foo']
#? float()
d['bar'][0]
#?
d['baz']
#?
d.foo
#?
d.bar
#! []
d.foo
#? []
Foo.set
#? ['setdefault']
d.setdefaul
#? 5 ["'foo"]
d['fo']
#? 5 ['"bar"']
d["bar"]
class Bar(Foo):
another_variable: int
#? int()
another_variable
#?
an_int
def typed_dict_test_foo(arg: Bar):
a_string = arg['foo']
a_list_of_floats = arg['bar']
an_int = arg['an_int']
another_variable = arg['another_variable']
#? str()
a_string
#? list()
a_list_of_floats
#? float()
a_list_of_floats[0]
#? int()
an_int
#? int()
another_variable
#? ['isupper']
a_string.isuppe
#? ['pop']
a_list_of_floats.po
#? ['as_integer_ratio']
an_int.as_integer_rati