From c45c8ec8efb4bd904d007f4fda16496f6b548c72 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 01:04:01 +0100 Subject: [PATCH 01/14] Get some pytest fixtures working with some side effects --- jedi/file_io.py | 9 ++++++ jedi/inference/imports.py | 4 +-- jedi/inference/names.py | 2 ++ jedi/plugins/__init__.py | 12 +++---- jedi/plugins/pytest.py | 62 +++++++++++++++++++++++++++++++++++++ jedi/plugins/registry.py | 3 +- test/completion/conftest.py | 17 ++++++++++ test/completion/pytest.py | 45 +++++++++++++++++++++++++++ 8 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 jedi/plugins/pytest.py create mode 100644 test/completion/conftest.py create mode 100644 test/completion/pytest.py diff --git a/jedi/file_io.py b/jedi/file_io.py index 37d958ea5..012bff414 100644 --- a/jedi/file_io.py +++ b/jedi/file_io.py @@ -13,6 +13,12 @@ def list(self): def get_file_io(self, name): raise NotImplementedError + def get_parent_folder(self): + raise NotImplementedError + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.path) + class FolderIO(AbstractFolderIO): def list(self): @@ -21,6 +27,9 @@ def list(self): def get_file_io(self, name): return FileIO(os.path.join(self.path, name)) + def get_parent_folder(self): + return FolderIO(os.path.dirname(self.path)) + class FileIOFolderMixin(object): def get_parent_folder(self): diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index e28ce314d..ab2c75bd2 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -505,7 +505,7 @@ def _load_builtin_module(inference_state, import_names=None, sys_path=None): 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 here to ensure that a random path is still properly loaded into the Jedi @@ -552,7 +552,7 @@ def check_fs(file_io, base_names): if name not in code: return None 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): return None return m.as_context() diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 56d5daf61..8e358955d 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -9,6 +9,7 @@ from jedi.inference import docstrings from jedi.cache import memoize_method from jedi.inference.helpers import deep_ast_copy, infer_call_of_leaf +from jedi.plugins import plugin_manager def _merge_name_docs(names): @@ -482,6 +483,7 @@ def infer(self): class AnonymousParamName(_ActualTreeParamName): + @plugin_manager.decorate(name='infer_anonymous_param') def infer(self): values = super(AnonymousParamName, self).infer() if values: diff --git a/jedi/plugins/__init__.py b/jedi/plugins/__init__.py index df106cfca..23588bd44 100644 --- a/jedi/plugins/__init__.py +++ b/jedi/plugins/__init__.py @@ -14,18 +14,18 @@ def register(self, *plugins): self._registered_plugins.extend(plugins) self._build_functions() - def decorate(self): + def decorate(self, name=None): def decorator(callback): @wraps(callback) 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[name] = callback - self._cached_base_callbacks[name] = callback + built_functions[public_name] = callback + self._cached_base_callbacks[public_name] = callback return wrapper diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py new file mode 100644 index 000000000..3629b49e0 --- /dev/null +++ b/jedi/plugins/pytest.py @@ -0,0 +1,62 @@ +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): + 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): + function_context = value.as_context() + return function_context.get_return_values() + + def wrapper(param): + module = param.get_root_context() + fixtures = _goto_pytest_fixture(module, param.string_name) + if fixtures: + return ValueSet.from_sets( + get_returns(value) + for fixture in fixtures + for value in fixture.infer() + ) + return func(param) + 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 + + +@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): + yield name diff --git a/jedi/plugins/registry.py b/jedi/plugins/registry.py index 239132446..b39cbbe97 100644 --- a/jedi/plugins/registry.py +++ b/jedi/plugins/registry.py @@ -4,7 +4,8 @@ from jedi.plugins import stdlib from jedi.plugins import flask +from jedi.plugins import pytest from jedi.plugins import plugin_manager -plugin_manager.register(stdlib, flask) +plugin_manager.register(stdlib, flask, pytest) diff --git a/test/completion/conftest.py b/test/completion/conftest.py new file mode 100644 index 000000000..c9646b9b9 --- /dev/null +++ b/test/completion/conftest.py @@ -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 diff --git a/test/completion/pytest.py b/test/completion/pytest.py new file mode 100644 index 000000000..bf9bb20dd --- /dev/null +++ b/test/completion/pytest.py @@ -0,0 +1,45 @@ +# python > 2 +import pytest +from pytest import fixture + + +@pytest.fixture(scope='module') +def my_fixture() -> str: + pass + + +@fixture +def my_simple_fixture(): + return 1 + + +# ----------------- +# goto/infer +# ----------------- + +#! 18 'def my_conftest_fixture' +def test_x(my_conftest_fixture, my_fixture, my_not_existing_fixture): + #? str() + my_fixture + #? + my_not_existing_fixture + #? float() + return my_conftest_fixture + +#? 18 float() +def test_x(my_conftest_fixture, my_fixture): + pass + +# ----------------- +# completion +# ----------------- + +#? 34 ['my_fixture'] +def test_x(my_simple_fixture, my_fixture): + return +#? 18 ['my_simple_fixture'] +def test_x(my_simple_fixture): + return +#? 18 ['my_conftest_fixture'] +def test_x(my_conftest_fixture): + return From 148fffae2877509a95607d2113c4b2df8cf2df79 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 01:50:17 +0100 Subject: [PATCH 02/14] Make yield pytest fixtures work --- jedi/inference/value/function.py | 8 +++++--- jedi/plugins/pytest.py | 15 +++++++++++++-- test/completion/pytest.py | 21 +++++++++++++++++++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/jedi/inference/value/function.py b/jedi/inference/value/function.py index dfe190f90..18963f648 100644 --- a/jedi/inference/value/function.py +++ b/jedi/inference/value/function.py @@ -277,17 +277,19 @@ def merge_yield_values(self, is_async=False): 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): """ Created to be used by inheritance. """ inference_state = self.inference_state 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 if is_coroutine: - if is_generator: + if self.is_generator(): if inference_state.environment.version_info < (3, 6): return NO_VALUES async_generator_classes = inference_state.typing_module \ @@ -312,7 +314,7 @@ def infer(self): GenericClass(c, TupleGenericManager(generics)) for c in async_classes ).execute_annotation() else: - if is_generator: + if self.is_generator(): return ValueSet([iterable.Generator(inference_state, self)]) else: return self.get_return_values() diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 3629b49e0..052457371 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -17,8 +17,17 @@ def wrapper(value, arguments): 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() - return function_context.get_return_values() + if function_context.is_generator(): + return function_context.merge_yield_values() + else: + return function_context.get_return_values() def wrapper(param): module = param.get_root_context() @@ -59,4 +68,6 @@ def _iter_pytest_modules(module_context): class FixtureFilter(ParserTreeFilter): def _filter(self, names): for name in super(FixtureFilter, self)._filter(names): - yield name + if name.parent.type == 'funcdef': + # Class fixtures are not supported + yield name diff --git a/test/completion/pytest.py b/test/completion/pytest.py index bf9bb20dd..b8423c162 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -13,14 +13,25 @@ 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): +#! 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() @@ -30,6 +41,12 @@ def test_x(my_conftest_fixture, my_fixture, my_not_existing_fixture): def test_x(my_conftest_fixture, my_fixture): pass + +#! 18 ['param MyClassFixture'] +def test_x(MyClassFixture): + #? + MyClassFixture + # ----------------- # completion # ----------------- From 02320f832d0102e7fd402d56081fac060ee6bfb2 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 02:12:02 +0100 Subject: [PATCH 03/14] Check better for when something is a picture --- jedi/plugins/pytest.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 052457371..fe13a5a3c 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -7,6 +7,8 @@ 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 @@ -68,6 +70,20 @@ def _iter_pytest_modules(module_context): class FixtureFilter(ParserTreeFilter): def _filter(self, names): for name in super(FixtureFilter, self)._filter(names): - if name.parent.type == 'funcdef': + funcdef = name.parent + if funcdef.type == 'funcdef': # Class fixtures are not supported - yield name + 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 From 5a54d94aa59cf84ef0e18bfd31d10895945ab735 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 10:36:13 +0100 Subject: [PATCH 04/14] Make sure that infering params is possible from the API --- jedi/inference/__init__.py | 2 ++ test/completion/basic.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index e3c2db7ad..42d5c56a9 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -169,6 +169,8 @@ def infer(self, context, name): return imports.infer_import(context, name) if type_ == 'with_stmt': return tree_name_to_values(self, context, name) + elif type_ == 'param': + return context.py__getattribute__(name.value, position=name.end_pos) else: result = follow_error_node_imports_if_possible(context, name) if result is not None: diff --git a/test/completion/basic.py b/test/completion/basic.py index a74ca81c1..979f4f42d 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -299,6 +299,25 @@ def __init__(self, my_attr): for x in e.my_attr: 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("") # ----------------- # continuations From 8c737ba17ed66e7c08486442b05b4fb2966fb8ef Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 10:51:49 +0100 Subject: [PATCH 05/14] Make goto work for pytest fixtures --- jedi/inference/names.py | 4 ++++ jedi/plugins/pytest.py | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/jedi/inference/names.py b/jedi/inference/names.py index 8e358955d..effc2410b 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -483,6 +483,10 @@ def infer(self): 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): values = super(AnonymousParamName, self).infer() diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index fe13a5a3c..8cdfb609c 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -31,16 +31,25 @@ def get_returns(value): else: return function_context.get_return_values() - def wrapper(param): - module = param.get_root_context() - fixtures = _goto_pytest_fixture(module, param.string_name) + def wrapper(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) + return func(param_name) + return wrapper + + +def goto_anonymous_param(func): + def wrapper(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 From 70bf3d958626eb8c671fb32479e27259573e3f42 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 11:29:39 +0100 Subject: [PATCH 06/14] Deprecate Python 2 support --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 562f49574..094964b63 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,8 @@ Changelog - ``usages`` deprecated, use ``find_references`` instead - ``jedi.names`` deprecated, use ``jedi.Script(...).names()`` - ``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) +++++++++++++++++++ From a8782d007067b2be1e09ef5b9108211da34749a3 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 11:48:39 +0100 Subject: [PATCH 07/14] Make sure param completions work the right way --- jedi/api/completion.py | 15 +++++++++++++++ test/completion/basic.py | 29 +++++++++++++++++++++++++++++ test/completion/parser.py | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 465dd4f72..e28559fd5 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -216,6 +216,8 @@ def _complete_python(self, leaf): elif nonterminals[-1] in ('trailer', 'dotted_name') and nodes[-1] == '.': dot = self._module_node.get_leaf_for_position(self._position) completion_names += self._complete_trailer(dot.get_previous_leaf()) + elif self._is_parameter_completion(): + pass else: completion_names += self._complete_global_scope() completion_names += self._complete_inherited(is_function=False) @@ -234,6 +236,19 @@ def _complete_python(self, leaf): 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_keywords(self, allowed_transitions): for k in allowed_transitions: if isinstance(k, str) and k.isalpha(): diff --git a/test/completion/basic.py b/test/completion/basic.py index 979f4f42d..2cd85bcd0 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -319,6 +319,35 @@ 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 + +#? ['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 # ----------------- diff --git a/test/completion/parser.py b/test/completion/parser.py index db88824fd..e0159cac4 100644 --- a/test/completion/parser.py +++ b/test/completion/parser.py @@ -40,4 +40,4 @@ def with_param( class Foo(object): @property #? ['str'] - def bar(str + def bar(x=str From ff0e3ec8fb371cb01b71037a4e9af4142df7624a Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 11:52:14 +0100 Subject: [PATCH 08/14] Fix _BuiltinMappedMethod to use a ValueWrapper --- jedi/inference/filters.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/jedi/inference/filters.py b/jedi/inference/filters.py index 9001484b7..9bac3d897 100644 --- a/jedi/inference/filters.py +++ b/jedi/inference/filters.py @@ -246,24 +246,18 @@ def __repr__(self): 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.""" api_type = u'function' - def __init__(self, builtin_value, method, builtin_func): - super(_BuiltinMappedMethod, self).__init__( - builtin_value.inference_state, - parent_context=builtin_value - ) + def __init__(self, value, method, builtin_func): + super(_BuiltinMappedMethod, self).__init__(builtin_func) + self._value = value self._method = method - self._builtin_func = builtin_func def py__call__(self, arguments): # TODO add TypeError if params are given/or not correct. - return self._method(self.parent_context) - - def __getattr__(self, name): - return getattr(self._builtin_func, name) + return self._method(self._value) class SpecialMethodFilter(DictFilter): From 8611fcf8eab47afbbccad125464270702de38bea Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 11:59:40 +0100 Subject: [PATCH 09/14] Fix some tests --- jedi/inference/gradual/typing.py | 5 +++++ test/completion/completion.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jedi/inference/gradual/typing.py b/jedi/inference/gradual/typing.py index e1bf15661..861cc9f02 100644 --- a/jedi/inference/gradual/typing.py +++ b/jedi/inference/gradual/typing.py @@ -322,6 +322,11 @@ def __init__(self, inference_state, parent_context, tree_node, type_value_set): def py__call__(self, arguments): 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): @repack_with_argument_clinic('type, object, /') diff --git a/test/completion/completion.py b/test/completion/completion.py index b254e8d3f..f509a19c7 100644 --- a/test/completion/completion.py +++ b/test/completion/completion.py @@ -20,7 +20,7 @@ def x(): class MyClass: @pass_decorator def x(foo, -#? 5 ["tuple"] +#? 5 [] tuple, ): return 1 From 31936776a592636cad7c9414d1a213230805c2df Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 12:29:18 +0100 Subject: [PATCH 10/14] Make completion of pytest fixtures possible --- jedi/api/completion.py | 17 ++++++++++++++++- jedi/plugins/pytest.py | 12 ++++++++++++ test/completion/pytest.py | 12 ++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index e28559fd5..1b43657b7 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -18,6 +18,7 @@ from jedi.inference.value import TreeInstance from jedi.inference.gradual.conversion import convert_values from jedi.parser_utils import cut_value_at_position +from jedi.plugins import plugin_manager def get_signature_param_names(signatures): @@ -74,6 +75,13 @@ def get_flow_scope_node(module_node, position): return node +@plugin_manager.decorate() +def complete_param_names(context, function_name): + # Basically there's no way to do param completion. The plugins are + # responsible for this. + return [] + + class Completion: def __init__(self, inference_state, module_context, code_lines, position, signatures_callback, fuzzy=False): @@ -217,7 +225,14 @@ def _complete_python(self, leaf): dot = self._module_node.get_leaf_for_position(self._position) completion_names += self._complete_trailer(dot.get_previous_leaf()) elif self._is_parameter_completion(): - pass + 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) + function_name = stack_node.nodes[1] + + completion_names += complete_param_names(context, function_name) else: completion_names += self._complete_global_scope() completion_names += self._complete_inherited(is_function=False) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 8cdfb609c..b4e6d579d 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -53,6 +53,18 @@ def wrapper(param_name): return wrapper +def complete_param_names(func): + def wrapper(context, func_name): + module_context = context.get_root_context() + names = [] + for module_context in _iter_pytest_modules(module_context): + names += FixtureFilter(module_context).values() + if names: + return names + return func(context, func_name) + return wrapper + + def _goto_pytest_fixture(module_context, name): for module_context in _iter_pytest_modules(module_context): names = FixtureFilter(module_context).get(name) diff --git a/test/completion/pytest.py b/test/completion/pytest.py index b8423c162..b6f8adcab 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -54,9 +54,21 @@ def test_x(MyClassFixture): #? 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 From 4c22f4dbb1b00c1adfde0f0b4be803b4038a0f5d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 13:02:16 +0100 Subject: [PATCH 11/14] Fix completion for non-pytest params --- jedi/api/completion.py | 33 ++++++++++++++++++++++++--------- jedi/plugins/pytest.py | 16 +++++++++------- test/completion/pytest.py | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 1b43657b7..0602244b7 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -76,7 +76,7 @@ def get_flow_scope_node(module_node, position): @plugin_manager.decorate() -def complete_param_names(context, function_name): +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 [] @@ -225,14 +225,7 @@ def _complete_python(self, leaf): dot = self._module_node.get_leaf_for_position(self._position) completion_names += self._complete_trailer(dot.get_previous_leaf()) elif self._is_parameter_completion(): - 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) - function_name = stack_node.nodes[1] - - completion_names += complete_param_names(context, function_name) + completion_names += self._complete_params(leaf) else: completion_names += self._complete_global_scope() completion_names += self._complete_inherited(is_function=False) @@ -264,6 +257,28 @@ def _is_parameter_completion(self): # 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): for k in allowed_transitions: if isinstance(k, str) and k.isalpha(): diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index b4e6d579d..7ae2194df 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -54,14 +54,16 @@ def wrapper(param_name): def complete_param_names(func): - def wrapper(context, func_name): + def wrapper(context, func_name, decorator_nodes): module_context = context.get_root_context() - names = [] - for module_context in _iter_pytest_modules(module_context): - names += FixtureFilter(module_context).values() - if names: - return names - return func(context, func_name) + if func_name.startswith('test_') \ + or any('fixture' in n.get_code() for n in 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 diff --git a/test/completion/pytest.py b/test/completion/pytest.py index b6f8adcab..345fa34db 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -72,3 +72,29 @@ def test_x(my_con #? 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 From 7715655c9670f7d512deff621ba039041524a6a3 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 13:26:06 +0100 Subject: [PATCH 12/14] Fix selection of what is a pytest fixture and what isn't --- jedi/plugins/pytest.py | 45 ++++++++++++++++++++++++++++----------- test/completion/pytest.py | 18 ++++++++++++++++ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 7ae2194df..910f540c4 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -1,3 +1,4 @@ +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 @@ -32,23 +33,25 @@ def get_returns(value): return function_context.get_return_values() def wrapper(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() - ) + 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): - names = _goto_pytest_fixture(param_name.get_root_context(), param_name.string_name) - if names: - return names + 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 @@ -56,8 +59,7 @@ def wrapper(param_name): def complete_param_names(func): def wrapper(context, func_name, decorator_nodes): module_context = context.get_root_context() - if func_name.startswith('test_') \ - or any('fixture' in n.get_code() for n in decorator_nodes): + if _is_pytest_func(func_name, decorator_nodes): names = [] for module_context in _iter_pytest_modules(module_context): names += FixtureFilter(module_context).values() @@ -74,6 +76,23 @@ def _goto_pytest_fixture(module_context, name): 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') + 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 diff --git a/test/completion/pytest.py b/test/completion/pytest.py index 345fa34db..eed0217e2 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -47,6 +47,24 @@ 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 # ----------------- From 0931c5492d735c430f13bf3850b12865c843e699 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 13:30:53 +0100 Subject: [PATCH 13/14] Fix tests --- jedi/plugins/pytest.py | 2 ++ test/test_inference/test_imports.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/plugins/pytest.py b/jedi/plugins/pytest.py index 910f540c4..08ff09050 100644 --- a/jedi/plugins/pytest.py +++ b/jedi/plugins/pytest.py @@ -84,6 +84,8 @@ def _is_a_pytest_param(param_name): 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) diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index 5d628d056..935827438 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -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): 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.string_names == names From dc3d6a3975f3259d8a61361f081c821ae7211a6a Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 14:13:35 +0100 Subject: [PATCH 14/14] Fix python 2 tests --- test/completion/basic.py | 2 ++ test/completion/pytest.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/completion/basic.py b/test/completion/basic.py index 2cd85bcd0..28089debe 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -329,6 +329,8 @@ 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'] diff --git a/test/completion/pytest.py b/test/completion/pytest.py index eed0217e2..bb48387e3 100644 --- a/test/completion/pytest.py +++ b/test/completion/pytest.py @@ -1,4 +1,4 @@ -# python > 2 +# python > 2.7 import pytest from pytest import fixture