248 lines
8.5 KiB
Python
248 lines
8.5 KiB
Python
import glob
|
|
import os
|
|
import sys
|
|
|
|
from jedi._compatibility import exec_function, unicode
|
|
from jedi.parser import tree
|
|
from jedi.parser import Parser
|
|
from jedi.evaluate.cache import memoize_default
|
|
from jedi import debug
|
|
from jedi import common
|
|
from jedi import cache
|
|
|
|
|
|
def get_sys_path():
|
|
def check_virtual_env(sys_path):
|
|
""" Add virtualenv's site-packages to the `sys.path`."""
|
|
venv = os.getenv('VIRTUAL_ENV')
|
|
if not venv:
|
|
return
|
|
venv = os.path.abspath(venv)
|
|
p = _get_venv_sitepackages(venv)
|
|
if p not in sys_path:
|
|
sys_path.insert(0, p)
|
|
|
|
# Add all egg-links from the virtualenv.
|
|
for egg_link in glob.glob(os.path.join(p, '*.egg-link')):
|
|
with open(egg_link) as fd:
|
|
sys_path.insert(0, fd.readline().rstrip())
|
|
|
|
check_virtual_env(sys.path)
|
|
return [p for p in sys.path if p != ""]
|
|
|
|
|
|
def _get_venv_sitepackages(venv):
|
|
if os.name == 'nt':
|
|
p = os.path.join(venv, 'lib', 'site-packages')
|
|
else:
|
|
p = os.path.join(venv, 'lib', 'python%d.%d' % sys.version_info[:2],
|
|
'site-packages')
|
|
return p
|
|
|
|
|
|
def _execute_code(module_path, code):
|
|
c = "import os; from os.path import *; result=%s"
|
|
variables = {'__file__': module_path}
|
|
try:
|
|
exec_function(c % code, variables)
|
|
except Exception:
|
|
debug.warning('sys.path manipulation detected, but failed to evaluate.')
|
|
else:
|
|
try:
|
|
res = variables['result']
|
|
if isinstance(res, str):
|
|
return [os.path.abspath(res)]
|
|
except KeyError:
|
|
pass
|
|
return []
|
|
|
|
|
|
def _paths_from_assignment(evaluator, expr_stmt):
|
|
"""
|
|
Extracts the assigned strings from an assignment that looks as follows::
|
|
|
|
>>> sys.path[0:0] = ['module/path', 'another/module/path']
|
|
|
|
This function is in general pretty tolerant (and therefore 'buggy').
|
|
However, it's not a big issue usually to add more paths to Jedi's sys_path,
|
|
because it will only affect Jedi in very random situations and by adding
|
|
more paths than necessary, it usually benefits the general user.
|
|
"""
|
|
for assignee, operator in zip(expr_stmt.children[::2], expr_stmt.children[1::2]):
|
|
try:
|
|
assert operator in ['=', '+=']
|
|
assert tree.is_node(assignee, 'power') and len(assignee.children) > 1
|
|
c = assignee.children
|
|
assert c[0].type == 'name' and c[0].value == 'sys'
|
|
trailer = c[1]
|
|
assert trailer.children[0] == '.' and trailer.children[1].value == 'path'
|
|
# TODO Essentially we're not checking details on sys.path
|
|
# manipulation. Both assigment of the sys.path and changing/adding
|
|
# parts of the sys.path are the same: They get added to the current
|
|
# sys.path.
|
|
"""
|
|
execution = c[2]
|
|
assert execution.children[0] == '['
|
|
subscript = execution.children[1]
|
|
assert subscript.type == 'subscript'
|
|
assert ':' in subscript.children
|
|
"""
|
|
except AssertionError:
|
|
continue
|
|
|
|
from jedi.evaluate.iterable import get_iterator_types
|
|
from jedi.evaluate.precedence import is_string
|
|
for val in get_iterator_types(evaluator.eval_statement(expr_stmt)):
|
|
if is_string(val):
|
|
yield val.obj
|
|
|
|
|
|
def _paths_from_list_modifications(module_path, trailer1, trailer2):
|
|
""" extract the path from either "sys.path.append" or "sys.path.insert" """
|
|
# Guarantee that both are trailers, the first one a name and the second one
|
|
# a function execution with at least one param.
|
|
if not (tree.is_node(trailer1, 'trailer') and trailer1.children[0] == '.'
|
|
and tree.is_node(trailer2, 'trailer') and trailer2.children[0] == '('
|
|
and len(trailer2.children) == 3):
|
|
return []
|
|
|
|
name = trailer1.children[1].value
|
|
if name not in ['insert', 'append']:
|
|
return []
|
|
|
|
arg = trailer2.children[1]
|
|
if name == 'insert' and len(arg.children) in (3, 4): # Possible trailing comma.
|
|
arg = arg.children[2]
|
|
return _execute_code(module_path, arg.get_code())
|
|
|
|
|
|
def _check_module(evaluator, module):
|
|
def get_sys_path_powers(names):
|
|
for name in names:
|
|
power = name.parent.parent
|
|
if tree.is_node(power, 'power'):
|
|
c = power.children
|
|
if isinstance(c[0], tree.Name) and c[0].value == 'sys' \
|
|
and tree.is_node(c[1], 'trailer'):
|
|
n = c[1].children[1]
|
|
if isinstance(n, tree.Name) and n.value == 'path':
|
|
yield name, power
|
|
|
|
sys_path = list(get_sys_path()) # copy
|
|
try:
|
|
possible_names = module.used_names['path']
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
for name, power in get_sys_path_powers(possible_names):
|
|
stmt = name.get_definition()
|
|
if len(power.children) >= 4:
|
|
sys_path.extend(_paths_from_list_modifications(module.path, *power.children[2:4]))
|
|
elif name.get_definition().type == 'expr_stmt':
|
|
sys_path.extend(_paths_from_assignment(evaluator, stmt))
|
|
return sys_path
|
|
|
|
|
|
@memoize_default(evaluator_is_first_arg=True, default=[])
|
|
def sys_path_with_modifications(evaluator, module):
|
|
if module.path is None:
|
|
# Support for modules without a path is bad, therefore return the
|
|
# normal path.
|
|
return list(get_sys_path())
|
|
|
|
curdir = os.path.abspath(os.curdir)
|
|
with common.ignored(OSError):
|
|
os.chdir(os.path.dirname(module.path))
|
|
|
|
buildout_script_paths = set()
|
|
|
|
result = _check_module(evaluator, module)
|
|
result += _detect_django_path(module.path)
|
|
for buildout_script in _get_buildout_scripts(module.path):
|
|
for path in _get_paths_from_buildout_script(evaluator, buildout_script):
|
|
buildout_script_paths.add(path)
|
|
# cleanup, back to old directory
|
|
os.chdir(curdir)
|
|
return list(result) + list(buildout_script_paths)
|
|
|
|
|
|
def _get_paths_from_buildout_script(evaluator, buildout_script):
|
|
def load(buildout_script):
|
|
try:
|
|
with open(buildout_script, 'rb') as f:
|
|
source = common.source_to_unicode(f.read())
|
|
except IOError:
|
|
debug.dbg('Error trying to read buildout_script: %s', buildout_script)
|
|
return
|
|
|
|
p = Parser(evaluator.grammar, source, buildout_script)
|
|
cache.save_parser(buildout_script, p)
|
|
return p.module
|
|
|
|
cached = cache.load_parser(buildout_script)
|
|
module = cached and cached.module or load(buildout_script)
|
|
if not module:
|
|
return
|
|
|
|
for path in _check_module(evaluator, module):
|
|
yield path
|
|
|
|
|
|
def traverse_parents(path):
|
|
while True:
|
|
new = os.path.dirname(path)
|
|
if new == path:
|
|
return
|
|
path = new
|
|
yield path
|
|
|
|
|
|
def _get_parent_dir_with_file(path, filename):
|
|
for parent in traverse_parents(path):
|
|
if os.path.isfile(os.path.join(parent, filename)):
|
|
return parent
|
|
return None
|
|
|
|
|
|
def _detect_django_path(module_path):
|
|
""" Detects the path of the very well known Django library (if used) """
|
|
result = []
|
|
|
|
for parent in traverse_parents(module_path):
|
|
with common.ignored(IOError):
|
|
with open(parent + os.path.sep + 'manage.py'):
|
|
debug.dbg('Found django path: %s', module_path)
|
|
result.append(parent)
|
|
return result
|
|
|
|
|
|
def _get_buildout_scripts(module_path):
|
|
"""
|
|
if there is a 'buildout.cfg' file in one of the parent directories of the
|
|
given module it will return a list of all files in the buildout bin
|
|
directory that look like python files.
|
|
|
|
:param module_path: absolute path to the module.
|
|
:type module_path: str
|
|
"""
|
|
project_root = _get_parent_dir_with_file(module_path, 'buildout.cfg')
|
|
if not project_root:
|
|
return []
|
|
bin_path = os.path.join(project_root, 'bin')
|
|
if not os.path.exists(bin_path):
|
|
return []
|
|
extra_module_paths = []
|
|
for filename in os.listdir(bin_path):
|
|
try:
|
|
filepath = os.path.join(bin_path, filename)
|
|
with open(filepath, 'r') as f:
|
|
firstline = f.readline()
|
|
if firstline.startswith('#!') and 'python' in firstline:
|
|
extra_module_paths.append(filepath)
|
|
except IOError as e:
|
|
# either permission error or race cond. because file got deleted
|
|
# ignore
|
|
debug.warning(unicode(e))
|
|
continue
|
|
return extra_module_paths
|