From c45c8ec8efb4bd904d007f4fda16496f6b548c72 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Dec 2019 01:04:01 +0100 Subject: [PATCH] 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 37d958ea..012bff41 100644 --- a/jedi/file_io.py +++ b/jedi/file_io.py @@ -13,6 +13,12 @@ class AbstractFolderIO(object): 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 @@ class FolderIO(AbstractFolderIO): 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 e28ce314..ab2c75bd 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 get_module_contexts_containing_name(inference_state, module_contexts, name): 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 56d5daf6..8e358955 100644 --- a/jedi/inference/names.py +++ b/jedi/inference/names.py @@ -9,6 +9,7 @@ from jedi.inference.base_value import ValueSet, NO_VALUES 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 @@ class _ActualTreeParamName(BaseTreeParamName): 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 df106cfc..23588bd4 100644 --- a/jedi/plugins/__init__.py +++ b/jedi/plugins/__init__.py @@ -14,18 +14,18 @@ class _PluginManager(object): 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 00000000..3629b49e --- /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 23913244..b39cbbe9 100644 --- a/jedi/plugins/registry.py +++ b/jedi/plugins/registry.py @@ -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 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 00000000..c9646b9b --- /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 00000000..bf9bb20d --- /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