From 077ace47a2ca7ceb6c1e76701cf0f472511d6455 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Mon, 13 Dec 2021 14:11:19 +0100 Subject: [PATCH] NG: convert on setattr by default Not doing that from the get-go was an oversight. Fixes #835 --- changelog.d/835.breaking.rst | 4 ++++ changelog.d/886.breaking.rst | 4 ++++ docs/api.rst | 4 +++- src/attr/_next_gen.py | 14 +++++++++----- tests/test_next_gen.py | 25 +++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 changelog.d/835.breaking.rst create mode 100644 changelog.d/886.breaking.rst diff --git a/changelog.d/835.breaking.rst b/changelog.d/835.breaking.rst new file mode 100644 index 000000000..8cdf0412d --- /dev/null +++ b/changelog.d/835.breaking.rst @@ -0,0 +1,4 @@ +When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. +I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``. + +This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. diff --git a/changelog.d/886.breaking.rst b/changelog.d/886.breaking.rst new file mode 100644 index 000000000..8cdf0412d --- /dev/null +++ b/changelog.d/886.breaking.rst @@ -0,0 +1,4 @@ +When using ``@attr.define``, converters are now run by default when setting an attribute on an instance -- additionally to validators. +I.e. the new default is ``on_setattr=[attr.setters.convert, attr.setters.validate]``. + +This is unfortunately a breaking change, but it was an oversight, impossible to raise a ``DeprecationWarning`` about, and it's better to fix it now while the APIs are very fresh with few users. diff --git a/docs/api.rst b/docs/api.rst index afbc87025..1dcaf978e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -707,13 +707,15 @@ The most notable differences are: - *auto_exc=True* - *auto_detect=True* - *eq=True*, but *order=False* -- Validators run when you set an attribute (*on_setattr=attr.setters.validate*). +- Converters and validators are run when you set an attribute (*on_setattr=[attr.setters.convert, attr.setters.validate*]). - Some options that aren't relevant to Python 3 have been dropped. Please note that these are *defaults* and you're free to override them, just like before. Since the Python ecosystem has settled on the term ``field`` for defining attributes, we have also added `attr.field` as a substitute for `attr.ib`. +.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. note:: `attr.s` and `attr.ib` (and their serious business cousins) aren't going anywhere. diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 1d8acac36..6dd2ebc11 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -35,8 +35,10 @@ def define( match_args=True, ): r""" - The only behavioral differences are the handling of the *auto_attribs* - option: + Define an ``attrs`` class. + + The behavioral differences to `attr.s` are the handling of the + *auto_attribs* option: :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves exactly like `attr.s`. If left `None`, `attr.s` will try to guess: @@ -46,9 +48,11 @@ def define( 2. Otherwise it assumes *auto_attribs=False* and tries to collect `attr.ib`\ s. - and that mutable classes (``frozen=False``) validate on ``__setattr__``. + and that mutable classes (``frozen=False``) convert and validate on + ``__setattr__``. .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. """ def do_it(cls, auto_attribs): @@ -86,9 +90,9 @@ def wrap(cls): had_on_setattr = on_setattr not in (None, setters.NO_OP) - # By default, mutable classes validate on setattr. + # By default, mutable classes convert & validate on setattr. if frozen is False and on_setattr is None: - on_setattr = setters.validate + on_setattr = [setters.convert, setters.validate] # However, if we subclass a frozen class, we inherit the immutability # and disable on_setattr. diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index fce01ad45..fdc06b719 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -308,3 +308,28 @@ class MyException(Exception): assert "foo" == ei.value.x assert ei.value.__cause__ is None + + def test_converts_and_validates_by_default(self): + """ + If no on_setattr is set, assume setters.convert, setters.validate. + """ + + @attr.define + class C: + x: int = attr.field(converter=int) + + @x.validator + def _v(self, _, value): + if value < 10: + raise ValueError("must be >=10") + + inst = C(10) + + # Converts + inst.x = "11" + + assert 11 == inst.x + + # Validates + with pytest.raises(ValueError, match="must be >=10"): + inst.x = "9"