Get some pytest fixtures working with some side effects

This commit is contained in:
Dave Halter
2019-12-27 01:04:01 +01:00
parent dd89325441
commit c45c8ec8ef
8 changed files with 145 additions and 9 deletions

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

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

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,7 @@ class _ActualTreeParamName(BaseTreeParamName):
class AnonymousParamName(_ActualTreeParamName): class AnonymousParamName(_ActualTreeParamName):
@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:

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

62
jedi/plugins/pytest.py Normal file
View File

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

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)

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

45
test/completion/pytest.py Normal file
View File

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