210 lines
6.7 KiB
Python
210 lines
6.7 KiB
Python
import os
|
|
import sys
|
|
|
|
from jedi._compatibility import exec_function, unicode
|
|
from jedi.parser import representation as pr
|
|
from jedi.parser import Parser
|
|
from jedi.evaluate.cache import memoize_default
|
|
from jedi import debug
|
|
from jedi import common
|
|
|
|
|
|
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)
|
|
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')
|
|
if p not in sys_path:
|
|
sys_path.insert(0, p)
|
|
|
|
check_virtual_env(sys.path)
|
|
return [p for p in sys.path if 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.')
|
|
return None
|
|
try:
|
|
res = variables['result']
|
|
if isinstance(res, str):
|
|
return os.path.abspath(res)
|
|
else:
|
|
return None
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
def _paths_from_assignment(statement):
|
|
"""
|
|
extracts the assigned strings from an assignment that looks as follows::
|
|
|
|
>>> sys.path[0:0] = ['module/path', 'another/module/path']
|
|
"""
|
|
|
|
names = statement.get_defined_names()
|
|
if len(names) != 1:
|
|
return []
|
|
if [unicode(x) for x in names[0].names] != ['sys', 'path']:
|
|
return []
|
|
expressions = statement.expression_list()
|
|
if len(expressions) != 1 or not isinstance(expressions[0], pr.Array):
|
|
return
|
|
stmts = (s for s in expressions[0].values if isinstance(s, pr.Statement))
|
|
expression_lists = (s.expression_list() for s in stmts)
|
|
return [e.value for exprs in expression_lists for e in exprs
|
|
if isinstance(e, pr.Literal) and e.value]
|
|
|
|
|
|
def _paths_from_insert(module_path, exe):
|
|
""" extract the inserted module path from an "sys.path.insert" statement
|
|
"""
|
|
exe_type, exe.type = exe.type, pr.Array.NOARRAY
|
|
exe_pop = exe.values.pop(0)
|
|
res = _execute_code(module_path, exe.get_code())
|
|
exe.type = exe_type
|
|
exe.values.insert(0, exe_pop)
|
|
return res
|
|
|
|
|
|
def _paths_from_call_expression(module_path, call):
|
|
""" extract the path from either "sys.path.append" or "sys.path.insert" """
|
|
if call.execution is None:
|
|
return
|
|
n = call.name
|
|
if not isinstance(n, pr.Name) or len(n.names) != 3:
|
|
return
|
|
names = [unicode(x) for x in n.names]
|
|
if names[:2] != ['sys', 'path']:
|
|
return
|
|
cmd = names[2]
|
|
exe = call.execution
|
|
if cmd == 'insert' and len(exe) == 2:
|
|
path = _paths_from_insert(module_path, exe)
|
|
elif cmd == 'append' and len(exe) == 1:
|
|
path = _execute_code(module_path, exe.get_code())
|
|
return path and [path] or []
|
|
|
|
|
|
def _check_module(module):
|
|
try:
|
|
possible_stmts = module.used_names['path']
|
|
except KeyError:
|
|
return get_sys_path()
|
|
sys_path = list(get_sys_path()) # copy
|
|
statements = (p for p in possible_stmts if isinstance(p, pr.Statement))
|
|
for stmt in statements:
|
|
expressions = stmt.expression_list()
|
|
if len(expressions) == 1 and isinstance(expressions[0], pr.Call):
|
|
sys_path.extend(
|
|
_paths_from_call_expression(module.path, expressions[0]) or [])
|
|
elif (
|
|
hasattr(stmt, 'assignment_details') and
|
|
len(stmt.assignment_details) == 1
|
|
):
|
|
sys_path.extend(_paths_from_assignment(stmt) or [])
|
|
return sys_path
|
|
|
|
|
|
@memoize_default(evaluator_is_first_arg=True)
|
|
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))
|
|
|
|
result = _check_module(module)
|
|
result += _detect_django_path(module.path)
|
|
# buildout scripts often contain the same sys.path modifications
|
|
# the set here is used to avoid duplicate sys.path entries
|
|
buildout_paths = set()
|
|
for module_path in _get_buildout_scripts(module.path):
|
|
try:
|
|
with open(module_path, 'rb') as f:
|
|
source = f.read()
|
|
except IOError:
|
|
pass
|
|
else:
|
|
p = Parser(common.source_to_unicode(source), module_path)
|
|
for path in _check_module(p.module):
|
|
if path not in buildout_paths:
|
|
buildout_paths.add(path)
|
|
result.append(path)
|
|
# cleanup, back to old directory
|
|
os.chdir(curdir)
|
|
return list(result)
|
|
|
|
|
|
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
|