Skip to content

Commit

Permalink
Minor polish to #815
Browse files Browse the repository at this point in the history
Signed-off-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
hynek committed May 18, 2021
1 parent 6731eea commit 8c9a796
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 69 deletions.
3 changes: 2 additions & 1 deletion changelog.d/815.changes.rst
@@ -1,3 +1,4 @@
``__match_args__`` are now generated to support Python 3.10's
`Structural Pattern Matching <https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching>`_.
This can be controlled by ``match_args`` argument to the class decorators.
This can be controlled by the ``match_args`` argument to the class decorators on Python 3.10 and later.
On older versions, it is never added and the argument is ignored.
16 changes: 11 additions & 5 deletions src/attr/_make.py
Expand Up @@ -13,6 +13,7 @@
from . import _config, setters
from ._compat import (
PY2,
PY310,
PYPY,
isclass,
iteritems,
Expand Down Expand Up @@ -1422,10 +1423,11 @@ def attrs(
fields based on their types. See `transform-fields` for more details.
:param bool match_args:
If `True`, set ``__match_args__`` on the class to support `PEP 634
<https://www.python.org/dev/peps/pep-0634/>`_ (Structural Pattern
Matching). It is a tuple of all positional-only ``__init__`` parameter
names.
If `True` (default), set ``__match_args__`` on the class to support
`PEP 634 <https://www.python.org/dev/peps/pep-0634/>`_ (Structural
Pattern Matching). It is a tuple of all positional-only ``__init__``
parameter names on Python 3.10 and later. Ignored on older Python
versions.
.. versionadded:: 16.0.0 *slots*
.. versionadded:: 16.1.0 *frozen*
Expand Down Expand Up @@ -1577,7 +1579,11 @@ def wrap(cls):
" init must be True."
)

if match_args and not _has_own_attribute(cls, "__match_args__"):
if (
PY310
and match_args
and not _has_own_attribute(cls, "__match_args__")
):
builder.add_match_args()

return builder.build_class()
Expand Down
95 changes: 54 additions & 41 deletions tests/test_make.py
Expand Up @@ -21,7 +21,7 @@
import attr

from attr import _config
from attr._compat import PY2, ordered_dict
from attr._compat import PY2, PY310, ordered_dict
from attr._make import (
Attribute,
Factory,
Expand Down Expand Up @@ -2328,46 +2328,59 @@ def __setstate__(self, state):
assert True is i.called
assert None is getattr(C(), "__getstate__", None)

@pytest.mark.skipif(PY310, reason="Pre-3.10 only.")
def test_match_args_pre_310(self):
"""
__match_args__ is not created on Python versions older than 3.10.
"""

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

assert None is getattr(C, "__match_args__", None)


@pytest.mark.skipif(not PY310, reason="Structural pattern matching is 3.10+")
class TestMatchArgs(object):
"""
Tests for match_args and __match_args__ generation.
"""

def test_match_args(self):
"""
__match_args__ generation
__match_args__ is created by default on Python 3.10.
"""

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

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

def test_explicit_match_args(self):
"""
__match_args__ manually set is not overriden.
A custom __match_args__ set is not overwritten.
"""

ma = ()

@attr.s()
class C(object):
a = attr.ib()
@attr.define
class C:
a = attr.field()
__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):
"""
__match_args__ being set depending on match_args.
__match_args__ is set depending on match_args.
"""

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

if match_args:
assert hasattr(C, "__match_args__")
Expand All @@ -2376,21 +2389,21 @@ class C(object):

def test_match_args_kw_only(self):
"""
kw_only being set doesn't generate __match_args__
kw_only field is not included in __match_args__
kw_only classes don't generate __match_args__.
kw_only fields are not included in __match_args__.
"""

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

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

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

assert C.__match_args__ == ()

Expand All @@ -2399,33 +2412,33 @@ def test_match_args_argument(self):
match_args being False with inheritance.
"""

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

assert "__match_args__" not in X.__dict__

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

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

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

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

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

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

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

Expand All @@ -2435,13 +2448,13 @@ def test_make_class(self):
"""

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

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__ == ()
assert () == C1.__match_args__

C1 = make_class("C1", {"a": attr.ib(kw_only=True), "b": attr.ib()})
assert C1.__match_args__ == ("b",)
assert ("b",) == C1.__match_args__
47 changes: 25 additions & 22 deletions tests/test_pattern_matching.py
@@ -1,27 +1,30 @@
# flake8: noqa
# Python 3.10 issue in flake8 : https://github.com/PyCQA/pyflakes/issues/634
# Python 3.10 issue in flake8: https://github.com/PyCQA/pyflakes/issues/634
# Keep this file SHORT, until Black and flake8 can handle it.
import pytest

import attr

from attr._make import make_class
from attr import make_class


class TestPatternMatching(object):
class TestPatternMatching:
"""
Pattern matching syntax test cases.
"""

def test_simple_match_case(self):
@pytest.mark.parametrize("dec", [attr.s, attr.define, attr.frozen])
def test_simple_match_case(self, dec):
"""
Simple match case statement
Simple match case statement works as expected with all class
decorators.
"""

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

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

matched = False
c = C(a=1)
Expand All @@ -33,14 +36,14 @@ class C(object):

def test_explicit_match_args(self):
"""
Manually set empty __match_args__ will not match.
Does not overwrite a manually set empty __match_args__.
"""

ma = ()

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

c = C(a=1)
Expand All @@ -53,16 +56,16 @@ class C(object):

def test_match_args_kw_only(self):
"""
kw_only being set doesn't generate __match_args__
kw_only field is not included in __match_args__
kw_only classes don't generate __match_args__.
kw_only fields are not included in __match_args__.
"""

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

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

c = C(a=1, b=1)
msg = r"C\(\) accepts 1 positional sub-pattern \(2 given\)"
Expand All @@ -78,10 +81,10 @@ class C(object):

assert found

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

c = C(a=1, b=1)
msg = r"C\(\) accepts 0 positional sub-patterns \(2 given\)"
Expand Down

0 comments on commit 8c9a796

Please sign in to comment.