diff --git a/AUTHORS.txt b/AUTHORS.txt index 0777b930..f7817567 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -35,5 +35,6 @@ Ian Lee (@IanLee1521) Farkhad Khatamov (@hatamov) Kevin Kelley (@kelleyk) Sid Shanker (@squidarth) +Reinoud Elhorst (@reinhrst) Note: (@user) means a github user name. diff --git a/jedi/evaluate/finder.py b/jedi/evaluate/finder.py index 4cd92deb..3a96d403 100644 --- a/jedi/evaluate/finder.py +++ b/jedi/evaluate/finder.py @@ -23,6 +23,7 @@ from jedi.evaluate import representation as er from jedi.evaluate import dynamic from jedi.evaluate import compiled from jedi.evaluate import docstrings +from jedi.evaluate import pep0484 from jedi.evaluate import iterable from jedi.evaluate import imports from jedi.evaluate import analysis @@ -386,10 +387,11 @@ def _eval_param(evaluator, param, scope): and func.instance.is_generated and str(func.name) == '__init__': param = func.var.params[param.position_nr] - # Add docstring knowledge. + # Add pep0484 and docstring knowledge. + pep0484_hints = pep0484.follow_param(evaluator, param) doc_params = docstrings.follow_param(evaluator, param) - if doc_params: - return doc_params + if pep0484_hints or doc_params: + return list(set(pep0484_hints) | set(doc_params)) if isinstance(param, ExecutedParam): return res_new | param.eval(evaluator) diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py new file mode 100644 index 00000000..cb3e8a4c --- /dev/null +++ b/jedi/evaluate/pep0484.py @@ -0,0 +1,55 @@ +""" +PEP 0484 ( https://www.python.org/dev/peps/pep-0484/ ) describes type hints +through function annotations. There is a strong suggestion in this document +that only the type of type hinting defined in PEP0484 should be allowed +as annotations in future python versions. + +The (initial / probably incomplete) implementation todo list for pep-0484: +v Function parameter annotations with builtin/custom type classes +v Function returntype annotations with builtin/custom type classes +v Function parameter annotations with strings (forward reference) +v Function return type annotations with strings (forward reference) +x Local variable type hints +v Assigned types: `Url = str\ndef get(url:Url) -> str:` +x Type hints in `with` statements +x Stub files support +x support `@no_type_check` and `@no_type_check_decorator` +""" + +from itertools import chain +from jedi.parser import Parser, load_grammar +from jedi.evaluate.cache import memoize_default +from jedi.evaluate.compiled import CompiledObject + + +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 + isinstance(definition.obj, str)): + p = Parser(load_grammar(), definition.obj) + try: + element = p.module.children[0].children[0] + except (AttributeError, IndexError): + continue + element.parent = annotation.parent + definitions |= evaluator.eval_element(element) + else: + definitions.add(definition) + return list(chain.from_iterable( + evaluator.execute(d) for d in definitions)) + else: + return [] + + +@memoize_default(None, evaluator_is_first_arg=True) +def follow_param(evaluator, param): + annotation = param.annotation() + return _evaluate_for_annotation(evaluator, annotation) + + +@memoize_default(None, evaluator_is_first_arg=True) +def find_return_types(evaluator, func): + annotation = func.py__annotations__().get("return", None) + return _evaluate_for_annotation(evaluator, annotation) diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index 76fbf19a..ffad6555 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -49,6 +49,7 @@ from jedi.evaluate import compiled from jedi.evaluate import recursion from jedi.evaluate import iterable from jedi.evaluate import docstrings +from jedi.evaluate import pep0484 from jedi.evaluate import helpers from jedi.evaluate import param from jedi.evaluate import flow_analysis @@ -583,6 +584,20 @@ class Function(use_metaclass(CachedMetaClass, Wrapper)): else: return FunctionExecution(self._evaluator, self, params).get_return_types() + @memoize_default() + def py__annotations__(self): + parser_func = self.base + return_annotation = parser_func.annotation() + if return_annotation: + dct = {'return': return_annotation} + else: + dct = {} + for function_param in parser_func.params: + param_annotation = function_param.annotation() + if param_annotation is not None: + dct[function_param.name.value] = param_annotation + return dct + def py__class__(self): return compiled.get_special_object(self._evaluator, 'FUNCTION_CLASS') @@ -642,6 +657,7 @@ class FunctionExecution(Executed): else: returns = self.returns types = set(docstrings.find_return_types(self._evaluator, func)) + types |= set(pep0484.find_return_types(self._evaluator, func)) for r in returns: check = flow_analysis.break_check(self._evaluator, self, r) diff --git a/jedi/parser/tree.py b/jedi/parser/tree.py index 45b83256..d11f128f 100644 --- a/jedi/parser/tree.py +++ b/jedi/parser/tree.py @@ -873,7 +873,10 @@ class Function(ClassOrFunc): def annotation(self): try: - return self.children[6] # 6th element: def foo(...) -> bar + if self.children[3] == "->": + return self.children[4] + assert self.children[3] == ":" + return None except IndexError: return None @@ -952,6 +955,10 @@ class Lambda(Function): def is_generator(self): return False + def annotation(self): + # lambda functions do not support annotations + return None + @property def yields(self): return [] @@ -1404,8 +1411,14 @@ class Param(BaseNode): return None def annotation(self): - # Generate from tfpdef. - raise NotImplementedError + tfpdef = self._tfpdef() + if is_node(tfpdef, 'tfpdef'): + assert tfpdef.children[1] == ":" + assert len(tfpdef.children) == 3 + annotation = tfpdef.children[2] + return annotation + else: + return None def _tfpdef(self): """ diff --git a/test/completion/pep0484.py b/test/completion/pep0484.py new file mode 100644 index 00000000..a090b7fa --- /dev/null +++ b/test/completion/pep0484.py @@ -0,0 +1,161 @@ +""" Pep-0484 type hinting """ + +# python >= 3.2 + + +class A(): + pass + + +def function_parameters(a: A, b, c: str, d: int, e: str, f: str, g: int=4): + """ + :param e: if docstring and annotation agree, only one should be returned + :type e: str + :param f: if docstring and annotation disagree, both should be returned + :type f: int + """ + #? A() + a + #? + b + #? str() + c + #? int() + d + #? str() + e + #? int() str() + f + # int() + g + + +def return_unspecified(): + pass + +#? +return_unspecified() + + +def return_none() -> None: + """ + Return type None means the same as no return type as far as jedi + is concerned + """ + pass + +#? +return_none() + + +def return_str() -> str: + pass + +#? str() +return_str() + + +def return_custom_class() -> A: + pass + +#? A() +return_custom_class() + + +def return_annotation_and_docstring() -> str: + """ + :rtype: int + """ + pass + +#? str() int() +return_annotation_and_docstring() + + +def return_annotation_and_docstring_different() -> str: + """ + :rtype: str + """ + pass + +#? str() +return_annotation_and_docstring_different() + + +def annotation_forward_reference(b: "B") -> "B": + #? B() + b + +#? B() +annotation_forward_reference(1) +#? ["test_element"] +annotation_forward_reference(1).t + +class B: + test_element = 1 + pass + + +class SelfReference: + test_element = 1 + def test_method(self, x: "SelfReference") -> "SelfReference": + #? SelfReference() + x + #? ["test_element", "test_method"] + self.t + #? ["test_element", "test_method"] + x.t + #? ["test_element", "test_method"] + self.test_method(1).t + +#? SelfReference() +SelfReference().test_method() + +def function_with_non_pep_0484_annotation( + x: "I can put anything here", + xx: "", + yy: "\r\n\0;+*&^564835(---^&*34", + y: 3 + 3, + zz: float) -> int("42"): + # infers int from function call + #? int() + x + # infers int from function call + #? int() + xx + # infers int from function call + #? int() + yy + # infers str from function call + #? str() + y + #? float() + zz +#? +function_with_non_pep_0484_annotation(1, 2, 3, "force string") + +def function_forward_reference_dynamic( + x: return_str_type(), + y: "return_str_type()") -> None: + # technically should not be resolvable since out of scope, + # but jedi is not smart enough for that + #? str() + x + #? str() + y + +def return_str_type(): + return str + + +X = str +def function_with_assined_class_in_reference(x: X, y: "Y"): + #? str() + x + #? int() + y +Y = int + +def just_because_we_can(x: "flo" + "at"): + #? float() + x diff --git a/test/run.py b/test/run.py index b1725917..17a0405a 100755 --- a/test/run.py +++ b/test/run.py @@ -111,6 +111,7 @@ Tests look like this:: """ import os import re +import sys from ast import literal_eval from io import StringIO from functools import reduce @@ -127,7 +128,7 @@ TEST_USAGES = 3 class IntegrationTestCase(object): def __init__(self, test_type, correct, line_nr, column, start, line, - path=None): + path=None, skip=None): self.test_type = test_type self.correct = correct self.line_nr = line_nr @@ -135,7 +136,7 @@ class IntegrationTestCase(object): self.start = start self.line = line self.path = path - self.skip = None + self.skip = skip @property def module_name(self): @@ -234,10 +235,11 @@ class IntegrationTestCase(object): def collect_file_tests(lines, lines_to_execute): makecase = lambda t: IntegrationTestCase(t, correct, line_nr, column, - start, line) + start, line, path=None, skip=skip) start = None correct = None test_type = None + skip = None for line_nr, line in enumerate(lines, 1): if correct is not None: r = re.match('^(\d+)\s*(.*)$', correct) @@ -257,6 +259,15 @@ def collect_file_tests(lines, lines_to_execute): yield makecase(TEST_DEFINITIONS) correct = None else: + # check for python minimal version number + match = re.match(r" *# *python *>= *(\d+(?:\.\d+)?)$", line) + if match: + minimal_python_version = tuple( + map(int, match.group(1).split("."))) + if sys.version_info >= minimal_python_version: + skip = None + else: + skip = "Minimal python version %s" % match.groups(1) try: r = re.search(r'(?:^|(?<=\s))#([?!<])\s*([^\n]*)', line) # test_type is ? for completion and ! for goto_assignments diff --git a/test/test_evaluate/test_annotations.py b/test/test_evaluate/test_annotations.py index 1fefde3c..1a26a1bc 100644 --- a/test/test_evaluate/test_annotations.py +++ b/test/test_evaluate/test_annotations.py @@ -8,8 +8,8 @@ import pytest def test_simple_annotations(): """ Annotations only exist in Python 3. - At the moment we ignore them. So they should be parsed and not interfere - with anything. + If annotations adhere to PEP-0484, we use them (they override inference), + else they are parsed but ignored """ source = dedent("""\ @@ -27,3 +27,11 @@ def test_simple_annotations(): annot_ret('')""") assert [d.name for d in jedi.Script(source, ).goto_definitions()] == ['str'] + + source = dedent("""\ + def annot(a:int): + return a + + annot('')""") + + assert [d.name for d in jedi.Script(source, ).goto_definitions()] == ['int'] diff --git a/test/test_parser/test_parser_tree.py b/test/test_parser/test_parser_tree.py index 480230ba..28c7d271 100644 --- a/test/test_parser/test_parser_tree.py +++ b/test/test_parser/test_parser_tree.py @@ -12,10 +12,11 @@ from jedi.parser import tree as pt class TestsFunctionAndLambdaParsing(object): FIXTURES = [ - ('def my_function(x, y, z):\n return x + y * z\n', { + ('def my_function(x, y, z) -> str:\n return x + y * z\n', { 'name': 'my_function', 'call_sig': 'my_function(x, y, z)', 'params': ['x', 'y', 'z'], + 'annotation': "str", }), ('lambda x, y, z: x + y * z\n', { 'name': '', @@ -55,7 +56,11 @@ class TestsFunctionAndLambdaParsing(object): assert not node.yields def test_annotation(self, node, expected): - assert node.annotation() is expected.get('annotation', None) + expected_annotation = expected.get('annotation', None) + if expected_annotation is None: + assert node.annotation() is None + else: + assert node.annotation().value == expected_annotation def test_get_call_signature(self, node, expected): assert node.get_call_signature() == expected['call_sig']