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:
parent
22ae8473fb
commit
a14e1859b8
|
@ -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.
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue