The attribute module handles getting and setting attributes on abstract values. It is mostly used to:
__init__for instance creation and
__setitem__for modifying a dictionary.
We will refer to the object on which an attribute is being get or set as the target object.
attribute has two public methods:
We’ll start by looking at
set_attribute, since it’s by far the simpler of the
two top-level methods:
set_attributerecursively calls itself as needed to unpack the target object. For example, when passed a union,
set_attributeiterates through the union’s options and calls itself on each one.
_set_member, which first calls
_maybe_load_as_instance_attributeto make sure the requested attribute has been lazily loaded into the object’s
membersdict, then modifies the appropriate
get_attribute is considerably more complicated, due to the lengths it goes to
in order to emulate the behavior of the Python interpreter:
get_attributerecursively calls itself to unpack the target object.
Once the object has been unpacked, it is either a module, class instance,
class, or super instance (the result of a
super() call). The corresponding
helper method is called.
_get_module_attribute forwards to
_get_class_attribute forward to
_get_attribute_from_super_instance determines the super class and calls
_lookup_from_mro_and_handle_descriptors on it.
Python classes can define a magic method,
__getattribute__, that is called
unconditionally to implement attribute access, as well as a fallback method,
__getattr__, that is only called when normal attribute lookup fails. (See
the Python data model documentation for more
information.) To mimic this,
_get_attribute first calls
_get_attribute_computed("__getattribute__"), then calls either
_lookup_from_mro_and_handle_descriptors depending on
whether the target object is a class instance or a class, and finally calls
_lookup_from_mro to do attribute lookup on a class. The latter walks
the class’s MRO, calling
_get_attribute_flat - which in turn calls
_get_member - to check for the attribute on the class and its bases.
_maybe_load_as_instance_attributeto force lazy loading and then checks the
membersdict for the requested attribute.
Occasionally, an abstract class needs to bypass normal attribute lookup. In such
cases, the class can implement a
get_special_attribute method whose return
value will be immediately used. For example, HasSlots returns the
custom method implementations in its slots rather than the underlying
attribute.get_attribute takes an optional
valself parameter, a binding to an
abstract value related to the target object. When to pass
valself and what
value to use can be tricky:
valselfis strictly optional and should be a binding to the target object. When given,
valselfwill be included in the origins of the returned variable and therefore be taken into account in typegraph solver queries.
valselfis interpreted roughly as follows:
valself is a binding to an instance of the target class, then the
attribute lookup will ignore the class’s metaclass. For example, with:
class Foo(type): def f(cls): pass class Bar(metaclass=Foo): pass
get_attribute(node, Bar, 'f', valself=Binding(Instance(Bar))) will return
f existing on
Foo. The reasoning is that if the original
query were for an attribute
f on an instance of
Bar, taking the
metaclass into account would be unexpected.
valselfis a binding to the class, then the attribute lookup will use the metaclass. So
get_attribute(node, Bar, 'f', valself=Binding(Bar))will return
Foo.f, as we’re treating
Baras not only a class but also an instance of its metaclass.
Noneand we are looking up
__getitem__, then the class is treated as a type annotation.
None, then attribute lookup will behave the same as in case (1), except that methods will be returned unbound, since there is no instance of the class to bind to.