================================
Customizing class un/structuring
================================

This section deals with customizing the unstructuring and structuring processes in `cattrs`.

Using ``cattr.GenConverter``
********************************

The :mod:`cattr` module contains a :class:`~cattr.Converter` subclass, the :class:`~cattr.GenConverter`.
The ``GenConverter``, upon first encountering an ``attrs`` class, will use
the generation functions mentioned here to generate the specialized hooks for it,
register the hooks and use them.

Manual un/structuring hooks
***************************

You can write your own structuring and unstructuring functions and register
them for types using :py:attr:`Converter.register_structure_hook <cattr.Converter.register_structure_hook>` and
:py:attr:`Converter.register_unstructure_hook <cattr.Converter.register_unstructure_hook>`. This approach is the most
flexible but also requires the most amount of boilerplate.

Using ``cattr.gen`` generators
******************************

`cattrs` includes a module, :mod:`cattr.gen`, which allows for generating and
compiling specialized functions for unstructuring ``attrs`` classes.

One reason for generating these functions in advance is that they can bypass
a lot of `cattrs` machinery and be significantly faster than normal `cattrs`.

Another reason is that it's possible to override behavior on a per-attribute basis.

Currently, the overrides only support generating dictionary un/structuring functions
(as opposed to tuples), and support ``omit_if_default``, ``forbid_extra_keys``,
``rename`` and ``omit``.

``omit_if_default``
-------------------

This override can be applied on a per-class or per-attribute basis. The generated
unstructuring function will skip unstructuring values that are equal to their
default or factory values.

.. doctest::

    >>> from cattr.gen import make_dict_unstructure_fn, override
    >>>
    >>> @define
    ... class WithDefault:
    ...    a: int
    ...    b: dict = Factory(dict)
    >>>
    >>> c = cattr.Converter()
    >>> c.register_unstructure_hook(WithDefault, make_dict_unstructure_fn(WithDefault, c, b=override(omit_if_default=True)))
    >>> c.unstructure(WithDefault(1))
    {'a': 1}

Note that the per-attribute value overrides the per-class value. A side-effect
of this is the ability to force the presence of a subset of fields.
For example, consider a class with a `DateTime` field and a factory for it:
skipping the unstructuring of the `DateTime` field would be inconsistent and
based on the current time. So we apply the `omit_if_default` rule to the class,
but not to the `DateTime` field.

.. note::

    The parameter to `make_dict_unstructure_function` is named ``_cattrs_omit_if_default`` instead of just ``omit_if_default`` to avoid potential collisions with an override for a field named ``omit_if_default``.

.. doctest::

    >>> from pendulum import DateTime
    >>> from cattr.gen import make_dict_unstructure_fn, override
    >>>
    >>> @define
    ... class TestClass:
    ...     a: Optional[int] = None
    ...     b: DateTime = Factory(DateTime.utcnow)
    >>>
    >>> c = cattr.Converter()
    >>> hook = make_dict_unstructure_fn(TestClass, c, _cattrs_omit_if_default=True, b=override(omit_if_default=False))
    >>> c.register_unstructure_hook(TestClass, hook)
    >>> c.unstructure(TestClass())
    {'b': ...}

This override has no effect when generating structuring functions.

``forbid_extra_keys``
---------------------

By default ``cattrs`` is lenient in accepting unstructured input.  If extra
keys are present in a dictionary, they will be ignored when generating a
structured object.  Sometimes it may be desirable to enforce a stricter
contract, and to raise an error when unknown keys are present - in particular
when fields have default values this may help with catching typos.
`forbid_extra_keys` can also be enabled (or disabled) on a per-class basis when
creating structure hooks with ``make_dict_structure_fn``.

.. doctest::

    >>> from cattr.gen import make_dict_structure_fn
    >>>
    >>> @define
    ... class TestClass:
    ...    number: int = 1
    >>>
    >>> c = cattr.GenConverter(forbid_extra_keys=True)
    >>> c.structure({"nummber": 2}, TestClass)
    Traceback (most recent call last):
    ...
    Exception: Extra fields in constructor for TestClass: nummber
    >>> hook = make_dict_structure_fn(TestClass, c, _cattrs_forbid_extra_keys=False)
    >>> c.register_structure_hook(TestClass, hook)
    >>> c.structure({"nummber": 2}, TestClass)
    TestClass(number=1)

This behavior can only be applied to classes or to the default for the
`GenConverter`, and has no effect when generating unstructuring functions.

``rename``
----------

Using the rename override makes ``cattrs`` simply use the provided name instead
of the real attribute name. This is useful if an attribute name is a reserved
keyword in Python.

.. doctest::

    >>> from pendulum import DateTime
    >>> from cattr.gen import make_dict_unstructure_fn, make_dict_structure_fn, override
    >>>
    >>> @define
    ... class ExampleClass:
    ...     klass: Optional[int]
    >>>
    >>> c = cattr.Converter()
    >>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, klass=override(rename="class"))
    >>> st_hook = make_dict_structure_fn(ExampleClass, c, klass=override(rename="class"))
    >>> c.register_unstructure_hook(ExampleClass, unst_hook)
    >>> c.register_structure_hook(ExampleClass, st_hook)
    >>> c.unstructure(ExampleClass(1))
    {'class': 1}
    >>> c.structure({'class': 1}, ExampleClass)
    ExampleClass(klass=1)

``omit``
--------

This override can only be applied to individual attributes. Using the ``omit``
override will simply skip the attribute completely when generating a structuring
or unstructuring function.


.. doctest::

    >>> from cattr.gen import make_dict_unstructure_fn, override
    >>>
    >>> @define
    ... class ExampleClass:
    ...     an_int: int
    >>>
    >>> c = cattr.Converter()
    >>> unst_hook = make_dict_unstructure_fn(ExampleClass, c, an_int=override(omit=True))
    >>> c.register_unstructure_hook(ExampleClass, unst_hook)
    >>> c.unstructure(ExampleClass(1))
    {}

