Create the basics to work with TypedDict in the future

This commit is contained in:
Dave Halter
2020-01-26 19:25:23 +01:00
parent 18f84d3af7
commit 8eb980db73
6 changed files with 156 additions and 3 deletions

View File

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

View File

@@ -81,7 +81,8 @@ class TypingModuleName(NameWrapper):
elif name == 'TypedDict': elif name == 'TypedDict':
# TODO doesn't even exist in typeshed/typing.py, yet. But will be # TODO doesn't even exist in typeshed/typing.py, yet. But will be
# added soon. # 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'): elif name in ('no_type_check', 'no_type_check_decorator'):
# This is not necessary, as long as we are not doing type checking. # 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 for c in self._wrapped_name.infer(): # Fuck my life Python 2
@@ -339,3 +340,62 @@ class CastFunction(BaseTypingValue):
@repack_with_argument_clinic('type, object, /') @repack_with_argument_clinic('type, object, /')
def py__call__(self, type_value_set, object_value_set): def py__call__(self, type_value_set, object_value_set):
return type_value_set.execute_annotation() 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):
return ValueSet({self.inference_state.builtins_module})
def get_key_values(self):
from jedi.inference.compiled import create_simple_object
return ValueSet({create_simple_object(self.inference_state, 'baz')})
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.cache import inference_state_method_cache
from jedi.inference.gradual.stub_value import VersionInfo from jedi.inference.gradual.stub_value import VersionInfo
from jedi.inference.gradual import annotation from jedi.inference.gradual import annotation
from jedi.inference.gradual.typing import TypedDictClass
from jedi.inference.names import TreeNameDefinition from jedi.inference.names import TreeNameDefinition
from jedi.inference.context import CompForContext from jedi.inference.context import CompForContext
from jedi.inference.value.decorator import Decoratee from jedi.inference.value.decorator import Decoratee
@@ -748,6 +749,8 @@ def _apply_decorators(context, node):
parent_context=context, parent_context=context,
tree_node=node tree_node=node
) )
if decoratee_value.is_typeddict():
decoratee_value = TypedDictClass(decoratee_value)
else: else:
decoratee_value = FunctionValue.from_context(context, node) decoratee_value = FunctionValue.from_context(context, node)
initial = values = ValueSet([decoratee_value]) initial = values = ValueSet([decoratee_value])

View File

@@ -38,11 +38,11 @@ py__doc__() Returns the docstring for a value.
""" """
from jedi import debug from jedi import debug
from jedi._compatibility import use_metaclass 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, \ from jedi.inference.cache import inference_state_method_cache, CachedMetaClass, \
inference_state_method_generator_cache inference_state_method_generator_cache
from jedi.inference import compiled 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.filters import ParserTreeFilter
from jedi.inference.names import TreeNameDefinition, ValueName from jedi.inference.names import TreeNameDefinition, ValueName
from jedi.inference.arguments import unpack_arglist, ValuesArguments from jedi.inference.arguments import unpack_arglist, ValuesArguments
@@ -265,6 +265,35 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase
self.inference_state.builtins_module.py__getattribute__('object') self.inference_state.builtins_module.py__getattribute__('object')
)] )]
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): def py__getitem__(self, index_value_set, contextualized_node):
from jedi.inference.gradual.base import GenericClass from jedi.inference.gradual.base import GenericClass
if not index_value_set: if not index_value_set:

View File

@@ -293,6 +293,25 @@ def cut_value_at_position(leaf, position):
return ''.join(lines) 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 _function_is_x_method(method_name):
def wrapper(function_node): def wrapper(function_node):
""" """

View File

@@ -493,3 +493,44 @@ def dynamic_annotation(x: int):
#? int() #? int()
dynamic_annotation('') dynamic_annotation('')
# -------------------------
# TypeDict
# -------------------------
# python >= 3.8
class Foo(typing.TypedDict):
foo: str
bar: List[int]
foo
#! ['foo: str']
foo
#? str()
foo
#! ['class Foo']
d: Foo
#? str()
d['foo']
#? str()
d['bar'][0]
#?
d['baz']
#?
d.foo
#?
d.bar
#! []
d.foo
#? []
Foo.set
#? ['setdefault']
d.setdefaul
#? 5 ["'foo'"]
d['fo']
#? 5 ['"bar"']
d["bar"]