Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise a deprecation warning when evolve receives insta as a kw arg #1117

Merged
merged 10 commits into from Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/1117.change.md
@@ -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.
hynek marked this conversation as resolved.
Show resolved Hide resolved

Passing the instance using the `inst` keyword argument is now deprecated and will be removed in, or after, April 2024.
39 changes: 36 additions & 3 deletions src/attr/_funcs.py
Expand Up @@ -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.
Expand All @@ -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(
hynek marked this conversation as resolved.
Show resolved Hide resolved
f"evolve() takes 1 positional argument, but {len(args)} "
"were given"
) from None
else:
try:
inst = changes.pop("inst")
except KeyError:
raise TypeError(
hynek marked this conversation as resolved.
Show resolved Hide resolved
"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:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_funcs.py
Expand Up @@ -689,3 +689,47 @@ class Cls2:
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)