Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog.d/645.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
It is now possible to specify hooks that are called whenever an attribute is set **after** a class has been instantiated.

You can pass ``on_setattr`` both to ``@attr.s()`` to set the default for all attributes on a class, and to ``@attr.ib()`` to overwrite it for individual attributes.

``attrs`` also comes with a new module ``attr.setters`` that brings helpers that run validators, converters, or allow to freeze a subset of attributes.
5 changes: 5 additions & 0 deletions changelog.d/660.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
It is now possible to specify hooks that are called whenever an attribute is set **after** a class has been instantiated.

You can pass ``on_setattr`` both to ``@attr.s()`` to set the default for all attributes on a class, and to ``@attr.ib()`` to overwrite it for individual attributes.

``attrs`` also comes with a new module ``attr.setters`` that brings helpers that run validators, converters, or allow to freeze a subset of attributes.
49 changes: 43 additions & 6 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
Core
----

.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None)
.. autodata:: attr.NOTHING

.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None)

.. note::

Expand Down Expand Up @@ -102,7 +104,7 @@ Core
... class C(object):
... x = attr.ib()
>>> attr.fields(C).x
Attribute(name='x', 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)
Attribute(name='x', 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, on_setattr=None)


.. autofunction:: attr.make_class
Expand Down Expand Up @@ -145,7 +147,9 @@ Exceptions
----------

.. autoexception:: attr.exceptions.PythonTooOldError
.. autoexception:: attr.exceptions.FrozenError
.. autoexception:: attr.exceptions.FrozenInstanceError
.. autoexception:: attr.exceptions.FrozenAttributeError
.. autoexception:: attr.exceptions.AttrsAttributeNotFoundError
.. autoexception:: attr.exceptions.NotAnAttrsClassError
.. autoexception:: attr.exceptions.DefaultAlreadySetError
Expand Down Expand Up @@ -178,9 +182,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields(C)
(Attribute(name='x', 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), Attribute(name='y', 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))
(Attribute(name='x', 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, on_setattr=None), Attribute(name='y', 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, on_setattr=None))
>>> attr.fields(C)[1]
Attribute(name='y', 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)
Attribute(name='y', 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, on_setattr=None)
>>> attr.fields(C).y is attr.fields(C)[1]
True

Expand All @@ -195,9 +199,9 @@ Helpers
... x = attr.ib()
... y = attr.ib()
>>> attr.fields_dict(C)
{'x': Attribute(name='x', 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), 'y': Attribute(name='y', 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)}
{'x': Attribute(name='x', 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, on_setattr=None), 'y': Attribute(name='y', 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, on_setattr=None)}
>>> attr.fields_dict(C)['y']
Attribute(name='y', 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)
Attribute(name='y', 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, on_setattr=None)
>>> attr.fields_dict(C)['y'] is attr.fields(C).y
True

Expand Down Expand Up @@ -516,6 +520,39 @@ Converters
C(x='')


.. _api_setters:

Setters
-------

These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on_setattr`` arguments.

.. autofunction:: attr.setters.frozen
.. autofunction:: attr.setters.validate
.. autofunction:: attr.setters.convert
.. autofunction:: attr.setters.pipe
.. autodata:: attr.setters.NO_OP

For example, only ``x`` is frozen here:

.. doctest::

>>> @attr.s(on_setattr=attr.setters.frozen)
... class C(object):
... x = attr.ib()
... y = attr.ib(on_setattr=attr.setters.NO_OP)
>>> c = C(1, 2)
>>> c.y = 3
>>> c.y
3
>>> c.x = 4
Traceback (most recent call last):
...
attr.exceptions.FrozenAttributeError: ()

N.B. Please use `attr.s`'s *frozen* argument to freeze whole classes; it is more efficient.


Deprecated APIs
---------------

Expand Down
2 changes: 1 addition & 1 deletion docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ So it is fairly simple to build your own decorators on top of ``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),)
(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, on_setattr=None),)


.. warning::
Expand Down
10 changes: 8 additions & 2 deletions docs/how-does-it-work.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ This **static** approach was very much a design goal of ``attrs`` and what I str
Immutability
------------

In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises a `attr.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute.
In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attr.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute.

The same is true if you choose to freeze individual attributes using the `attr.setters.frozen` *on_setattr* hook -- except that the exception becomes `attr.exceptions.FrozenAttributeError`.

Both errors subclass `attr.exceptions.FrozenError`.

-----

Depending on whether a class is a dict class or a slotted class, ``attrs`` uses a different technique to circumvent that limitation in the ``__init__`` method.

Expand Down Expand Up @@ -88,7 +94,7 @@ This is (still) slower than a plain assignment:
........................................
Median +- std dev: 676 ns +- 16 ns

So on a standard notebook the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds).
So on a laptop computer the difference is about 300 nanoseconds (1 second is 1,000,000,000 nanoseconds).
It's certainly something you'll feel in a hot loop but shouldn't matter in normal code.
Pick what's more important to you.

Expand Down
3 changes: 0 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@
EXTRAS_REQUIRE["dev"] = (
EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"]
)
EXTRAS_REQUIRE["azure-pipelines"] = EXTRAS_REQUIRE["tests"] + [
"pytest-azurepipelines"
]

###############################################################################

Expand Down
3 changes: 2 additions & 1 deletion src/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from functools import partial

from . import converters, exceptions, filters, validators
from . import converters, exceptions, filters, setters, validators
from ._config import get_run_validators, set_run_validators
from ._funcs import asdict, assoc, astuple, evolve, has
from ._make import (
Expand Down Expand Up @@ -63,6 +63,7 @@
"make_class",
"s",
"set_run_validators",
"setters",
"validate",
"validators",
]
13 changes: 13 additions & 0 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ from typing import (
from . import exceptions as exceptions
from . import filters as filters
from . import converters as converters
from . import setters as setters
from . import validators as validators

from ._version_info import VersionInfo
Expand All @@ -41,6 +42,10 @@ _ConverterType = Callable[[Any], _T]
_FilterType = Callable[[Attribute[_T], _T], bool]
_ReprType = Callable[[Any], str]
_ReprArgType = Union[bool, _ReprType]
_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any]
_OnSetAttrArgType = Union[
_OnSetAttrType, List[_OnSetAttrType], setters._NoOpType
]
# FIXME: in reality, if multiple validators are passed they must be in a list or tuple,
# but those are invariant and so would prevent subtypes of _ValidatorType from working
# when passed in a list or tuple.
Expand Down Expand Up @@ -74,6 +79,7 @@ class Attribute(Generic[_T]):
metadata: Dict[Any, Any]
type: Optional[Type[_T]]
kw_only: bool
on_setattr: _OnSetAttrType

# NOTE: We had several choices for the annotation to use for type arg:
# 1) Type[_T]
Expand Down Expand Up @@ -113,6 +119,7 @@ def attrib(
kw_only: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...

# This form catches an explicit None or no default and infers the type from the other arguments.
Expand All @@ -131,6 +138,7 @@ def attrib(
kw_only: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...

# This form catches an explicit default argument.
Expand All @@ -149,6 +157,7 @@ def attrib(
kw_only: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _T: ...

# This form covers type=non-Type: e.g. forward references (str), Any
Expand All @@ -167,6 +176,7 @@ def attrib(
kw_only: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Any: ...
@overload
def attrs(
Expand All @@ -189,6 +199,7 @@ def attrs(
order: Optional[bool] = ...,
auto_detect: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> _C: ...
@overload
def attrs(
Expand All @@ -211,6 +222,7 @@ def attrs(
order: Optional[bool] = ...,
auto_detect: bool = ...,
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> Callable[[_C], _C]: ...

# TODO: add support for returning NamedTuple from the mypy plugin
Expand Down Expand Up @@ -242,6 +254,7 @@ def make_class(
auto_exc: bool = ...,
eq: Optional[bool] = ...,
order: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
) -> type: ...

# _funcs --
Expand Down
Loading