diff --git a/jedi/inference/base_value.py b/jedi/inference/base_value.py index 96f43395..3705d7bc 100644 --- a/jedi/inference/base_value.py +++ b/jedi/inference/base_value.py @@ -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 diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index 3b3cdf17..64e1e43c 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -81,7 +81,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 +340,62 @@ 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): + 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 diff --git a/jedi/inference/syntax_tree.py b/jedi/inference/syntax_tree.py index 6512fa76..51bc960a 100644 --- a/jedi/inference/syntax_tree.py +++ b/jedi/inference/syntax_tree.py @@ -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]) diff --git a/jedi/inference/value/klass.py b/jedi/inference/value/klass.py index b7d79646..743c17cf 100644 --- a/jedi/inference/value/klass.py +++ b/jedi/inference/value/klass.py @@ -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 @@ -265,6 +265,35 @@ class ClassValue(use_metaclass(CachedMetaClass, ClassMixin, FunctionAndClassBase 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): from jedi.inference.gradual.base import GenericClass if not index_value_set: diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 0f7ba429..7f69a80c 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -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): """ diff --git a/test/completion/pep0484_typing.py b/test/completion/pep0484_typing.py index 9104ca11..7a48aca9 100644 --- a/test/completion/pep0484_typing.py +++ b/test/completion/pep0484_typing.py @@ -493,3 +493,44 @@ def dynamic_annotation(x: int): #? int() 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"]