diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 117174f1..617db37f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,11 @@ Changelog --------- +0.13.3 (2019-02-24) ++++++++++++++++++++ + +- Fixed an issue with embedded Pytho, see https://github.com/davidhalter/jedi-vim/issues/870 + 0.13.2 (2018-12-15) +++++++++++++++++++ diff --git a/jedi/__init__.py b/jedi/__init__.py index e1b901f0..cb2e3254 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -36,7 +36,7 @@ As you see Jedi is pretty simple and allows you to concentrate on writing a good text editor, while still having very good IDE features for Python. """ -__version__ = '0.13.2' +__version__ = '0.13.3' from jedi.api import Script, Interpreter, set_debug_function, \ preload_module, names diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 9b5edc86..f8f93c10 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -348,6 +348,11 @@ try: except NameError: NotADirectoryError = IOError +try: + PermissionError = PermissionError +except NameError: + PermissionError = IOError + def no_unicode_pprint(dct): """ diff --git a/jedi/api/environment.py b/jedi/api/environment.py index ea34d90b..a6616c5a 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -153,9 +153,9 @@ def _get_virtual_env_from_var(): variable is considered to be safe / controlled by the user solely. """ var = os.environ.get('VIRTUAL_ENV') - if var is not None: + if var: if var == sys.prefix: - return SameEnvironment() + return _try_get_same_env() try: return create_environment(var, safe=False) @@ -184,9 +184,47 @@ def get_default_environment(): if virtual_env is not None: return virtual_env - # If no VirtualEnv is found, use the environment we're already + return _try_get_same_env() + + +def _try_get_same_env(): + env = SameEnvironment() + if not os.path.basename(env.executable).lower().startswith('python'): + # This tries to counter issues with embedding. In some cases (e.g. + # VIM's Python Mac/Windows, sys.executable is /foo/bar/vim. This + # happens, because for Mac a function called `_NSGetExecutablePath` is + # used and for Windows `GetModuleFileNameW`. These are both platform + # specific functions. For all other systems sys.executable should be + # alright. However here we try to generalize: + # + # 1. Check if the executable looks like python (heuristic) + # 2. In case it's not try to find the executable + # 3. In case we don't find it use an interpreter environment. + # + # The last option will always work, but leads to potential crashes of + # Jedi - which is ok, because it happens very rarely and even less, + # because the code below should work for most cases. + if os.name == 'nt': + # The first case would be a virtualenv and the second a normal + # Python installation. + checks = (r'Scripts\python.exe', 'python.exe') + else: + # For unix it looks like Python is always in a bin folder. + checks = ( + 'bin/python%s.%s' % (sys.version_info[0], sys.version[1]), + 'bin/python%s' % (sys.version_info[0]), + 'bin/python', + ) + for check in checks: + guess = os.path.join(sys.exec_prefix, check) + if os.path.isfile(guess): + # Bingo - We think we have our Python. + return Environment(guess) + # It looks like there is no reasonable Python to be found. + return InterpreterEnvironment() + # If no virtualenv is found, use the environment we're already # using. - return SameEnvironment() + return env def get_cached_default_environment(): diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index 2f7af1b1..f7ce7536 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -130,7 +130,10 @@ def get_stack_at_position(grammar, code_lines, module_node, pos): p.parse(tokens=tokenize_without_endmarker(code)) except EndMarkerReached: return p.stack - raise SystemError("This really shouldn't happen. There's a bug in Jedi.") + raise SystemError( + "This really shouldn't happen. There's a bug in Jedi:\n%s" + % list(tokenize_without_endmarker(code)) + ) def evaluate_goto_definition(evaluator, context, leaf): diff --git a/jedi/api/project.py b/jedi/api/project.py index eed8f3f9..1e4bc08c 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -1,7 +1,7 @@ import os import json -from jedi._compatibility import FileNotFoundError, NotADirectoryError +from jedi._compatibility import FileNotFoundError, NotADirectoryError, PermissionError from jedi.api.environment import SameEnvironment, \ get_cached_default_environment from jedi.api.exceptions import WrongVersion @@ -151,7 +151,7 @@ def _is_django_path(directory): try: with open(os.path.join(directory, 'manage.py'), 'rb') as f: return b"DJANGO_SETTINGS_MODULE" in f.read() - except (FileNotFoundError, NotADirectoryError): + except (FileNotFoundError, NotADirectoryError, PermissionError): return False return False @@ -167,7 +167,7 @@ def get_default_project(path=None): for dir in traverse_parents(check, include_current=True): try: return Project.load(dir) - except (FileNotFoundError, NotADirectoryError): + except (FileNotFoundError, NotADirectoryError, PermissionError): pass if first_no_init_file is None: diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index 1a60d18f..a03be567 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -97,7 +97,7 @@ class MixedObjectFilter(compiled.CompiledObjectFilter): @evaluator_function_cache() def _load_module(evaluator, path): - module_node = evaluator.grammar.parse( + module_node = evaluator.parse( path=path, cache=True, diff_cache=settings.fast_parser, diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index 8f5bf0cf..c9d57359 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -514,9 +514,10 @@ class SelfAttributeFilter(ClassFilter): for name in names: trailer = name.parent if trailer.type == 'trailer' \ - and len(trailer.children) == 2 \ + and len(trailer.parent.children) == 2 \ and trailer.children[0] == '.': if name.is_definition() and self._access_possible(name): + # TODO filter non-self assignments. yield name def _convert_names(self, names): diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index ba0dd38a..189f55b2 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -172,7 +172,7 @@ def get_module_names(module, all_scopes): # parent_scope. There's None as a parent, because nodes in the module # node have the parent module and not suite as all the others. # Therefore it's important to catch that case. - names = [n for n in names if get_parent_scope(n).parent in (module, None)] + names = [n for n in names if get_parent_scope(n) == module] return names diff --git a/jedi/evaluate/syntax_tree.py b/jedi/evaluate/syntax_tree.py index 8a377f16..7f4a2cd3 100644 --- a/jedi/evaluate/syntax_tree.py +++ b/jedi/evaluate/syntax_tree.py @@ -572,7 +572,8 @@ def tree_name_to_contexts(evaluator, context, tree_name): filters = [next(filters)] return finder.find(filters, attribute_lookup=False) elif node.type not in ('import_from', 'import_name'): - raise ValueError("Should not happen. type: %s", node.type) + context = evaluator.create_context(context, tree_name) + return eval_atom(context, tree_name) typ = node.type if typ == 'for_stmt': @@ -612,6 +613,8 @@ def tree_name_to_contexts(evaluator, context, tree_name): # the static analysis report. exceptions = context.eval_node(tree_name.get_previous_sibling().get_previous_sibling()) types = exceptions.execute_evaluated() + elif node.type == 'param': + types = NO_CONTEXTS else: raise ValueError("Should not happen. type: %s" % typ) return types diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 3a700a60..b80e624f 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -247,11 +247,16 @@ def get_parent_scope(node, include_flows=False): Returns the underlying scope. """ scope = node.parent - while scope is not None: - if include_flows and isinstance(scope, tree.Flow): + if scope is None: + return None # It's a module already. + if scope.type in ('funcdef', 'classdef') and scope.name == node: + scope = scope.parent + if scope.parent is None: # The module scope. + return scope + + while True: + if include_flows and isinstance(scope, tree.Flow) or is_scope(scope): return scope - if is_scope(scope): - break scope = scope.parent return scope diff --git a/test/completion/classes.py b/test/completion/classes.py index d850d0cd..92d6b5bb 100644 --- a/test/completion/classes.py +++ b/test/completion/classes.py @@ -36,6 +36,7 @@ class TestClass(object): self2.var_inst = first_param self2.second = second_param self2.first = first_param + self2.first.var_on_argument = 5 a = 3 def var_func(self): diff --git a/test/test_api/test_classes.py b/test/test_api/test_classes.py index d44f70b9..cd6c0c9b 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -102,7 +102,7 @@ def test_function_call_signature_in_doc(Script): def test_param_docstring(): - param = jedi.names("def test(parameter): pass")[1] + param = jedi.names("def test(parameter): pass", all_scopes=True)[1] assert param.name == 'parameter' assert param.docstring() == '' diff --git a/test/test_api/test_defined_names.py b/test/test_api/test_defined_names.py index 81b7c5b6..4df1181f 100644 --- a/test/test_api/test_defined_names.py +++ b/test/test_api/test_defined_names.py @@ -96,3 +96,25 @@ def test_names_twice(environment): defs = names(source=source, environment=environment) assert defs[0].defined_names() == [] + + +def test_simple_name(environment): + defs = names('foo', references=True, environment=environment) + assert not defs[0]._name.infer() + + +def test_no_error(environment): + code = dedent(""" + def foo(a, b): + if a == 10: + if b is None: + print("foo") + a = 20 + """) + func_name, = names(code) + print(func_name.defined_names()) + a, b, a20 = func_name.defined_names() + assert a.name == 'a' + assert b.name == 'b' + assert a20.name == 'a' + assert a20.goto_assignments() == [a20] diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 6e2cf36b..cdbe4051 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -131,6 +131,17 @@ def test_get_default_environment_from_env_does_not_use_safe(tmpdir, monkeypatch) assert env.path == 'fake' +@pytest.mark.parametrize('virtualenv', ['', 'fufuuuuu', sys.prefix]) +def test_get_default_environment_when_embedded(monkeypatch, virtualenv): + # When using Python embedded, sometimes the executable is not a Python + # executable. + executable_name = 'RANDOM_EXE' + monkeypatch.setattr(sys, 'executable', executable_name) + monkeypatch.setenv('VIRTUAL_ENV', virtualenv) + env = get_default_environment() + assert env.executable != executable_name + + def test_changing_venv(venv_path, monkeypatch): monkeypatch.setitem(os.environ, 'VIRTUAL_ENV', venv_path) get_cached_default_environment()