diff --git a/jedi/evaluate/iterable.py b/jedi/evaluate/iterable.py index b11b95a1..5b0871fd 100644 --- a/jedi/evaluate/iterable.py +++ b/jedi/evaluate/iterable.py @@ -29,6 +29,7 @@ from jedi.evaluate import compiled from jedi.evaluate import helpers from jedi.evaluate.cache import CachedMetaClass, memoize_default from jedi.evaluate import analysis +from jedi.evaluate import pep0484 class IterableWrapper(tree.Base): @@ -560,12 +561,23 @@ def py__iter__types(evaluator, types, node=None): def py__getitem__(evaluator, types, index, node): + from jedi.evaluate.representation import Class result = set() # Index handling. if isinstance(index, (compiled.CompiledObject, Slice)): index = index.obj + # special case: PEP0484 typing module, see + # https://github.com/davidhalter/jedi/issues/663 + for typ in list(types): + if isinstance(typ, Class): + replacementclass = \ + pep0484.get_typing_replacement_class(evaluator, typ) + if replacementclass: + types.remove(typ) + result |= replacementclass.py__getitem__(index) + if type(index) not in (float, int, str, unicode, slice): # If the index is not clearly defined, we have to get all the # possiblities. @@ -588,7 +600,7 @@ def py__getitem__(evaluator, types, index, node): except IndexError: result |= py__iter__types(evaluator, set([typ])) except KeyError: - # Must be a dict. Lists don't raise IndexErrors. + # Must be a dict. Lists don't raise KeyErrors. result |= typ.dict_values() return result diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index 4eec2fed..761886a4 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -20,9 +20,10 @@ x support for type hint comments `# type: (int, str) -> int`. See comment from from itertools import chain -from jedi.parser import Parser, load_grammar, ParseError +from jedi.parser import Parser, load_grammar, ParseError, tree, ParserWithRecovery from jedi.evaluate.cache import memoize_default -from jedi.evaluate.compiled import CompiledObject +from jedi.evaluate import compiled +from textwrap import dedent from jedi import debug @@ -30,7 +31,7 @@ def _evaluate_for_annotation(evaluator, annotation): if annotation is not None: definitions = set() for definition in evaluator.eval_element(annotation): - if (isinstance(definition, CompiledObject) and + if (isinstance(definition, compiled.CompiledObject) and isinstance(definition.obj, str)): try: p = Parser(load_grammar(), definition.obj, start='eval_input') @@ -60,3 +61,40 @@ def follow_param(evaluator, param): def find_return_types(evaluator, func): annotation = func.py__annotations__().get("return", None) return _evaluate_for_annotation(evaluator, annotation) + + +# TODO: Memoize +def get_typing_replacement_module(): + """ + The idea is to return our jedi replacement for the PEP-0484 typing module + as discussed at https://github.com/davidhalter/jedi/issues/663 + """ + + code = dedent(""" + from collections import abc + + class MakeSequence: + def __getitem__(self, indextype): + class Sequence(abc.Sequence): + def __getitem__(self) -> indextype: + pass + return Sequence + """) + p = ParserWithRecovery(load_grammar(), code) + return p.module + + +def get_typing_replacement_class(evaluator, typ): + if not typ.base.get_parent_until(tree.Module).name.value == "typing": + return None + # we assume that any class using [] in a module called + # "typing" with a name for which we have a replacement + # should be replaced by that class. This is not 100% + # airtight but I don't have a better idea to check that it's + # actually the PEP-0484 typing module and not some other + typing = get_typing_replacement_module() + types = evaluator.find_types(typing, "Make" + typ.name.value) + if not types: + return None + else: + return list(types)[0] diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index c460efd2..35359dbd 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -469,6 +469,24 @@ class Class(use_metaclass(CachedMetaClass, Wrapper)): def py__class__(self): return compiled.create(self._evaluator, type) + def py__getitem__(self, index): + instances = self.py__call__(()) + assert len(instances) == 1 + instance = list(instances)[0] + try: + method = instance.get_subscope_by_name('__getitem__') + except KeyError: + debug.warning('No __getitem__, cannot access the array.') + return set() + else: + # it feels like this should be handled somewhere else, + # not sure where though + if isinstance(index, type(self)): + index_obj = index + else: + index_obj = compiled.create(self._evaluator, index) + return instance._evaluator.execute_evaluated(method, index_obj) + @property def params(self): return self.get_subscope_by_name('__init__').params diff --git a/test/completion/pep0484.py b/test/completion/pep0484.py index fc08460f..ba9044bc 100644 --- a/test/completion/pep0484.py +++ b/test/completion/pep0484.py @@ -157,3 +157,26 @@ Y = int def just_because_we_can(x: "flo" + "at"): #? float() x + +import typing +def we_can_has_sequence( + p: typing.Sequence[int], + q: typing.Sequence[B], + r: "typing.Sequence[int]", + s: typing.Sequence["int"]): + #? ["count"] + p.c + #? int() + p[1] + #? ["count"] + q.c + #? B() + q[1] + #? ["count"] + r.c + #? int() + r[1] + #? ["count"] + s.c + #? int() + s[1]