diff --git a/README.mdown b/README.mdown index f9d41562..501c092a 100644 --- a/README.mdown +++ b/README.mdown @@ -47,6 +47,7 @@ Jedi supports many of the widely used Python features: - support for list.append, set.add, list.extend, etc. - (nested) list comprehensions / ternary expressions - relative imports + - `getattr()` / `__getattr__` / `__getattribute__` - function annotations (py3k feature, are ignored right now, but being parsed. I don't know what to do with them.) - class decorators (py3k feature, are being ignored too, until I find a use @@ -55,13 +56,11 @@ Jedi supports many of the widely used Python features: However, it does not yet support (and probably will in future versions, because they are on my todo list): - - `getattr()` / `__getattr__` / `__getattribute__` - `sys.path` modifications + - assert / isinstance - manipulations of instances outside the instance variables, without using functions - - mro - operation support -> \_\_mul\_\_, \_\_add\_\_, etc. - - assert / isinstance It does not support (and most probably will not in future versions): - metaclasses (how could an auto-completion ever support this) @@ -72,7 +71,7 @@ Caveats ------- This framework should work for both Python 2/3. However, some things were just -not as *pythonic* Python 2 as things should be. To keep things simple, some +not as *pythonic* in Python 2 as things should be. To keep things simple, some things have been held back: - Classes: Always Python 3 like, therefore all classes inherit from `object`. diff --git a/evaluate.py b/evaluate.py index 18e49466..a53edf4e 100644 --- a/evaluate.py +++ b/evaluate.py @@ -216,10 +216,8 @@ class Instance(use_metaclass(CachedMetaClass, Executable)): return names def get_subscope_by_name(self, name): - for sub in reversed(self.base.subscopes): - if sub.name.get_code() == name: - return InstanceElement(self, sub, True) - raise KeyError("Couldn't find subscope.") + sub = self.base.get_subscope_by_name(name) + return InstanceElement(self, sub, True) def execute_subscope_by_name(self, name, args=None): if args is None: @@ -383,6 +381,12 @@ class Class(use_metaclass(CachedMetaClass, parsing.Base)): result += super_result return result + def get_subscope_by_name(self, name): + for sub in reversed(self.subscopes): + if sub.name.get_code() == name: + return sub + raise KeyError("Couldn't find subscope.") + @property def name(self): return self.base.name @@ -480,6 +484,37 @@ class Execution(Executable): def get_return_types(self, evaluate_generator=False): """ Get the return types of a function. """ stmts = [] + if self.base.parent() == builtin.builtin_scope \ + and not isinstance(self.base, (Generator, Array)): + func_name = str(self.base.name) + + # some implementations of builtins: + if func_name == 'getattr': + # follow the first param + try: + objects = follow_call_list([self.var_args[0]]) + names = follow_call_list([self.var_args[1]]) + except IndexError: + debug.warning('getattr() called with to few args.') + return [] + + for obj in objects: + if not isinstance(obj, (Instance, Class)): + debug.warning('getattr called without instance') + return [] + + for name in names: + key = name.var_args.get_only_subelement() + try: + stmts.append(obj.get_subscope_by_name(key)) + except KeyError: + debug.warning('called getattr() without string') + #if not (isinstance(name, Instance) \ + #and name.var_args: + #debug.warning('getattr called without instance') + #return [] + return stmts + if self.base.isinstance(Class): # There maybe executions of executions. stmts = [Instance(self.base, self.var_args)] @@ -1119,6 +1154,10 @@ def get_scopes_for_name(scope, name_str, position=None, search_global=False): # if there are results, ignore the other scopes if result: break + + if not result and isinstance(scope, Instance): + # getattr() / __getattr__ / __getattribute__ + result += check_getattr(scope, name_str) debug.dbg('sfn filter "%s" in %s: %s' % (name_str, scope, result)) return result @@ -1143,11 +1182,36 @@ def get_scopes_for_name(scope, name_str, position=None, search_global=False): if isinstance(scope, Instance): scope_generator = scope.scope_generator() else: - names = get_defined_names_for_position(scope, position) + if isinstance(scope, Class): + # classes are only available directly via chaining? + # strange stuff... + names = scope.get_defined_names() + else: + names = get_defined_names_for_position(scope, position) scope_generator = iter([(scope, names)]) return descriptor_check(remove_statements(filter_name(scope_generator))) +def check_getattr(inst, name_str): + result = [] + # str is important to lose the NamePart! + name = parsing.Call(str(name_str), parsing.Call.STRING, (0, 0), inst) + args = helpers.generate_param_array([name]) + try: + result = inst.execute_subscope_by_name('__getattr__', args) + except KeyError: + pass + if not result: + # this is a little bit special. `__getattribute__` is executed + # before anything else. But: I know no use case, where this + # could be practical and the jedi would return wrong types. If + # you ever have something, let me know! + try: + result = inst.execute_subscope_by_name('__getattribute__', args) + except KeyError: + pass + return result + def get_iterator_types(inputs): """ Returns the types of any iterator (arrays, yields, __iter__, etc). """ diff --git a/test/completion/classes.py b/test/completion/classes.py index 1b2f2450..683d1c2b 100644 --- a/test/completion/classes.py +++ b/test/completion/classes.py @@ -464,3 +464,44 @@ a[1] WithoutMethod.blub(WithoutMethod()) #? str() WithoutMethod.blub(B()) + +# ----------------- +# __getattr__ / getattr() / __getattribute__ +# ----------------- + +#? str().upper +getattr(str(), 'upper') +#? str.upper +getattr(str, 'upper') + +# some strange getattr calls +#? +getattr(str, 1) +#? +getattr() +#? +getattr(str) +#? +getattr(getattr, 1) + + +class Base(): + def ret(self, b): + return b + +class Wrapper(): + def __init__(self, obj): + self.obj = obj + + def __getattr__(self, name): + return getattr(self.obj, name) + +class Wrapper2(): + def __getattribute__(self, name): + return getattr(Base(), name) + +#? int() +Wrapper(Base()).ret(3) + +#? int() +Wrapper2(Base()).ret(3)