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) +++++++++++++++++++ diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 465dd4f72..0602244b7 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, 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): @@ -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) @@ -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(): 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/__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/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): 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/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..effc2410b 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,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: 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/__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..08ff09050 --- /dev/null +++ b/jedi/plugins/pytest.py @@ -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 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/basic.py b/test/completion/basic.py index a74ca81c1..28089debe 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -299,6 +299,56 @@ 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("") + +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 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 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/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 diff --git a/test/completion/pytest.py b/test/completion/pytest.py new file mode 100644 index 000000000..bb48387e3 --- /dev/null +++ b/test/completion/pytest.py @@ -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 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