diff --git a/jedi/evaluate/compiled.py b/jedi/evaluate/compiled.py index 5e716e63..b5250110 100644 --- a/jedi/evaluate/compiled.py +++ b/jedi/evaluate/compiled.py @@ -2,20 +2,40 @@ Imitate the parser representation. """ import inspect +import re -from jedi._compatibility import builtins +from jedi._compatibility import builtins as _builtins, is_py3k +from jedi import debug from jedi.cache import underscore_memoization +# TODO +# unbound methods such as pyqtSignals have no __name__ +# if not hasattr(func, "__name__"): + class PyObject(object): def __init__(self, obj, parent=None, instantiated=False): self.obj = obj self.parent = parent self.instantiated = instantiated + self.doc = inspect.getdoc(obj) + + @underscore_memoization + def _parse_function_doc(self): + if self.doc is None: + return '', '' + + return _parse_function_doc(self.doc) def get_defined_names(self): - for name in dir(self.obj): - yield PyName(self, name) + # We don't want to execute properties, therefore we have to try to get + # the class + cls = self + if not (inspect.isclass(cls) or inspect.ismodule(cls)): + cls = PyObject(self.obj.__class__, self.parent) + + for name in dir(cls): + yield PyName(cls, name) def isinstance(self, *obj): return isinstance(self, obj) @@ -30,6 +50,8 @@ class PyObject(object): return [PyObject(self.obj, self.parent, True)] elif inspect.isbuiltin(self.obj) or inspect.ismethod(self.obj) \ or inspect.ismethoddescriptor(self.obj): + + self.doc return [] else: return [] @@ -48,15 +70,93 @@ class PyName(object): def parent(self): try: # this has a builtin_function_or_method - o = getattr(self._obj.obj, self._name) + return PyObject(getattr(self._obj.obj, self._name), self._obj) except AttributeError: # happens e.g. in properties of # PyQt4.QtGui.QStyleOptionComboBox.currentText # -> just set it to None - return PyObject(obj, py_builtin) - return PyObject(o, self._obj) + return PyObject(None, builtin) def get_code(self): return self._name -py_builtin = PyObject(builtins) + +docstr_defaults = { + 'floating point number': 'float', + 'character': 'str', + 'integer': 'int', + 'dictionary': 'dict', +} + +if is_py3k: + #docstr_defaults['file object'] = 'import io; return io.TextIOWrapper()' + pass # TODO reenable +else: + docstr_defaults['file object'] = file + + +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) + if ret == ret_str and ret not in ['None', 'object', 'tuple', 'set']: + debug.dbg('not working', ret_str) + + ret = ('return ' if 'return' not in ret else '') + ret + return param_str, ret + + +builtin = PyObject(_builtins)