Merge branch 'linter' of https://github.com/reinhrst/jedi into pep484

Conflicts:
	AUTHORS.txt
This commit is contained in:
Dave Halter
2015-12-17 23:46:20 +01:00
9 changed files with 285 additions and 13 deletions

View File

@@ -35,5 +35,6 @@ Ian Lee (@IanLee1521) <IanLee1521@gmail.com>
Farkhad Khatamov (@hatamov) <comsgn@gmail.com> Farkhad Khatamov (@hatamov) <comsgn@gmail.com>
Kevin Kelley (@kelleyk) <kelleyk@kelleyk.net> Kevin Kelley (@kelleyk) <kelleyk@kelleyk.net>
Sid Shanker (@squidarth) <sid.p.shanker@gmail.com> Sid Shanker (@squidarth) <sid.p.shanker@gmail.com>
Reinoud Elhorst (@reinhrst)
Note: (@user) means a github user name. Note: (@user) means a github user name.

View File

@@ -23,6 +23,7 @@ from jedi.evaluate import representation as er
from jedi.evaluate import dynamic from jedi.evaluate import dynamic
from jedi.evaluate import compiled from jedi.evaluate import compiled
from jedi.evaluate import docstrings from jedi.evaluate import docstrings
from jedi.evaluate import pep0484
from jedi.evaluate import iterable from jedi.evaluate import iterable
from jedi.evaluate import imports from jedi.evaluate import imports
from jedi.evaluate import analysis 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__': and func.instance.is_generated and str(func.name) == '__init__':
param = func.var.params[param.position_nr] 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) doc_params = docstrings.follow_param(evaluator, param)
if doc_params: if pep0484_hints or doc_params:
return doc_params return list(set(pep0484_hints) | set(doc_params))
if isinstance(param, ExecutedParam): if isinstance(param, ExecutedParam):
return res_new | param.eval(evaluator) return res_new | param.eval(evaluator)

55
jedi/evaluate/pep0484.py Normal file
View File

@@ -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)

View File

@@ -49,6 +49,7 @@ from jedi.evaluate import compiled
from jedi.evaluate import recursion from jedi.evaluate import recursion
from jedi.evaluate import iterable from jedi.evaluate import iterable
from jedi.evaluate import docstrings from jedi.evaluate import docstrings
from jedi.evaluate import pep0484
from jedi.evaluate import helpers from jedi.evaluate import helpers
from jedi.evaluate import param from jedi.evaluate import param
from jedi.evaluate import flow_analysis from jedi.evaluate import flow_analysis
@@ -583,6 +584,20 @@ class Function(use_metaclass(CachedMetaClass, Wrapper)):
else: else:
return FunctionExecution(self._evaluator, self, params).get_return_types() 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): def py__class__(self):
return compiled.get_special_object(self._evaluator, 'FUNCTION_CLASS') return compiled.get_special_object(self._evaluator, 'FUNCTION_CLASS')
@@ -642,6 +657,7 @@ class FunctionExecution(Executed):
else: else:
returns = self.returns returns = self.returns
types = set(docstrings.find_return_types(self._evaluator, func)) types = set(docstrings.find_return_types(self._evaluator, func))
types |= set(pep0484.find_return_types(self._evaluator, func))
for r in returns: for r in returns:
check = flow_analysis.break_check(self._evaluator, self, r) check = flow_analysis.break_check(self._evaluator, self, r)

View File

@@ -873,7 +873,10 @@ class Function(ClassOrFunc):
def annotation(self): def annotation(self):
try: 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: except IndexError:
return None return None
@@ -952,6 +955,10 @@ class Lambda(Function):
def is_generator(self): def is_generator(self):
return False return False
def annotation(self):
# lambda functions do not support annotations
return None
@property @property
def yields(self): def yields(self):
return [] return []
@@ -1404,8 +1411,14 @@ class Param(BaseNode):
return None return None
def annotation(self): def annotation(self):
# Generate from tfpdef. tfpdef = self._tfpdef()
raise NotImplementedError 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): def _tfpdef(self):
""" """

161
test/completion/pep0484.py Normal file
View File

@@ -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

View File

@@ -111,6 +111,7 @@ Tests look like this::
""" """
import os import os
import re import re
import sys
from ast import literal_eval from ast import literal_eval
from io import StringIO from io import StringIO
from functools import reduce from functools import reduce
@@ -127,7 +128,7 @@ TEST_USAGES = 3
class IntegrationTestCase(object): class IntegrationTestCase(object):
def __init__(self, test_type, correct, line_nr, column, start, line, def __init__(self, test_type, correct, line_nr, column, start, line,
path=None): path=None, skip=None):
self.test_type = test_type self.test_type = test_type
self.correct = correct self.correct = correct
self.line_nr = line_nr self.line_nr = line_nr
@@ -135,7 +136,7 @@ class IntegrationTestCase(object):
self.start = start self.start = start
self.line = line self.line = line
self.path = path self.path = path
self.skip = None self.skip = skip
@property @property
def module_name(self): def module_name(self):
@@ -234,10 +235,11 @@ class IntegrationTestCase(object):
def collect_file_tests(lines, lines_to_execute): def collect_file_tests(lines, lines_to_execute):
makecase = lambda t: IntegrationTestCase(t, correct, line_nr, column, makecase = lambda t: IntegrationTestCase(t, correct, line_nr, column,
start, line) start, line, path=None, skip=skip)
start = None start = None
correct = None correct = None
test_type = None test_type = None
skip = None
for line_nr, line in enumerate(lines, 1): for line_nr, line in enumerate(lines, 1):
if correct is not None: if correct is not None:
r = re.match('^(\d+)\s*(.*)$', correct) r = re.match('^(\d+)\s*(.*)$', correct)
@@ -257,6 +259,15 @@ def collect_file_tests(lines, lines_to_execute):
yield makecase(TEST_DEFINITIONS) yield makecase(TEST_DEFINITIONS)
correct = None correct = None
else: 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: try:
r = re.search(r'(?:^|(?<=\s))#([?!<])\s*([^\n]*)', line) r = re.search(r'(?:^|(?<=\s))#([?!<])\s*([^\n]*)', line)
# test_type is ? for completion and ! for goto_assignments # test_type is ? for completion and ! for goto_assignments

View File

@@ -8,8 +8,8 @@ import pytest
def test_simple_annotations(): def test_simple_annotations():
""" """
Annotations only exist in Python 3. Annotations only exist in Python 3.
At the moment we ignore them. So they should be parsed and not interfere If annotations adhere to PEP-0484, we use them (they override inference),
with anything. else they are parsed but ignored
""" """
source = dedent("""\ source = dedent("""\
@@ -27,3 +27,11 @@ def test_simple_annotations():
annot_ret('')""") annot_ret('')""")
assert [d.name for d in jedi.Script(source, ).goto_definitions()] == ['str'] 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']

View File

@@ -12,10 +12,11 @@ from jedi.parser import tree as pt
class TestsFunctionAndLambdaParsing(object): class TestsFunctionAndLambdaParsing(object):
FIXTURES = [ 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', 'name': 'my_function',
'call_sig': 'my_function(x, y, z)', 'call_sig': 'my_function(x, y, z)',
'params': ['x', 'y', 'z'], 'params': ['x', 'y', 'z'],
'annotation': "str",
}), }),
('lambda x, y, z: x + y * z\n', { ('lambda x, y, z: x + y * z\n', {
'name': '<lambda>', 'name': '<lambda>',
@@ -55,7 +56,11 @@ class TestsFunctionAndLambdaParsing(object):
assert not node.yields assert not node.yields
def test_annotation(self, node, expected): 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): def test_get_call_signature(self, node, expected):
assert node.get_call_signature() == expected['call_sig'] assert node.get_call_signature() == expected['call_sig']