summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Foord <fuzzyman@voidspace.org.uk>2010-11-20 15:07:30 (GMT)
committerMichael Foord <fuzzyman@voidspace.org.uk>2010-11-20 15:07:30 (GMT)
commit95fc51dfda805c2c1aa7aacf9a100d90c8747ffc (patch)
tree9666c61139b78e7cc7fcb63eb21aded0f97a3fff
parent89197fe93c4a3f3e983721b0325b7bb5613c7e9c (diff)
downloadcpython-95fc51dfda805c2c1aa7aacf9a100d90c8747ffc.zip
cpython-95fc51dfda805c2c1aa7aacf9a100d90c8747ffc.tar.gz
cpython-95fc51dfda805c2c1aa7aacf9a100d90c8747ffc.tar.bz2
Issue 9732: addition of getattr_static to the inspect module
-rw-r--r--Doc/glossary.rst8
-rw-r--r--Doc/library/inspect.rst67
-rw-r--r--Lib/inspect.py64
-rw-r--r--Lib/test/test_inspect.py152
-rw-r--r--Misc/NEWS2
-rw-r--r--Misc/python-wing4.wpr4
6 files changed, 295 insertions, 2 deletions
diff --git a/Doc/glossary.rst b/Doc/glossary.rst
index ec75e03..4b0ab70 100644
--- a/Doc/glossary.rst
+++ b/Doc/glossary.rst
@@ -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`.
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 1f5e72e..2f09348 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -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
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 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)
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 @@ 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()
diff --git a/Misc/NEWS b/Misc/NEWS
index b9e8d16..68769fb 100644
--- 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.
diff --git a/Misc/python-wing4.wpr b/Misc/python-wing4.wpr
index cc2cf23..c3f1537 100644
--- a/Misc/python-wing4.wpr
+++ b/Misc/python-wing4.wpr
@@ -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,