2015-03-23 09:32:13 +00:00
Extending
=========
Each `` attrs `` -decorated class has a `` __attrs_attrs__ `` class attribute.
2019-09-09 13:02:16 +00:00
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__)
2018-10-05 14:49:12 +00:00
... return cls
2015-03-23 09:32:13 +00:00
>>> @print_attrs
... @attr.s
... class C(object):
... a = attr.ib()
2020-04-06 09:41:52 +00:00
(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 ::
2019-09-09 13:02:16 +00:00
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
2020-07-20 08:10:03 +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,
2019-09-09 13:02:16 +00:00
- 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'
>>>
2019-09-22 13:07:19 +00:00
>>> 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
2019-09-22 13:07:19 +00:00
... 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'>