Convert extending.rst to md
This commit is contained in:
parent
3473b4d3e2
commit
9fd0f82ff0
|
@ -477,7 +477,7 @@ All ``attrs`` attributes may include arbitrary metadata in the form of a read-on
|
|||
Metadata is not used by ``attrs``, and is meant to enable rich functionality in third-party libraries.
|
||||
The metadata dictionary follows the normal dictionary rules: keys need to be hashable, and both keys and values are recommended to be immutable.
|
||||
|
||||
If you're the author of a third-party library with ``attrs`` integration, please see `Extending Metadata <extending_metadata>`.
|
||||
If you're the author of a third-party library with ``attrs`` integration, please see `Extending Metadata <extending-metadata>`.
|
||||
|
||||
|
||||
Types
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
# Extending
|
||||
|
||||
Each *attrs*-decorated class has a `__attrs_attrs__` class attribute.
|
||||
It's a tuple of {class}`attrs.Attribute` carrying metadata about each attribute.
|
||||
|
||||
So it is fairly simple to build your own decorators on top of *attrs*:
|
||||
|
||||
```{doctest}
|
||||
>>> from attr import define
|
||||
>>> def print_attrs(cls):
|
||||
... print(cls.__attrs_attrs__)
|
||||
... return cls
|
||||
>>> @print_attrs
|
||||
... @define
|
||||
... class C:
|
||||
... a: int
|
||||
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),)
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
The {func}`attrs.define` / {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:
|
||||
|
||||
```python
|
||||
@a
|
||||
@b
|
||||
def f():
|
||||
pass
|
||||
```
|
||||
|
||||
is just [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) for:
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
Another common use case is to overwrite *attrs*'s defaults.
|
||||
|
||||
|
||||
### Mypy
|
||||
|
||||
Unfortunately, decorator wrapping 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:
|
||||
|
||||
```python
|
||||
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 using your project's `mypy.ini`:
|
||||
|
||||
```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.
|
||||
:::
|
||||
|
||||
|
||||
### Pyright
|
||||
|
||||
Generic decorator wrapping is supported in [*Pyright*](https://github.com/microsoft/pyright) via their [`dataclass_transform`] specification.
|
||||
|
||||
For a custom wrapping of the form:
|
||||
|
||||
```
|
||||
def custom_define(f):
|
||||
return attr.define(f)
|
||||
```
|
||||
|
||||
This is implemented via a `__dataclass_transform__` type decorator in the custom extension's `.pyi` of the form:
|
||||
|
||||
```
|
||||
def __dataclass_transform__(
|
||||
*,
|
||||
eq_default: bool = True,
|
||||
order_default: bool = False,
|
||||
kw_only_default: bool = False,
|
||||
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
|
||||
) -> Callable[[_T], _T]: ...
|
||||
|
||||
@__dataclass_transform__(field_descriptors=(attr.attrib, attr.field))
|
||||
def custom_define(f): ...
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
`dataclass_transform` is supported **provisionally** as of `pyright` 1.1.135.
|
||||
|
||||
Both the *Pyright* [`dataclass_transform`] specification and *attrs* implementation may change in future versions.
|
||||
:::
|
||||
|
||||
## Types
|
||||
|
||||
*attrs* offers two ways of attaching type information to attributes:
|
||||
|
||||
- {pep}`526` annotations,
|
||||
- and the *type* argument to {func}`attr.ib`.
|
||||
|
||||
This information is available to you:
|
||||
|
||||
```{doctest}
|
||||
>>> from attr import attrib, define, field, fields
|
||||
>>> @define
|
||||
... class C:
|
||||
... x: int = field()
|
||||
... y = attrib(type=str)
|
||||
>>> fields(C).x.type
|
||||
<class 'int'>
|
||||
>>> 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 {class}`~attrs.Attribute` instances immutable too.
|
||||
|
||||
- To avoid metadata key collisions, consider exposing your metadata keys from your modules.:
|
||||
|
||||
```
|
||||
from mylib import MY_METADATA_KEY
|
||||
|
||||
@define
|
||||
class C:
|
||||
x = field(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 `field` wrappers for your specific metadata.
|
||||
This is a more graceful approach if your users don't require metadata from other libraries.
|
||||
|
||||
```{eval-rst}
|
||||
.. doctest::
|
||||
|
||||
>>> from attr import fields, NOTHING
|
||||
>>> MY_TYPE_METADATA = '__my_type_metadata'
|
||||
>>>
|
||||
>>> def typed(
|
||||
... cls, default=NOTHING, validator=None, repr=True,
|
||||
... eq=True, order=None, hash=None, init=True, metadata=None,
|
||||
... converter=None
|
||||
... ):
|
||||
... metadata = metadata or {}
|
||||
... metadata[MY_TYPE_METADATA] = cls
|
||||
... return field(
|
||||
... default=default, validator=validator, repr=repr,
|
||||
... eq=eq, order=order, hash=hash, init=init,
|
||||
... metadata=metadata, converter=converter
|
||||
... )
|
||||
>>>
|
||||
>>> @define
|
||||
... class C:
|
||||
... x: int = typed(int, default=1, init=False)
|
||||
>>> fields(C).x.metadata[MY_TYPE_METADATA]
|
||||
<class 'int'>
|
||||
|
||||
```
|
||||
|
||||
(transform-fields)=
|
||||
|
||||
## Automatic Field Transformation and Modification
|
||||
|
||||
*attrs* allows you to automatically modify or transform the class' fields while the class is being created.
|
||||
You do this by passing a *field_transformer* hook to {func}`~attrs.define` (and friends).
|
||||
Its main purpose is to automatically add converters to attributes based on their type to aid the development of API clients and other typed data loaders.
|
||||
|
||||
This hook must have the following signature:
|
||||
|
||||
```{eval-rst}
|
||||
.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute]
|
||||
:noindex:
|
||||
```
|
||||
|
||||
- *cls* is your class right *before* it is being converted into an attrs class.
|
||||
This means it does not yet have the `__attrs_attrs__` attribute.
|
||||
- *fields* is a list of all `attrs.Attribute` instances that will later be set to `__attrs_attrs__`.
|
||||
You can modify these attributes any way you want:
|
||||
You can add converters, change types, and even remove attributes completely or create new ones!
|
||||
|
||||
For example, let's assume that you really don't like floats:
|
||||
|
||||
```{doctest}
|
||||
>>> def drop_floats(cls, fields):
|
||||
... return [f for f in fields if f.type not in {float, 'float'}]
|
||||
...
|
||||
>>> @frozen(field_transformer=drop_floats)
|
||||
... class Data:
|
||||
... a: int
|
||||
... b: float
|
||||
... c: str
|
||||
...
|
||||
>>> Data(42, "spam")
|
||||
Data(a=42, c='spam')
|
||||
```
|
||||
|
||||
A more realistic example would be to automatically convert data that you, e.g., load from JSON:
|
||||
|
||||
```{doctest}
|
||||
>>> from datetime import datetime
|
||||
>>>
|
||||
>>> def auto_convert(cls, fields):
|
||||
... results = []
|
||||
... for field in fields:
|
||||
... if field.converter is not None:
|
||||
... results.append(field)
|
||||
... continue
|
||||
... if field.type in {datetime, 'datetime'}:
|
||||
... converter = (lambda d: datetime.fromisoformat(d) if isinstance(d, str) else d)
|
||||
... else:
|
||||
... converter = None
|
||||
... results.append(field.evolve(converter=converter))
|
||||
... return results
|
||||
...
|
||||
>>> @frozen(field_transformer=auto_convert)
|
||||
... class Data:
|
||||
... a: int
|
||||
... b: str
|
||||
... c: datetime
|
||||
...
|
||||
>>> from_json = {"a": 3, "b": "spam", "c": "2020-05-04T13:37:00"}
|
||||
>>> Data(**from_json) # ****
|
||||
Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37))
|
||||
```
|
||||
|
||||
Or, perhaps you would prefer to generate dataclass-compatible `__init__` signatures via a default field *alias*.
|
||||
Note, *field_transformer* operates on {class}`attrs.Attribute` instances before the default private-attribute handling is applied so explicit user-provided aliases can be detected.
|
||||
|
||||
```{doctest}
|
||||
>>> def dataclass_names(cls, fields):
|
||||
... return [
|
||||
... field.evolve(alias=field.name)
|
||||
... if not field.alias
|
||||
... else field
|
||||
... for field in fields
|
||||
... ]
|
||||
...
|
||||
>>> @frozen(field_transformer=dataclass_names)
|
||||
... class Data:
|
||||
... public: int
|
||||
... _private: str
|
||||
... explicit: str = field(alias="aliased_name")
|
||||
...
|
||||
>>> Data(public=42, _private="spam", aliased_name="yes")
|
||||
Data(public=42, _private='spam', explicit='yes')
|
||||
```
|
||||
|
||||
## Customize Value Serialization in `asdict()`
|
||||
|
||||
*attrs* allows you to serialize instances of *attrs* classes to dicts using the {func}`attrs.asdict` function.
|
||||
However, the result can not always be serialized since most data types will remain as they are:
|
||||
|
||||
```{eval-rst}
|
||||
.. doctest::
|
||||
|
||||
>>> import json
|
||||
>>> import datetime
|
||||
>>> from attrs import asdict
|
||||
>>>
|
||||
>>> @frozen
|
||||
... class Data:
|
||||
... dt: datetime.datetime
|
||||
...
|
||||
>>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37)))
|
||||
>>> data
|
||||
{'dt': datetime.datetime(2020, 5, 4, 13, 37)}
|
||||
>>> json.dumps(data)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: Object of type datetime is not JSON serializable
|
||||
```
|
||||
|
||||
To help you with this, {func}`~attrs.asdict` allows you to pass a *value_serializer* hook.
|
||||
It has the signature
|
||||
|
||||
```{eval-rst}
|
||||
.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any
|
||||
:noindex:
|
||||
```
|
||||
|
||||
```{doctest}
|
||||
>>> from attr import asdict
|
||||
>>> def serialize(inst, field, value):
|
||||
... if isinstance(value, datetime.datetime):
|
||||
... return value.isoformat()
|
||||
... return value
|
||||
...
|
||||
>>> data = asdict(
|
||||
... Data(datetime.datetime(2020, 5, 4, 13, 37)),
|
||||
... value_serializer=serialize,
|
||||
... )
|
||||
>>> data
|
||||
{'dt': '2020-05-04T13:37:00'}
|
||||
>>> json.dumps(data)
|
||||
'{"dt": "2020-05-04T13:37:00"}'
|
||||
```
|
||||
|
||||
[`dataclass_transform`]: https://github.com/microsoft/pyright/blob/main/specs/dataclass_transforms.md
|
|
@ -1,335 +0,0 @@
|
|||
Extending
|
||||
=========
|
||||
|
||||
Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute.
|
||||
It's a tuple of `attrs.Attribute` carrying metadata about each attribute.
|
||||
|
||||
So it is fairly simple to build your own decorators on top of ``attrs``:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from attr import define
|
||||
>>> def print_attrs(cls):
|
||||
... print(cls.__attrs_attrs__)
|
||||
... return cls
|
||||
>>> @print_attrs
|
||||
... @define
|
||||
... class C:
|
||||
... a: int
|
||||
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=<class 'int'>, converter=None, kw_only=False, inherited=False, on_setattr=None, alias='a'),)
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
The `attrs.define`/`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.
|
||||
|
||||
Another common use case is to overwrite ``attrs``'s defaults.
|
||||
|
||||
Mypy
|
||||
^^^^
|
||||
|
||||
Unfortunately, decorator wrapping 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 using 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.
|
||||
|
||||
Pyright
|
||||
^^^^^^^
|
||||
|
||||
Generic decorator wrapping is supported in `pyright <https://github.com/microsoft/pyright>`_ via their dataclass_transform_ specification.
|
||||
|
||||
For a custom wrapping of the form::
|
||||
|
||||
def custom_define(f):
|
||||
return attr.define(f)
|
||||
|
||||
This is implemented via a ``__dataclass_transform__`` type decorator in the custom extension's ``.pyi`` of the form::
|
||||
|
||||
def __dataclass_transform__(
|
||||
*,
|
||||
eq_default: bool = True,
|
||||
order_default: bool = False,
|
||||
kw_only_default: bool = False,
|
||||
field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()),
|
||||
) -> Callable[[_T], _T]: ...
|
||||
|
||||
@__dataclass_transform__(field_descriptors=(attr.attrib, attr.field))
|
||||
def custom_define(f): ...
|
||||
|
||||
.. warning::
|
||||
|
||||
``dataclass_transform`` is supported **provisionally** as of ``pyright`` 1.1.135.
|
||||
|
||||
Both the ``pyright`` dataclass_transform_ specification and ``attrs`` implementation may change in future versions.
|
||||
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
``attrs`` offers two ways of attaching type information to attributes:
|
||||
|
||||
- :pep:`526` annotations,
|
||||
- and the *type* argument to `attr.ib`.
|
||||
|
||||
This information is available to you:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from attr import attrib, define, field, fields
|
||||
>>> @define
|
||||
... class C:
|
||||
... x: int = field()
|
||||
... y = attrib(type=str)
|
||||
>>> fields(C).x.type
|
||||
<class 'int'>
|
||||
>>> 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
|
||||
|
||||
@define
|
||||
class C:
|
||||
x = field(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 ``field`` wrappers for your specific metadata.
|
||||
This is a more graceful approach if your users don't require metadata from other libraries.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from attr import fields, NOTHING
|
||||
>>> MY_TYPE_METADATA = '__my_type_metadata'
|
||||
>>>
|
||||
>>> def typed(
|
||||
... cls, default=NOTHING, validator=None, repr=True,
|
||||
... eq=True, order=None, hash=None, init=True, metadata=None,
|
||||
... converter=None
|
||||
... ):
|
||||
... metadata = metadata or {}
|
||||
... metadata[MY_TYPE_METADATA] = cls
|
||||
... return field(
|
||||
... default=default, validator=validator, repr=repr,
|
||||
... eq=eq, order=order, hash=hash, init=init,
|
||||
... metadata=metadata, converter=converter
|
||||
... )
|
||||
>>>
|
||||
>>> @define
|
||||
... class C:
|
||||
... x: int = typed(int, default=1, init=False)
|
||||
>>> fields(C).x.metadata[MY_TYPE_METADATA]
|
||||
<class 'int'>
|
||||
|
||||
|
||||
.. _transform-fields:
|
||||
|
||||
Automatic Field Transformation and Modification
|
||||
-----------------------------------------------
|
||||
|
||||
``attrs`` allows you to automatically modify or transform the class' fields while the class is being created.
|
||||
You do this by passing a *field_transformer* hook to `attr.define` (and its friends).
|
||||
Its main purpose is to automatically add converters to attributes based on their type to aid the development of API clients and other typed data loaders.
|
||||
|
||||
This hook must have the following signature:
|
||||
|
||||
.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute]
|
||||
:noindex:
|
||||
|
||||
- *cls* is your class right *before* it is being converted into an attrs class.
|
||||
This means it does not yet have the ``__attrs_attrs__`` attribute.
|
||||
|
||||
- *fields* is a list of all `attrs.Attribute` instances that will later be set to ``__attrs_attrs__``.
|
||||
You can modify these attributes any way you want:
|
||||
You can add converters, change types, and even remove attributes completely or create new ones!
|
||||
|
||||
For example, let's assume that you really don't like floats:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> def drop_floats(cls, fields):
|
||||
... return [f for f in fields if f.type not in {float, 'float'}]
|
||||
...
|
||||
>>> @frozen(field_transformer=drop_floats)
|
||||
... class Data:
|
||||
... a: int
|
||||
... b: float
|
||||
... c: str
|
||||
...
|
||||
>>> Data(42, "spam")
|
||||
Data(a=42, c='spam')
|
||||
|
||||
A more realistic example would be to automatically convert data that you, e.g., load from JSON:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>>
|
||||
>>> def auto_convert(cls, fields):
|
||||
... results = []
|
||||
... for field in fields:
|
||||
... if field.converter is not None:
|
||||
... results.append(field)
|
||||
... continue
|
||||
... if field.type in {datetime, 'datetime'}:
|
||||
... converter = (lambda d: datetime.fromisoformat(d) if isinstance(d, str) else d)
|
||||
... else:
|
||||
... converter = None
|
||||
... results.append(field.evolve(converter=converter))
|
||||
... return results
|
||||
...
|
||||
>>> @frozen(field_transformer=auto_convert)
|
||||
... class Data:
|
||||
... a: int
|
||||
... b: str
|
||||
... c: datetime
|
||||
...
|
||||
>>> from_json = {"a": 3, "b": "spam", "c": "2020-05-04T13:37:00"}
|
||||
>>> Data(**from_json) # ****
|
||||
Data(a=3, b='spam', c=datetime.datetime(2020, 5, 4, 13, 37))
|
||||
|
||||
Or, perhaps you would prefer to generate dataclass-compatible ``__init__`` signatures via a default field ``alias``.
|
||||
Note, ``field_transformer`` operates on `attrs.Attribute` instances before the default private-attribute handling is applied so explicit user-provided aliases can be detected.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> def dataclass_names(cls, fields):
|
||||
... return [
|
||||
... field.evolve(alias=field.name)
|
||||
... if not field.alias
|
||||
... else field
|
||||
... for field in fields
|
||||
... ]
|
||||
...
|
||||
>>> @frozen(field_transformer=dataclass_names)
|
||||
... class Data:
|
||||
... public: int
|
||||
... _private: str
|
||||
... explicit: str = field(alias="aliased_name")
|
||||
...
|
||||
>>> Data(public=42, _private="spam", aliased_name="yes")
|
||||
Data(public=42, _private='spam', explicit='yes')
|
||||
|
||||
|
||||
Customize Value Serialization in ``asdict()``
|
||||
---------------------------------------------
|
||||
|
||||
``attrs`` allows you to serialize instances of ``attrs`` classes to dicts using the `attrs.asdict` function.
|
||||
However, the result can not always be serialized since most data types will remain as they are:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> import json
|
||||
>>> import datetime
|
||||
>>> from attrs import asdict
|
||||
>>>
|
||||
>>> @frozen
|
||||
... class Data:
|
||||
... dt: datetime.datetime
|
||||
...
|
||||
>>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37)))
|
||||
>>> data
|
||||
{'dt': datetime.datetime(2020, 5, 4, 13, 37)}
|
||||
>>> json.dumps(data)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: Object of type datetime is not JSON serializable
|
||||
|
||||
To help you with this, `attr.asdict` allows you to pass a *value_serializer* hook.
|
||||
It has the signature
|
||||
|
||||
.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any
|
||||
:noindex:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from attr import asdict
|
||||
>>> def serialize(inst, field, value):
|
||||
... if isinstance(value, datetime.datetime):
|
||||
... return value.isoformat()
|
||||
... return value
|
||||
...
|
||||
>>> data = asdict(
|
||||
... Data(datetime.datetime(2020, 5, 4, 13, 37)),
|
||||
... value_serializer=serialize,
|
||||
... )
|
||||
>>> data
|
||||
{'dt': '2020-05-04T13:37:00'}
|
||||
>>> json.dumps(data)
|
||||
'{"dt": "2020-05-04T13:37:00"}'
|
||||
|
||||
*****
|
||||
|
||||
.. _dataclass_transform: https://github.com/microsoft/pyright/blob/master/specs/dataclass_transforms.md
|
|
@ -189,7 +189,7 @@ def attrib(
|
|||
returned value will be used as the new value of the attribute. The
|
||||
value is converted before being passed to the validator, if any.
|
||||
:param metadata: An arbitrary mapping, to be used by third-party
|
||||
components. See `extending_metadata`.
|
||||
components. See `extending-metadata`.
|
||||
|
||||
:param type: The type of the attribute. Nowadays, the preferred method to
|
||||
specify the type is using a variable annotation (see :pep:`526`).
|
||||
|
|
Loading…
Reference in New Issue