Skip to content

Commit ac8df61

Browse files
committed
changed to reuse and docs first pass
* Added material in "Other goodies" section in `examples.md` * docstrings for `field`, `attrib`, and `reuse` *Added towncrier changelog message
1 parent bf46813 commit ac8df61

7 files changed

Lines changed: 194 additions & 64 deletions

File tree

changelog.d/pr1429.change.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
Added `Attribute.reuse(**kwargs)`, which allows you to reuse (and simultaneously `evolve()`) attribute definitions from already defined classes:
2+
3+
```py
4+
@define
5+
class A:
6+
a: int = 100
7+
8+
@define
9+
class B(A):
10+
a = fields(B).a.reuse(default=200)
11+
12+
assert B().a == 200
13+
```
14+
15+
To preserve attribute ordering of `reuse`d attributes, `inherited` was exposed as a public keyword argument to `attrib` and friends. Setting `inherited=True` manually on a *attrs* field definition simply acts as a flag to tell *attrs* to use the parent class ordering, *if* it can find an attribute in the parent MRO with the same name. Otherwise, the field is simply added to the class's attribute list as if `inherited=False`.
16+
17+
```py
18+
@define
19+
class Parent:
20+
x: int = 1
21+
y: int = 2
22+
23+
@define
24+
class ChildOrder(Parent):
25+
x = fields(Parent).x.reuse(default=3)
26+
z: int = 4
27+
28+
assert repr(ChildOrder()) == "ChildOrder(y=2, x=3, z=4)"
29+
30+
@define
31+
class ParentOrder(Parent):
32+
x = fields(Parent).y.reuse(default=3, inherited=True)
33+
z = fields(ChildOrder).z.reuse(inherited=True) # `inherited` does nothing here
34+
35+
assert repr(ParentOrder()) == "ParentOrder(x=3, y=2, z=4)"
36+
```
37+
38+
Incidentally, because this behavior was added to `field`, this gives you a little more control of attribute ordering even when not using `reuse()`:
39+
40+
```py
41+
@define
42+
class Parent:
43+
a: int
44+
b: int = 10
45+
46+
class Child(Parent):
47+
a: str = field(default="test", inherited=True)
48+
49+
assert repr(Child()) == "Child(a='test', b=10)"
50+
```

docs/api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Core
3838
.. autofunction:: field
3939

4040
.. autoclass:: Attribute
41-
:members: evolve
41+
:members: evolve, reuse
4242

4343
For example:
4444

docs/examples.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,53 @@ If you need to dynamically make a class with {func}`~attrs.make_class` and it ne
747747
True
748748
```
749749

750+
In certain situations, you might want to reuse part (or all) of an existing attribute instead of entirely redefining it. This pattern is common in inheritance schemes, where you might just want to change one parameter of an attribute (i.e. default value) while leaving all other parts unchanged. For this purpose, *attrs* offers {meth}`.Attribute.reuse`:
751+
752+
```{doctest}
753+
>>> @define
754+
... class A:
755+
... a: int = field(default=10)
756+
...
757+
... @a.validator
758+
... def very_complex_validator(self, attr, value):
759+
... print("Validator runs!")
760+
761+
>>> @define
762+
... class B(A):
763+
... a = fields(A).a.reuse(default=20)
764+
765+
>>> B()
766+
Validator runs!
767+
B(a=20)
768+
```
769+
770+
This method inherits all of the keyword arguments from {func}`~attrs.field`, which works identically to defining a new attribute with the same parameters.
771+
772+
While this feature is mostly intended for making working with inherited classes easier, there's nothing requiring `reuse`d attributes actually be part of a parent class:
773+
774+
```{doctest}
775+
>>> @define
776+
... class C: # does not inherit class `A`
777+
... a = fields(A).a.reuse(factory=lambda: 100)
778+
... # And now that `a` is in scope of `C`, field decorators work again:
779+
... @a.validator
780+
... def my_new_validator_func(self, attr, value):
781+
... print("Another validator function!")
782+
783+
>>> C()
784+
Validator runs!
785+
Another validator function!
786+
C(a=100)
787+
```
788+
789+
This in combination with {func}`~attrs.make_class` makes a very powerful suite of *attrs* class manipulation tools both before and after class creation:
790+
791+
```{doctest}
792+
>>> C3 = make_class("C3", {"x": fields(C2).x.reuse(), "y": fields(C2).y.reuse()})
793+
>>> fields(C2) == fields(C3)
794+
True
795+
```
796+
750797
Sometimes, you want to have your class's `__init__` method do more than just
751798
the initialization, validation, etc. that gets done for you automatically when
752799
using `@define`.

src/attr/__init__.pyi

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,14 @@ class Attribute(Generic[_T]):
139139
alias: str | None
140140

141141
def evolve(self, **changes: Any) -> "Attribute[Any]": ...
142-
def to_field(self) -> Any: ...
142+
@overload
143+
def reuse(self, **changes: Any) -> Any: ...
144+
@overload
145+
def reuse(self, type: _T, **changes: Any) -> _T: ...
146+
@overload
147+
def reuse(self, default: _T, **changes: Any) -> _T: ...
148+
@overload
149+
def reuse(self, type: _T | None, **changes: Any) -> _T: ...
143150

144151
# NOTE: We had several choices for the annotation to use for type arg:
145152
# 1) Type[_T]
@@ -182,6 +189,7 @@ def attrib(
182189
order: _EqOrderType | None = ...,
183190
on_setattr: _OnSetAttrArgType | None = ...,
184191
alias: str | None = ...,
192+
inherited: bool | None = ...,
185193
) -> Any: ...
186194

187195
# This form catches an explicit None or no default and infers the type from the
@@ -206,6 +214,7 @@ def attrib(
206214
order: _EqOrderType | None = ...,
207215
on_setattr: _OnSetAttrArgType | None = ...,
208216
alias: str | None = ...,
217+
inherited: bool | None = ...,
209218
) -> _T: ...
210219

211220
# This form catches an explicit default argument.
@@ -229,6 +238,7 @@ def attrib(
229238
order: _EqOrderType | None = ...,
230239
on_setattr: _OnSetAttrArgType | None = ...,
231240
alias: str | None = ...,
241+
inherited: bool | None = ...,
232242
) -> _T: ...
233243

234244
# This form covers type=non-Type: e.g. forward references (str), Any
@@ -252,6 +262,7 @@ def attrib(
252262
order: _EqOrderType | None = ...,
253263
on_setattr: _OnSetAttrArgType | None = ...,
254264
alias: str | None = ...,
265+
inherited: bool | None = ...,
255266
) -> Any: ...
256267
@overload
257268
@dataclass_transform(order_default=True, field_specifiers=(attrib, field))

src/attr/_make.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def attrib(
157157
*eq*, *order*, and *cmp* also accept a custom callable
158158
.. versionchanged:: 21.1.0 *cmp* undeprecated
159159
.. versionadded:: 22.2.0 *alias*
160+
.. versionadded:: 25.4.0 *inherited*
160161
"""
161162
eq, eq_key, order, order_key = _determine_attrib_eq_order(
162163
cmp, eq, order, True
@@ -2550,34 +2551,55 @@ def evolve(self, **changes):
25502551

25512552
return new
25522553

2553-
def to_field(self):
2554+
def reuse(
2555+
self,
2556+
**field_kwargs,
2557+
):
25542558
"""
2555-
Converts this attribute back into a raw :py:class:`_CountingAttr` object,
2556-
such that it can be used to annotate newly `define`d classes. This is
2557-
useful if you want to reuse part (or all) of fields defined in other
2558-
attrs classes that have already been resolved into their finalized state.
2559+
Converts this attribute back into a raw :py:func:`attrs.field` object,
2560+
such that it can be used to annotate newly ``@define``-d classes. This
2561+
is useful if you want to reuse part (or all) of fields defined in other
2562+
attrs classes that have already been resolved into their finalized
2563+
:py:class:`.Attribute`.
2564+
2565+
Args:
2566+
field_kwargs: Any valid keyword argument to :py:func:`attrs.field`.
25592567
25602568
.. versionadded:: 25.4.0
25612569
"""
2562-
return _CountingAttr(
2563-
default=self.default,
2564-
validator=self.validator,
2565-
repr=self.repr,
2566-
cmp=None,
2567-
hash=self.hash,
2568-
init=self.init,
2569-
converter=self.converter,
2570-
metadata=self.metadata,
2571-
type=self.type,
2572-
kw_only=self.kw_only,
2573-
eq=self.eq,
2574-
eq_key=self.eq_key,
2575-
order=self.order,
2576-
order_key=self.order_key,
2577-
on_setattr=self.on_setattr,
2578-
alias=self.alias,
2579-
inherited=self.inherited,
2580-
)
2570+
args = {
2571+
"validator": self.validator,
2572+
"repr": self.repr,
2573+
"cmp": None,
2574+
"hash": self.hash,
2575+
"init": self.init,
2576+
"converter": self.converter,
2577+
"metadata": self.metadata,
2578+
"type": self.type,
2579+
"kw_only": self.kw_only,
2580+
"eq": self.eq,
2581+
"order": self.order,
2582+
"on_setattr": self.on_setattr,
2583+
"alias": self.alias,
2584+
"inherited": self.inherited,
2585+
}
2586+
2587+
# Map the single "validator" object back down to it's aliased pair.
2588+
# Additionally, we help the user out a little bit by automatically
2589+
# overwriting the compliment `default` or `factory` function when
2590+
# overriding; so if a field already has a `default=3`, using
2591+
# `reuse(factory=lambda: 3)` won't complain about having both kinds of
2592+
# defaults defined.
2593+
if "default" not in field_kwargs and "factory" not in field_kwargs:
2594+
if isinstance(self.default, Factory):
2595+
field_kwargs["factory"] = self.default.factory
2596+
else:
2597+
field_kwargs["default"] = self.default
2598+
2599+
args.update(field_kwargs)
2600+
2601+
# Send through attrib so we reuse the same errors + syntax sugar
2602+
return attrib(**args)
25812603

25822604
# Don't use _add_pickle since fields(Attribute) doesn't work
25832605
def __getstate__(self):

src/attr/_next_gen.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def field(
429429
order=None,
430430
on_setattr=None,
431431
alias=None,
432-
inherited=None,
432+
inherited=False,
433433
):
434434
"""
435435
Create a new :term:`field` / :term:`attribute` on a class.
@@ -566,7 +566,7 @@ def field(
566566
``__init__`` method. If left None, default to ``name`` stripped
567567
of leading underscores. See `private-attributes`.
568568
569-
inherited (bool | None):
569+
inherited (bool):
570570
Ensure this attribute inherits the ordering of the parent attribute
571571
with the same name. If no parent attribute with the same name
572572
exists, this field is treated as normal.

0 commit comments

Comments
 (0)