Merge branch 'pytest', fixes parts of #791

This commit is contained in:
Dave Halter
2019-12-27 14:13:46 +01:00
18 changed files with 410 additions and 26 deletions
+2
View File
@@ -21,6 +21,8 @@ Changelog
- ``usages`` deprecated, use ``find_references`` instead - ``usages`` deprecated, use ``find_references`` instead
- ``jedi.names`` deprecated, use ``jedi.Script(...).names()`` - ``jedi.names`` deprecated, use ``jedi.Script(...).names()``
- ``BaseDefinition.goto_assignments`` renamed to ``BaseDefinition.goto`` - ``BaseDefinition.goto_assignments`` renamed to ``BaseDefinition.goto``
- Python 2 support deprecated. For this release it is best effort. Python 2 has
reached the end of its life and now it's just about a smooth transition.
0.15.2 (2019-12-20) 0.15.2 (2019-12-20)
+++++++++++++++++++ +++++++++++++++++++
+45
View File
@@ -18,6 +18,7 @@ from jedi.inference.context import get_global_filters
from jedi.inference.value import TreeInstance from jedi.inference.value import TreeInstance
from jedi.inference.gradual.conversion import convert_values from jedi.inference.gradual.conversion import convert_values
from jedi.parser_utils import cut_value_at_position from jedi.parser_utils import cut_value_at_position
from jedi.plugins import plugin_manager
def get_signature_param_names(signatures): def get_signature_param_names(signatures):
@@ -74,6 +75,13 @@ def get_flow_scope_node(module_node, position):
return node return node
@plugin_manager.decorate()
def complete_param_names(context, function_name, decorator_nodes):
# Basically there's no way to do param completion. The plugins are
# responsible for this.
return []
class Completion: class Completion:
def __init__(self, inference_state, module_context, code_lines, position, def __init__(self, inference_state, module_context, code_lines, position,
signatures_callback, fuzzy=False): signatures_callback, fuzzy=False):
@@ -216,6 +224,8 @@ class Completion:
elif nonterminals[-1] in ('trailer', 'dotted_name') and nodes[-1] == '.': elif nonterminals[-1] in ('trailer', 'dotted_name') and nodes[-1] == '.':
dot = self._module_node.get_leaf_for_position(self._position) dot = self._module_node.get_leaf_for_position(self._position)
completion_names += self._complete_trailer(dot.get_previous_leaf()) completion_names += self._complete_trailer(dot.get_previous_leaf())
elif self._is_parameter_completion():
completion_names += self._complete_params(leaf)
else: else:
completion_names += self._complete_global_scope() completion_names += self._complete_global_scope()
completion_names += self._complete_inherited(is_function=False) completion_names += self._complete_inherited(is_function=False)
@@ -234,6 +244,41 @@ class Completion:
return completion_names return completion_names
def _is_parameter_completion(self):
tos = self.stack[-1]
if tos.nonterminal == 'lambdef' and len(tos.nodes) == 1:
# We are at the position `lambda `, where basically the next node
# is a param.
return True
if tos.nonterminal in 'parameters':
# Basically we are at the position `foo(`, there's nothing there
# yet, so we have no `typedargslist`.
return True
# var args is for lambdas and typed args for normal functions
return tos.nonterminal in ('typedargslist', 'varargslist') and tos.nodes[-1] == ','
def _complete_params(self, leaf):
stack_node = self.stack[-2]
if stack_node.nonterminal == 'parameters':
stack_node = self.stack[-3]
if stack_node.nonterminal == 'funcdef':
context = get_user_context(self._module_context, self._position)
node = search_ancestor(leaf, 'error_node', 'funcdef')
if node.type == 'error_node':
n = node.children[0]
if n.type == 'decorators':
decorators = n.children
elif n.type == 'decorator':
decorators = [n]
else:
decorators = []
else:
decorators = node.get_decorators()
function_name = stack_node.nodes[1]
return complete_param_names(context, function_name.value, decorators)
return []
def _complete_keywords(self, allowed_transitions): def _complete_keywords(self, allowed_transitions):
for k in allowed_transitions: for k in allowed_transitions:
if isinstance(k, str) and k.isalpha(): if isinstance(k, str) and k.isalpha():
+9
View File
@@ -13,6 +13,12 @@ class AbstractFolderIO(object):
def get_file_io(self, name): def get_file_io(self, name):
raise NotImplementedError raise NotImplementedError
def get_parent_folder(self):
raise NotImplementedError
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.path)
class FolderIO(AbstractFolderIO): class FolderIO(AbstractFolderIO):
def list(self): def list(self):
@@ -21,6 +27,9 @@ class FolderIO(AbstractFolderIO):
def get_file_io(self, name): def get_file_io(self, name):
return FileIO(os.path.join(self.path, name)) return FileIO(os.path.join(self.path, name))
def get_parent_folder(self):
return FolderIO(os.path.dirname(self.path))
class FileIOFolderMixin(object): class FileIOFolderMixin(object):
def get_parent_folder(self): def get_parent_folder(self):
+2
View File
@@ -169,6 +169,8 @@ class InferenceState(object):
return imports.infer_import(context, name) return imports.infer_import(context, name)
if type_ == 'with_stmt': if type_ == 'with_stmt':
return tree_name_to_values(self, context, name) return tree_name_to_values(self, context, name)
elif type_ == 'param':
return context.py__getattribute__(name.value, position=name.end_pos)
else: else:
result = follow_error_node_imports_if_possible(context, name) result = follow_error_node_imports_if_possible(context, name)
if result is not None: if result is not None:
+5 -11
View File
@@ -246,24 +246,18 @@ class MergedFilter(object):
return '%s(%s)' % (self.__class__.__name__, ', '.join(str(f) for f in self._filters)) return '%s(%s)' % (self.__class__.__name__, ', '.join(str(f) for f in self._filters))
class _BuiltinMappedMethod(Value): class _BuiltinMappedMethod(ValueWrapper):
"""``Generator.__next__`` ``dict.values`` methods and so on.""" """``Generator.__next__`` ``dict.values`` methods and so on."""
api_type = u'function' api_type = u'function'
def __init__(self, builtin_value, method, builtin_func): def __init__(self, value, method, builtin_func):
super(_BuiltinMappedMethod, self).__init__( super(_BuiltinMappedMethod, self).__init__(builtin_func)
builtin_value.inference_state, self._value = value
parent_context=builtin_value
)
self._method = method self._method = method
self._builtin_func = builtin_func
def py__call__(self, arguments): def py__call__(self, arguments):
# TODO add TypeError if params are given/or not correct. # TODO add TypeError if params are given/or not correct.
return self._method(self.parent_context) return self._method(self._value)
def __getattr__(self, name):
return getattr(self._builtin_func, name)
class SpecialMethodFilter(DictFilter): class SpecialMethodFilter(DictFilter):
+5
View File
@@ -322,6 +322,11 @@ class NewType(Value):
def py__call__(self, arguments): def py__call__(self, arguments):
return self._type_value_set.execute_annotation() return self._type_value_set.execute_annotation()
@property
def name(self):
from jedi.inference.compiled.value import CompiledValueName
return CompiledValueName(self, 'NewType')
class CastFunction(BaseTypingValue): class CastFunction(BaseTypingValue):
@repack_with_argument_clinic('type, object, /') @repack_with_argument_clinic('type, object, /')
+2 -2
View File
@@ -505,7 +505,7 @@ def _load_builtin_module(inference_state, import_names=None, sys_path=None):
return module return module
def _load_module_from_path(inference_state, file_io, base_names): def load_module_from_path(inference_state, file_io, base_names=None):
""" """
This should pretty much only be used for get_modules_containing_name. It's This should pretty much only be used for get_modules_containing_name. It's
here to ensure that a random path is still properly loaded into the Jedi here to ensure that a random path is still properly loaded into the Jedi
@@ -552,7 +552,7 @@ def get_module_contexts_containing_name(inference_state, module_contexts, name):
if name not in code: if name not in code:
return None return None
new_file_io = KnownContentFileIO(file_io.path, code) new_file_io = KnownContentFileIO(file_io.path, code)
m = _load_module_from_path(inference_state, new_file_io, base_names) m = load_module_from_path(inference_state, new_file_io, base_names)
if isinstance(m, compiled.CompiledObject): if isinstance(m, compiled.CompiledObject):
return None return None
return m.as_context() return m.as_context()
+6
View File
@@ -9,6 +9,7 @@ from jedi.inference.base_value import ValueSet, NO_VALUES
from jedi.inference import docstrings from jedi.inference import docstrings
from jedi.cache import memoize_method from jedi.cache import memoize_method
from jedi.inference.helpers import deep_ast_copy, infer_call_of_leaf from jedi.inference.helpers import deep_ast_copy, infer_call_of_leaf
from jedi.plugins import plugin_manager
def _merge_name_docs(names): def _merge_name_docs(names):
@@ -482,6 +483,11 @@ class _ActualTreeParamName(BaseTreeParamName):
class AnonymousParamName(_ActualTreeParamName): class AnonymousParamName(_ActualTreeParamName):
@plugin_manager.decorate(name='goto_anonymous_param')
def goto(self):
return super(AnonymousParamName, self).goto()
@plugin_manager.decorate(name='infer_anonymous_param')
def infer(self): def infer(self):
values = super(AnonymousParamName, self).infer() values = super(AnonymousParamName, self).infer()
if values: if values:
+5 -3
View File
@@ -277,17 +277,19 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin):
for lazy_value in self.get_yield_lazy_values() for lazy_value in self.get_yield_lazy_values()
) )
def is_generator(self):
return bool(get_yield_exprs(self.inference_state, self.tree_node))
def infer(self): def infer(self):
""" """
Created to be used by inheritance. Created to be used by inheritance.
""" """
inference_state = self.inference_state inference_state = self.inference_state
is_coroutine = self.tree_node.parent.type in ('async_stmt', 'async_funcdef') is_coroutine = self.tree_node.parent.type in ('async_stmt', 'async_funcdef')
is_generator = bool(get_yield_exprs(inference_state, self.tree_node))
from jedi.inference.gradual.base import GenericClass from jedi.inference.gradual.base import GenericClass
if is_coroutine: if is_coroutine:
if is_generator: if self.is_generator():
if inference_state.environment.version_info < (3, 6): if inference_state.environment.version_info < (3, 6):
return NO_VALUES return NO_VALUES
async_generator_classes = inference_state.typing_module \ async_generator_classes = inference_state.typing_module \
@@ -312,7 +314,7 @@ class BaseFunctionExecutionContext(ValueContext, TreeContextMixin):
GenericClass(c, TupleGenericManager(generics)) for c in async_classes GenericClass(c, TupleGenericManager(generics)) for c in async_classes
).execute_annotation() ).execute_annotation()
else: else:
if is_generator: if self.is_generator():
return ValueSet([iterable.Generator(inference_state, self)]) return ValueSet([iterable.Generator(inference_state, self)])
else: else:
return self.get_return_values() return self.get_return_values()
+6 -6
View File
@@ -14,18 +14,18 @@ class _PluginManager(object):
self._registered_plugins.extend(plugins) self._registered_plugins.extend(plugins)
self._build_functions() self._build_functions()
def decorate(self): def decorate(self, name=None):
def decorator(callback): def decorator(callback):
@wraps(callback) @wraps(callback)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return built_functions[name](*args, **kwargs) return built_functions[public_name](*args, **kwargs)
name = callback.__name__ public_name = name or callback.__name__
assert name not in self._built_functions assert public_name not in self._built_functions
built_functions = self._built_functions built_functions = self._built_functions
built_functions[name] = callback built_functions[public_name] = callback
self._cached_base_callbacks[name] = callback self._cached_base_callbacks[public_name] = callback
return wrapper return wrapper
+133
View File
@@ -0,0 +1,133 @@
from parso.python.tree import search_ancestor
from jedi._compatibility import FileNotFoundError
from jedi.inference.cache import inference_state_method_cache
from jedi.inference.imports import load_module_from_path
from jedi.inference.filters import ParserTreeFilter
from jedi.inference.base_value import NO_VALUES, ValueSet
def execute(callback):
def wrapper(value, arguments):
# This might not be necessary anymore in pytest 4/5, definitely needed
# for pytest 3.
if value.py__name__() == 'fixture' \
and value.parent_context.py__name__() == '_pytest.fixtures':
return NO_VALUES
return callback(value, arguments)
return wrapper
def infer_anonymous_param(func):
def get_returns(value):
if value.tree_node.annotation is not None:
return value.execute_with_values()
# In pytest we need to differentiate between generators and normal
# returns.
# Parameters still need to be anonymous, .as_context() ensures that.
function_context = value.as_context()
if function_context.is_generator():
return function_context.merge_yield_values()
else:
return function_context.get_return_values()
def wrapper(param_name):
if _is_a_pytest_param(param_name):
module = param_name.get_root_context()
fixtures = _goto_pytest_fixture(module, param_name.string_name)
if fixtures:
return ValueSet.from_sets(
get_returns(value)
for fixture in fixtures
for value in fixture.infer()
)
return func(param_name)
return wrapper
def goto_anonymous_param(func):
def wrapper(param_name):
if _is_a_pytest_param(param_name):
names = _goto_pytest_fixture(param_name.get_root_context(), param_name.string_name)
if names:
return names
return func(param_name)
return wrapper
def complete_param_names(func):
def wrapper(context, func_name, decorator_nodes):
module_context = context.get_root_context()
if _is_pytest_func(func_name, decorator_nodes):
names = []
for module_context in _iter_pytest_modules(module_context):
names += FixtureFilter(module_context).values()
if names:
return names
return func(context, func_name, decorator_nodes)
return wrapper
def _goto_pytest_fixture(module_context, name):
for module_context in _iter_pytest_modules(module_context):
names = FixtureFilter(module_context).get(name)
if names:
return names
def _is_a_pytest_param(param_name):
"""
Pytest params are either in a `test_*` function or have a pytest fixture
with the decorator @pytest.fixture.
This is a heuristic and will work in most cases.
"""
funcdef = search_ancestor(param_name.tree_name, 'funcdef')
if funcdef is None: # A lambda
return False
decorators = funcdef.get_decorators()
return _is_pytest_func(funcdef.name.value, decorators)
def _is_pytest_func(func_name, decorator_nodes):
return func_name.startswith('test_') \
or any('fixture' in n.get_code() for n in decorator_nodes)
@inference_state_method_cache()
def _iter_pytest_modules(module_context):
yield module_context
folder = module_context.get_value().file_io.get_parent_folder()
sys_path = module_context.inference_state.get_sys_path()
while any(folder.path.startswith(p) for p in sys_path):
file_io = folder.get_file_io('conftest.py')
try:
m = load_module_from_path(module_context.inference_state, file_io)
yield m.as_context()
except FileNotFoundError:
pass
folder = folder.get_parent_folder()
class FixtureFilter(ParserTreeFilter):
def _filter(self, names):
for name in super(FixtureFilter, self)._filter(names):
funcdef = name.parent
if funcdef.type == 'funcdef':
# Class fixtures are not supported
decorated = funcdef.parent
if decorated.type == 'decorated' and self._is_fixture(decorated):
yield name
def _is_fixture(self, decorated):
for decorator in decorated.children:
dotted_name = decorator.children[1]
# A heuristic, this makes it faster.
if 'fixture' in dotted_name.get_code():
for value in self.parent_context.infer_node(dotted_name):
if value.name.get_qualified_names(include_module_names=True) \
== ('_pytest', 'fixtures', 'fixture'):
return True
return False
+2 -1
View File
@@ -4,7 +4,8 @@ This is not a plugin, this is just the place were plugins are registered.
from jedi.plugins import stdlib from jedi.plugins import stdlib
from jedi.plugins import flask from jedi.plugins import flask
from jedi.plugins import pytest
from jedi.plugins import plugin_manager from jedi.plugins import plugin_manager
plugin_manager.register(stdlib, flask) plugin_manager.register(stdlib, flask, pytest)
+50
View File
@@ -299,6 +299,56 @@ except MyException as e:
for x in e.my_attr: for x in e.my_attr:
pass pass
# -----------------
# params
# -----------------
my_param = 1
#? 9 str()
def foo1(my_param):
my_param = 3.0
foo1("")
my_type = float()
#? 20 float()
def foo2(my_param: my_type):
pass
foo2("")
#? 20 int()
def foo3(my_param=my_param):
pass
foo3("")
some_default = ''
#? []
def foo(my_t
#? []
def foo(my_t, my_ty
#? ['some_default']
def foo(my_t=some_defa
#? ['some_default']
def foo(my_t=some_defa, my_t2=some_defa
# python > 2.7
#? ['my_type']
def foo(my_t: lala=some_defa, my_t2: my_typ
#? ['my_type']
def foo(my_t: lala=some_defa, my_t2: my_typ
#? []
def foo(my_t: lala=some_defa, my_t
#? []
lambda my_t
#? []
lambda my_, my_t
#? ['some_default']
lambda x=some_defa
#? ['some_default']
lambda y, x=some_defa
# Just make sure we're not in some weird parsing recovery after opening brackets
def
# ----------------- # -----------------
# continuations # continuations
+1 -1
View File
@@ -20,7 +20,7 @@ tuple
class MyClass: class MyClass:
@pass_decorator @pass_decorator
def x(foo, def x(foo,
#? 5 ["tuple"] #? 5 []
tuple, tuple,
): ):
return 1 return 1
+17
View File
@@ -0,0 +1,17 @@
# Exists only for completion/pytest.py
import pytest
@pytest.fixture()
def my_other_conftest_fixture():
return 1.0
@pytest.fixture()
def my_conftest_fixture(my_other_conftest_fixture):
return my_other_conftest_fixture
def my_not_existing_fixture():
return 3 # Just a normal function
+1 -1
View File
@@ -40,4 +40,4 @@ str(def
class Foo(object): class Foo(object):
@property @property
#? ['str'] #? ['str']
def bar(str def bar(x=str
+118
View File
@@ -0,0 +1,118 @@
# python > 2.7
import pytest
from pytest import fixture
@pytest.fixture(scope='module')
def my_fixture() -> str:
pass
@fixture
def my_simple_fixture():
return 1
@fixture
def my_yield_fixture():
yield 1
@fixture
class MyClassFixture():
pass
# -----------------
# goto/infer
# -----------------
#! 18 ['def my_conftest_fixture']
def test_x(my_conftest_fixture, my_fixture, my_not_existing_fixture, my_yield_fixture):
#? str()
my_fixture
#? int()
my_yield_fixture
#?
my_not_existing_fixture
#? float()
return my_conftest_fixture
#? 18 float()
def test_x(my_conftest_fixture, my_fixture):
pass
#! 18 ['param MyClassFixture']
def test_x(MyClassFixture):
#?
MyClassFixture
#? 15
def lala(my_fixture):
pass
@pytest.fixture
#? 15 str()
def lala(my_fixture):
pass
#! 15 ['param my_fixture']
def lala(my_fixture):
pass
@pytest.fixture
#! 15 ['def my_fixture']
def lala(my_fixture):
pass
# -----------------
# completion
# -----------------
#? 34 ['my_fixture']
def test_x(my_simple_fixture, my_fixture):
return
#? 34 ['my_fixture']
def test_x(my_simple_fixture, my_fixture):
return
#? ['my_fixture']
def test_x(my_simple_fixture, my_f
return
#? 18 ['my_simple_fixture']
def test_x(my_simple_fixture):
return
#? ['my_simple_fixture']
def test_x(my_simp
return
#? ['my_conftest_fixture']
def test_x(my_con
return
#? 18 ['my_conftest_fixture']
def test_x(my_conftest_fixture):
return
#? []
def lala(my_con
return
@pytest.fixture
#? ['my_conftest_fixture']
def lala(my_con
return
@pytest.fixture
#? 15 ['my_conftest_fixture']
def lala(my_con):
return
@pytest.fixture
@some_decorator
#? ['my_conftest_fixture']
def lala(my_con
return
@pytest.fixture
@some_decorator
#? 15 ['my_conftest_fixture']
def lala(my_con):
return
+1 -1
View File
@@ -342,7 +342,7 @@ def test_get_modules_containing_name(inference_state, path, goal, is_package):
) )
def test_load_module_from_path(inference_state, path, base_names, is_package, names): def test_load_module_from_path(inference_state, path, base_names, is_package, names):
file_io = KnownContentFileIO(path, '') file_io = KnownContentFileIO(path, '')
m = imports._load_module_from_path(inference_state, file_io, base_names) m = imports.load_module_from_path(inference_state, file_io, base_names)
assert m.is_package() == is_package assert m.is_package() == is_package
assert m.string_names == names assert m.string_names == names