From cc0c4cc3080e1884ab7a5a2082c085e5d4cd7b95 Mon Sep 17 00:00:00 2001 From: Marc Zimmermann Date: Fri, 1 Feb 2019 10:53:28 +0100 Subject: [PATCH 01/15] fixing permission denied errors with project.json --- jedi/_compatibility.py | 5 +++++ jedi/api/project.py | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) 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/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: From 2f86f549f5ec908c996f09e351164192df7aead8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 16 Feb 2019 04:31:52 +0100 Subject: [PATCH 02/15] Improve an error message, see #1279 --- jedi/api/helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index 2489cd9b..7cf4bc6f 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): From efd8861d6253833748b9c1d930d3591595031a62 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 21 Feb 2019 10:16:17 +0100 Subject: [PATCH 03/15] Remove an used function --- jedi/cache.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jedi/cache.py b/jedi/cache.py index 93e2bd7f..80d34f99 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -123,8 +123,6 @@ def time_cache(seconds): result = func(*args, **kwargs) cache[key] = time.time(), result return result - - wrapper.clear_cache = lambda: cache.clear() return wrapper return decorator From b4a4dacebdcec303d68155c33f9cdf8746a5809d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 21 Feb 2019 10:18:40 +0100 Subject: [PATCH 04/15] Fix embedded Python with Jedi (see comments in source code), fixes davidhalter/jedi-vim#870 --- jedi/api/environment.py | 34 +++++++++++++++++++++++++++++-- test/test_api/test_environment.py | 9 ++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index ea34d90b..5dac55c2 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -184,9 +184,39 @@ def get_default_environment(): if virtual_env is not None: return virtual_env - # If no VirtualEnv is found, use the environment we're already + 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',) + 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/test/test_api/test_environment.py b/test/test_api/test_environment.py index 6e2cf36b..834f0ebd 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -131,6 +131,15 @@ def test_get_default_environment_from_env_does_not_use_safe(tmpdir, monkeypatch) assert env.path == 'fake' +def test_get_default_environment_when_embedded(monkeypatch): + # When using Python embedded, sometimes the executable is not a Python + # executable. + executable_name = 'RANDOM_EXE' + monkeypatch.setattr(sys, 'executable', executable_name) + 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() From 48b137a7f50e6a3ed3aafc130ccee55cd8ca1402 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 21 Feb 2019 17:54:01 +0100 Subject: [PATCH 05/15] Revert "Remove an used function" This reverts commit efd8861d6253833748b9c1d930d3591595031a62. --- jedi/cache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jedi/cache.py b/jedi/cache.py index 80d34f99..93e2bd7f 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -123,6 +123,8 @@ def time_cache(seconds): result = func(*args, **kwargs) cache[key] = time.time(), result return result + + wrapper.clear_cache = lambda: cache.clear() return wrapper return decorator From a79d386eba83ed821c0db3df05b5ab778122ddee Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 22 Feb 2019 00:24:55 +0100 Subject: [PATCH 06/15] Cleanup SameEnvironment and use the same logic for creation in virtualenvs --- jedi/api/environment.py | 8 ++++++-- test/test_api/test_environment.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index 5dac55c2..38e21f1c 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,6 +184,10 @@ def get_default_environment(): if virtual_env is not None: return virtual_env + 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. diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 834f0ebd..cdbe4051 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -131,11 +131,13 @@ def test_get_default_environment_from_env_does_not_use_safe(tmpdir, monkeypatch) assert env.path == 'fake' -def test_get_default_environment_when_embedded(monkeypatch): +@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 From 8d313e014fa15eaec9fae38080ff19d462bfdf1e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 22 Feb 2019 00:32:27 +0100 Subject: [PATCH 07/15] Check for specific Python versions first on unix, see davidhalter/jedi-vim#870 --- jedi/api/environment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index 38e21f1c..b9616cce 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -210,7 +210,10 @@ def _try_get_same_env(): checks = (r'Scripts\python.exe', 'python.exe') else: # For unix it looks like Python is always in a bin folder. - checks = ('bin/python',) + checks = ( + 'bin/python%s.%s' % (sys.version_info[0], sys.version[1]), + 'bin/python', + ) for check in checks: guess = os.path.join(sys.exec_prefix, check) if os.path.isfile(guess): From 9bb8f335c9e34bdb32f158b981d39b1f187241e8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 22 Feb 2019 01:04:01 +0100 Subject: [PATCH 08/15] A small improvement for environments see comment in https://github.com/davidhalter/jedi/commit/8d313e014fa15eaec9fae38080ff19d462bfdf1e --- jedi/api/environment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index b9616cce..a6616c5a 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -212,6 +212,7 @@ def _try_get_same_env(): # 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: From 0bf8a69024aff8599bd1f323c4feba2703ef3d59 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 24 Feb 2019 18:45:07 +0100 Subject: [PATCH 09/15] v13.3 release notes --- CHANGELOG.rst | 5 +++++ jedi/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) 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 9d687fa6..d23739be 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 From 8ac7d1fdb64630fb3552b6b14ce2c8573a616cf3 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 24 Feb 2019 19:56:17 +0100 Subject: [PATCH 10/15] Use the internal parse function to avoid UnicodeDecodeError in mixed, fixes #1277 --- jedi/evaluate/compiled/mixed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index f2cb2152..39840128 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -104,7 +104,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, From 5a2e3ee8e3a34bca54cd6896f866d8b5a6d756a7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 25 Feb 2019 00:25:57 +0100 Subject: [PATCH 11/15] Filter self names in a more correct way, fixes #1275 --- jedi/evaluate/context/instance.py | 2 +- test/completion/classes.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index db94c91a..36da32fc 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -443,7 +443,7 @@ 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): yield name diff --git a/test/completion/classes.py b/test/completion/classes.py index 1c192370..4bd2963b 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): From 2dd2d06bca890ab7028c337f5195f819ef2d42c7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 25 Feb 2019 00:27:27 +0100 Subject: [PATCH 12/15] Add a todo --- jedi/evaluate/context/instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jedi/evaluate/context/instance.py b/jedi/evaluate/context/instance.py index 36da32fc..2289554b 100644 --- a/jedi/evaluate/context/instance.py +++ b/jedi/evaluate/context/instance.py @@ -446,6 +446,7 @@ class SelfAttributeFilter(ClassFilter): 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): From eac69aef2b801ed6806e6cc3f069a0f4c12e8119 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 25 Feb 2019 21:48:57 +0100 Subject: [PATCH 13/15] Infer names in the correct way, fixes #1286 --- jedi/evaluate/syntax_tree.py | 6 +++++- test/test_api/test_defined_names.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate/syntax_tree.py b/jedi/evaluate/syntax_tree.py index 720ea349..2b90a29a 100644 --- a/jedi/evaluate/syntax_tree.py +++ b/jedi/evaluate/syntax_tree.py @@ -21,6 +21,7 @@ from jedi.evaluate.pep0484 import _evaluate_for_annotation from jedi.evaluate.context import ClassContext, FunctionContext from jedi.evaluate.context import iterable from jedi.evaluate.context import TreeInstance, CompiledInstance +from jedi.evaluate.filters import TreeNameDefinition, ParamName from jedi.evaluate.finder import NameFinder from jedi.evaluate.helpers import is_string, is_literal, is_number, is_compiled from jedi.evaluate.compiled.access import COMPARISON_OPERATORS @@ -526,8 +527,11 @@ def tree_name_to_contexts(evaluator, context, tree_name): # which means the function itself. filters = [next(filters)] return finder.find(filters, attribute_lookup=False) + elif node.type == 'param': + raise NotImplementedError 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': diff --git a/test/test_api/test_defined_names.py b/test/test_api/test_defined_names.py index 81b7c5b6..ed5f1eb2 100644 --- a/test/test_api/test_defined_names.py +++ b/test/test_api/test_defined_names.py @@ -96,3 +96,8 @@ 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() From 94f2677752fe2849cf70b6640f9812a83ae56fc9 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 26 Feb 2019 00:20:14 +0100 Subject: [PATCH 14/15] Fix names selection and params, fixes #1283 --- jedi/evaluate/helpers.py | 2 +- jedi/evaluate/syntax_tree.py | 4 ++-- jedi/parser_utils.py | 3 +++ test/test_api/test_defined_names.py | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index c94a1fbe..497b81c6 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 2b90a29a..5a2d5dc8 100644 --- a/jedi/evaluate/syntax_tree.py +++ b/jedi/evaluate/syntax_tree.py @@ -527,8 +527,6 @@ def tree_name_to_contexts(evaluator, context, tree_name): # which means the function itself. filters = [next(filters)] return finder.find(filters, attribute_lookup=False) - elif node.type == 'param': - raise NotImplementedError elif node.type not in ('import_from', 'import_name'): context = evaluator.create_context(context, tree_name) return eval_atom(context, tree_name) @@ -571,6 +569,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 212d70f2..2ee67f04 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -243,6 +243,9 @@ def get_parent_scope(node, include_flows=False): Returns the underlying scope. """ scope = node.parent + if scope.type in ('funcdef', 'classdef') and scope.name == node: + scope = scope.parent + while scope is not None: if include_flows and isinstance(scope, tree.Flow): return scope diff --git a/test/test_api/test_defined_names.py b/test/test_api/test_defined_names.py index ed5f1eb2..4df1181f 100644 --- a/test/test_api/test_defined_names.py +++ b/test/test_api/test_defined_names.py @@ -101,3 +101,20 @@ def test_names_twice(environment): 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] From 17136e03d28fd472644c555973cbf0199ebcc379 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Wed, 27 Feb 2019 13:08:21 +0100 Subject: [PATCH 15/15] Fix get_parent_scope --- jedi/parser_utils.py | 10 ++++++---- test/test_api/test_classes.py | 2 +- test/test_evaluate/test_stdlib.py | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 2ee67f04..869aaedb 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -243,14 +243,16 @@ def get_parent_scope(node, include_flows=False): Returns the underlying scope. """ scope = node.parent + 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 scope is not None: - if include_flows and isinstance(scope, tree.Flow): + 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/test_api/test_classes.py b/test/test_api/test_classes.py index d320966a..1e4f188e 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -99,7 +99,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_evaluate/test_stdlib.py b/test/test_evaluate/test_stdlib.py index d3f0e620..a851699d 100644 --- a/test/test_evaluate/test_stdlib.py +++ b/test/test_evaluate/test_stdlib.py @@ -41,6 +41,7 @@ def test_namedtuple_list(Script): assert completions == {'legs', 'length', 'large'} +@pytest.mark.skip(reason='TODO Please remove this once typeshed is merged') def test_namedtuple_content(Script): source = dedent("""\ import collections