1
0
forked from VimPlug/jedi

A lot of small improvements.

This commit is contained in:
Dave Halter
2016-10-22 17:40:42 +02:00
parent 4ccfbb4962
commit 2e6603cc2e
16 changed files with 174 additions and 149 deletions

View File

@@ -109,7 +109,7 @@ class Evaluator(object):
self.recursion_detector = recursion.RecursionDetector(self)
self.execution_recursion_detector = recursion.ExecutionRecursionDetector(self)
def wrap(self, element, parent_context=None):
def wrap(self, element, parent_context):
if isinstance(element, (er.Wrapper, er.InstanceElement,
er.ModuleContext, er.FunctionExecution, er.Instance, compiled.CompiledObject)) or element is None:
# TODO this is so ugly, please refactor.
@@ -118,7 +118,7 @@ class Evaluator(object):
if element.type == 'classdef':
return er.ClassContext(self, element, parent_context)
elif element.type == 'funcdef':
return er.Function(self, element)
return er.Function(self, parent_context, element)
elif element.type == 'lambda':
return er.LambdaWrapper(self, element)
elif element.type == 'file_input':
@@ -126,7 +126,7 @@ class Evaluator(object):
else:
return element
def find_types(self, scope, name_str, position=None, search_global=False,
def find_types(self, context, name_str, position=None, search_global=False,
is_goto=False):
"""
This is the search function. The most important part to debug.
@@ -136,7 +136,7 @@ class Evaluator(object):
:param position: Position of the last statement -> tuple of line, column
:return: List of Names. Their parents are the types.
"""
f = finder.NameFinder(self, scope, name_str, position)
f = finder.NameFinder(self, context, name_str, position)
filters = f.get_filters(search_global)
if is_goto:
return f.filter_name(filters)
@@ -341,8 +341,7 @@ class Evaluator(object):
if isinstance(atom, tree.Name):
# This is the first global lookup.
stmt = atom.get_definition()
scope = stmt.get_parent_until(tree.IsScope, include_current=True)
if isinstance(scope, (tree.Function, er.FunctionExecution)):
if isinstance(context, er.FunctionExecution):
# Adjust scope: If the name is not in the suite, it's a param
# default or annotation and will be resolved as part of the
# parent scope.
@@ -355,7 +354,7 @@ class Evaluator(object):
# We only need to adjust the start_pos for statements, because
# there the name cannot be used.
stmt = atom
return self.find_types(scope, atom, stmt.start_pos, search_global=True)
return self.find_types(context, atom, stmt.start_pos, search_global=True)
elif isinstance(atom, tree.Literal):
return set([compiled.create(self, atom.eval())])
else:
@@ -421,7 +420,7 @@ class Evaluator(object):
if self.is_analysis:
arguments.eval_all()
if obj.isinstance(er.Function):
if isinstance(obj, er.Function):
obj = obj.get_decorated_func()
debug.dbg('execute: %s %s', obj, arguments)
@@ -532,7 +531,15 @@ class Evaluator(object):
search_global=True, is_goto=True)
def create_context(self, node):
scope = node.get_parent_scope()
if scope.get_parent_scope() is not None:
raise NotImplementedError
return self.wrap(scope)
def from_scope(scope):
parent_context = None
parent_scope = scope.get_parent_scope()
if parent_scope is not None:
parent_context = from_scope(parent_scope)
return self.wrap(scope, parent_context=parent_context)
if node.is_scope():
scope = node
else:
scope = node.get_parent_scope()
return from_scope(scope)

View File

@@ -10,9 +10,10 @@ 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.parser.tree import Param, Base, Operator
from jedi.parser.tree import Param, Operator
from jedi.evaluate.helpers import FakeName
from jedi.evaluate.filters import AbstractFilter, AbstractNameDefinition
from jedi.evaluate.context import Context
from . import fake
@@ -36,22 +37,27 @@ class CheckAttribute(object):
return partial(self.func, instance)
class CompiledObject(Base):
class CompiledObject(Context):
# 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, evaluator, obj, parent=None):
def __init__(self, evaluator, obj, parent_context=None):
self._evaluator = evaluator
self.obj = obj
self.parent = parent
self.parent_context = parent_context
def get_root_node(self):
# To make things a bit easier with filters we add this method here.
return self.get_root_context()
@CheckAttribute
def py__call__(self, params):
if inspect.isclass(self.obj):
from jedi.evaluate.representation import Instance
return set([Instance(self._evaluator, self, params)])
return set([self])
return set([Instance(self._evaluator, self.parent_context, self, params)])
else:
return set(self._execute_function(params))
@@ -206,7 +212,7 @@ class CompiledObject(Base):
name = self._get_class().__name__
except AttributeError:
name = repr(self.obj)
return FakeName(name, self)
return CompiledContextName(self, name)
def _execute_function(self, params):
if self.type != 'funcdef':
@@ -267,15 +273,21 @@ class CompiledName(AbstractNameDefinition):
name = None
return '<%s: (%s).%s>' % (type(self).__name__, name, self.string_name)
def is_definition(self):
return True
@underscore_memoization
def infer(self):
module = self._compiled_obj.get_parent_until()
module = self._compiled_obj.get_root_context()
return [_create_from_name(self._evaluator, module, self._compiled_obj, self.string_name)]
class CompiledContextName(AbstractNameDefinition):
def __init__(self, parent_context, name):
self.string_name = name
self.parent_context = parent_context
def infer(self):
return [self.parent_context]
class LazyNamesDict(object):
"""
A names_dict instance for compiled objects, resembles the parser.tree.

View File

@@ -11,7 +11,6 @@ import types
from jedi._compatibility import is_py3, builtins, unicode, is_py34
from jedi.parser import ParserWithRecovery, load_grammar
from jedi.parser import tree as pt
from jedi.evaluate.helpers import FakeName
modules = {}
@@ -69,14 +68,14 @@ def _load_faked_module(module):
if module_name == 'builtins' and not is_py3:
# There are two implementations of `open` for either python 2/3.
# -> Rename the python2 version (`look at fake/builtins.pym`).
open_func = search_scope(module, 'open')
open_func.children[1] = FakeName('open_python3')
open_func = search_scope(module, 'open_python2')
open_func.children[1] = FakeName('open')
open_func = _search_scope(module, 'open')
open_func.children[1].value = 'open_python3'
open_func = _search_scope(module, 'open_python2')
open_func.children[1].value = 'open'
return module
def search_scope(scope, obj_name):
def _search_scope(scope, obj_name):
for s in scope.subscopes:
if str(s.name) == obj_name:
return s
@@ -120,7 +119,7 @@ def _faked(module, obj, name):
# for methods.
if name is None:
if inspect.isbuiltin(obj):
return search_scope(faked_mod, obj.__name__), faked_mod
return _search_scope(faked_mod, obj.__name__), faked_mod
elif not inspect.isclass(obj):
# object is a method or descriptor
try:
@@ -128,22 +127,22 @@ def _faked(module, obj, name):
except AttributeError:
return None, None
else:
cls = search_scope(faked_mod, objclass.__name__)
cls = _search_scope(faked_mod, objclass.__name__)
if cls is None:
return None, None
return search_scope(cls, obj.__name__), faked_mod
return _search_scope(cls, obj.__name__), faked_mod
else:
if obj == module:
return search_scope(faked_mod, name), faked_mod
return _search_scope(faked_mod, name), faked_mod
else:
try:
cls_name = obj.__name__
except AttributeError:
return None, None
cls = search_scope(faked_mod, cls_name)
cls = _search_scope(faked_mod, cls_name)
if cls is None:
return None, None
return search_scope(cls, name), faked_mod
return _search_scope(cls, name), faked_mod
return None, None

View File

@@ -12,35 +12,48 @@ from jedi.common import to_list
class AbstractNameDefinition(object):
start_pos = None
string_name = None
parent_context = None
@abstractmethod
def infer(self):
raise NotImplementedError
def get_root_context(self):
if self.parent_context is None:
return self
return self.parent_context.get_root_context()
class TreeNameDefinition(AbstractNameDefinition):
def __repr__(self):
if self.start_pos is None:
return '<%s: %s>' % (type(self).__name__, self.string_name)
return '<%s: %s@%s>' % (type(self).__name__, self.string_name, self.start_pos)
class ContextName(AbstractNameDefinition):
def __init__(self, parent_context, name):
self.parent_context = parent_context
self._name = name
def get_parent_flow_context(self):
return self.parent_context
self.name = name
@property
def string_name(self):
return self._name.value
return self.name.value
@property
def start_pos(self):
return self._name.start_pos
return self.name.start_pos
def infer(self):
return [self.parent_context]
class TreeNameDefinition(ContextName):
def get_parent_flow_context(self):
return self.parent_context
def infer(self):
# Refactor this, should probably be here.
from jedi.evaluate.finder import _name_to_types
return _name_to_types(self.parent_context._evaluator, self.parent_context, self._name, None)
def __repr__(self):
return '<%s: %s@%s>' % (type(self).__name__, self.string_name, self.start_pos)
return _name_to_types(self.parent_context._evaluator, self.parent_context, self.name, None)
class AbstractFilter(object):
@@ -102,8 +115,8 @@ class ParserTreeFilter(AbstractUsedNamesFilter):
def _check_flows(self, names):
for name in sorted(names, key=lambda name: name.start_pos, reverse=True):
stmt = name.get_definition()
name_scope = self._evaluator.wrap(stmt.get_parent_scope())
check = flow_analysis.UNSURE
#name_scope = self._evaluator.wrap(stmt.get_parent_scope())
#check = flow_analysis.break_check(self._evaluator, name_scope,
# stmt, self._origin_scope)
if check is not flow_analysis.UNREACHABLE:
@@ -181,8 +194,7 @@ def get_global_filters(evaluator, context, until_position, origin_scope):
until_position = None
in_func = True
node = context.parent_context
context = evaluator.wrap(node)
context = context.parent_context
# Add builtins to the global scope.
for filter in evaluator.BUILTINS.get_filters(search_global=True):

View File

@@ -95,10 +95,10 @@ def filter_definition_names(names, origin, position=None):
class NameFinder(object):
def __init__(self, evaluator, scope, name_str, position=None):
def __init__(self, evaluator, context, name_str, position=None):
self._evaluator = evaluator
# Make sure that it's not just a syntax tree node.
self.scope = evaluator.wrap(scope)
self.context = context
self.name_str = name_str
self.position = position
self._found_predefined_if_name = None
@@ -123,7 +123,7 @@ class NameFinder(object):
if not isinstance(self.name_str, (str, unicode)): # TODO Remove?
if attribute_lookup:
analysis.add_attribute_error(self._evaluator,
self.scope, self.name_str)
self.context, self.name_str)
else:
message = ("NameError: name '%s' is not defined."
% self.name_str)
@@ -140,9 +140,9 @@ class NameFinder(object):
origin_scope = None
if search_global:
return get_global_filters(self._evaluator, self.scope, self.position, origin_scope)
return get_global_filters(self._evaluator, self.context, self.position, origin_scope)
else:
return self.scope.get_filters(search_global, self.position, origin_scope=origin_scope)
return self.context.get_filters(search_global, self.position, origin_scope=origin_scope)
def names_dict_lookup(self, names_dict, position):
def get_param(scope, el):
@@ -167,7 +167,7 @@ class NameFinder(object):
stmt = name.get_definition()
name_scope = self._evaluator.wrap(stmt.get_parent_scope())
if isinstance(self.scope, er.Instance) and not isinstance(name_scope, er.Instance):
if isinstance(self.context, er.Instance) and not isinstance(name_scope, er.Instance):
# Instances should not be checked for positioning, because we
# don't know in which order the functions are called.
last_names.append(name)
@@ -210,7 +210,7 @@ class NameFinder(object):
# deliver types.
self._found_predefined_if_name = types
else:
check = flow_analysis.break_check(self._evaluator, self.scope,
check = flow_analysis.break_check(self._evaluator, self.context,
origin_scope)
if check is flow_analysis.UNREACHABLE:
self._found_predefined_if_name = set()
@@ -249,7 +249,7 @@ class NameFinder(object):
if names:
break
debug.dbg('finder.filter_name "%s" in (%s): %s@%s', self.name_str,
self.scope, names, self.position)
self.context, names, self.position)
return list(self._clean_names(names))
def _clean_names(self, names):
@@ -311,13 +311,13 @@ class NameFinder(object):
for name in names:
new_types = name.infer()
if isinstance(self.scope, (er.ClassContext, er.Instance)) and attribute_lookup:
if isinstance(self.context, (er.ClassContext, er.Instance)) and attribute_lookup:
types |= set(self._resolve_descriptors(name, new_types))
else:
types |= set(new_types)
if not names and isinstance(self.scope, er.Instance):
if not names and isinstance(self.context, er.Instance):
# handling __getattr__ / __getattribute__
return self._check_getattr(self.scope)
return self._check_getattr(self.context)
return types
@@ -336,7 +336,7 @@ class NameFinder(object):
except AttributeError:
result.add(r)
else:
result |= desc_return(self.scope)
result |= desc_return(self.context)
return result
@@ -365,7 +365,7 @@ def _name_to_types(evaluator, context, name, scope):
for_types = iterable.py__iter__types(evaluator, container_types, typ.children[3])
types = check_tuple_assignments(evaluator, for_types, name)
elif isinstance(typ, tree.Param):
types = _eval_param(evaluator, typ, scope)
types = _eval_param(evaluator, context, typ, scope)
elif typ.isinstance(tree.ExprStmt):
types = _remove_statements(evaluator, context, typ, name)
elif typ.isinstance(tree.WithStmt):
@@ -373,7 +373,7 @@ def _name_to_types(evaluator, context, name, scope):
elif isinstance(typ, tree.Import):
types = imports.ImportWrapper(evaluator, name).follow()
elif typ.isinstance(tree.Function, tree.Class):
types = [evaluator.wrap(typ)]
types = [evaluator.wrap(typ, parent_context=context)]
elif typ.type == 'global_stmt':
for s in _get_global_stmt_scopes(evaluator, typ, name):
finder = NameFinder(evaluator, s, str(name))
@@ -427,7 +427,7 @@ def _remove_statements(evaluator, context, stmt, name):
return types
def _eval_param(evaluator, param, scope):
def _eval_param(evaluator, context, param, scope):
res_new = set()
func = param.get_parent_scope()
@@ -441,8 +441,9 @@ def _eval_param(evaluator, param, scope):
if isinstance(scope, er.InstanceElement):
res_new.add(scope.instance)
else:
inst = er.Instance(evaluator, evaluator.wrap(cls),
Arguments(evaluator, ()), is_generated=True)
inst = er.Instance(evaluator, context.parent_context.parent_context, context.parent_context,
Arguments(evaluator, context, ()),
is_generated=True)
res_new.add(inst)
return res_new

View File

@@ -1,6 +1,7 @@
import copy
from itertools import chain
from jedi.evaluate.filters import AbstractNameDefinition
from jedi.parser import tree
@@ -172,6 +173,7 @@ class FakeName(tree.Name):
In case is_definition is defined (not None), that bool value will be
returned.
"""
raise NotImplementedError
super(FakeName, self).__init__(name_str, start_pos)
self.parent = parent
self._is_definition = is_definition
@@ -186,15 +188,11 @@ class FakeName(tree.Name):
return self._is_definition
class LazyName(FakeName):
class LazyName(AbstractNameDefinition):
def __init__(self, name, parent_callback, is_definition=None):
super(LazyName, self).__init__(name, is_definition=is_definition)
# TODO remove is_definition
self.string_name = name
self._parent_callback = parent_callback
@property
def parent(self):
def infer(self):
return self._parent_callback()
@parent.setter
def parent(self, value):
pass # Do nothing, super classes can try to set the parent.

View File

@@ -777,7 +777,7 @@ def check_array_instances(evaluator, instance):
ai = _ArrayInstance(evaluator, instance)
from jedi.evaluate import param
return param.Arguments(evaluator, [AlreadyEvaluated([ai])])
return param.Arguments(evaluator, instance, [AlreadyEvaluated([ai])])
class _ArrayInstance(IterableWrapper):

View File

@@ -130,6 +130,9 @@ class ExecutionRecursionDetector(object):
self.recursion_level -= 1
def push_execution(self, execution):
self.execution_funcs.add(execution.base)
self.parent_execution_funcs.append(execution.base)
return True # Remove
in_par_execution_funcs = execution.base in self.parent_execution_funcs
in_execution_funcs = execution.base in self.execution_funcs
self.recursion_level += 1

View File

@@ -56,38 +56,17 @@ from jedi.evaluate import param
from jedi.evaluate import flow_analysis
from jedi.evaluate import imports
from jedi.evaluate.filters import ParserTreeFilter, FunctionExecutionFilter, \
GlobalNameFilter, DictFilter
GlobalNameFilter, DictFilter, ContextName
from jedi.evaluate.context import Context
class Context(object):
def __init__(self, evaluator, parent_context=None):
self._evaluator = evaluator
self.parent_context = parent_context
def get_parent_flow_context(self):
return self.parent_context
def get_root_context(self):
context = self
while True:
if context.parent_context is None:
return context
context = context.parent_context
class FlowContext(Context):
def get_parent_flow_context(self):
if 1:
return self.parent_context
class Executed(Context, tree.Base):
class Executed(Context):
"""
An instance is also an executable - because __init__ is called
:param var_args: The param input array, consist of a parser node or a list.
"""
def __init__(self, evaluator, base, var_args=()):
self._evaluator = evaluator
def __init__(self, evaluator, parent_context, base, var_args):
super(Executed, self).__init__(evaluator, parent_context=parent_context)
self.base = base
self.var_args = var_args
@@ -97,25 +76,21 @@ class Executed(Context, tree.Base):
def get_parent_until(self, *args, **kwargs):
return tree.Base.get_parent_until(self, *args, **kwargs)
@common.safe_property
def parent(self):
return self.base.parent
class Instance(use_metaclass(CachedMetaClass, Executed)):
"""
This class is used to evaluate instances.
"""
def __init__(self, evaluator, base, var_args, is_generated=False):
super(Instance, self).__init__(evaluator, base, var_args)
def __init__(self, evaluator, parent_context, base, var_args, is_generated=False):
super(Instance, self).__init__(evaluator, parent_context, base, var_args)
self.decorates = None
# Generated instances are classes that are just generated by self
# (No var_args) used.
self.is_generated = is_generated
self._init_execution = None
if base.name.get_code() in ['list', 'set'] \
and evaluator.BUILTINS == base.get_parent_until():
if base.name.string_name in ['list', 'set'] \
and evaluator.BUILTINS == parent_context.get_root_context():
# compare the module path with the builtin name.
self.var_args = iterable.check_array_instances(evaluator, self)
elif not is_generated:
@@ -157,7 +132,7 @@ class Instance(use_metaclass(CachedMetaClass, Executed)):
func = self.get_subscope_by_name('__init__')
except KeyError:
return None
return FunctionExecution(self._evaluator, func, self.var_args)
return FunctionExecution(self._evaluator, self, func, self.var_args)
def _get_func_self_name(self, func):
"""
@@ -280,8 +255,7 @@ class Instance(use_metaclass(CachedMetaClass, Executed)):
@property
@underscore_memoization
def name(self):
name = self.base.name
return helpers.FakeName(unicode(name), self, name.start_pos)
return ContextName(self, self.base.name)
def __getattr__(self, name):
if name not in ['start_pos', 'end_pos', 'get_imports', 'type',
@@ -400,6 +374,7 @@ def get_instance_el(evaluator, instance, var, is_class_var=False):
in quite a lot of cases, which includes Nodes like ``power``, that need to
know where a self name comes from for example.
"""
return var
if isinstance(var, tree.Name):
parent = get_instance_el(evaluator, instance, var.parent, is_class_var)
return InstanceName(var, parent)
@@ -465,8 +440,7 @@ class InstanceElement(use_metaclass(CachedMetaClass, tree.Base)):
@property
@memoize_default()
def name(self):
name = self.var.name
return helpers.FakeName(unicode(name), self, name.start_pos)
return ContextName(self.var.name, self)
def __iter__(self):
for el in self.var.__iter__():
@@ -519,7 +493,7 @@ class Wrapper(tree.Base):
@underscore_memoization
def name(self):
name = self.base.name
return helpers.FakeName(unicode(name), self, name.start_pos)
return ContextName(self, name)
class ClassContext(use_metaclass(CachedMetaClass, Context, Wrapper)):
@@ -574,7 +548,7 @@ class ClassContext(use_metaclass(CachedMetaClass, Context, Wrapper)):
return [compiled.create(self._evaluator, object)]
def py__call__(self, params):
return set([Instance(self._evaluator, self, params)])
return set([Instance(self._evaluator, self.parent_context, self, params)])
def py__class__(self):
return compiled.create(self._evaluator, type)
@@ -628,13 +602,13 @@ class ClassContext(use_metaclass(CachedMetaClass, Context, Wrapper)):
return "<e%s of %s>" % (type(self).__name__, self.base)
class Function(use_metaclass(CachedMetaClass, Wrapper)):
class Function(use_metaclass(CachedMetaClass, Context, Wrapper)):
"""
Needed because of decorators. Decorators are evaluated here.
"""
def __init__(self, evaluator, func, is_decorated=False):
def __init__(self, evaluator, parent_context, func, is_decorated=False):
""" This should not be called directly """
self._evaluator = evaluator
super(Function, self).__init__(evaluator, parent_context)
self.base = self.base_func = func
self.is_decorated = is_decorated
# A property that is set by the decorator resolution.
@@ -716,7 +690,7 @@ class Function(use_metaclass(CachedMetaClass, Wrapper)):
if self.base.is_generator():
return set([iterable.Generator(self._evaluator, self, params)])
else:
return FunctionExecution(self._evaluator, self, params).get_return_types()
return FunctionExecution(self._evaluator, self.parent_context, self, params).get_return_types()
@memoize_default()
def py__annotations__(self):
@@ -767,24 +741,24 @@ class FunctionExecution(Executed):
"""
type = 'funcdef'
def __init__(self, evaluator, base, *args, **kwargs):
super(FunctionExecution, self).__init__(evaluator, base, *args, **kwargs)
def __init__(self, evaluator, parent_context, base, var_args):
super(FunctionExecution, self).__init__(evaluator, parent_context, base, var_args)
self._copy_dict = {}
self._original_function = funcdef = base.base_func
self._original_function = funcdef = base
if isinstance(funcdef, mixed.MixedObject):
# The extra information in mixed is not needed anymore. We can just
# unpack it and give it the tree object.
funcdef = funcdef.definition
# Just overwrite the old version. We don't need it anymore.
funcdef = helpers.deep_ast_copy(funcdef, new_elements=self._copy_dict)
for child in funcdef.children:
if child.type not in ('operator', 'keyword'):
#funcdef = helpers.deep_ast_copy(funcdef, new_elements=self._copy_dict)
#for child in funcdef.children:
#if child.type not in ('operator', 'keyword'):
# Not all nodes are properly copied by deep_ast_copy.
child.parent = self
self.children = funcdef.children
self.names_dict = funcdef.names_dict
self._copied_funcdef = funcdef
#child.parent = self
#self.children = funcdef.children
#self.names_dict = funcdef.names_dict
#self._copied_funcdef = funcdef
@memoize_default(default=set())
@recursion.execution_recursion_decorator
@@ -940,6 +914,7 @@ class GlobalName(helpers.FakeName):
We need to mark global names somehow. Otherwise they are just normal
names that are not definitions.
"""
raise NotImplementedError
super(GlobalName, self).__init__(name.value, name.parent,
name.start_pos, is_definition=True)
@@ -1010,7 +985,7 @@ class ModuleContext(use_metaclass(CachedMetaClass, tree.Module, Wrapper)):
@property
@memoize_default()
def name(self):
return helpers.FakeName(unicode(self.base.name), self, (1, 0))
return ContextName(self, self.base.name)
def _get_init_directory(self):
"""
@@ -1101,6 +1076,7 @@ class ModuleContext(use_metaclass(CachedMetaClass, tree.Module, Wrapper)):
if path is not None and path.endswith(os.path.sep + '__init__.py'):
mods = pkgutil.iter_modules([os.path.dirname(path)])
for module_loader, name, is_pkg in mods:
raise NotImplementedError
fake_n = helpers.FakeName(name)
# It's obviously a relative import to the current module.
imp = helpers.FakeImport(fake_n, self, level=1)

View File

@@ -35,7 +35,7 @@ def execute(evaluator, obj, arguments):
except AttributeError:
pass
else:
if obj.parent == evaluator.BUILTINS:
if obj.parent_context == evaluator.BUILTINS:
module_name = 'builtins'
elif isinstance(obj.parent, tree.Module):
module_name = str(obj.parent.name)