Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add narrative chapter about initialization #370

Merged
merged 13 commits into from Apr 24, 2018
49 changes: 0 additions & 49 deletions docs/api.rst
Expand Up @@ -143,55 +143,6 @@ Core
y = attr.ib()


Influencing Initialization
++++++++++++++++++++++++++

Generally speaking, it's best to keep logic out of your ``__init__``.
The moment you need a finer control over how your class is instantiated, it's usually best to use a classmethod factory or to apply the `builder pattern <https://en.wikipedia.org/wiki/Builder_pattern>`_.

However, sometimes you need to do that one quick thing after your class is initialized.
And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatically detected and run after ``attrs`` is done initializing your instance:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib()
... y = attr.ib(init=False)
... def __attrs_post_init__(self):
... self.y = self.x + 1
>>> C(1)
C(x=1, y=2)

Please note that you can't directly set attributes on frozen classes:

.. doctest::

>>> @attr.s(frozen=True)
... class FrozenBroken(object):
... x = attr.ib()
... y = attr.ib(init=False)
... def __attrs_post_init__(self):
... self.y = self.x + 1
>>> FrozenBroken(1)
Traceback (most recent call last):
...
attr.exceptions.FrozenInstanceError: can't set attribute

If you need to set attributes on a frozen class, you'll have to resort to the :ref:`same trick <how-frozen>` as ``attrs`` and use :meth:`object.__setattr__`:

.. doctest::

>>> @attr.s(frozen=True)
... class Frozen(object):
... x = attr.ib()
... y = attr.ib(init=False)
... def __attrs_post_init__(self):
... object.__setattr__(self, "y", self.x + 1)
>>> Frozen(1)
Frozen(x=1, y=2)


.. _helpers:

Helpers
Expand Down
92 changes: 17 additions & 75 deletions docs/examples.rst
Expand Up @@ -218,8 +218,6 @@ Other times, all you want is a tuple and ``attrs`` won't let you down:
True




Defaults
--------

Expand Down Expand Up @@ -301,18 +299,7 @@ Although your initializers should do as little as possible (ideally: just initia

``attrs`` offers two ways to define validators for each attribute and it's up to you to choose which one suites better your style and project.


Decorator
~~~~~~~~~

The more straightforward way is by using the attribute's ``validator`` method as a decorator.
The method has to accept three arguments:

#. the *instance* that's being validated (aka ``self``),
#. the *attribute* that it's validating, and finally
#. the *value* that is passed for it.

If the value does not pass the validator's standards, it just raises an appropriate exception.
You can use a decorator:

.. doctest::

Expand All @@ -330,15 +317,7 @@ If the value does not pass the validator's standards, it just raises an appropri
...
ValueError: x must be smaller or equal to 42


Callables
~~~~~~~~~

If you want to re-use your validators, you should have a look at the ``validator`` argument to :func:`attr.ib()`.

It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach.

Since the validators runs *after* the instance is initialized, you can refer to other attributes while validating:
...or a callable...

.. doctest::

Expand All @@ -357,34 +336,7 @@ Since the validators runs *after* the instance is initialized, you can refer to
...
ValueError: 'x' has to be smaller than 'y'!

This example also shows of some syntactic sugar for using the :func:`attr.validators.and_` validator: if you pass a list, all validators have to pass.

``attrs`` won't intercept your changes to those attributes but you can always call :func:`attr.validate` on any instance to verify that it's still valid:

.. doctest::

>>> i = C(4, 5)
>>> i.x = 5 # works, no magic here
>>> attr.validate(i)
Traceback (most recent call last):
...
ValueError: 'x' has to be smaller than 'y'!

``attrs`` ships with a bunch of validators, make sure to :ref:`check them out <api_validators>` before writing your own:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')

Of course you can mix and match the two approaches at your convenience:
...or both at once:

.. doctest::

Expand All @@ -406,16 +358,22 @@ Of course you can mix and match the two approaches at your convenience:
...
ValueError: value out of bounds

And finally you can disable validators globally:

>>> attr.set_run_validators(False)
>>> C("128")
C(x='128')
>>> attr.set_run_validators(True)
>>> C("128")
``attrs`` ships with a bunch of validators, make sure to :ref:`check them out <api_validators>` before writing your own:

.. doctest::

>>> @attr.s
... class C(object):
... x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
...
TypeError: ("'x' must be <class 'int'> (got '128' that is a <class 'str'>).", Attribute(name='x', default=NOTHING, validator=[<instance_of validator for type <class 'int'>>, <function fits_byte at 0x10fd7a0d0>], repr=True, cmp=True, hash=True, init=True, metadata=mappingproxy({}), type=None, converter=None), <class 'int'>, '128')
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')

Check out :ref:`validators` for more details.


Conversion
Expand All @@ -433,23 +391,7 @@ This can be useful for doing type-conversions on values that you don't want to f
>>> o.x
1

Converters are run *before* validators, so you can use validators to check the final form of the value.

.. doctest::

>>> def validate_x(instance, attribute, value):
... if value < 0:
... raise ValueError("x must be be at least 0.")
>>> @attr.s
... class C(object):
... x = attr.ib(converter=int, validator=validate_x)
>>> o = C("0")
>>> o.x
0
>>> C("-1")
Traceback (most recent call last):
...
ValueError: x must be be at least 0.
Check out :ref:`converters` for more details.


.. _metadata:
Expand Down
4 changes: 4 additions & 0 deletions docs/index.rst
Expand Up @@ -36,6 +36,9 @@ Day-to-Day Usage
================

- Once you're comfortable with the concepts, our :doc:`api` contains all information you need to use ``attrs`` to its fullest.
- Instance initialization is one of ``attrs`` key feature areas.
Our goal is to relieve you from writing as much code as possible.
:doc:`init` gives you an overview what ``attrs`` has to offer and explains some related philosophies we believe in.
- If you want to put objects into sets or use them as keys in dictionaries, they have to be hashable.
The simplest way to do that is to use frozen classes, but the topic is more complex than it seems and :doc:`hashing` will give you a primer on what to look out for.
- ``attrs`` is built for extension from the ground up.
Expand Down Expand Up @@ -70,6 +73,7 @@ Full Table of Contents
overview
why
examples
init
hashing
api
extending
Expand Down