Issue 9732: addition of getattr_static to the inspect module
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 5f92787..57d8c72 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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)
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 08e5022..88c57d3 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -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()