Skip to content

Commit

Permalink
NG: make frozen classes comfortably subclassable (#687)
Browse files Browse the repository at this point in the history
* NG: make frozen classes comfortably subclassable

* Add newsfragment

* This ain't markdown

* Address review
  • Loading branch information
hynek committed Sep 5, 2020
1 parent 4875590 commit 504eefe
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 7 deletions.
2 changes: 2 additions & 0 deletions changelog.d/687.change.rst
@@ -0,0 +1,2 @@
The ergonomics of creating frozen classes using ``@define(frozen=True)`` and sub=classing frozen classes has been improved:
you don't have to set ``on_setattr=None`` anymore.
40 changes: 33 additions & 7 deletions src/attr/_next_gen.py
Expand Up @@ -10,7 +10,7 @@
from attr.exceptions import UnannotatedAttributeError

from . import setters
from ._make import NOTHING, attrib, attrs
from ._make import NOTHING, _frozen_setattrs, attrib, attrs


def define(
Expand All @@ -32,10 +32,10 @@ def define(
order=False,
auto_detect=True,
getstate_setstate=None,
on_setattr=setters.validate,
on_setattr=None,
):
r"""
The only behavioral difference is the handling of the *auto_attribs*
The only behavioral differences are the handling of the *auto_attribs*
option:
:param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves
Expand All @@ -46,6 +46,7 @@ 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__``.
.. versionadded:: 20.1.0
"""
Expand Down Expand Up @@ -73,11 +74,36 @@ def do_it(cls, auto_attribs):
on_setattr=on_setattr,
)

if auto_attribs is not None:
return do_it(maybe_cls, auto_attribs)

def wrap(cls):
# Making this a wrapper ensures this code runs during class creation.
"""
Making this a wrapper ensures this code runs during class creation.
We also ensure that frozen-ness of classes is inherited.
"""
nonlocal frozen, on_setattr

had_on_setattr = on_setattr not in (None, setters.NO_OP)

# By default, mutable classes validate on setattr.
if frozen is False and on_setattr is None:
on_setattr = setters.validate

# However, if we subclass a frozen class, we inherit the immutability
# and disable on_setattr.
for base_cls in cls.__bases__:
if base_cls.__setattr__ is _frozen_setattrs:
if had_on_setattr:
raise ValueError(
"Frozen classes can't use on_setattr "
"(frozen-ness was inherited)."
)

on_setattr = setters.NO_OP
break

if auto_attribs is not None:
return do_it(cls, auto_attribs)

try:
return do_it(cls, True)
except UnannotatedAttributeError:
Expand Down
65 changes: 65 additions & 0 deletions tests/test_next_gen.py
Expand Up @@ -2,6 +2,8 @@
Python 3-only integration tests for provisional next generation APIs.
"""

import re

import pytest

import attr
Expand Down Expand Up @@ -173,3 +175,66 @@ def __eq__(self, o):

with pytest.raises(ValueError):
C() == C()

def test_subclass_frozen(self):
"""
It's possible to subclass an `attr.frozen` class and the frozen-ness is
inherited.
"""

@attr.frozen
class A:
a: int

@attr.frozen
class B(A):
b: int

@attr.define(on_setattr=attr.setters.NO_OP)
class C(B):
c: int

assert B(1, 2) == B(1, 2)
assert C(1, 2, 3) == C(1, 2, 3)

with pytest.raises(attr.exceptions.FrozenInstanceError):
A(1).a = 1

with pytest.raises(attr.exceptions.FrozenInstanceError):
B(1, 2).a = 1

with pytest.raises(attr.exceptions.FrozenInstanceError):
B(1, 2).b = 2

with pytest.raises(attr.exceptions.FrozenInstanceError):
C(1, 2, 3).c = 3

def test_catches_frozen_on_setattr(self):
"""
Passing frozen=True and on_setattr hooks is caught, even if the
immutability is inherited.
"""

@attr.define(frozen=True)
class A:
pass

with pytest.raises(
ValueError, match="Frozen classes can't use on_setattr."
):

@attr.define(frozen=True, on_setattr=attr.setters.validate)
class B:
pass

with pytest.raises(
ValueError,
match=re.escape(
"Frozen classes can't use on_setattr "
"(frozen-ness was inherited)."
),
):

@attr.define(on_setattr=attr.setters.validate)
class C(A):
pass

0 comments on commit 504eefe

Please sign in to comment.