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

Add __match_args__ to support match case destructuring in Python 3.10 #815

Merged
merged 20 commits into from May 18, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -15,6 +15,13 @@ Changes for the upcoming release can be found in the `"changelog.d" directory <h

.. towncrier release notes start

21.3.0.dev0 (2021-05-14)
tirkarthi marked this conversation as resolved.
Show resolved Hide resolved
No significant changes.


----


21.2.0 (2021-05-07)
-------------------

Expand Down
2 changes: 2 additions & 0 deletions changelog.d/815.changes.rst
@@ -0,0 +1,2 @@
Add ``__match_args__`` to generated class to support match case destructuring
in Python 3.10. This can be controlled by ``match_args`` argument to ``@attrib.s``.
tirkarthi marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion docs/api.rst
Expand Up @@ -28,7 +28,7 @@ Core

.. autodata:: attr.NOTHING

.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None)
.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True)

.. note::

Expand Down
2 changes: 2 additions & 0 deletions src/attr/__init__.pyi
Expand Up @@ -315,6 +315,7 @@ def attrs(
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> _C: ...
@overload
@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field))
Expand All @@ -341,6 +342,7 @@ def attrs(
getstate_setstate: Optional[bool] = ...,
on_setattr: Optional[_OnSetAttrArgType] = ...,
field_transformer: Optional[_FieldTransformer] = ...,
match_args: bool = ...,
) -> Callable[[_C], _C]: ...
@overload
@__dataclass_transform__(field_descriptors=(attrib, field))
Expand Down
17 changes: 17 additions & 0 deletions src/attr/_make.py
Expand Up @@ -973,6 +973,13 @@ def add_init(self):

return self

def add_match_args(self):
self._cls_dict["__match_args__"] = tuple(
field.name
for field in self._attrs
if field.init and not field.kw_only
)

def add_attrs_init(self):
self._cls_dict["__attrs_init__"] = self._add_method_dunders(
_make_init(
Expand Down Expand Up @@ -1198,6 +1205,7 @@ def attrs(
getstate_setstate=None,
on_setattr=None,
field_transformer=None,
match_args=True,
):
r"""
A class decorator that adds `dunder
Expand Down Expand Up @@ -1413,6 +1421,11 @@ def attrs(
this, e.g., to automatically add converters or validators to
fields based on their types. See `transform-fields` for more details.

:param bool match_args:
If `True` it sets __match_args__ in the class to support PEP 634. It
is a tuple of __init__ parameter names that are only positional
arguments.
tirkarthi marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
.. versionadded:: 16.3.0 *str*
Expand Down Expand Up @@ -1446,6 +1459,7 @@ def attrs(
``init=False`` injects ``__attrs_init__``
.. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__``
.. versionchanged:: 21.1.0 *cmp* undeprecated
.. versionadded:: 21.3.0 *match_args*
"""
if auto_detect and PY2:
raise PythonTooOldError(
Expand Down Expand Up @@ -1562,6 +1576,9 @@ def wrap(cls):
" init must be True."
)

if match_args and "__match_args__" not in cls.__dict__:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use _has_own_attribute for the second check.

I'm aware that the implementation is currently obtuse and depending on how Larry's typing proposal will go, we'll fix it centrally eventually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is also somewhat inconsistent with the rest of attrs, where we overwrite unless auto_detect is set to True (default in new APIs). Is this the behavior of dataclasses?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also doesn't make sense to add it on Pythons older than 3.10, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the initial implementation of the pattern matching PEP there was no flag to disable __match_args__ generation but there was a request raised later on a flag to opt-out though __match_args__ = () being set manually was proposed. Discussion : https://bugs.python.org/issue43764

builder.add_match_args()

return builder.build_class()

# maybe_cls's type depends on the usage of the decorator. It's a class
Expand Down
118 changes: 118 additions & 0 deletions tests/test_make.py
Expand Up @@ -2327,3 +2327,121 @@ def __setstate__(self, state):

assert True is i.called
assert None is getattr(C(), "__getstate__", None)


class TestMatchArgs(object):
"""
Tests for match_args and __match_args__ generation.
"""

def test_match_args(self):
"""
Test __match_args__ generation
"""

@attr.s()
class C(object):
a = attr.ib()

assert C.__match_args__ == ("a",)

def test_explicit_match_args(self):
"""
Test __match_args__ manually set is not overriden.
"""

ma = ()

@attr.s()
class C(object):
a = attr.ib()
__match_args__ = ma

assert C(42).__match_args__ is ma

@pytest.mark.parametrize("match_args", [True, False])
def test_match_args_attr_set(self, match_args):
"""
Test __match_args__ being set depending on match_args.
"""

@attr.s(match_args=match_args)
class C(object):
a = attr.ib()

if match_args:
assert hasattr(C, "__match_args__")
else:
assert not hasattr(C, "__match_args__")

def test_match_args_kw_only(self):
"""
Test kw_only being set doesn't generate __match_args__
Test kw_only field is not included in __match_args__
"""

@attr.s()
class C(object):
a = attr.ib(kw_only=True)
b = attr.ib()

assert C.__match_args__ == ("b",)

@attr.s(match_args=True, kw_only=True)
class C(object):
a = attr.ib()
b = attr.ib()

assert C.__match_args__ == ()

def test_match_args_argument(self):
"""
Test match_args being False with inheritance.
"""

@attr.s(match_args=False)
class X(object):
a = attr.ib()

assert "__match_args__" not in X.__dict__

@attr.s(match_args=False)
class Y(object):
a = attr.ib()
__match_args__ = ("b",)

assert Y.__match_args__ == ("b",)

@attr.s(match_args=False)
class Z(Y):
z = attr.ib()

assert Z.__match_args__ == ("b",)

@attr.s()
class A(object):
a = attr.ib()
z = attr.ib()

@attr.s(match_args=False)
class B(A):
b = attr.ib()

assert B.__match_args__ == ("a", "z")

def test_make_class(self):
"""
Test match_args generation with make_class.
"""

C1 = make_class("C1", ["a", "b"])
assert C1.__match_args__ == ("a", "b")

C1 = make_class("C1", ["a", "b"], match_args=False)
assert not hasattr(C1, "__match_args__")

C1 = make_class("C1", ["a", "b"], kw_only=True)
assert C1.__match_args__ == ()

C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()})
assert C1.__match_args__ == ("b",)
7 changes: 7 additions & 0 deletions tests/typing_example.py
Expand Up @@ -257,3 +257,10 @@ class FactoryTest:
a: List[int] = attr.ib(default=attr.Factory(list))
b: List[Any] = attr.ib(default=attr.Factory(list, False))
c: List[int] = attr.ib(default=attr.Factory((lambda s: s.a), True))


# Check match_args stub
@attr.s(match_args=False)
class MatchArgs:
a: int = attr.ib()
b: int = attr.ib()