[Python-checkins] bpo-43764: Add match_args=False parameter to dataclass decorator and to make_dataclasses function. (GH-25337)

ericvsmith webhook-mailer at python.org
Sat Apr 10 21:28:46 EDT 2021


https://github.com/python/cpython/commit/750f484752763fe9ac1d6455780aabcb67f25508
commit: 750f484752763fe9ac1d6455780aabcb67f25508
branch: master
author: Eric V. Smith <ericvsmith at users.noreply.github.com>
committer: ericvsmith <ericvsmith at users.noreply.github.com>
date: 2021-04-10T21:28:42-04:00
summary:

bpo-43764: Add match_args=False parameter to dataclass decorator and to make_dataclasses function. (GH-25337)

Add match_args=False parameter to dataclass decorator and to make_dataclass function.

files:
A Misc/NEWS.d/next/Library/2021-04-10-18-23-09.bpo-43764.Le5KJp.rst
M Doc/library/dataclasses.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py

diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst
index 133cc0a065cac..0c0c7a8ddabfc 100644
--- a/Doc/library/dataclasses.rst
+++ b/Doc/library/dataclasses.rst
@@ -46,7 +46,7 @@ directly specified in the ``InventoryItem`` definition shown above.
 Module-level decorators, classes, and functions
 -----------------------------------------------
 
-.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
+.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True)
 
    This function is a :term:`decorator` that is used to add generated
    :term:`special method`\s to classes, as described below.
@@ -79,7 +79,7 @@ Module-level decorators, classes, and functions
      class C:
          ...
 
-     @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
+     @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True)
      class C:
         ...
 
@@ -161,6 +161,14 @@ Module-level decorators, classes, and functions
      :meth:`__setattr__` or :meth:`__delattr__` is defined in the class, then
      :exc:`TypeError` is raised.  See the discussion below.
 
+   - ``match_args``: If true (the default is ``True``), the
+     ``__match_args__`` tuple will be created from the list of
+     parameters to the generated :meth:`__init__` method (even if
+     :meth:`__init__` is not generated, see above).  If false, or if
+     ``__match_args__`` is already defined in the class, then
+     ``__match_args__`` will not be generated.
+
+
    ``field``\s may optionally specify a default value, using normal
    Python syntax::
 
@@ -325,7 +333,7 @@ Module-level decorators, classes, and functions
 
    Raises :exc:`TypeError` if ``instance`` is not a dataclass instance.
 
-.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
+.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True)
 
    Creates a new dataclass with name ``cls_name``, fields as defined
    in ``fields``, base classes as given in ``bases``, and initialized
@@ -333,8 +341,9 @@ Module-level decorators, classes, and functions
    iterable whose elements are each either ``name``, ``(name, type)``,
    or ``(name, type, Field)``.  If just ``name`` is supplied,
    ``typing.Any`` is used for ``type``.  The values of ``init``,
-   ``repr``, ``eq``, ``order``, ``unsafe_hash``, and ``frozen`` have
-   the same meaning as they do in :func:`dataclass`.
+   ``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``, and
+   ``match_args`` have the same meaning as they do in
+   :func:`dataclass`.
 
    This function is not strictly required, because any Python
    mechanism for creating a new class with ``__annotations__`` can
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index ceda8220f1f6c..e8eb2060f6df1 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -154,12 +154,17 @@
 
 # __match_args__
 #
-# |  no   |  yes  |  <--- class has __match_args__ in __dict__?
-# +=======+=======+
-# | add   |       |  <- the default
-# +=======+=======+
-# __match_args__ is always added unless the class already defines it. It is a
-# tuple of __init__ parameter names; non-init fields must be matched by keyword.
+#    +--- match_args= parameter
+#    |
+#    v    |       |       |
+#         |  no   |  yes  |  <--- class has __match_args__ in __dict__?
+# +=======+=======+=======+
+# | False |       |       |
+# +-------+-------+-------+
+# | True  | add   |       |  <- the default
+# +=======+=======+=======+
+# __match_args__ is a tuple of __init__ parameter names; non-init fields must
+# be matched by keyword.
 
 
 # Raised when an attempt is made to modify a frozen class.
@@ -830,7 +835,8 @@ def _hash_exception(cls, fields, globals):
 # version of this table.
 
 
-def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
+def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
+                   match_args):
     # Now that dicts retain insertion order, there's no reason to use
     # an ordered dict.  I am leveraging that ordering here, because
     # derived class fields overwrite base class fields, but the order
@@ -1016,8 +1022,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
         cls.__doc__ = (cls.__name__ +
                        str(inspect.signature(cls)).replace(' -> NoneType', ''))
 
-    if '__match_args__' not in cls.__dict__:
-        cls.__match_args__ = tuple(f.name for f in field_list if f.init)
+    if match_args:
+        _set_new_attribute(cls, '__match_args__',
+                           tuple(f.name for f in field_list if f.init))
 
     abc.update_abstractmethods(cls)
 
@@ -1025,7 +1032,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
 
 
 def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
-              unsafe_hash=False, frozen=False):
+              unsafe_hash=False, frozen=False, match_args=True):
     """Returns the same class as was passed in, with dunder methods
     added based on the fields defined in the class.
 
@@ -1035,11 +1042,13 @@ def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
     repr is true, a __repr__() method is added. If order is true, rich
     comparison dunder methods are added. If unsafe_hash is true, a
     __hash__() method function is added. If frozen is true, fields may
-    not be assigned to after instance creation.
+    not be assigned to after instance creation. If match_args is true,
+    the __match_args__ tuple is added.
     """
 
     def wrap(cls):
-        return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
+        return _process_class(cls, init, repr, eq, order, unsafe_hash,
+                              frozen, match_args)
 
     # See if we're being called as @dataclass or @dataclass().
     if cls is None:
@@ -1198,7 +1207,7 @@ def _astuple_inner(obj, tuple_factory):
 
 def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
                    repr=True, eq=True, order=False, unsafe_hash=False,
-                   frozen=False):
+                   frozen=False, match_args=True):
     """Return a new dynamically created dataclass.
 
     The dataclass name will be 'cls_name'.  'fields' is an iterable
@@ -1259,7 +1268,8 @@ class C(Base):
     # of generic dataclassses.
     cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace))
     return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
-                     unsafe_hash=unsafe_hash, frozen=frozen)
+                     unsafe_hash=unsafe_hash, frozen=frozen,
+                     match_args=match_args)
 
 
 def replace(obj, /, **changes):
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 29f29e1e6895e..4beed69e45bbb 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -3440,6 +3440,47 @@ class X:
             c: int
         self.assertEqual(X.__match_args__, ("a", "b", "c"))
 
+    def test_match_args_argument(self):
+        @dataclass(match_args=False)
+        class X:
+            a: int
+        self.assertNotIn('__match_args__', X.__dict__)
+
+        @dataclass(match_args=False)
+        class Y:
+            a: int
+            __match_args__ = ('b',)
+        self.assertEqual(Y.__match_args__, ('b',))
+
+        @dataclass(match_args=False)
+        class Z(Y):
+            z: int
+        self.assertEqual(Z.__match_args__, ('b',))
+
+        # Ensure parent dataclass __match_args__ is seen, if child class
+        # specifies match_args=False.
+        @dataclass
+        class A:
+            a: int
+            z: int
+        @dataclass(match_args=False)
+        class B(A):
+            b: int
+        self.assertEqual(B.__match_args__, ('a', 'z'))
+
+    def test_make_dataclasses(self):
+        C = make_dataclass('C', [('x', int), ('y', int)])
+        self.assertEqual(C.__match_args__, ('x', 'y'))
+
+        C = make_dataclass('C', [('x', int), ('y', int)], match_args=True)
+        self.assertEqual(C.__match_args__, ('x', 'y'))
+
+        C = make_dataclass('C', [('x', int), ('y', int)], match_args=False)
+        self.assertNotIn('__match__args__', C.__dict__)
+
+        C = make_dataclass('C', [('x', int), ('y', int)], namespace={'__match_args__': ('z',)})
+        self.assertEqual(C.__match_args__, ('z',))
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2021-04-10-18-23-09.bpo-43764.Le5KJp.rst b/Misc/NEWS.d/next/Library/2021-04-10-18-23-09.bpo-43764.Le5KJp.rst
new file mode 100644
index 0000000000000..555aad06e32ca
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-04-10-18-23-09.bpo-43764.Le5KJp.rst
@@ -0,0 +1,2 @@
+Add match_args parameter to @dataclass decorator to allow suppression of
+__match_args__ generation.



More information about the Python-checkins mailing list