Skip to content

Commit a14e185

Browse files
authored
Raise a deprecation warning when evolve receives insta as a kw arg (#1117)
* Raise a deprecation warning when evolve receives insta as a kw arg Fixes #1109 * Add news fragment * Raise a better error * Handle too many pos args * Lazy import * Trim traceback * Add test evolving a field named inst * Spelling * Spin positively
1 parent 22ae847 commit a14e185

File tree

3 files changed

+83
-3
lines changed

3 files changed

+83
-3
lines changed

changelog.d/1117.change.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
It is now possible for `attrs.evolve()` (and `attr.evolve()`) to change fields named `inst` if the instance is passed as a positional argument.
2+
3+
Passing the instance using the `inst` keyword argument is now deprecated and will be removed in, or after, April 2024.

src/attr/_funcs.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,10 @@ def assoc(inst, **changes):
351351
return new
352352

353353

354-
def evolve(inst, **changes):
354+
def evolve(*args, **changes):
355355
"""
356-
Create a new instance, based on *inst* with *changes* applied.
356+
Create a new instance, based on the first positional argument with
357+
*changes* applied.
357358
358359
:param inst: Instance of a class with *attrs* attributes.
359360
:param changes: Keyword changes in the new copy.
@@ -365,8 +366,40 @@ def evolve(inst, **changes):
365366
:raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
366367
class.
367368
368-
.. versionadded:: 17.1.0
369+
.. versionadded:: 17.1.0
370+
.. deprecated:: 23.1.0
371+
It is now deprecated to pass the instance using the keyword argument
372+
*inst*. It will raise a warning until at least April 2024, after which
373+
it will become an error. Always pass the instance as a positional
374+
argument.
369375
"""
376+
# Try to get instance by positional argument first.
377+
# Use changes otherwise and warn it'll break.
378+
if args:
379+
try:
380+
(inst,) = args
381+
except ValueError:
382+
raise TypeError(
383+
f"evolve() takes 1 positional argument, but {len(args)} "
384+
"were given"
385+
) from None
386+
else:
387+
try:
388+
inst = changes.pop("inst")
389+
except KeyError:
390+
raise TypeError(
391+
"evolve() missing 1 required positional argument: 'inst'"
392+
) from None
393+
394+
import warnings
395+
396+
warnings.warn(
397+
"Passing the instance per keyword argument is deprecated and "
398+
"will stop working in, or after, April 2024.",
399+
DeprecationWarning,
400+
stacklevel=2,
401+
)
402+
370403
cls = inst.__class__
371404
attrs = fields(cls)
372405
for a in attrs:

tests/test_funcs.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,47 @@ class Cls2:
689689
assert Cls1({"foo": 42, "param2": 42}) == attr.evolve(
690690
obj1a, param1=obj2b
691691
)
692+
693+
def test_inst_kw(self):
694+
"""
695+
If `inst` is passed per kw argument, a warning is raised.
696+
See #1109
697+
"""
698+
699+
@attr.s
700+
class C:
701+
pass
702+
703+
with pytest.warns(DeprecationWarning) as wi:
704+
evolve(inst=C())
705+
706+
assert __file__ == wi.list[0].filename
707+
708+
def test_no_inst(self):
709+
"""
710+
Missing inst argument raises a TypeError like Python would.
711+
"""
712+
with pytest.raises(TypeError, match=r"evolve\(\) missing 1"):
713+
evolve(x=1)
714+
715+
def test_too_many_pos_args(self):
716+
"""
717+
More than one positional argument raises a TypeError like Python would.
718+
"""
719+
with pytest.raises(
720+
TypeError,
721+
match=r"evolve\(\) takes 1 positional argument, but 2 were given",
722+
):
723+
evolve(1, 2)
724+
725+
def test_can_change_inst(self):
726+
"""
727+
If the instance is passed by positional argument, a field named `inst`
728+
can be changed.
729+
"""
730+
731+
@attr.define
732+
class C:
733+
inst: int
734+
735+
assert C(42) == evolve(C(23), inst=42)

0 commit comments

Comments
 (0)