Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Initial) support for pep0484 annotation type hints #661

Merged
merged 21 commits into from Dec 20, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fadf4f4
initial poc pep-0484 type hints
reinhrst Dec 13, 2015
5a8c46d
seperate parser and testing code
reinhrst Dec 13, 2015
c02668a
Build in version-dependency in integration tests
reinhrst Dec 13, 2015
68cbabe
pep0484 tests only on python >= 3.2
reinhrst Dec 13, 2015
7e8112d
pep0484 return type support
reinhrst Dec 13, 2015
c61f39c
add test for annotations to test_parser_tree
reinhrst Dec 13, 2015
f8debac
forward reference pep-0484
reinhrst Dec 13, 2015
7f8b878
if both docstring and annotations are present, use both for function …
reinhrst Dec 13, 2015
0f08dc6
Addinf myself to AUTHORS
reinhrst Dec 13, 2015
be399c8
clean out the last_* fields of sys before importing it.
reinhrst Dec 13, 2015
576fdf8
better separation pep0484 code and py__annotation__() function
reinhrst Dec 14, 2015
6ce076f
more elaborate tests
reinhrst Dec 14, 2015
0f6fb23
override annotation() in Lambda, instead of checking in Function on type
reinhrst Dec 14, 2015
626fa60
Revert "clean out the last_* fields of sys before importing it."
reinhrst Dec 14, 2015
3cef8b6
string-annotations should only be interpreted by the pep-0484 code, n…
reinhrst Dec 14, 2015
1258875
add test that jedi doesn't break in case of non-pep-0484 comments
reinhrst Dec 14, 2015
35fda38
test dynamic annotation and dynamic forward reference
reinhrst Dec 15, 2015
1e6397b
check 'assigned types'-support (comes out of the jedi-box), and add t…
reinhrst Dec 15, 2015
8bf2fe7
add some more non-pep0484-junk to the test
reinhrst Dec 17, 2015
6bee214
catch error in certain non-pep0484 annotations
reinhrst Dec 17, 2015
160b6fc
show off some power :)
reinhrst Dec 17, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.txt
Expand Up @@ -34,5 +34,6 @@ Phillip Berndt (@phillipberndt) <phillip.berndt@gmail.com>
Ian Lee (@IanLee1521) <IanLee1521@gmail.com>
Farkhad Khatamov (@hatamov) <comsgn@gmail.com>
Kevin Kelley (@kelleyk) <kelleyk@kelleyk.net>
Reinoud Elhorst (@reinhrst)

Note: (@user) means a github user name.
8 changes: 5 additions & 3 deletions jedi/evaluate/finder.py
Expand Up @@ -23,6 +23,7 @@
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
Expand Down Expand Up @@ -387,10 +388,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)
Expand Down
55 changes: 55 additions & 0 deletions 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)
16 changes: 16 additions & 0 deletions jedi/evaluate/representation.py
Expand Up @@ -49,6 +49,7 @@
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
Expand Down Expand Up @@ -580,6 +581,20 @@ def py__call__(self, params):
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')

Expand Down Expand Up @@ -639,6 +654,7 @@ def get_return_types(self, check_yields=False):
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)
Expand Down
19 changes: 16 additions & 3 deletions jedi/parser/tree.py
Expand Up @@ -866,7 +866,10 @@ def is_generator(self):

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

Expand Down Expand Up @@ -945,6 +948,10 @@ def params(self):
def is_generator(self):
return False

def annotation(self):
# lambda functions do not support annotations
return None

@property
def yields(self):
return []
Expand Down Expand Up @@ -1403,8 +1410,14 @@ def default(self):
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):
"""
Expand Down
161 changes: 161 additions & 0 deletions 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
17 changes: 14 additions & 3 deletions test/run.py
Expand Up @@ -111,6 +111,7 @@
"""
import os
import re
import sys
from ast import literal_eval
from io import StringIO
from functools import reduce
Expand All @@ -127,15 +128,15 @@

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
self.column = column
self.start = start
self.line = line
self.path = path
self.skip = None
self.skip = skip

@property
def module_name(self):
Expand Down Expand Up @@ -234,10 +235,11 @@ def run_usages(self, compare_cb):

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)
Expand All @@ -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
Expand Down