Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 86561) +++ Misc/NEWS (working copy) @@ -25,6 +25,8 @@ complex zeros on systems where the log1p function fails to respect the sign of zero. This fixes a test failure on AIX. +- Issue #9732: Addition of getattr_static to the inspect module. + - Issue #10446: Module documentation generated by pydoc now links to a version-specific online reference manual. Index: Misc/python-wing4.wpr =================================================================== --- Misc/python-wing4.wpr (revision 86561) +++ Misc/python-wing4.wpr (working copy) @@ -5,7 +5,9 @@ ################################################################## [project attributes] proj.directory-list = [{'dirloc': loc('..'), - 'excludes': [u'Lib/__pycache__'], + 'excludes': [u'Lib/__pycache__', + u'Doc/build', + u'build'], 'filter': '*', 'include_hidden': False, 'recursive': True, Index: Doc/library/inspect.rst =================================================================== --- Doc/library/inspect.rst (revision 86561) +++ Doc/library/inspect.rst (working copy) @@ -563,3 +563,70 @@ entry in the list represents the caller; the last entry represents where the exception was raised. + +Fetching attributes statically +------------------------------ + +Both :func:`getattr` and :func:`hasattr` can trigger code execution when +fetching or checking for the existence of attributes. Descriptors, like +properties, will be invoked and `__getattr__` and `__getattribute__` may be +called. + +For cases where you want passive introspection, like documentation tools, this +can be inconvenient. `getattr_static` has the same signature as :func:`getattr` +but avoids executing code when it fetches attributes. + +.. function:: getattr_static(obj, attr, default=None) + + Retrieve attributes without triggering dynamic lookup via the + descriptor protocol, `__getattr__` or `__getattribute__`. + + Note: this function may not be able to retrieve all attributes + that getattr can fetch (like dynamically created attributes) + and may find attributes that getattr can't (like descriptors + that raise AttributeError). It can also return descriptors objects + instead of instance members. + +There are several cases that will break `getattr_static` or be handled +incorrectly. These are pathological enough not to worry about (i.e. if you do +any of these then you deserve to have everything break anyway): + +* `__dict__` existing (e.g. as a property) but returning the + wrong dictionary or even returning something other than a + dictionary +* classes created with `__slots__` that then have the `__slots__` + member deleted from the class, or a fake `__slots__` attribute + attached to the instance, or any other monkeying with + `__slots__` +* objects that lie about their type by having __class__ as a + descriptor (`getattr_static` traverses the :term:`MRO` of whatever type + `obj.__class__` returns instead of the real type) +* type objects that lie about their :term:`MRO` + +Descriptors are not resolved (for example slot descriptors or +getset descriptors on objects implemented in C). The descriptor +is returned instead of the underlying attribute. + +You can handling these with code like the following. Note that +for arbitrary getset descriptors invoking these may trigger +code execution:: + + # example code for resolving the builtin descriptor types + class _foo(object): + __slots__ = ['foo'] + + slot_descriptor = type(_foo.foo) + getset_descriptor = type(type(open(__file__)).name) + wrapper_descriptor = type(str.__dict__['__add__']) + descriptor_types = (slot_descriptor, getset_descriptor, wrapper_descriptor) + + result = getattr_static(some_object, 'foo') + if type(result) in descriptor_types: + try: + result = result.__get__() + except AttributeError: + # descriptors can raise AttributeError to + # indicate there is no underlying value + # in which case the descriptor itself will + # have to do + pass Index: Doc/glossary.rst =================================================================== --- Doc/glossary.rst (revision 86561) +++ Doc/glossary.rst (working copy) @@ -435,6 +435,14 @@ its first :term:`argument` (which is usually called ``self``). See :term:`function` and :term:`nested scope`. + method resolution order + Method Resolution Order is the order in which base classes are searched + for a member during lookup. See `The Python 2.3 Method Resolution Order + `_. + + MRO + See :term:`method resolution order`. + mutable Mutable objects can change their value but keep their :func:`id`. See also :term:`immutable`. Index: Lib/inspect.py =================================================================== --- Lib/inspect.py (revision 86561) +++ Lib/inspect.py (working copy) @@ -1054,3 +1054,67 @@ def trace(context=1): """Return a list of records for the stack below the current exception.""" return getinnerframes(sys.exc_info()[2], context) + + +# ------------------------------------------------ static version of getattr + +_sentinel = object() + +def _check_instance(obj, attr): + instance_dict = {} + try: + instance_dict = object.__getattribute__(obj, "__dict__") + except AttributeError: + pass + return instance_dict.get(attr, _sentinel) + + +def _check_class(klass, attr): + for entry in getmro(klass): + try: + return entry.__dict__[attr] + except KeyError: + pass + return _sentinel + + +def getattr_static(obj, attr, default=_sentinel): + """Retrieve attributes without triggering dynamic lookup via the + descriptor protocol, __getattr__ or __getattribute__. + + Note: this function may not be able to retrieve all attributes + that getattr can fetch (like dynamically created attributes) + and may find attributes that getattr can't (like descriptors + that raise AttributeError). It can also return descriptor objects + instead of instance members in some cases. See the + documentation for details. + """ + instance_result = _sentinel + if not isinstance(obj, type): + instance_result = _check_instance(obj, attr) + klass = obj.__class__ + else: + klass = obj + + klass_result = _check_class(klass, attr) + + if instance_result is not _sentinel and klass_result is not _sentinel: + if (_check_class(type(klass_result), '__get__') is not _sentinel and + _check_class(type(klass_result), '__set__') is not _sentinel): + return klass_result + + if instance_result is not _sentinel: + return instance_result + if klass_result is not _sentinel: + return klass_result + + if obj is klass: + # for types we check the metaclass too + for entry in getmro(type(klass)): + try: + return entry.__dict__[attr] + except KeyError: + pass + if default is not _sentinel: + return default + raise AttributeError(attr) Index: Lib/test/test_inspect.py =================================================================== --- Lib/test/test_inspect.py (revision 86561) +++ Lib/test/test_inspect.py (working copy) @@ -706,12 +706,162 @@ locs = dict(locs or {}, inst=self.inst) return (func, 'inst,' + call_params_string, locs) + +class TestGetattrStatic(unittest.TestCase): + + def test_basic(self): + class Thing(object): + x = object() + + thing = Thing() + self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x) + self.assertEqual(inspect.getattr_static(thing, 'x', None), Thing.x) + with self.assertRaises(AttributeError): + inspect.getattr_static(thing, 'y') + + self.assertEqual(inspect.getattr_static(thing, 'y', 3), 3) + + def test_inherited(self): + class Thing(object): + x = object() + class OtherThing(Thing): + pass + + something = OtherThing() + self.assertEqual(inspect.getattr_static(something, 'x'), Thing.x) + + def test_instance_attr(self): + class Thing(object): + x = 2 + def __init__(self, x): + self.x = x + thing = Thing(3) + self.assertEqual(inspect.getattr_static(thing, 'x'), 3) + del thing.x + self.assertEqual(inspect.getattr_static(thing, 'x'), 2) + + def test_property(self): + class Thing(object): + @property + def x(self): + raise AttributeError("I'm pretending not to exist") + thing = Thing() + self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x) + + def test_descriptor(self): + class descriptor(object): + def __get__(*_): + raise AttributeError("I'm pretending not to exist") + desc = descriptor() + class Thing(object): + x = desc + thing = Thing() + self.assertEqual(inspect.getattr_static(thing, 'x'), desc) + + def test_classAttribute(self): + class Thing(object): + x = object() + + self.assertEqual(inspect.getattr_static(Thing, 'x'), Thing.x) + + def test_inherited_classattribute(self): + class Thing(object): + x = object() + class OtherThing(Thing): + pass + + self.assertEqual(inspect.getattr_static(OtherThing, 'x'), Thing.x) + + def test_slots(self): + class Thing(object): + y = 'bar' + __slots__ = ['x'] + def __init__(self): + self.x = 'foo' + thing = Thing() + self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x) + self.assertEqual(inspect.getattr_static(thing, 'y'), 'bar') + + del thing.x + self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x) + + def test_metaclass(self): + class meta(type): + attr = 'foo' + class Thing(object, metaclass=meta): + pass + self.assertEqual(inspect.getattr_static(Thing, 'attr'), 'foo') + + class sub(meta): + pass + class OtherThing(object, metaclass=sub): + x = 3 + self.assertEqual(inspect.getattr_static(OtherThing, 'attr'), 'foo') + + class OtherOtherThing(OtherThing): + pass + # this test is odd, but it was added as it exposed a bug + self.assertEqual(inspect.getattr_static(OtherOtherThing, 'x'), 3) + + def test_no_dict_no_slots(self): + self.assertEqual(inspect.getattr_static(1, 'foo', None), None) + self.assertNotEqual(inspect.getattr_static('foo', 'lower'), None) + + def test_no_dict_no_slots_instance_member(self): + # returns descriptor + with open(__file__) as handle: + self.assertEqual(inspect.getattr_static(handle, 'name'), type(handle).name) + + def test_inherited_slots(self): + # returns descriptor + class Thing(object): + __slots__ = ['x'] + def __init__(self): + self.x = 'foo' + + class OtherThing(Thing): + pass + # it would be nice if this worked... + # we get the descriptor instead of the instance attribute + self.assertEqual(inspect.getattr_static(OtherThing(), 'x'), Thing.x) + + def test_descriptor(self): + class descriptor(object): + def __get__(self, instance, owner): + return 3 + class Foo(object): + d = descriptor() + + foo = Foo() + + # for a non data descriptor we return the instance attribute + foo.__dict__['d'] = 1 + self.assertEqual(inspect.getattr_static(foo, 'd'), 1) + + # if the descriptor is a data-desciptor we should return the + # descriptor + descriptor.__set__ = lambda s, i, v: None + self.assertEqual(inspect.getattr_static(foo, 'd'), Foo.__dict__['d']) + + + def test_metaclass_with_descriptor(self): + class descriptor(object): + def __get__(self, instance, owner): + return 3 + class meta(type): + d = descriptor() + class Thing(object, metaclass=meta): + pass + self.assertEqual(inspect.getattr_static(Thing, 'd'), meta.__dict__['d']) + + def test_main(): run_unittest( TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases, TestInterpreterStack, TestClassesAndFunctions, TestPredicates, TestGetcallargsFunctions, TestGetcallargsMethods, - TestGetcallargsUnboundMethods) + TestGetcallargsUnboundMethods, TestGetattrStatic + ) if __name__ == "__main__": test_main()