Merge branch 'master' of github.com:davidhalter/jedi

This commit is contained in:
Dave Halter
2019-05-21 13:39:27 +02:00
9 changed files with 116 additions and 32 deletions

View File

@@ -3,7 +3,9 @@ To ensure compatibility from Python ``2.7`` - ``3.x``, a module has been
created. Clearly there is huge need to use conforming syntax. created. Clearly there is huge need to use conforming syntax.
""" """
from __future__ import print_function from __future__ import print_function
import atexit
import errno import errno
import functools
import sys import sys
import os import os
import re import re
@@ -11,6 +13,7 @@ import pkgutil
import warnings import warnings
import inspect import inspect
import subprocess import subprocess
import weakref
try: try:
import importlib import importlib
except ImportError: except ImportError:
@@ -635,3 +638,49 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
if _access_check(name, mode): if _access_check(name, mode):
return name return name
return None return None
if not is_py3:
# Simplified backport of Python 3 weakref.finalize:
# https://github.com/python/cpython/blob/ded4737989316653469763230036b04513cb62b3/Lib/weakref.py#L502-L662
class finalize(object):
"""Class for finalization of weakrefable objects.
finalize(obj, func, *args, **kwargs) returns a callable finalizer
object which will be called when obj is garbage collected. The
first time the finalizer is called it evaluates func(*arg, **kwargs)
and returns the result. After this the finalizer is dead, and
calling it just returns None.
When the program exits any remaining finalizers will be run.
"""
# Finalizer objects don't have any state of their own.
# This ensures that they cannot be part of a ref-cycle.
__slots__ = ()
_registry = {}
def __init__(self, obj, func, *args, **kwargs):
info = functools.partial(func, *args, **kwargs)
info.weakref = weakref.ref(obj, self)
self._registry[self] = info
def __call__(self):
"""Return func(*args, **kwargs) if alive."""
info = self._registry.pop(self, None)
if info:
return info()
@classmethod
def _exitfunc(cls):
if not cls._registry:
return
for finalizer in list(cls._registry):
try:
finalizer(None)
except Exception:
sys.excepthook(*sys.exc_info())
assert finalizer not in cls._registry
atexit.register(finalize._exitfunc)
weakref.finalize = finalize

View File

@@ -102,6 +102,7 @@ class BaseDefinition(object):
to Jedi, :meth:`jedi.Script.goto_definitions` should return a list of to Jedi, :meth:`jedi.Script.goto_definitions` should return a list of
definition for ``sys``, ``f``, ``C`` and ``x``. definition for ``sys``, ``f``, ``C`` and ``x``.
>>> from jedi._compatibility import no_unicode_pprint
>>> from jedi import Script >>> from jedi import Script
>>> source = ''' >>> source = '''
... import keyword ... import keyword
@@ -127,9 +128,11 @@ class BaseDefinition(object):
so that it is easy to relate the result to the source code. so that it is easy to relate the result to the source code.
>>> defs = sorted(defs, key=lambda d: d.line) >>> defs = sorted(defs, key=lambda d: d.line)
>>> defs # doctest: +NORMALIZE_WHITESPACE >>> no_unicode_pprint(defs) # doctest: +NORMALIZE_WHITESPACE
[<Definition module keyword>, <Definition class C>, [<Definition full_name='keyword', description='module keyword'>,
<Definition instance D>, <Definition def f>] <Definition full_name='__main__.C', description='class C'>,
<Definition full_name='__main__.D', description='instance D'>,
<Definition full_name='__main__.f', description='def f'>]
Finally, here is what you can get from :attr:`type`: Finally, here is what you can get from :attr:`type`:
@@ -207,7 +210,7 @@ class BaseDefinition(object):
>>> source = 'import json' >>> source = 'import json'
>>> script = Script(source, path='example.py') >>> script = Script(source, path='example.py')
>>> d = script.goto_definitions()[0] >>> d = script.goto_definitions()[0]
>>> print(d.module_name) # doctest: +ELLIPSIS >>> print(d.module_name) # doctest: +ELLIPSIS
json json
""" """
return self._get_module().name.string_name return self._get_module().name.string_name
@@ -515,6 +518,7 @@ class Definition(BaseDefinition):
Example: Example:
>>> from jedi._compatibility import no_unicode_pprint
>>> from jedi import Script >>> from jedi import Script
>>> source = ''' >>> source = '''
... def f(): ... def f():
@@ -527,8 +531,9 @@ class Definition(BaseDefinition):
>>> script = Script(source, column=3) # line is maximum by default >>> script = Script(source, column=3) # line is maximum by default
>>> defs = script.goto_definitions() >>> defs = script.goto_definitions()
>>> defs = sorted(defs, key=lambda d: d.line) >>> defs = sorted(defs, key=lambda d: d.line)
>>> defs >>> no_unicode_pprint(defs) # doctest: +NORMALIZE_WHITESPACE
[<Definition def f>, <Definition class C>] [<Definition full_name='__main__.f', description='def f'>,
<Definition full_name='__main__.C', description='class C'>]
>>> str(defs[0].description) # strip literals in python2 >>> str(defs[0].description) # strip literals in python2
'def f' 'def f'
>>> str(defs[1].description) >>> str(defs[1].description)

View File

@@ -12,7 +12,6 @@ import sys
import subprocess import subprocess
import socket import socket
import errno import errno
import weakref
import traceback import traceback
from functools import partial from functools import partial
from threading import Thread from threading import Thread
@@ -22,7 +21,7 @@ except ImportError:
from Queue import Queue, Empty # python 2.7 from Queue import Queue, Empty # python 2.7
from jedi._compatibility import queue, is_py3, force_unicode, \ from jedi._compatibility import queue, is_py3, force_unicode, \
pickle_dump, pickle_load, GeneralizedPopen pickle_dump, pickle_load, GeneralizedPopen, weakref
from jedi import debug from jedi import debug
from jedi.cache import memoize_method from jedi.cache import memoize_method
from jedi.evaluate.compiled.subprocess import functions from jedi.evaluate.compiled.subprocess import functions
@@ -37,7 +36,6 @@ _MAIN_PATH = os.path.join(os.path.dirname(__file__), '__main__.py')
def _enqueue_output(out, queue): def _enqueue_output(out, queue):
for line in iter(out.readline, b''): for line in iter(out.readline, b''):
queue.put(line) queue.put(line)
out.close()
def _add_stderr_to_debug(stderr_queue): def _add_stderr_to_debug(stderr_queue):
@@ -56,6 +54,22 @@ def _get_function(name):
return getattr(functions, name) return getattr(functions, name)
def _cleanup_process(process, thread):
try:
process.kill()
process.wait()
except OSError:
# Raised if the process is already killed.
pass
thread.join()
for stream in [process.stdin, process.stdout, process.stderr]:
try:
stream.close()
except OSError:
# Raised if the stream is broken.
pass
class _EvaluatorProcess(object): class _EvaluatorProcess(object):
def __init__(self, evaluator): def __init__(self, evaluator):
self._evaluator_weakref = weakref.ref(evaluator) self._evaluator_weakref = weakref.ref(evaluator)
@@ -145,6 +159,7 @@ class CompiledSubprocess(object):
def __init__(self, executable): def __init__(self, executable):
self._executable = executable self._executable = executable
self._evaluator_deletion_queue = queue.deque() self._evaluator_deletion_queue = queue.deque()
self._cleanup_callable = lambda: None
def __repr__(self): def __repr__(self):
pid = os.getpid() pid = os.getpid()
@@ -182,6 +197,12 @@ class CompiledSubprocess(object):
) )
t.daemon = True t.daemon = True
t.start() t.start()
# Ensure the subprocess is properly cleaned up when the object
# is garbage collected.
self._cleanup_callable = weakref.finalize(self,
_cleanup_process,
process,
t)
return process return process
def run(self, evaluator, function, args=(), kwargs={}): def run(self, evaluator, function, args=(), kwargs={}):
@@ -202,18 +223,7 @@ class CompiledSubprocess(object):
def _kill(self): def _kill(self):
self.is_crashed = True self.is_crashed = True
try: self._cleanup_callable()
self._get_process().kill()
self._get_process().wait()
except (AttributeError, TypeError):
# If the Python process is terminating, it will remove some modules
# earlier than others and in general it's unclear how to deal with
# that so we just ignore the exceptions here.
pass
def __del__(self):
if not self.is_crashed:
self._kill()
def _send(self, evaluator_id, function, args=(), kwargs={}): def _send(self, evaluator_id, function, args=(), kwargs={}):
if self.is_crashed: if self.is_crashed:

View File

@@ -359,10 +359,11 @@ def get_global_filters(evaluator, context, until_position, origin_scope):
First we get the names from the function scope. First we get the names from the function scope.
>>> no_unicode_pprint(filters[0]) #doctest: +ELLIPSIS >>> no_unicode_pprint(filters[0]) # doctest: +ELLIPSIS
MergedFilter(<ParserTreeFilter: ...>, <GlobalNameFilter: ...>) MergedFilter(<ParserTreeFilter: ...>, <GlobalNameFilter: ...>)
>>> sorted(str(n) for n in filters[0].values()) >>> sorted(str(n) for n in filters[0].values()) # doctest: +NORMALIZE_WHITESPACE
['<TreeNameDefinition: func@(3, 4)>', '<TreeNameDefinition: x@(2, 0)>'] ['<TreeNameDefinition: string_name=func start_pos=(3, 4)>',
'<TreeNameDefinition: string_name=x start_pos=(2, 0)>']
>>> filters[0]._filters[0]._until_position >>> filters[0]._filters[0]._until_position
(4, 0) (4, 0)
>>> filters[0]._filters[1]._until_position >>> filters[0]._filters[1]._until_position
@@ -380,7 +381,7 @@ def get_global_filters(evaluator, context, until_position, origin_scope):
Finally, it yields the builtin filter, if `include_builtin` is Finally, it yields the builtin filter, if `include_builtin` is
true (default). true (default).
>>> list(filters[3].values()) #doctest: +ELLIPSIS >>> list(filters[3].values()) # doctest: +ELLIPSIS
[...] [...]
""" """
from jedi.evaluate.context.function import FunctionExecutionContext from jedi.evaluate.context.function import FunctionExecutionContext

View File

@@ -13,14 +13,20 @@ else:
TestCase = unittest.TestCase TestCase = unittest.TestCase
import os import os
import pytest
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
import functools from functools import partial, wraps
test_dir = dirname(abspath(__file__)) test_dir = dirname(abspath(__file__))
root_dir = dirname(test_dir) root_dir = dirname(test_dir)
sample_int = 1 # This is used in completion/imports.py sample_int = 1 # This is used in completion/imports.py
skip_if_windows = partial(pytest.param,
marks=pytest.mark.skipif("sys.platform=='win32'"))
skip_if_not_windows = partial(pytest.param,
marks=pytest.mark.skipif("sys.platform!='win32'"))
def get_example_dir(name): def get_example_dir(name):
return join(test_dir, 'examples', name) return join(test_dir, 'examples', name)
@@ -34,7 +40,7 @@ def cwd_at(path):
:arg path: relative path from repository root (e.g., ``'jedi'``). :arg path: relative path from repository root (e.g., ``'jedi'``).
""" """
def decorator(func): def decorator(func):
@functools.wraps(func) @wraps(func)
def wrapper(Script, **kwargs): def wrapper(Script, **kwargs):
with set_cwd(path): with set_cwd(path):
return func(Script, **kwargs) return func(Script, **kwargs)

View File

@@ -357,9 +357,11 @@ def collect_dir_tests(base_dir, test_files, check_thirdparty=False):
path = os.path.join(base_dir, f_name) path = os.path.join(base_dir, f_name)
if is_py3: if is_py3:
source = open(path, encoding='utf-8').read() with open(path, encoding='utf-8') as f:
source = f.read()
else: else:
source = unicode(open(path).read(), 'UTF-8') with open(path) as f:
source = unicode(f.read(), 'UTF-8')
for case in collect_file_tests(path, StringIO(source), for case in collect_file_tests(path, StringIO(source),
lines_to_execute): lines_to_execute):

View File

@@ -38,7 +38,9 @@ def test_find_module_not_package():
assert is_package is False assert is_package is False
pkg_zip_path = os.path.join(os.path.dirname(__file__), 'zipped_imports/pkg.zip') pkg_zip_path = os.path.join(os.path.dirname(__file__),
'zipped_imports',
'pkg.zip')
def test_find_module_package_zipped(Script, evaluator, environment): def test_find_module_package_zipped(Script, evaluator, environment):

View File

@@ -4,6 +4,7 @@ import sys
import shutil import shutil
import pytest import pytest
from ..helpers import skip_if_windows, skip_if_not_windows
from jedi.evaluate import sys_path from jedi.evaluate import sys_path
from jedi.api.environment import create_environment from jedi.api.environment import create_environment
@@ -87,8 +88,12 @@ _s = ['/a', '/b', '/c/d/']
(['/foo'], '/foo/bar/__init__.py', ('bar',), True), (['/foo'], '/foo/bar/__init__.py', ('bar',), True),
(['/foo'], '/foo/bar/baz/__init__.py', ('bar', 'baz'), True), (['/foo'], '/foo/bar/baz/__init__.py', ('bar', 'baz'), True),
(['/foo'], '/foo/bar.so', ('bar',), False),
(['/foo'], '/foo/bar/__init__.so', ('bar',), True), skip_if_windows(['/foo'], '/foo/bar.so', ('bar',), False),
skip_if_windows(['/foo'], '/foo/bar/__init__.so', ('bar',), True),
skip_if_not_windows(['/foo'], '/foo/bar.pyd', ('bar',), False),
skip_if_not_windows(['/foo'], '/foo/bar/__init__.pyd', ('bar',), True),
(['/foo'], '/x/bar.py', None, False), (['/foo'], '/x/bar.py', None, False),
(['/foo'], '/foo/bar.xyz', ('bar.xyz',), False), (['/foo'], '/foo/bar.xyz', ('bar.xyz',), False),

View File

@@ -8,6 +8,8 @@ deps =
py34: typing py34: typing
# numpydoc for typing scipy stack # numpydoc for typing scipy stack
numpydoc numpydoc
# sphinx, a dependency of numpydoc, dropped Python 2 support in version 2.0
sphinx < 2.0
cov: coverage cov: coverage
# Overwrite the parso version (only used sometimes). # Overwrite the parso version (only used sometimes).
# git+https://github.com/davidhalter/parso.git # git+https://github.com/davidhalter/parso.git
@@ -16,6 +18,8 @@ setenv =
# https://github.com/tomchristie/django-rest-framework/issues/1957 # https://github.com/tomchristie/django-rest-framework/issues/1957
# tox corrupts __pycache__, solution from here: # tox corrupts __pycache__, solution from here:
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
# Enable all warnings.
PYTHONWARNINGS=always
# To test Jedi in different versions than the same Python version, set a # To test Jedi in different versions than the same Python version, set a
# different test environment. # different test environment.
env27: JEDI_TEST_ENVIRONMENT=27 env27: JEDI_TEST_ENVIRONMENT=27