532 lines
16 KiB
Python
532 lines
16 KiB
Python
"""
|
|
Imitate the parser representation.
|
|
"""
|
|
import inspect
|
|
import re
|
|
import sys
|
|
import os
|
|
from functools import partial
|
|
|
|
from jedi._compatibility import builtins as _builtins, unicode
|
|
from jedi import debug
|
|
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.evaluate.helpers import FakeName
|
|
from . import fake
|
|
|
|
|
|
_sep = os.path.sep
|
|
if os.path.altsep is not None:
|
|
_sep += os.path.altsep
|
|
_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep)))
|
|
del _sep
|
|
|
|
|
|
class CheckAttribute(object):
|
|
"""Raises an AttributeError if the attribute X isn't available."""
|
|
def __init__(self, func):
|
|
self.func = func
|
|
# Remove the py in front of e.g. py__call__.
|
|
self.check_name = func.__name__[2:]
|
|
|
|
def __get__(self, instance, owner):
|
|
# This might raise an AttributeError. That's wanted.
|
|
getattr(instance.obj, self.check_name)
|
|
return partial(self.func, instance)
|
|
|
|
|
|
class CompiledObject(Base):
|
|
# comply with the parser
|
|
start_pos = 0, 0
|
|
path = None # modules have this attribute - set it to None.
|
|
used_names = {} # To be consistent with modules.
|
|
|
|
def __init__(self, obj, parent=None):
|
|
self.obj = obj
|
|
self.parent = parent
|
|
|
|
@property
|
|
def py__call__(self):
|
|
def actual(evaluator, params):
|
|
if inspect.isclass(self.obj):
|
|
from jedi.evaluate.representation import Instance
|
|
return [Instance(evaluator, self, params)]
|
|
else:
|
|
return list(self._execute_function(evaluator, params))
|
|
|
|
# Might raise an AttributeError, which is intentional.
|
|
self.obj.__call__
|
|
return actual
|
|
|
|
@CheckAttribute
|
|
def py__class__(self, evaluator):
|
|
return CompiledObject(self.obj.__class__, parent=self.parent)
|
|
|
|
@CheckAttribute
|
|
def py__mro__(self, evaluator):
|
|
return tuple(create(evaluator, cls, self.parent) for cls in self.obj.__mro__)
|
|
|
|
@CheckAttribute
|
|
def py__bases__(self, evaluator):
|
|
return tuple(create(evaluator, cls) for cls in self.obj.__bases__)
|
|
|
|
def py__bool__(self):
|
|
return bool(self.obj)
|
|
|
|
def py__file__(self):
|
|
return self.obj.__file__
|
|
|
|
def is_class(self):
|
|
return inspect.isclass(self.obj)
|
|
|
|
@property
|
|
def doc(self):
|
|
return inspect.getdoc(self.obj) or ''
|
|
|
|
@property
|
|
def params(self):
|
|
params_str, ret = self._parse_function_doc()
|
|
tokens = params_str.split(',')
|
|
if inspect.ismethoddescriptor(self._cls().obj):
|
|
tokens.insert(0, 'self')
|
|
params = []
|
|
for p in tokens:
|
|
parts = [FakeName(part) for part in p.strip().split('=')]
|
|
if len(parts) > 1:
|
|
parts.insert(1, Operator(zero_position_modifier, '=', (0, 0)))
|
|
params.append(Param(parts, self))
|
|
return params
|
|
|
|
def __repr__(self):
|
|
return '<%s: %s>' % (type(self).__name__, repr(self.obj))
|
|
|
|
@underscore_memoization
|
|
def _parse_function_doc(self):
|
|
if self.doc is None:
|
|
return '', ''
|
|
|
|
return _parse_function_doc(self.doc)
|
|
|
|
def api_type(self):
|
|
if fake.is_class_instance(self.obj):
|
|
return 'instance'
|
|
|
|
cls = self._cls().obj
|
|
if inspect.isclass(cls):
|
|
return 'class'
|
|
elif inspect.ismodule(cls):
|
|
return 'module'
|
|
elif inspect.isbuiltin(cls) or inspect.ismethod(cls) \
|
|
or inspect.ismethoddescriptor(cls):
|
|
return 'function'
|
|
|
|
@property
|
|
def type(self):
|
|
"""Imitate the tree.Node.type values."""
|
|
cls = self._cls().obj
|
|
if inspect.isclass(cls):
|
|
return 'classdef'
|
|
elif inspect.ismodule(cls):
|
|
return 'file_input'
|
|
elif inspect.isbuiltin(cls) or inspect.ismethod(cls) \
|
|
or inspect.ismethoddescriptor(cls):
|
|
return 'funcdef'
|
|
|
|
@underscore_memoization
|
|
def _cls(self):
|
|
# Ensures that a CompiledObject is returned that is not an instance (like list)
|
|
if fake.is_class_instance(self.obj):
|
|
try:
|
|
c = self.obj.__class__
|
|
except AttributeError:
|
|
# happens with numpy.core.umath._UFUNC_API (you get it
|
|
# automatically by doing `import numpy`.
|
|
c = type(None)
|
|
return CompiledObject(c, self.parent)
|
|
return self
|
|
|
|
@property
|
|
def names_dict(self):
|
|
# For compatibility with `representation.Class`.
|
|
return self.names_dicts(False)[0]
|
|
|
|
def names_dicts(self, search_global, is_instance=False):
|
|
return self._names_dict_ensure_one_dict(is_instance)
|
|
|
|
@memoize_method
|
|
def _names_dict_ensure_one_dict(self, is_instance):
|
|
"""
|
|
search_global shouldn't change the fact that there's one dict, this way
|
|
there's only one `object`.
|
|
"""
|
|
return [LazyNamesDict(self._cls(), is_instance)]
|
|
|
|
def get_subscope_by_name(self, name):
|
|
if name in dir(self._cls().obj):
|
|
return CompiledName(self._cls(), name).parent
|
|
else:
|
|
raise KeyError("CompiledObject doesn't have an attribute '%s'." % name)
|
|
|
|
def get_index_types(self, evaluator, index_array=()):
|
|
# If the object doesn't have `__getitem__`, just raise the
|
|
# AttributeError.
|
|
if not hasattr(self.obj, '__getitem__'):
|
|
debug.warning('Tried to call __getitem__ on non-iterable.')
|
|
return []
|
|
if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict):
|
|
# Get rid of side effects, we won't call custom `__getitem__`s.
|
|
return []
|
|
|
|
result = []
|
|
from jedi.evaluate.iterable import create_indexes_or_slices
|
|
for typ in create_indexes_or_slices(evaluator, index_array):
|
|
index = None
|
|
try:
|
|
index = typ.obj
|
|
new = self.obj[index]
|
|
except (KeyError, IndexError, TypeError, AttributeError):
|
|
# Just try, we don't care if it fails, except for slices.
|
|
if isinstance(index, slice):
|
|
result.append(self)
|
|
else:
|
|
result.append(CompiledObject(new))
|
|
if not result:
|
|
try:
|
|
for obj in self.obj:
|
|
result.append(CompiledObject(obj))
|
|
except TypeError:
|
|
pass # self.obj maynot have an __iter__ method.
|
|
return result
|
|
|
|
@property
|
|
def name(self):
|
|
# might not exist sometimes (raises AttributeError)
|
|
return FakeName(self._cls().obj.__name__, self)
|
|
|
|
def _execute_function(self, evaluator, params):
|
|
if self.type != 'funcdef':
|
|
return
|
|
|
|
for name in self._parse_function_doc()[1].split():
|
|
try:
|
|
bltn_obj = _create_from_name(builtin, builtin, name)
|
|
except AttributeError:
|
|
continue
|
|
else:
|
|
if isinstance(bltn_obj, CompiledObject) and bltn_obj.obj is None:
|
|
# We want everything except None.
|
|
continue
|
|
for result in evaluator.execute(bltn_obj, params):
|
|
yield result
|
|
|
|
@property
|
|
@underscore_memoization
|
|
def subscopes(self):
|
|
"""
|
|
Returns only the faked scopes - the other ones are not important for
|
|
internal analysis.
|
|
"""
|
|
module = self.get_parent_until()
|
|
faked_subscopes = []
|
|
for name in dir(self._cls().obj):
|
|
f = fake.get_faked(module.obj, self.obj, name)
|
|
if f:
|
|
f.parent = self
|
|
faked_subscopes.append(f)
|
|
return faked_subscopes
|
|
|
|
def is_scope(self):
|
|
return True
|
|
|
|
def get_self_attributes(self):
|
|
return [] # Instance compatibility
|
|
|
|
def get_imports(self):
|
|
return [] # Builtins don't have imports
|
|
|
|
|
|
class LazyNamesDict(object):
|
|
"""
|
|
A names_dict instance for compiled objects, resembles the parser.tree.
|
|
"""
|
|
def __init__(self, compiled_obj, is_instance):
|
|
self._compiled_obj = compiled_obj
|
|
self._is_instance = is_instance
|
|
|
|
def __iter__(self):
|
|
return (v[0].value for v in self.values())
|
|
|
|
@memoize_method
|
|
def __getitem__(self, name):
|
|
try:
|
|
getattr(self._compiled_obj.obj, name)
|
|
except AttributeError:
|
|
raise KeyError('%s in %s not found.' % (name, self._compiled_obj))
|
|
return [CompiledName(self._compiled_obj, name)]
|
|
|
|
def values(self):
|
|
obj = self._compiled_obj.obj
|
|
|
|
values = []
|
|
for name in dir(obj):
|
|
try:
|
|
values.append(self[name])
|
|
except KeyError:
|
|
# The dir function can be wrong.
|
|
pass
|
|
|
|
# dir doesn't include the type names.
|
|
if not inspect.ismodule(obj) and obj != type and not self._is_instance:
|
|
values += _type_names_dict.values()
|
|
return values
|
|
|
|
|
|
class CompiledName(FakeName):
|
|
def __init__(self, obj, name):
|
|
super(CompiledName, self).__init__(name)
|
|
self._obj = obj
|
|
self.name = name
|
|
|
|
def __repr__(self):
|
|
try:
|
|
name = self._obj.name # __name__ is not defined all the time
|
|
except AttributeError:
|
|
name = None
|
|
return '<%s: (%s).%s>' % (type(self).__name__, name, self.name)
|
|
|
|
def is_definition(self):
|
|
return True
|
|
|
|
@property
|
|
@underscore_memoization
|
|
def parent(self):
|
|
module = self._obj.get_parent_until()
|
|
return _create_from_name(module, self._obj, self.name)
|
|
|
|
@parent.setter
|
|
def parent(self, value):
|
|
pass # Just ignore this, FakeName tries to overwrite the parent attribute.
|
|
|
|
|
|
def dotted_from_fs_path(fs_path, sys_path=None):
|
|
"""
|
|
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
|
|
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__.'):
|
|
# We are calculating the path. __init__ files are not interesting.
|
|
fs_path = os.path.dirname(fs_path)
|
|
|
|
# prefer
|
|
# - UNIX
|
|
# /path/to/pythonX.Y/lib-dynload
|
|
# /path/to/pythonX.Y/site-packages
|
|
# - Windows
|
|
# C:\path\to\DLLs
|
|
# C:\path\to\Lib\site-packages
|
|
# over
|
|
# - UNIX
|
|
# /path/to/pythonX.Y
|
|
# - Windows
|
|
# C:\path\to\Lib
|
|
path = ''
|
|
for s in sys_path:
|
|
if (fs_path.startswith(s) and len(path) < len(s)):
|
|
path = s
|
|
return _path_re.sub('', fs_path[len(path):].lstrip(os.path.sep)).replace(os.path.sep, '.')
|
|
|
|
|
|
def load_module(path=None, name=None):
|
|
if path is not None:
|
|
dotted_path = dotted_from_fs_path(path)
|
|
else:
|
|
dotted_path = name
|
|
|
|
sys_path = get_sys_path()
|
|
if dotted_path is None:
|
|
p, _, dotted_path = path.partition(os.path.sep)
|
|
sys_path.insert(0, p)
|
|
|
|
temp, sys.path = sys.path, sys_path
|
|
try:
|
|
__import__(dotted_path)
|
|
except RuntimeError:
|
|
if 'PySide' in dotted_path or 'PyQt' in dotted_path:
|
|
# RuntimeError: the PyQt4.QtCore and PyQt5.QtCore modules both wrap
|
|
# the QObject class.
|
|
# See https://github.com/davidhalter/jedi/pull/483
|
|
return None
|
|
raise
|
|
except ImportError:
|
|
# If a module is "corrupt" or not really a Python module or whatever.
|
|
debug.warning('Module %s not importable.', path)
|
|
return None
|
|
finally:
|
|
sys.path = temp
|
|
|
|
# Just access the cache after import, because of #59 as well as the very
|
|
# complicated import structure of Python.
|
|
module = sys.modules[dotted_path]
|
|
|
|
return CompiledObject(module)
|
|
|
|
|
|
docstr_defaults = {
|
|
'floating point number': 'float',
|
|
'character': 'str',
|
|
'integer': 'int',
|
|
'dictionary': 'dict',
|
|
'string': 'str',
|
|
}
|
|
|
|
|
|
def _parse_function_doc(doc):
|
|
"""
|
|
Takes a function and returns the params and return value as a tuple.
|
|
This is nothing more than a docstring parser.
|
|
|
|
TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None
|
|
TODO docstrings like 'tuple of integers'
|
|
"""
|
|
# parse round parentheses: def func(a, (b,c))
|
|
try:
|
|
count = 0
|
|
start = doc.index('(')
|
|
for i, s in enumerate(doc[start:]):
|
|
if s == '(':
|
|
count += 1
|
|
elif s == ')':
|
|
count -= 1
|
|
if count == 0:
|
|
end = start + i
|
|
break
|
|
param_str = doc[start + 1:end]
|
|
except (ValueError, UnboundLocalError):
|
|
# ValueError for doc.index
|
|
# UnboundLocalError for undefined end in last line
|
|
debug.dbg('no brackets found - no param')
|
|
end = 0
|
|
param_str = ''
|
|
else:
|
|
# remove square brackets, that show an optional param ( = None)
|
|
def change_options(m):
|
|
args = m.group(1).split(',')
|
|
for i, a in enumerate(args):
|
|
if a and '=' not in a:
|
|
args[i] += '=None'
|
|
return ','.join(args)
|
|
|
|
while True:
|
|
param_str, changes = re.subn(r' ?\[([^\[\]]+)\]',
|
|
change_options, param_str)
|
|
if changes == 0:
|
|
break
|
|
param_str = param_str.replace('-', '_') # see: isinstance.__doc__
|
|
|
|
# parse return value
|
|
r = re.search('-[>-]* ', doc[end:end + 7])
|
|
if r is None:
|
|
ret = ''
|
|
else:
|
|
index = end + r.end()
|
|
# get result type, which can contain newlines
|
|
pattern = re.compile(r'(,\n|[^\n-])+')
|
|
ret_str = pattern.match(doc, index).group(0).strip()
|
|
# New object -> object()
|
|
ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str)
|
|
|
|
ret = docstr_defaults.get(ret_str, ret_str)
|
|
|
|
return param_str, ret
|
|
|
|
|
|
class Builtin(CompiledObject):
|
|
@memoize_method
|
|
def get_by_name(self, name):
|
|
return self.names_dict[name][0].parent
|
|
|
|
|
|
def _a_generator(foo):
|
|
"""Used to have an object to return for generators."""
|
|
yield 42
|
|
yield foo
|
|
|
|
|
|
def _create_from_name(module, parent, name):
|
|
faked = fake.get_faked(module.obj, parent.obj, name)
|
|
# only functions are necessary.
|
|
if faked is not None:
|
|
faked.parent = parent
|
|
return faked
|
|
|
|
try:
|
|
obj = getattr(parent.obj, name)
|
|
except AttributeError:
|
|
# happens e.g. in properties of
|
|
# PyQt4.QtGui.QStyleOptionComboBox.currentText
|
|
# -> just set it to None
|
|
obj = None
|
|
return CompiledObject(obj, parent)
|
|
|
|
|
|
builtin = Builtin(_builtins)
|
|
magic_function_class = CompiledObject(type(load_module), parent=builtin)
|
|
generator_obj = CompiledObject(_a_generator(1.0))
|
|
_type_names_dict = builtin.get_by_name('type').names_dict
|
|
none_obj = builtin.get_by_name('None')
|
|
false_obj = builtin.get_by_name('False')
|
|
true_obj = builtin.get_by_name('True')
|
|
object_obj = builtin.get_by_name('object')
|
|
|
|
|
|
def keyword_from_value(obj):
|
|
if obj is None:
|
|
return none_obj
|
|
elif obj is False:
|
|
return false_obj
|
|
elif obj is True:
|
|
return true_obj
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
|
|
def compiled_objects_cache(func):
|
|
def wrapper(evaluator, obj, parent=builtin, module=None):
|
|
# Do a very cheap form of caching here.
|
|
key = id(obj), id(parent), id(module)
|
|
try:
|
|
return evaluator.compiled_cache[key][0]
|
|
except KeyError:
|
|
result = func(evaluator, obj, parent, module)
|
|
# Need to cache all of them, otherwise the id could be overwritten.
|
|
evaluator.compiled_cache[key] = result, obj, parent, module
|
|
return result
|
|
return wrapper
|
|
|
|
|
|
@compiled_objects_cache
|
|
def create(evaluator, obj, parent=builtin, module=None):
|
|
"""
|
|
A very weird interface class to this module. The more options provided the
|
|
more acurate loading compiled objects is.
|
|
"""
|
|
|
|
if not inspect.ismodule(obj):
|
|
faked = fake.get_faked(module and module.obj, obj)
|
|
if faked is not None:
|
|
faked.parent = parent
|
|
return faked
|
|
|
|
try:
|
|
if parent == builtin and obj.__module__ in ('builtins', '__builtin__'):
|
|
return builtin.get_by_name(obj.__name__)
|
|
except AttributeError:
|
|
pass
|
|
|
|
return CompiledObject(obj, parent)
|