114 lines
3.5 KiB
ReStructuredText
114 lines
3.5 KiB
ReStructuredText
.. _extending:
|
|
|
|
Extending
|
|
=========
|
|
|
|
Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute.
|
|
It is a tuple of :class:`attr.Attribute` carrying meta-data about each attribute.
|
|
|
|
So it is fairly simple to build your own decorators on top of ``attrs``:
|
|
|
|
.. doctest::
|
|
|
|
>>> import attr
|
|
>>> def print_attrs(cls):
|
|
... print(cls.__attrs_attrs__)
|
|
... return cls
|
|
>>> @print_attrs
|
|
... @attr.s
|
|
... class C(object):
|
|
... a = attr.ib()
|
|
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),)
|
|
|
|
|
|
.. warning::
|
|
|
|
The :func:`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place!
|
|
That means that is has to come *after* your decorator because::
|
|
|
|
@a
|
|
@b
|
|
def f():
|
|
pass
|
|
|
|
is just `syntactic sugar <https://en.wikipedia.org/wiki/Syntactic_sugar>`_ for::
|
|
|
|
def original_f():
|
|
pass
|
|
|
|
f = a(b(original_f))
|
|
|
|
|
|
Wrapping the Decorator
|
|
----------------------
|
|
|
|
A more elegant way can be to wrap ``attrs`` altogether and build a class `DSL <https://en.wikipedia.org/wiki/Domain-specific_language>`_ on top of it.
|
|
|
|
An example for that is the package `environ_config <https://github.com/hynek/environ_config>`_ that uses ``attrs`` under the hood to define environment-based configurations declaratively without exposing ``attrs`` APIs at all.
|
|
|
|
|
|
Types
|
|
-----
|
|
|
|
``attrs`` offers two ways of attaching type information to attributes:
|
|
|
|
- `PEP 526 <https://www.python.org/dev/peps/pep-0526/>`_ annotations on Python 3.6 and later,
|
|
- and the *type* argument to :func:`attr.ib`.
|
|
|
|
This information is available to you:
|
|
|
|
.. doctest::
|
|
|
|
>>> import attr
|
|
>>> @attr.s
|
|
... class C(object):
|
|
... x: int = attr.ib()
|
|
... y = attr.ib(type=str)
|
|
>>> attr.fields(C).x.type
|
|
<class 'int'>
|
|
>>> attr.fields(C).y.type
|
|
<class 'str'>
|
|
|
|
Currently, ``attrs`` doesn't do anything with this information but it's very useful if you'd like to write your own validators or serializers!
|
|
|
|
|
|
.. _extending_metadata:
|
|
|
|
Metadata
|
|
--------
|
|
|
|
If you're the author of a third-party library with ``attrs`` integration, you may want to take advantage of attribute metadata.
|
|
|
|
Here are some tips for effective use of metadata:
|
|
|
|
- Try making your metadata keys and values immutable.
|
|
This keeps the entire ``Attribute`` instances immutable too.
|
|
|
|
- To avoid metadata key collisions, consider exposing your metadata keys from your modules.::
|
|
|
|
from mylib import MY_METADATA_KEY
|
|
|
|
@attr.s
|
|
class C(object):
|
|
x = attr.ib(metadata={MY_METADATA_KEY: 1})
|
|
|
|
Metadata should be composable, so consider supporting this approach even if you decide implementing your metadata in one of the following ways.
|
|
|
|
- Expose ``attr.ib`` wrappers for your specific metadata.
|
|
This is a more graceful approach if your users don't require metadata from other libraries.
|
|
|
|
.. doctest::
|
|
|
|
>>> MY_TYPE_METADATA = '__my_type_metadata'
|
|
>>>
|
|
>>> def typed(cls, default=attr.NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata={}, type=None, converter=None):
|
|
... metadata = dict() if not metadata else metadata
|
|
... metadata[MY_TYPE_METADATA] = cls
|
|
... return attr.ib(default, validator, repr, cmp, hash, init, metadata, type, converter)
|
|
>>>
|
|
>>> @attr.s
|
|
... class C(object):
|
|
... x = typed(int, default=1, init=False)
|
|
>>> attr.fields(C).x.metadata[MY_TYPE_METADATA]
|
|
<class 'int'>
|