Skip to content

Commit

Permalink
Merge branch 'pytest', fixes parts of #791
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhalter committed Dec 27, 2019
2 parents dd89325 + dc3d6a3 commit b4163a3
Show file tree
Hide file tree
Showing 18 changed files with 410 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
+++++++++++++++++++
Expand Down
45 changes: 45 additions & 0 deletions jedi/api/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -74,6 +75,13 @@ def get_flow_scope_node(module_node, position):
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:
def __init__(self, inference_state, module_context, code_lines, position,
signatures_callback, fuzzy=False):
Expand Down Expand Up @@ -216,6 +224,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():
completion_names += self._complete_params(leaf)
else:
completion_names += self._complete_global_scope()
completion_names += self._complete_inherited(is_function=False)
Expand All @@ -234,6 +244,41 @@ 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_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():
Expand Down
9 changes: 9 additions & 0 deletions jedi/file_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions jedi/inference/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 5 additions & 11 deletions jedi/inference/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions jedi/inference/gradual/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, /')
Expand Down
4 changes: 2 additions & 2 deletions jedi/inference/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions jedi/inference/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -482,6 +483,11 @@ 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()
if values:
Expand Down
8 changes: 5 additions & 3 deletions jedi/inference/value/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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()
Expand Down
12 changes: 6 additions & 6 deletions jedi/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
133 changes: 133 additions & 0 deletions jedi/plugins/pytest.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion jedi/plugins/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading

0 comments on commit b4163a3

Please sign in to comment.