mirror of
https://github.com/davidhalter/jedi.git
synced 2025-12-06 14:04:26 +08:00
Merge branch 'linter' of https://github.com/reinhrst/jedi into pep484
Conflicts: AUTHORS.txt
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
55
jedi/evaluate/pep0484.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
161
test/completion/pep0484.py
Normal 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
|
||||||
17
test/run.py
17
test/run.py
@@ -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
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user