]> granicus.if.org Git - python/commitdiff
Issue 9732: addition of getattr_static to the inspect module
authorMichael Foord <fuzzyman@voidspace.org.uk>
Sat, 20 Nov 2010 15:07:30 +0000 (15:07 +0000)
committerMichael Foord <fuzzyman@voidspace.org.uk>
Sat, 20 Nov 2010 15:07:30 +0000 (15:07 +0000)
Doc/glossary.rst
Doc/library/inspect.rst
Lib/inspect.py
Lib/test/test_inspect.py
Misc/NEWS
Misc/python-wing4.wpr

index ec75e0379a60319173fd6591ca10d3fafa61849e..4b0ab7075f30675140bebccd94f42711e3ee7b09 100644 (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`.
index 1f5e72ed7227e31cfdc027741994feb452a9cc02..2f0934840f09ceba5755c3fbb61229eebe4576a9 100644 (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
index 5f927873a7a47c46b9a08e9b96a762696f25bfcd..57d8c724d608eb2beb02248816fdf61cb92d7135 100644 (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)
index 08e5022e63009087ba615b6c10b9ebba5d52d6bd..88c57d31e1eb66b2a6d505ddc3dbf34c7021781a 100644 (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()
index b9e8d1636441911bade523a517aa0b0cc39b7e9a..68769fb2a0700b9cdc68fb3547934641208d3811 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -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.
 
index cc2cf2323ecfadc034290c363c271e4b8fd2bcb5..c3f1537cdc4300bf328083204efc3170d83bb529 100644 (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,