attrs/docs/extending.rst

162 lines
4.9 KiB
ReStructuredText
Raw Normal View History

2015-03-23 09:32:13 +00:00
Extending
=========
Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute.
It is a tuple of `attr.Attribute` carrying meta-data about each attribute.
2015-03-23 09:32:13 +00:00
So it is fairly simple to build your own decorators on top of ``attrs``:
.. doctest::
>>> import attr
2016-08-15 13:59:10 +00:00
>>> def print_attrs(cls):
... print(cls.__attrs_attrs__)
... return cls
2015-03-23 09:32:13 +00:00
>>> @print_attrs
... @attr.s
... class C(object):
... a = attr.ib()
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False),)
2015-03-23 09:32:13 +00:00
.. warning::
The `attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place!
2015-03-23 09:32:13 +00:00
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))
2016-11-19 08:47:03 +00:00
2017-10-26 15:55:45 +00:00
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.
2019-10-01 14:21:36 +00:00
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.
2017-10-26 15:55:45 +00:00
Another common use case is to overwrite ``attrs``'s defaults.
Unfortunately, this currently `confuses <https://github.com/python/mypy/issues/5406>`_ mypy's ``attrs`` plugin.
At the moment, the best workaround is to hold your nose, write a fake mypy plugin, and mutate a bunch of global variables::
from mypy.plugin import Plugin
from mypy.plugins.attrs import (
attr_attrib_makers,
attr_class_makers,
attr_dataclass_makers,
)
# These work just like `attr.dataclass`.
attr_dataclass_makers.add("my_module.method_looks_like_attr_dataclass")
# This works just like `attr.s`.
attr_class_makers.add("my_module.method_looks_like_attr_s")
# These are our `attr.ib` makers.
attr_attrib_makers.add("my_module.method_looks_like_attrib")
class MyPlugin(Plugin):
# Our plugin does nothing but it has to exist so this file gets loaded.
pass
def plugin(version):
return MyPlugin
Then tell mypy about your plugin usin your project's ``mypy.ini``:
.. code:: ini
[mypy]
plugins=<path to file>
.. warning::
Please note that it is currently *impossible* to let mypy know that you've changed defaults like *eq* or *order*.
You can only use this trick to tell mypy that a class is actually an ``attrs`` class.
2017-10-26 15:55:45 +00:00
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 `attr.ib`.
2017-10-26 15:55:45 +00:00
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!
2016-11-19 08:47:03 +00:00
.. _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,
... eq=True, order=None, hash=None, init=True, metadata={},
... type=None, converter=None
... ):
2016-11-19 08:47:03 +00:00
... metadata = dict() if not metadata else metadata
... metadata[MY_TYPE_METADATA] = cls
... return attr.ib(
... default=default, validator=validator, repr=repr,
... eq=eq, order=order, hash=hash, init=init,
... metadata=metadata, type=type, converter=converter
... )
2016-11-19 08:47:03 +00:00
>>>
>>> @attr.s
... class C(object):
... x = typed(int, default=1, init=False)
>>> attr.fields(C).x.metadata[MY_TYPE_METADATA]
<class 'int'>