#! /usr/bin/env python3 import unittest from static import getattr_static class TestGetattrStatic(unittest.TestCase): def test_basic(self): class Thing(object): x = object() thing = Thing() self.assertEqual(getattr_static(thing, 'x'), Thing.x) self.assertEqual(getattr_static(thing, 'x', None), Thing.x) with self.assertRaises(AttributeError): getattr_static(thing, 'y') self.assertEqual(getattr_static(thing, 'y', 3), 3) def test_inherited(self): class Thing(object): x = object() class OtherThing(Thing): pass something = OtherThing() self.assertEqual(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(getattr_static(thing, 'x'), 3) del thing.x self.assertEqual(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(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(getattr_static(thing, 'x'), desc) def test_classAttribute(self): class Thing(object): x = object() self.assertEqual(getattr_static(Thing, 'x'), Thing.x) def test_inherited_classattribute(self): class Thing(object): x = object() class OtherThing(Thing): pass self.assertEqual(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(getattr_static(thing, 'x'), 'foo') self.assertEqual(getattr_static(thing, 'y'), 'bar') del thing.x self.assertEqual(getattr_static(thing, 'x'), Thing.x) def test_metaclass(self): class meta(type): attr = 'foo' class Thing(object, metaclass=meta): pass self.assertEqual(getattr_static(Thing, 'attr'), 'foo') class sub(meta): pass class OtherThing(object, metaclass=sub): x = 3 self.assertEqual(getattr_static(OtherThing, 'attr'), 'foo') class OtherOtherThing(OtherThing): pass # this test is odd, but it was added as it exposed a bug self.assertEqual(getattr_static(OtherOtherThing, 'x'), 3) def test_no_dict_no_slots(self): self.assertEqual(getattr_static(1, 'foo', None), None) self.assertNotEqual(getattr_static('foo', 'lower'), None) @unittest.expectedFailure def test_no_dict_no_slots_instance_member(self): # it would be nice if this worked... # we get the descriptor instead of the instance attribute with open(__file__) as handle: self.assertEqual(getattr_static(handle, 'name'), __file__) @unittest.expectedFailure def test_inherited_slots(self): 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(getattr_static(OtherThing(), 'x'), 'foo') """ Cases that will break `getattr_static`, all pathological enough not to worry about (i.e. if you do any of these then you deserve to have everything break anyway): * `__dict__` existing (e.g. as a property) but not returning a dictionary * classes created with `__slots__` that then have the `__slots__` member deleted from the class (or otherwise monkeyed with) Cases handled incorrectly: 1. where a descriptor with a `__set__` method is shadowed by an instance member we return the instance member in preference to the descriptor, unlike `getattr` 2. types implemented in C may have neither `__dict__` nor `__slots__`, in this case we will be unable to find instance members and return the attribute descriptor instead 3. classes that inherit from a class with `__slots__` (whether or not they use `__slots__` themselves) will return the slot descriptor for instance members 'owned' by a slot on a base class 4. objects that lie about being a type by having __class__ as a descriptor (we traverse the mro of whatever type `obj.__class__` returns instead of the real type) 1 could be fixed but the code would be annoying. Is it worth fixing? 2 could be detected and where fetching an attribute from an instance fails but an attribute descriptor is found on the type we could try it's __get__ method. Worth it? 3 could be detected if we find a slot descriptor on a type trying its __get__ method. Worth it? 4 could be fixed by using type everywhere instead of __class__. We also can't use isinstance that uses __class__. If an object is lying about __class__ then it obviously *intends* us to look at the 'faked' version. However, this breaks the 'no code execution' purpose of getattr_static and is inconsistent with the rest of our behaviour. Worth fixing? Fixing *all* of these (or any) will significantly complicate the implimentation. Fetching an uninitialized instance member from an instance of a class with __slots__ returns the slot descriptor rather than raising an AttributeError as the descriptor does. As the slot descriptor is a Python implementation detail perhaps we are better off propagating the exception here. (?) On the other hand, the descriptor is available on the class and the job of this function is to fetch members when they are available... I'm not aware of any other caveats / potential pitfalls. Please point them out to me. :-) """ if __name__ == '__main__': unittest.main()