Issue 9732: addition of getattr_static to the inspect module

This commit is contained in:
Michael Foord 2010-11-20 15:07:30 +00:00
parent 89197fe93c
commit 95fc51dfda
6 changed files with 295 additions and 2 deletions

View File

@ -435,6 +435,14 @@ Glossary
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
<http://www.python.org/download/releases/2.3/mro/>`_.
MRO
See :term:`method resolution order`.
mutable
Mutable objects can change their value but keep their :func:`id`. See
also :term:`immutable`.

View File

@ -563,3 +563,70 @@ line.
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 :meth:`__getattr__` and :meth:`__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):
* :data:`~object.__dict__` existing (e.g. as a property) but returning the
wrong dictionary or even returning something other than a
dictionary
* classes created with :data:`~object.__slots__` that 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 handle 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

View File

@ -1054,3 +1054,67 @@ def stack(context=1):
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)

View File

@ -706,12 +706,162 @@ class TestGetcallargsUnboundMethods(TestGetcallargsMethods):
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()

View File

@ -25,6 +25,8 @@ Library
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.

View File

@ -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,