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
This commit is contained in:
Hynek Schlawack 2023-04-05 08:33:02 +02:00 committed by GitHub
parent 22ae8473fb
commit a14e1859b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 83 additions and 3 deletions

View File

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

View File

@ -351,9 +351,10 @@ def assoc(inst, **changes):
return new
def evolve(inst, **changes):
def evolve(*args, **changes):
"""
Create a new instance, based on *inst* with *changes* applied.
Create a new instance, based on the first positional argument with
*changes* applied.
:param inst: Instance of a class with *attrs* attributes.
:param changes: Keyword changes in the new copy.
@ -365,8 +366,40 @@ def evolve(inst, **changes):
:raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs*
class.
.. versionadded:: 17.1.0
.. versionadded:: 17.1.0
.. deprecated:: 23.1.0
It is now deprecated to pass the instance using the keyword argument
*inst*. It will raise a warning until at least April 2024, after which
it will become an error. Always pass the instance as a positional
argument.
"""
# Try to get instance by positional argument first.
# Use changes otherwise and warn it'll break.
if args:
try:
(inst,) = args
except ValueError:
raise TypeError(
f"evolve() takes 1 positional argument, but {len(args)} "
"were given"
) from None
else:
try:
inst = changes.pop("inst")
except KeyError:
raise TypeError(
"evolve() missing 1 required positional argument: 'inst'"
) from None
import warnings
warnings.warn(
"Passing the instance per keyword argument is deprecated and "
"will stop working in, or after, April 2024.",
DeprecationWarning,
stacklevel=2,
)
cls = inst.__class__
attrs = fields(cls)
for a in attrs:

View File

@ -689,3 +689,47 @@ class TestEvolve:
assert Cls1({"foo": 42, "param2": 42}) == attr.evolve(
obj1a, param1=obj2b
)
def test_inst_kw(self):
"""
If `inst` is passed per kw argument, a warning is raised.
See #1109
"""
@attr.s
class C:
pass
with pytest.warns(DeprecationWarning) as wi:
evolve(inst=C())
assert __file__ == wi.list[0].filename
def test_no_inst(self):
"""
Missing inst argument raises a TypeError like Python would.
"""
with pytest.raises(TypeError, match=r"evolve\(\) missing 1"):
evolve(x=1)
def test_too_many_pos_args(self):
"""
More than one positional argument raises a TypeError like Python would.
"""
with pytest.raises(
TypeError,
match=r"evolve\(\) takes 1 positional argument, but 2 were given",
):
evolve(1, 2)
def test_can_change_inst(self):
"""
If the instance is passed by positional argument, a field named `inst`
can be changed.
"""
@attr.define
class C:
inst: int
assert C(42) == evolve(C(23), inst=42)