forked from VimPlug/jedi
Merge dev into linter.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
jedi/_compatibility.py
|
jedi/_compatibility.py
|
||||||
|
jedi/evaluate/site.py
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
language: python
|
language: python
|
||||||
|
sudo: false
|
||||||
env:
|
env:
|
||||||
- TOXENV=py26
|
- TOXENV=py26
|
||||||
- TOXENV=py27
|
- TOXENV=py27
|
||||||
|
|||||||
@@ -33,5 +33,6 @@ Savor d'Isavano (@KenetJervet) <newelevenken@163.com>
|
|||||||
Phillip Berndt (@phillipberndt) <phillip.berndt@gmail.com>
|
Phillip Berndt (@phillipberndt) <phillip.berndt@gmail.com>
|
||||||
Ian Lee (@IanLee1521) <IanLee1521@gmail.com>
|
Ian Lee (@IanLee1521) <IanLee1521@gmail.com>
|
||||||
Farkhad Khatamov (@hatamov) <comsgn@gmail.com>
|
Farkhad Khatamov (@hatamov) <comsgn@gmail.com>
|
||||||
|
Kevin Kelley (@kelleyk) <kelleyk@kelleyk.net>
|
||||||
|
|
||||||
Note: (@user) means a github user name.
|
Note: (@user) means a github user name.
|
||||||
|
|||||||
25
conftest.py
25
conftest.py
@@ -1,8 +1,9 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import jedi
|
import pytest
|
||||||
|
|
||||||
|
import jedi
|
||||||
|
|
||||||
collect_ignore = ["setup.py"]
|
collect_ignore = ["setup.py"]
|
||||||
|
|
||||||
@@ -47,3 +48,25 @@ def pytest_unconfigure(config):
|
|||||||
global jedi_cache_directory_orig, jedi_cache_directory_temp
|
global jedi_cache_directory_orig, jedi_cache_directory_temp
|
||||||
jedi.settings.cache_directory = jedi_cache_directory_orig
|
jedi.settings.cache_directory = jedi_cache_directory_orig
|
||||||
shutil.rmtree(jedi_cache_directory_temp)
|
shutil.rmtree(jedi_cache_directory_temp)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def clean_jedi_cache(request):
|
||||||
|
"""
|
||||||
|
Set `jedi.settings.cache_directory` to a temporary directory during test.
|
||||||
|
|
||||||
|
Note that you can't use built-in `tmpdir` and `monkeypatch`
|
||||||
|
fixture here because their scope is 'function', which is not used
|
||||||
|
in 'session' scope fixture.
|
||||||
|
|
||||||
|
This fixture is activated in ../pytest.ini.
|
||||||
|
"""
|
||||||
|
from jedi import settings
|
||||||
|
old = settings.cache_directory
|
||||||
|
tmp = tempfile.mkdtemp(prefix='jedi-test-')
|
||||||
|
settings.cache_directory = tmp
|
||||||
|
|
||||||
|
@request.addfinalizer
|
||||||
|
def restore():
|
||||||
|
settings.cache_directory = old
|
||||||
|
shutil.rmtree(tmp)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def find_module_py33(string, path=None):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# See #491. Importlib might raise a ValueError, to avoid this, we
|
# See #491. Importlib might raise a ValueError, to avoid this, we
|
||||||
# just raise an ImportError to fix the issue.
|
# just raise an ImportError to fix the issue.
|
||||||
raise ImportError("Originally ValueError: " + e.message)
|
raise ImportError("Originally ValueError: " + str(e))
|
||||||
|
|
||||||
if loader is None:
|
if loader is None:
|
||||||
raise ImportError("Couldn't find a loader for {0}".format(string))
|
raise ImportError("Couldn't find a loader for {0}".format(string))
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from jedi.evaluate.param import try_iter_content
|
|||||||
from jedi.evaluate.cache import memoize_default
|
from jedi.evaluate.cache import memoize_default
|
||||||
from jedi.evaluate.helpers import FakeName, get_module_names
|
from jedi.evaluate.helpers import FakeName, get_module_names
|
||||||
from jedi.evaluate.finder import global_names_dict_generator, filter_definition_names
|
from jedi.evaluate.finder import global_names_dict_generator, filter_definition_names
|
||||||
|
from jedi.evaluate.sys_path import get_venv_path
|
||||||
|
|
||||||
# Jedi uses lots and lots of recursion. By setting this a little bit higher, we
|
# Jedi uses lots and lots of recursion. By setting this a little bit higher, we
|
||||||
# can remove some "maximum recursion depth" errors.
|
# can remove some "maximum recursion depth" errors.
|
||||||
@@ -58,6 +59,18 @@ class Script(object):
|
|||||||
You can either use the ``source`` parameter or ``path`` to read a file.
|
You can either use the ``source`` parameter or ``path`` to read a file.
|
||||||
Usually you're going to want to use both of them (in an editor).
|
Usually you're going to want to use both of them (in an editor).
|
||||||
|
|
||||||
|
The script might be analyzed in a different ``sys.path`` than |jedi|:
|
||||||
|
|
||||||
|
- if `sys_path` parameter is not ``None``, it will be used as ``sys.path``
|
||||||
|
for the script;
|
||||||
|
|
||||||
|
- if `sys_path` parameter is ``None`` and ``VIRTUAL_ENV`` environment
|
||||||
|
variable is defined, ``sys.path`` for the specified environment will be
|
||||||
|
guessed (see :func:`jedi.evaluate.sys_path.get_venv_path`) and used for
|
||||||
|
the script;
|
||||||
|
|
||||||
|
- otherwise ``sys.path`` will match that of |jedi|.
|
||||||
|
|
||||||
:param source: The source code of the current file, separated by newlines.
|
:param source: The source code of the current file, separated by newlines.
|
||||||
:type source: str
|
:type source: str
|
||||||
:param line: The line to perform actions on (starting with 1).
|
:param line: The line to perform actions on (starting with 1).
|
||||||
@@ -73,9 +86,13 @@ class Script(object):
|
|||||||
:param source_encoding: The encoding of ``source``, if it is not a
|
:param source_encoding: The encoding of ``source``, if it is not a
|
||||||
``unicode`` object (default ``'utf-8'``).
|
``unicode`` object (default ``'utf-8'``).
|
||||||
:type encoding: str
|
:type encoding: str
|
||||||
|
:param sys_path: ``sys.path`` to use during analysis of the script
|
||||||
|
:type sys_path: list
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, source=None, line=None, column=None, path=None,
|
def __init__(self, source=None, line=None, column=None, path=None,
|
||||||
encoding='utf-8', source_path=None, source_encoding=None):
|
encoding='utf-8', source_path=None, source_encoding=None,
|
||||||
|
sys_path=None):
|
||||||
if source_path is not None:
|
if source_path is not None:
|
||||||
warnings.warn("Use path instead of source_path.", DeprecationWarning)
|
warnings.warn("Use path instead of source_path.", DeprecationWarning)
|
||||||
path = source_path
|
path = source_path
|
||||||
@@ -109,7 +126,11 @@ class Script(object):
|
|||||||
self._parser = UserContextParser(self._grammar, self.source, path,
|
self._parser = UserContextParser(self._grammar, self.source, path,
|
||||||
self._pos, self._user_context,
|
self._pos, self._user_context,
|
||||||
self._parsed_callback)
|
self._parsed_callback)
|
||||||
self._evaluator = Evaluator(self._grammar)
|
if sys_path is None:
|
||||||
|
venv = os.getenv('VIRTUAL_ENV')
|
||||||
|
if venv:
|
||||||
|
sys_path = list(get_venv_path(venv))
|
||||||
|
self._evaluator = Evaluator(self._grammar, sys_path=sys_path)
|
||||||
debug.speed('init')
|
debug.speed('init')
|
||||||
|
|
||||||
def _parsed_callback(self, parser):
|
def _parsed_callback(self, parser):
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ class BaseDefinition(object):
|
|||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
followed = followed[0] # only check the first one.
|
followed = followed[0] # only check the first one.
|
||||||
|
|
||||||
if followed.type == 'funcdef':
|
if followed.type in ('funcdef', 'lambda'):
|
||||||
if isinstance(followed, er.InstanceElement):
|
if isinstance(followed, er.InstanceElement):
|
||||||
params = followed.params[1:]
|
params = followed.params[1:]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ that are not used are just being ignored.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
import sys
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from jedi.parser import tree
|
from jedi.parser import tree
|
||||||
@@ -79,7 +80,7 @@ from jedi.evaluate import helpers
|
|||||||
|
|
||||||
|
|
||||||
class Evaluator(object):
|
class Evaluator(object):
|
||||||
def __init__(self, grammar):
|
def __init__(self, grammar, sys_path=None):
|
||||||
self.grammar = grammar
|
self.grammar = grammar
|
||||||
self.memoize_cache = {} # for memoize decorators
|
self.memoize_cache = {} # for memoize decorators
|
||||||
# To memorize modules -> equals `sys.modules`.
|
# To memorize modules -> equals `sys.modules`.
|
||||||
@@ -91,6 +92,14 @@ class Evaluator(object):
|
|||||||
self.predefined_if_name_dict_dict = {}
|
self.predefined_if_name_dict_dict = {}
|
||||||
self.is_analysis = False
|
self.is_analysis = False
|
||||||
|
|
||||||
|
if sys_path is None:
|
||||||
|
sys_path = sys.path
|
||||||
|
self.sys_path = copy.copy(sys_path)
|
||||||
|
try:
|
||||||
|
self.sys_path.remove('')
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def wrap(self, element):
|
def wrap(self, element):
|
||||||
if isinstance(element, tree.Class):
|
if isinstance(element, tree.Class):
|
||||||
return er.Class(self, element)
|
return er.Class(self, element)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from functools import partial
|
|||||||
from jedi._compatibility import builtins as _builtins, unicode
|
from jedi._compatibility import builtins as _builtins, unicode
|
||||||
from jedi import debug
|
from jedi import debug
|
||||||
from jedi.cache import underscore_memoization, memoize_method
|
from jedi.cache import underscore_memoization, memoize_method
|
||||||
from jedi.evaluate.sys_path import get_sys_path
|
|
||||||
from jedi.parser.tree import Param, Base, Operator, zero_position_modifier
|
from jedi.parser.tree import Param, Base, Operator, zero_position_modifier
|
||||||
from jedi.evaluate.helpers import FakeName
|
from jedi.evaluate.helpers import FakeName
|
||||||
from . import fake
|
from . import fake
|
||||||
@@ -309,15 +308,12 @@ class CompiledName(FakeName):
|
|||||||
pass # Just ignore this, FakeName tries to overwrite the parent attribute.
|
pass # Just ignore this, FakeName tries to overwrite the parent attribute.
|
||||||
|
|
||||||
|
|
||||||
def dotted_from_fs_path(fs_path, sys_path=None):
|
def dotted_from_fs_path(fs_path, sys_path):
|
||||||
"""
|
"""
|
||||||
Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e.
|
Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e.
|
||||||
compares the path with sys.path and then returns the dotted_path. If the
|
compares the path with sys.path and then returns the dotted_path. If the
|
||||||
path is not in the sys.path, just returns None.
|
path is not in the sys.path, just returns None.
|
||||||
"""
|
"""
|
||||||
if sys_path is None:
|
|
||||||
sys_path = get_sys_path()
|
|
||||||
|
|
||||||
if os.path.basename(fs_path).startswith('__init__.'):
|
if os.path.basename(fs_path).startswith('__init__.'):
|
||||||
# We are calculating the path. __init__ files are not interesting.
|
# We are calculating the path. __init__ files are not interesting.
|
||||||
fs_path = os.path.dirname(fs_path)
|
fs_path = os.path.dirname(fs_path)
|
||||||
@@ -341,13 +337,13 @@ def dotted_from_fs_path(fs_path, sys_path=None):
|
|||||||
return _path_re.sub('', fs_path[len(path):].lstrip(os.path.sep)).replace(os.path.sep, '.')
|
return _path_re.sub('', fs_path[len(path):].lstrip(os.path.sep)).replace(os.path.sep, '.')
|
||||||
|
|
||||||
|
|
||||||
def load_module(path=None, name=None):
|
def load_module(evaluator, path=None, name=None):
|
||||||
|
sys_path = evaluator.sys_path
|
||||||
if path is not None:
|
if path is not None:
|
||||||
dotted_path = dotted_from_fs_path(path)
|
dotted_path = dotted_from_fs_path(path, sys_path=sys_path)
|
||||||
else:
|
else:
|
||||||
dotted_path = name
|
dotted_path = name
|
||||||
|
|
||||||
sys_path = get_sys_path()
|
|
||||||
if dotted_path is None:
|
if dotted_path is None:
|
||||||
p, _, dotted_path = path.partition(os.path.sep)
|
p, _, dotted_path = path.partition(os.path.sep)
|
||||||
sys_path.insert(0, p)
|
sys_path.insert(0, p)
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ class Importer(object):
|
|||||||
module_file.close()
|
module_file.close()
|
||||||
|
|
||||||
if module_file is None and not module_path.endswith('.py'):
|
if module_file is None and not module_path.endswith('.py'):
|
||||||
module = compiled.load_module(module_path)
|
module = compiled.load_module(self._evaluator, module_path)
|
||||||
else:
|
else:
|
||||||
module = _load_module(self._evaluator, module_path, source, sys_path)
|
module = _load_module(self._evaluator, module_path, source, sys_path)
|
||||||
|
|
||||||
@@ -440,12 +440,15 @@ def _load_module(evaluator, path=None, source=None, sys_path=None):
|
|||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
source = f.read()
|
source = f.read()
|
||||||
else:
|
else:
|
||||||
return compiled.load_module(path)
|
return compiled.load_module(evaluator, path)
|
||||||
p = path
|
p = path
|
||||||
p = fast.FastParser(evaluator.grammar, common.source_to_unicode(source), p)
|
p = fast.FastParser(evaluator.grammar, common.source_to_unicode(source), p)
|
||||||
cache.save_parser(path, p)
|
cache.save_parser(path, p)
|
||||||
return p.module
|
return p.module
|
||||||
|
|
||||||
|
if sys_path is None:
|
||||||
|
sys_path = evaluator.sys_path
|
||||||
|
|
||||||
cached = cache.load_parser(path)
|
cached = cache.load_parser(path)
|
||||||
module = load(source) if cached is None else cached.module
|
module = load(source) if cached is None else cached.module
|
||||||
module = evaluator.wrap(module)
|
module = evaluator.wrap(module)
|
||||||
|
|||||||
110
jedi/evaluate/site.py
Normal file
110
jedi/evaluate/site.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""An adapted copy of relevant site-packages functionality from Python stdlib.
|
||||||
|
|
||||||
|
This file contains some functions related to handling site-packages in Python
|
||||||
|
with jedi-specific modifications:
|
||||||
|
|
||||||
|
- the functions operate on sys_path argument rather than global sys.path
|
||||||
|
|
||||||
|
- in .pth files "import ..." lines that allow execution of arbitrary code are
|
||||||
|
skipped to prevent code injection into jedi interpreter
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
|
||||||
|
# 2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def makepath(*paths):
|
||||||
|
dir = os.path.join(*paths)
|
||||||
|
try:
|
||||||
|
dir = os.path.abspath(dir)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return dir, os.path.normcase(dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _init_pathinfo(sys_path):
|
||||||
|
"""Return a set containing all existing directory entries from sys_path"""
|
||||||
|
d = set()
|
||||||
|
for dir in sys_path:
|
||||||
|
try:
|
||||||
|
if os.path.isdir(dir):
|
||||||
|
dir, dircase = makepath(dir)
|
||||||
|
d.add(dircase)
|
||||||
|
except TypeError:
|
||||||
|
continue
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def addpackage(sys_path, sitedir, name, known_paths):
|
||||||
|
"""Process a .pth file within the site-packages directory:
|
||||||
|
For each line in the file, either combine it with sitedir to a path
|
||||||
|
and add that to known_paths, or execute it if it starts with 'import '.
|
||||||
|
"""
|
||||||
|
if known_paths is None:
|
||||||
|
known_paths = _init_pathinfo(sys_path)
|
||||||
|
reset = 1
|
||||||
|
else:
|
||||||
|
reset = 0
|
||||||
|
fullname = os.path.join(sitedir, name)
|
||||||
|
try:
|
||||||
|
f = open(fullname, "r")
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
with f:
|
||||||
|
for n, line in enumerate(f):
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if line.startswith(("import ", "import\t")):
|
||||||
|
# Change by immerrr: don't evaluate import lines to prevent
|
||||||
|
# code injection into jedi through pth files.
|
||||||
|
#
|
||||||
|
# exec(line)
|
||||||
|
continue
|
||||||
|
line = line.rstrip()
|
||||||
|
dir, dircase = makepath(sitedir, line)
|
||||||
|
if not dircase in known_paths and os.path.exists(dir):
|
||||||
|
sys_path.append(dir)
|
||||||
|
known_paths.add(dircase)
|
||||||
|
except Exception:
|
||||||
|
print("Error processing line {:d} of {}:\n".format(n+1, fullname),
|
||||||
|
file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
for record in traceback.format_exception(*sys.exc_info()):
|
||||||
|
for line in record.splitlines():
|
||||||
|
print(' '+line, file=sys.stderr)
|
||||||
|
print("\nRemainder of file ignored", file=sys.stderr)
|
||||||
|
break
|
||||||
|
if reset:
|
||||||
|
known_paths = None
|
||||||
|
return known_paths
|
||||||
|
|
||||||
|
|
||||||
|
def addsitedir(sys_path, sitedir, known_paths=None):
|
||||||
|
"""Add 'sitedir' argument to sys_path if missing and handle .pth files in
|
||||||
|
'sitedir'"""
|
||||||
|
if known_paths is None:
|
||||||
|
known_paths = _init_pathinfo(sys_path)
|
||||||
|
reset = 1
|
||||||
|
else:
|
||||||
|
reset = 0
|
||||||
|
sitedir, sitedircase = makepath(sitedir)
|
||||||
|
if not sitedircase in known_paths:
|
||||||
|
sys_path.append(sitedir) # Add path component
|
||||||
|
known_paths.add(sitedircase)
|
||||||
|
try:
|
||||||
|
names = os.listdir(sitedir)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
names = [name for name in names if name.endswith(".pth")]
|
||||||
|
for name in sorted(names):
|
||||||
|
addpackage(sys_path, sitedir, name, known_paths)
|
||||||
|
if reset:
|
||||||
|
known_paths = None
|
||||||
|
return known_paths
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from jedi.evaluate.site import addsitedir
|
||||||
|
|
||||||
from jedi._compatibility import exec_function, unicode
|
from jedi._compatibility import exec_function, unicode
|
||||||
from jedi.parser import tree
|
from jedi.parser import tree
|
||||||
@@ -11,24 +12,51 @@ from jedi import common
|
|||||||
from jedi import cache
|
from jedi import cache
|
||||||
|
|
||||||
|
|
||||||
def get_sys_path():
|
def get_venv_path(venv):
|
||||||
def check_virtual_env(sys_path):
|
"""Get sys.path for specified virtual environment."""
|
||||||
""" Add virtualenv's site-packages to the `sys.path`."""
|
sys_path = _get_venv_path_dirs(venv)
|
||||||
venv = os.getenv('VIRTUAL_ENV')
|
with common.ignored(ValueError):
|
||||||
if not venv:
|
sys_path.remove('')
|
||||||
return
|
sys_path = _get_sys_path_with_egglinks(sys_path)
|
||||||
venv = os.path.abspath(venv)
|
# As of now, get_venv_path_dirs does not scan built-in pythonpath and
|
||||||
p = _get_venv_sitepackages(venv)
|
# user-local site-packages, let's approximate them using path from Jedi
|
||||||
if p not in sys_path:
|
# interpreter.
|
||||||
sys_path.insert(0, p)
|
return sys_path + sys.path
|
||||||
|
|
||||||
# Add all egg-links from the virtualenv.
|
|
||||||
for egg_link in glob.glob(os.path.join(p, '*.egg-link')):
|
def _get_sys_path_with_egglinks(sys_path):
|
||||||
|
"""Find all paths including those referenced by egg-links.
|
||||||
|
|
||||||
|
Egg-link-referenced directories are inserted into path immediately after
|
||||||
|
the directory on which their links were found. Such directories are not
|
||||||
|
taken into consideration by normal import mechanism, but they are traversed
|
||||||
|
when doing pkg_resources.require.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for p in sys_path:
|
||||||
|
result.append(p)
|
||||||
|
# pkg_resources does not define a specific order for egg-link files
|
||||||
|
# using os.listdir to enumerate them, we're sorting them to have
|
||||||
|
# reproducible tests.
|
||||||
|
for egg_link in sorted(glob.glob(os.path.join(p, '*.egg-link'))):
|
||||||
with open(egg_link) as fd:
|
with open(egg_link) as fd:
|
||||||
sys_path.insert(0, fd.readline().rstrip())
|
for line in fd:
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
result.append(os.path.join(p, line))
|
||||||
|
# pkg_resources package only interprets the first
|
||||||
|
# non-empty line in egg-link files.
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
check_virtual_env(sys.path)
|
|
||||||
return [p for p in sys.path if p != ""]
|
def _get_venv_path_dirs(venv):
|
||||||
|
"""Get sys.path for venv without starting up the interpreter."""
|
||||||
|
venv = os.path.abspath(venv)
|
||||||
|
sitedir = _get_venv_sitepackages(venv)
|
||||||
|
sys_path = []
|
||||||
|
addsitedir(sys_path, sitedir)
|
||||||
|
return sys_path
|
||||||
|
|
||||||
|
|
||||||
def _get_venv_sitepackages(venv):
|
def _get_venv_sitepackages(venv):
|
||||||
@@ -109,7 +137,6 @@ def _paths_from_list_modifications(module_path, trailer1, trailer2):
|
|||||||
name = trailer1.children[1].value
|
name = trailer1.children[1].value
|
||||||
if name not in ['insert', 'append']:
|
if name not in ['insert', 'append']:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
arg = trailer2.children[1]
|
arg = trailer2.children[1]
|
||||||
if name == 'insert' and len(arg.children) in (3, 4): # Possible trailing comma.
|
if name == 'insert' and len(arg.children) in (3, 4): # Possible trailing comma.
|
||||||
arg = arg.children[2]
|
arg = arg.children[2]
|
||||||
@@ -117,6 +144,9 @@ def _paths_from_list_modifications(module_path, trailer1, trailer2):
|
|||||||
|
|
||||||
|
|
||||||
def _check_module(evaluator, module):
|
def _check_module(evaluator, module):
|
||||||
|
"""
|
||||||
|
Detect sys.path modifications within module.
|
||||||
|
"""
|
||||||
def get_sys_path_powers(names):
|
def get_sys_path_powers(names):
|
||||||
for name in names:
|
for name in names:
|
||||||
power = name.parent.parent
|
power = name.parent.parent
|
||||||
@@ -128,10 +158,12 @@ def _check_module(evaluator, module):
|
|||||||
if isinstance(n, tree.Name) and n.value == 'path':
|
if isinstance(n, tree.Name) and n.value == 'path':
|
||||||
yield name, power
|
yield name, power
|
||||||
|
|
||||||
sys_path = list(get_sys_path()) # copy
|
sys_path = list(evaluator.sys_path) # copy
|
||||||
try:
|
try:
|
||||||
possible_names = module.used_names['path']
|
possible_names = module.used_names['path']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
# module.used_names is MergedNamesDict whose getitem never throws
|
||||||
|
# keyerror, this is superfluous.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for name, power in get_sys_path_powers(possible_names):
|
for name, power in get_sys_path_powers(possible_names):
|
||||||
@@ -148,7 +180,7 @@ def sys_path_with_modifications(evaluator, module):
|
|||||||
if module.path is None:
|
if module.path is None:
|
||||||
# Support for modules without a path is bad, therefore return the
|
# Support for modules without a path is bad, therefore return the
|
||||||
# normal path.
|
# normal path.
|
||||||
return list(get_sys_path())
|
return list(evaluator.sys_path)
|
||||||
|
|
||||||
curdir = os.path.abspath(os.curdir)
|
curdir = os.path.abspath(os.curdir)
|
||||||
with common.ignored(OSError):
|
with common.ignored(OSError):
|
||||||
|
|||||||
@@ -825,6 +825,15 @@ def _create_params(parent, argslist_list):
|
|||||||
class Function(ClassOrFunc):
|
class Function(ClassOrFunc):
|
||||||
"""
|
"""
|
||||||
Used to store the parsed contents of a python function.
|
Used to store the parsed contents of a python function.
|
||||||
|
|
||||||
|
Children:
|
||||||
|
0) <Keyword: def>
|
||||||
|
1) <Name>
|
||||||
|
2) parameter list (including open-paren and close-paren <Operator>s)
|
||||||
|
3) <Operator: :>
|
||||||
|
4) Node() representing function body
|
||||||
|
5) ??
|
||||||
|
6) annotation (if present)
|
||||||
"""
|
"""
|
||||||
__slots__ = ('listeners',)
|
__slots__ = ('listeners',)
|
||||||
type = 'funcdef'
|
type = 'funcdef'
|
||||||
@@ -837,6 +846,7 @@ class Function(ClassOrFunc):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self):
|
def params(self):
|
||||||
|
# Contents of parameter lit minus the leading <Operator: (> and the trailing <Operator: )>.
|
||||||
return self.children[2].children[1:-1]
|
return self.children[2].children[1:-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -868,10 +878,13 @@ class Function(ClassOrFunc):
|
|||||||
|
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
func_name = func_name or self.children[1]
|
func_name = func_name or self.name
|
||||||
code = unicode(func_name) + self.children[2].get_code()
|
code = unicode(func_name) + self._get_paramlist_code()
|
||||||
return '\n'.join(textwrap.wrap(code, width))
|
return '\n'.join(textwrap.wrap(code, width))
|
||||||
|
|
||||||
|
def _get_paramlist_code(self):
|
||||||
|
return self.children[2].get_code()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def doc(self):
|
def doc(self):
|
||||||
""" Return a document string including call signature. """
|
""" Return a document string including call signature. """
|
||||||
@@ -897,6 +910,12 @@ class Function(ClassOrFunc):
|
|||||||
class Lambda(Function):
|
class Lambda(Function):
|
||||||
"""
|
"""
|
||||||
Lambdas are basically trimmed functions, so give it the same interface.
|
Lambdas are basically trimmed functions, so give it the same interface.
|
||||||
|
|
||||||
|
Children:
|
||||||
|
0) <Keyword: lambda>
|
||||||
|
*) <Param x> for each argument x
|
||||||
|
-2) <Operator: :>
|
||||||
|
-1) Node() representing body
|
||||||
"""
|
"""
|
||||||
type = 'lambda'
|
type = 'lambda'
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
@@ -905,9 +924,17 @@ class Lambda(Function):
|
|||||||
# We don't want to call the Function constructor, call its parent.
|
# We don't want to call the Function constructor, call its parent.
|
||||||
super(Function, self).__init__(children)
|
super(Function, self).__init__(children)
|
||||||
self.listeners = set() # not used here, but in evaluation.
|
self.listeners = set() # not used here, but in evaluation.
|
||||||
lst = self.children[1:-2] # After `def foo`
|
lst = self.children[1:-2] # Everything between `lambda` and the `:` operator is a parameter.
|
||||||
self.children[1:-2] = _create_params(self, lst)
|
self.children[1:-2] = _create_params(self, lst)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# Borrow the position of the <Keyword: lambda> AST node.
|
||||||
|
return Name(self.children[0].position_modifier, '<lambda>', self.children[0].start_pos)
|
||||||
|
|
||||||
|
def _get_paramlist_code(self):
|
||||||
|
return '(' + ''.join(param.get_code() for param in self.params).strip() + ')'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self):
|
def params(self):
|
||||||
return self.children[1:-2]
|
return self.children[1:-2]
|
||||||
@@ -915,6 +942,7 @@ class Lambda(Function):
|
|||||||
def is_generator(self):
|
def is_generator(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
def yields(self):
|
def yields(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
addopts = --doctest-modules
|
addopts = --doctest-modules
|
||||||
|
|
||||||
# Ignore broken files in blackbox test directories
|
# Ignore broken files in blackbox test directories
|
||||||
norecursedirs = .* docs completion refactor absolute_import namespace_package scripts extensions speed static_analysis not_in_sys_path buildout_project egg-link init_extension_module
|
norecursedirs = .* docs completion refactor absolute_import namespace_package scripts extensions speed static_analysis not_in_sys_path buildout_project sample_venvs init_extension_module
|
||||||
|
|
||||||
# Activate `clean_jedi_cache` fixture for all tests. This should be
|
# Activate `clean_jedi_cache` fixture for all tests. This should be
|
||||||
# fine as long as we are using `clean_jedi_cache` as a session scoped
|
# fine as long as we are using `clean_jedi_cache` as a session scoped
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import re
|
import re
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -125,25 +123,3 @@ def isolated_jedi_cache(monkeypatch, tmpdir):
|
|||||||
"""
|
"""
|
||||||
from jedi import settings
|
from jedi import settings
|
||||||
monkeypatch.setattr(settings, 'cache_directory', str(tmpdir))
|
monkeypatch.setattr(settings, 'cache_directory', str(tmpdir))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
|
||||||
def clean_jedi_cache(request):
|
|
||||||
"""
|
|
||||||
Set `jedi.settings.cache_directory` to a temporary directory during test.
|
|
||||||
|
|
||||||
Note that you can't use built-in `tmpdir` and `monkeypatch`
|
|
||||||
fixture here because their scope is 'function', which is not used
|
|
||||||
in 'session' scope fixture.
|
|
||||||
|
|
||||||
This fixture is activated in ../pytest.ini.
|
|
||||||
"""
|
|
||||||
from jedi import settings
|
|
||||||
old = settings.cache_directory
|
|
||||||
tmp = tempfile.mkdtemp(prefix='jedi-test-')
|
|
||||||
settings.cache_directory = tmp
|
|
||||||
|
|
||||||
@request.addfinalizer
|
|
||||||
def restore():
|
|
||||||
settings.cache_directory = old
|
|
||||||
shutil.rmtree(tmp)
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This file is here to force git to create the directory, as *.pth files only
|
||||||
|
# add existing directories.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
./dir-from-foo-pth
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import smth; smth.extend_path()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
./relative/egg-link/path
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
sys.path.append('/path/from/smth.py')
|
||||||
|
|
||||||
|
|
||||||
|
def extend_path():
|
||||||
|
sys.path.append('/path/from/smth.py:extend_path')
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# This file is here to force git to create the directory, as *.pth files only
|
||||||
|
# add existing directories.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/path/from/egg-link
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
./dir-from-foo-pth
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import smth; smth.extend_path()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
./relative/egg-link/path
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
sys.path.append('/path/from/smth.py')
|
||||||
|
|
||||||
|
|
||||||
|
def extend_path():
|
||||||
|
sys.path.append('/path/from/smth.py:extend_path')
|
||||||
@@ -46,11 +46,8 @@ def test_flask_ext(script, name):
|
|||||||
"""flask.ext.foo is really imported from flaskext.foo or flask_foo.
|
"""flask.ext.foo is really imported from flaskext.foo or flask_foo.
|
||||||
"""
|
"""
|
||||||
path = os.path.join(os.path.dirname(__file__), 'flask-site-packages')
|
path = os.path.join(os.path.dirname(__file__), 'flask-site-packages')
|
||||||
sys.path.append(path)
|
completions = jedi.Script(script, sys_path=[path]).completions()
|
||||||
try:
|
assert name in [c.name for c in completions]
|
||||||
assert name in [c.name for c in jedi.Script(script).completions()]
|
|
||||||
finally:
|
|
||||||
sys.path.remove(path)
|
|
||||||
|
|
||||||
|
|
||||||
@cwd_at('test/test_evaluate/')
|
@cwd_at('test/test_evaluate/')
|
||||||
@@ -64,3 +61,19 @@ def test_import_unique():
|
|||||||
defs = jedi.Script(src, path='example.py').goto_definitions()
|
defs = jedi.Script(src, path='example.py').goto_definitions()
|
||||||
defs = [d._definition for d in defs]
|
defs = [d._definition for d in defs]
|
||||||
assert len(defs) == len(set(defs))
|
assert len(defs) == len(set(defs))
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_works_with_sys_path_param(tmpdir):
|
||||||
|
foo_path = tmpdir.join('foo')
|
||||||
|
bar_path = tmpdir.join('bar')
|
||||||
|
foo_path.join('module.py').write('foo = 123', ensure=True)
|
||||||
|
bar_path.join('module.py').write('bar = 123', ensure=True)
|
||||||
|
foo_completions = jedi.Script('import module; module.',
|
||||||
|
sys_path=[foo_path.strpath]).completions()
|
||||||
|
bar_completions = jedi.Script('import module; module.',
|
||||||
|
sys_path=[bar_path.strpath]).completions()
|
||||||
|
assert 'foo' in [c.name for c in foo_completions]
|
||||||
|
assert 'bar' not in [c.name for c in foo_completions]
|
||||||
|
|
||||||
|
assert 'bar' in [c.name for c in bar_completions]
|
||||||
|
assert 'foo' not in [c.name for c in bar_completions]
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import jedi
|
import jedi
|
||||||
import sys
|
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_package():
|
def test_namespace_package():
|
||||||
sys.path.insert(0, join(dirname(__file__), 'namespace_package/ns1'))
|
sys_path = [join(dirname(__file__), d)
|
||||||
sys.path.insert(1, join(dirname(__file__), 'namespace_package/ns2'))
|
for d in ['namespace_package/ns1', 'namespace_package/ns2']]
|
||||||
try:
|
|
||||||
|
def script_with_path(*args, **kwargs):
|
||||||
|
return jedi.Script(sys_path=sys_path, *args, **kwargs)
|
||||||
|
|
||||||
# goto definition
|
# goto definition
|
||||||
assert jedi.Script('from pkg import ns1_file').goto_definitions()
|
assert script_with_path('from pkg import ns1_file').goto_definitions()
|
||||||
assert jedi.Script('from pkg import ns2_file').goto_definitions()
|
assert script_with_path('from pkg import ns2_file').goto_definitions()
|
||||||
assert not jedi.Script('from pkg import ns3_file').goto_definitions()
|
assert not script_with_path('from pkg import ns3_file').goto_definitions()
|
||||||
|
|
||||||
# goto assignment
|
# goto assignment
|
||||||
tests = {
|
tests = {
|
||||||
@@ -22,12 +24,12 @@ def test_namespace_package():
|
|||||||
'from pkg import foo': 'ns1!',
|
'from pkg import foo': 'ns1!',
|
||||||
}
|
}
|
||||||
for source, solution in tests.items():
|
for source, solution in tests.items():
|
||||||
ass = jedi.Script(source).goto_assignments()
|
ass = script_with_path(source).goto_assignments()
|
||||||
assert len(ass) == 1
|
assert len(ass) == 1
|
||||||
assert ass[0].description == "foo = '%s'" % solution
|
assert ass[0].description == "foo = '%s'" % solution
|
||||||
|
|
||||||
# completion
|
# completion
|
||||||
completions = jedi.Script('from pkg import ').completions()
|
completions = script_with_path('from pkg import ').completions()
|
||||||
names = [str(c.name) for c in completions] # str because of unicode
|
names = [str(c.name) for c in completions] # str because of unicode
|
||||||
compare = ['foo', 'ns1_file', 'ns1_folder', 'ns2_folder', 'ns2_file',
|
compare = ['foo', 'ns1_file', 'ns1_folder', 'ns2_folder', 'ns2_file',
|
||||||
'pkg_resources', 'pkgutil', '__name__', '__path__',
|
'pkg_resources', 'pkgutil', '__name__', '__path__',
|
||||||
@@ -44,12 +46,8 @@ def test_namespace_package():
|
|||||||
'import pkg as x': 'ns1!',
|
'import pkg as x': 'ns1!',
|
||||||
}
|
}
|
||||||
for source, solution in tests.items():
|
for source, solution in tests.items():
|
||||||
for c in jedi.Script(source + '; x.').completions():
|
for c in script_with_path(source + '; x.').completions():
|
||||||
if c.name == 'foo':
|
if c.name == 'foo':
|
||||||
completion = c
|
completion = c
|
||||||
solution = "statement: foo = '%s'" % solution
|
solution = "statement: foo = '%s'" % solution
|
||||||
assert completion.description == solution
|
assert completion.description == solution
|
||||||
|
|
||||||
finally:
|
|
||||||
sys.path.pop(0)
|
|
||||||
sys.path.pop(0)
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
|
from glob import glob
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from jedi._compatibility import unicode
|
from jedi._compatibility import unicode
|
||||||
from jedi.parser import Parser, load_grammar
|
from jedi.parser import Parser, load_grammar
|
||||||
@@ -19,13 +23,37 @@ def test_paths_from_assignment():
|
|||||||
assert paths('sys.path, other = ["a"], 2') == set()
|
assert paths('sys.path, other = ["a"], 2') == set()
|
||||||
|
|
||||||
|
|
||||||
def test_get_sys_path(monkeypatch):
|
# Currently venv site-packages resolution only seeks pythonX.Y/site-packages
|
||||||
monkeypatch.setenv('VIRTUAL_ENV', os.path.join(os.path.dirname(__file__),
|
# that belong to the same version as the interpreter to avoid issues with
|
||||||
'egg-link', 'venv'))
|
# cross-version imports. "venvs/" dir contains "venv27" and "venv34" that
|
||||||
def sitepackages_dir(venv):
|
# mimic venvs created for py2.7 and py3.4 respectively. If test runner is
|
||||||
return os.path.join(venv, 'lib', 'python3.4', 'site-packages')
|
# invoked with one of those versions, the test below will be run for the
|
||||||
|
# matching directory.
|
||||||
|
CUR_DIR = os.path.dirname(__file__)
|
||||||
|
VENVS = list(glob(
|
||||||
|
os.path.join(CUR_DIR, 'sample_venvs/venv%d%d' % sys.version_info[:2])))
|
||||||
|
|
||||||
monkeypatch.setattr('jedi.evaluate.sys_path._get_venv_sitepackages',
|
|
||||||
sitepackages_dir)
|
|
||||||
|
|
||||||
assert '/path/from/egg-link' in sys_path.get_sys_path()
|
@pytest.mark.parametrize('venv', VENVS)
|
||||||
|
def test_get_venv_path(venv):
|
||||||
|
pjoin = os.path.join
|
||||||
|
venv_path = sys_path.get_venv_path(venv)
|
||||||
|
|
||||||
|
site_pkgs = (glob(pjoin(venv, 'lib', 'python*', 'site-packages')) +
|
||||||
|
glob(pjoin(venv, 'lib', 'site-packages')))[0]
|
||||||
|
ETALON = [
|
||||||
|
site_pkgs,
|
||||||
|
pjoin('/path', 'from', 'egg-link'),
|
||||||
|
pjoin(site_pkgs, '.', 'relative', 'egg-link', 'path'),
|
||||||
|
pjoin(site_pkgs, 'dir-from-foo-pth'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ensure that pth and egg-link paths were added.
|
||||||
|
assert venv_path[:len(ETALON)] == ETALON
|
||||||
|
|
||||||
|
# Ensure that none of venv dirs leaked to the interpreter.
|
||||||
|
assert not set(sys.path).intersection(ETALON)
|
||||||
|
|
||||||
|
# Ensure that "import ..." lines were ignored.
|
||||||
|
assert pjoin('/path', 'from', 'smth.py') not in venv_path
|
||||||
|
assert pjoin('/path', 'from', 'smth.py:extend_path') not in venv_path
|
||||||
|
|||||||
64
test/test_parser/test_parser_tree.py
Normal file
64
test/test_parser/test_parser_tree.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 # This file contains Unicode characters.
|
||||||
|
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from jedi._compatibility import u, unicode
|
||||||
|
from jedi.parser import Parser, load_grammar
|
||||||
|
from jedi.parser import tree as pt
|
||||||
|
|
||||||
|
|
||||||
|
class TestsFunctionAndLambdaParsing(object):
|
||||||
|
|
||||||
|
FIXTURES = [
|
||||||
|
('def my_function(x, y, z):\n return x + y * z\n', {
|
||||||
|
'name': 'my_function',
|
||||||
|
'call_sig': 'my_function(x, y, z)',
|
||||||
|
'params': ['x', 'y', 'z'],
|
||||||
|
}),
|
||||||
|
('lambda x, y, z: x + y * z\n', {
|
||||||
|
'name': '<lambda>',
|
||||||
|
'call_sig': '<lambda>(x, y, z)',
|
||||||
|
'params': ['x', 'y', 'z'],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.fixture(params=FIXTURES)
|
||||||
|
def node(self, request):
|
||||||
|
parsed = Parser(load_grammar(), dedent(u(request.param[0])))
|
||||||
|
request.keywords['expected'] = request.param[1]
|
||||||
|
return parsed.module.subscopes[0]
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def expected(self, request, node):
|
||||||
|
return request.keywords['expected']
|
||||||
|
|
||||||
|
def test_name(self, node, expected):
|
||||||
|
assert isinstance(node.name, pt.Name)
|
||||||
|
assert unicode(node.name) == u(expected['name'])
|
||||||
|
|
||||||
|
def test_params(self, node, expected):
|
||||||
|
assert isinstance(node.params, list)
|
||||||
|
assert all(isinstance(x, pt.Param) for x in node.params)
|
||||||
|
assert [unicode(x.name) for x in node.params] == [u(x) for x in expected['params']]
|
||||||
|
|
||||||
|
def test_is_generator(self, node, expected):
|
||||||
|
assert node.is_generator() is expected.get('is_generator', False)
|
||||||
|
|
||||||
|
def test_yields(self, node, expected):
|
||||||
|
# TODO: There's a comment in the code noting that the current implementation is incorrect. This returns an
|
||||||
|
# empty list at the moment (not e.g. False).
|
||||||
|
if expected.get('yields', False):
|
||||||
|
assert node.yields
|
||||||
|
else:
|
||||||
|
assert not node.yields
|
||||||
|
|
||||||
|
def test_annotation(self, node, expected):
|
||||||
|
assert node.annotation() is expected.get('annotation', None)
|
||||||
|
|
||||||
|
def test_get_call_signature(self, node, expected):
|
||||||
|
assert node.get_call_signature() == expected['call_sig']
|
||||||
|
|
||||||
|
def test_doc(self, node, expected):
|
||||||
|
assert node.doc == expected.get('doc') or (expected['call_sig'] + '\n\n')
|
||||||
@@ -66,8 +66,9 @@ class TestRegression(TestCase):
|
|||||||
src1 = "def r(a): return a"
|
src1 = "def r(a): return a"
|
||||||
# Other fictional modules in another place in the fs.
|
# Other fictional modules in another place in the fs.
|
||||||
src2 = 'from .. import setup; setup.r(1)'
|
src2 = 'from .. import setup; setup.r(1)'
|
||||||
imports.load_module(os.path.abspath(fname), src2)
|
script = Script(src1, path='../setup.py')
|
||||||
result = Script(src1, path='../setup.py').goto_definitions()
|
imports.load_module(script._evaluator, os.path.abspath(fname), src2)
|
||||||
|
result = script.goto_definitions()
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0].description == 'class int'
|
assert result[0].description == 'class int'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user