Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 95fc51d

Browse files
author
Michael Foord
committed
Issue 9732: addition of getattr_static to the inspect module
1 parent 89197fe commit 95fc51d

6 files changed

Lines changed: 295 additions & 2 deletions

File tree

Doc/glossary.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,14 @@ Glossary
435435
its first :term:`argument` (which is usually called ``self``).
436436
See :term:`function` and :term:`nested scope`.
437437

438+
method resolution order
439+
Method Resolution Order is the order in which base classes are searched
440+
for a member during lookup. See `The Python 2.3 Method Resolution Order
441+
<http://www.python.org/download/releases/2.3/mro/>`_.
442+
443+
MRO
444+
See :term:`method resolution order`.
445+
438446
mutable
439447
Mutable objects can change their value but keep their :func:`id`. See
440448
also :term:`immutable`.

Doc/library/inspect.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,3 +563,70 @@ line.
563563
entry in the list represents the caller; the last entry represents where the
564564
exception was raised.
565565

566+
567+
Fetching attributes statically
568+
------------------------------
569+
570+
Both :func:`getattr` and :func:`hasattr` can trigger code execution when
571+
fetching or checking for the existence of attributes. Descriptors, like
572+
properties, will be invoked and :meth:`__getattr__` and :meth:`__getattribute__`
573+
may be called.
574+
575+
For cases where you want passive introspection, like documentation tools, this
576+
can be inconvenient. `getattr_static` has the same signature as :func:`getattr`
577+
but avoids executing code when it fetches attributes.
578+
579+
.. function:: getattr_static(obj, attr, default=None)
580+
581+
Retrieve attributes without triggering dynamic lookup via the
582+
descriptor protocol, `__getattr__` or `__getattribute__`.
583+
584+
Note: this function may not be able to retrieve all attributes
585+
that getattr can fetch (like dynamically created attributes)
586+
and may find attributes that getattr can't (like descriptors
587+
that raise AttributeError). It can also return descriptors objects
588+
instead of instance members.
589+
590+
There are several cases that will break `getattr_static` or be handled
591+
incorrectly. These are pathological enough not to worry about (i.e. if you do
592+
any of these then you deserve to have everything break anyway):
593+
594+
* :data:`~object.__dict__` existing (e.g. as a property) but returning the
595+
wrong dictionary or even returning something other than a
596+
dictionary
597+
* classes created with :data:`~object.__slots__` that have the `__slots__`
598+
member deleted from the class, or a fake `__slots__` attribute
599+
attached to the instance, or any other monkeying with
600+
`__slots__`
601+
* objects that lie about their type by having `__class__` as a
602+
descriptor (`getattr_static` traverses the :term:`MRO` of whatever type
603+
`obj.__class__` returns instead of the real type)
604+
* type objects that lie about their :term:`MRO`
605+
606+
Descriptors are not resolved (for example slot descriptors or
607+
getset descriptors on objects implemented in C). The descriptor
608+
is returned instead of the underlying attribute.
609+
610+
You can handle these with code like the following. Note that
611+
for arbitrary getset descriptors invoking these may trigger
612+
code execution::
613+
614+
# example code for resolving the builtin descriptor types
615+
class _foo(object):
616+
__slots__ = ['foo']
617+
618+
slot_descriptor = type(_foo.foo)
619+
getset_descriptor = type(type(open(__file__)).name)
620+
wrapper_descriptor = type(str.__dict__['__add__'])
621+
descriptor_types = (slot_descriptor, getset_descriptor, wrapper_descriptor)
622+
623+
result = getattr_static(some_object, 'foo')
624+
if type(result) in descriptor_types:
625+
try:
626+
result = result.__get__()
627+
except AttributeError:
628+
# descriptors can raise AttributeError to
629+
# indicate there is no underlying value
630+
# in which case the descriptor itself will
631+
# have to do
632+
pass

Lib/inspect.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,3 +1054,67 @@ def stack(context=1):
10541054
def trace(context=1):
10551055
"""Return a list of records for the stack below the current exception."""
10561056
return getinnerframes(sys.exc_info()[2], context)
1057+
1058+
1059+
# ------------------------------------------------ static version of getattr
1060+
1061+
_sentinel = object()
1062+
1063+
def _check_instance(obj, attr):
1064+
instance_dict = {}
1065+
try:
1066+
instance_dict = object.__getattribute__(obj, "__dict__")
1067+
except AttributeError:
1068+
pass
1069+
return instance_dict.get(attr, _sentinel)
1070+
1071+
1072+
def _check_class(klass, attr):
1073+
for entry in getmro(klass):
1074+
try:
1075+
return entry.__dict__[attr]
1076+
except KeyError:
1077+
pass
1078+
return _sentinel
1079+
1080+
1081+
def getattr_static(obj, attr, default=_sentinel):
1082+
"""Retrieve attributes without triggering dynamic lookup via the
1083+
descriptor protocol, __getattr__ or __getattribute__.
1084+
1085+
Note: this function may not be able to retrieve all attributes
1086+
that getattr can fetch (like dynamically created attributes)
1087+
and may find attributes that getattr can't (like descriptors
1088+
that raise AttributeError). It can also return descriptor objects
1089+
instead of instance members in some cases. See the
1090+
documentation for details.
1091+
"""
1092+
instance_result = _sentinel
1093+
if not isinstance(obj, type):
1094+
instance_result = _check_instance(obj, attr)
1095+
klass = obj.__class__
1096+
else:
1097+
klass = obj
1098+
1099+
klass_result = _check_class(klass, attr)
1100+
1101+
if instance_result is not _sentinel and klass_result is not _sentinel:
1102+
if (_check_class(type(klass_result), '__get__') is not _sentinel and
1103+
_check_class(type(klass_result), '__set__') is not _sentinel):
1104+
return klass_result
1105+
1106+
if instance_result is not _sentinel:
1107+
return instance_result
1108+
if klass_result is not _sentinel:
1109+
return klass_result
1110+
1111+
if obj is klass:
1112+
# for types we check the metaclass too
1113+
for entry in getmro(type(klass)):
1114+
try:
1115+
return entry.__dict__[attr]
1116+
except KeyError:
1117+
pass
1118+
if default is not _sentinel:
1119+
return default
1120+
raise AttributeError(attr)

Lib/test/test_inspect.py

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,12 +706,162 @@ def _getAssertEqualParams(self, func, call_params_string, locs=None):
706706
locs = dict(locs or {}, inst=self.inst)
707707
return (func, 'inst,' + call_params_string, locs)
708708

709+
710+
class TestGetattrStatic(unittest.TestCase):
711+
712+
def test_basic(self):
713+
class Thing(object):
714+
x = object()
715+
716+
thing = Thing()
717+
self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x)
718+
self.assertEqual(inspect.getattr_static(thing, 'x', None), Thing.x)
719+
with self.assertRaises(AttributeError):
720+
inspect.getattr_static(thing, 'y')
721+
722+
self.assertEqual(inspect.getattr_static(thing, 'y', 3), 3)
723+
724+
def test_inherited(self):
725+
class Thing(object):
726+
x = object()
727+
class OtherThing(Thing):
728+
pass
729+
730+
something = OtherThing()
731+
self.assertEqual(inspect.getattr_static(something, 'x'), Thing.x)
732+
733+
def test_instance_attr(self):
734+
class Thing(object):
735+
x = 2
736+
def __init__(self, x):
737+
self.x = x
738+
thing = Thing(3)
739+
self.assertEqual(inspect.getattr_static(thing, 'x'), 3)
740+
del thing.x
741+
self.assertEqual(inspect.getattr_static(thing, 'x'), 2)
742+
743+
def test_property(self):
744+
class Thing(object):
745+
@property
746+
def x(self):
747+
raise AttributeError("I'm pretending not to exist")
748+
thing = Thing()
749+
self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x)
750+
751+
def test_descriptor(self):
752+
class descriptor(object):
753+
def __get__(*_):
754+
raise AttributeError("I'm pretending not to exist")
755+
desc = descriptor()
756+
class Thing(object):
757+
x = desc
758+
thing = Thing()
759+
self.assertEqual(inspect.getattr_static(thing, 'x'), desc)
760+
761+
def test_classAttribute(self):
762+
class Thing(object):
763+
x = object()
764+
765+
self.assertEqual(inspect.getattr_static(Thing, 'x'), Thing.x)
766+
767+
def test_inherited_classattribute(self):
768+
class Thing(object):
769+
x = object()
770+
class OtherThing(Thing):
771+
pass
772+
773+
self.assertEqual(inspect.getattr_static(OtherThing, 'x'), Thing.x)
774+
775+
def test_slots(self):
776+
class Thing(object):
777+
y = 'bar'
778+
__slots__ = ['x']
779+
def __init__(self):
780+
self.x = 'foo'
781+
thing = Thing()
782+
self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x)
783+
self.assertEqual(inspect.getattr_static(thing, 'y'), 'bar')
784+
785+
del thing.x
786+
self.assertEqual(inspect.getattr_static(thing, 'x'), Thing.x)
787+
788+
def test_metaclass(self):
789+
class meta(type):
790+
attr = 'foo'
791+
class Thing(object, metaclass=meta):
792+
pass
793+
self.assertEqual(inspect.getattr_static(Thing, 'attr'), 'foo')
794+
795+
class sub(meta):
796+
pass
797+
class OtherThing(object, metaclass=sub):
798+
x = 3
799+
self.assertEqual(inspect.getattr_static(OtherThing, 'attr'), 'foo')
800+
801+
class OtherOtherThing(OtherThing):
802+
pass
803+
# this test is odd, but it was added as it exposed a bug
804+
self.assertEqual(inspect.getattr_static(OtherOtherThing, 'x'), 3)
805+
806+
def test_no_dict_no_slots(self):
807+
self.assertEqual(inspect.getattr_static(1, 'foo', None), None)
808+
self.assertNotEqual(inspect.getattr_static('foo', 'lower'), None)
809+
810+
def test_no_dict_no_slots_instance_member(self):
811+
# returns descriptor
812+
with open(__file__) as handle:
813+
self.assertEqual(inspect.getattr_static(handle, 'name'), type(handle).name)
814+
815+
def test_inherited_slots(self):
816+
# returns descriptor
817+
class Thing(object):
818+
__slots__ = ['x']
819+
def __init__(self):
820+
self.x = 'foo'
821+
822+
class OtherThing(Thing):
823+
pass
824+
# it would be nice if this worked...
825+
# we get the descriptor instead of the instance attribute
826+
self.assertEqual(inspect.getattr_static(OtherThing(), 'x'), Thing.x)
827+
828+
def test_descriptor(self):
829+
class descriptor(object):
830+
def __get__(self, instance, owner):
831+
return 3
832+
class Foo(object):
833+
d = descriptor()
834+
835+
foo = Foo()
836+
837+
# for a non data descriptor we return the instance attribute
838+
foo.__dict__['d'] = 1
839+
self.assertEqual(inspect.getattr_static(foo, 'd'), 1)
840+
841+
# if the descriptor is a data-desciptor we should return the
842+
# descriptor
843+
descriptor.__set__ = lambda s, i, v: None
844+
self.assertEqual(inspect.getattr_static(foo, 'd'), Foo.__dict__['d'])
845+
846+
847+
def test_metaclass_with_descriptor(self):
848+
class descriptor(object):
849+
def __get__(self, instance, owner):
850+
return 3
851+
class meta(type):
852+
d = descriptor()
853+
class Thing(object, metaclass=meta):
854+
pass
855+
self.assertEqual(inspect.getattr_static(Thing, 'd'), meta.__dict__['d'])
856+
857+
709858
def test_main():
710859
run_unittest(
711860
TestDecorators, TestRetrievingSourceCode, TestOneliners, TestBuggyCases,
712861
TestInterpreterStack, TestClassesAndFunctions, TestPredicates,
713862
TestGetcallargsFunctions, TestGetcallargsMethods,
714-
TestGetcallargsUnboundMethods)
863+
TestGetcallargsUnboundMethods, TestGetattrStatic
864+
)
715865

716866
if __name__ == "__main__":
717867
test_main()

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Library
2525
complex zeros on systems where the log1p function fails to respect
2626
the sign of zero. This fixes a test failure on AIX.
2727

28+
- Issue #9732: Addition of getattr_static to the inspect module.
29+
2830
- Issue #10446: Module documentation generated by pydoc now links to a
2931
version-specific online reference manual.
3032

Misc/python-wing4.wpr

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
##################################################################
66
[project attributes]
77
proj.directory-list = [{'dirloc': loc('..'),
8-
'excludes': [u'Lib/__pycache__'],
8+
'excludes': [u'Lib/__pycache__',
9+
u'Doc/build',
10+
u'build'],
911
'filter': '*',
1012
'include_hidden': False,
1113
'recursive': True,

0 commit comments

Comments
 (0)