summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsobolevn <mail@sobolevn.me>2024-09-27 16:20:49 (GMT)
committerGitHub <noreply@github.com>2024-09-27 16:20:49 (GMT)
commit9c7657f09914254724683d91177aed7947637be5 (patch)
treea3866271b1a8ad5602e12f64ac03de1a7b463c66
parent0a3577bdfcb7132c92a3f7fb2ac231bc346383c0 (diff)
downloadcpython-9c7657f09914254724683d91177aed7947637be5.zip
cpython-9c7657f09914254724683d91177aed7947637be5.tar.gz
cpython-9c7657f09914254724683d91177aed7947637be5.tar.bz2
gh-113878: Add `doc` parameter to `dataclasses.field` (gh-114051)
If using `slots=True`, the `doc` parameter ends up in the `__slots__` dict. The `doc` parameter is also in the corresponding `Field` object.
-rw-r--r--Doc/library/dataclasses.rst6
-rw-r--r--Lib/dataclasses.py54
-rw-r--r--Lib/test/test_dataclasses/__init__.py25
-rw-r--r--Lib/test/test_pydoc/test_pydoc.py8
-rw-r--r--Misc/NEWS.d/next/Library/2024-01-14-11-43-31.gh-issue-113878.dmEIN3.rst9
5 files changed, 81 insertions, 21 deletions
diff --git a/Doc/library/dataclasses.rst b/Doc/library/dataclasses.rst
index 1457392..51c1a42 100644
--- a/Doc/library/dataclasses.rst
+++ b/Doc/library/dataclasses.rst
@@ -231,7 +231,7 @@ Module contents
follows a field with a default value. This is true whether this
occurs in a single class, or as a result of class inheritance.
-.. function:: field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)
+.. function:: field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None)
For common and simple use cases, no other functionality is
required. There are, however, some dataclass features that
@@ -300,6 +300,10 @@ Module contents
.. versionadded:: 3.10
+ - ``doc``: optional docstring for this field.
+
+ .. versionadded:: 3.13
+
If the default value of a field is specified by a call to
:func:`!field`, then the class attribute for this field will be
replaced by the specified *default* value. If *default* is not
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index f5cb97e..bdda7cc 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -283,11 +283,12 @@ class Field:
'compare',
'metadata',
'kw_only',
+ 'doc',
'_field_type', # Private: not to be used by user code.
)
def __init__(self, default, default_factory, init, repr, hash, compare,
- metadata, kw_only):
+ metadata, kw_only, doc):
self.name = None
self.type = None
self.default = default
@@ -300,6 +301,7 @@ class Field:
if metadata is None else
types.MappingProxyType(metadata))
self.kw_only = kw_only
+ self.doc = doc
self._field_type = None
@recursive_repr()
@@ -315,6 +317,7 @@ class Field:
f'compare={self.compare!r},'
f'metadata={self.metadata!r},'
f'kw_only={self.kw_only!r},'
+ f'doc={self.doc!r},'
f'_field_type={self._field_type}'
')')
@@ -382,7 +385,7 @@ class _DataclassParams:
# so that a type checker can be told (via overloads) that this is a
# function whose type depends on its parameters.
def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
- hash=None, compare=True, metadata=None, kw_only=MISSING):
+ hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None):
"""Return an object to identify dataclass fields.
default is the default value of the field. default_factory is a
@@ -394,7 +397,7 @@ def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
comparison functions. metadata, if specified, must be a mapping
which is stored but not otherwise examined by dataclass. If kw_only
is true, the field will become a keyword-only parameter to
- __init__().
+ __init__(). doc is an optional docstring for this field.
It is an error to specify both default and default_factory.
"""
@@ -402,7 +405,7 @@ def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
if default is not MISSING and default_factory is not MISSING:
raise ValueError('cannot specify both default and default_factory')
return Field(default, default_factory, init, repr, hash, compare,
- metadata, kw_only)
+ metadata, kw_only, doc)
def _fields_in_init_order(fields):
@@ -1174,7 +1177,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
if weakref_slot and not slots:
raise TypeError('weakref_slot is True but slots is False')
if slots:
- cls = _add_slots(cls, frozen, weakref_slot)
+ cls = _add_slots(cls, frozen, weakref_slot, fields)
abc.update_abstractmethods(cls)
@@ -1239,7 +1242,32 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
return False
-def _add_slots(cls, is_frozen, weakref_slot):
+def _create_slots(defined_fields, inherited_slots, field_names, weakref_slot):
+ # The slots for our class. Remove slots from our base classes. Add
+ # '__weakref__' if weakref_slot was given, unless it is already present.
+ seen_docs = False
+ slots = {}
+ for slot in itertools.filterfalse(
+ inherited_slots.__contains__,
+ itertools.chain(
+ # gh-93521: '__weakref__' also needs to be filtered out if
+ # already present in inherited_slots
+ field_names, ('__weakref__',) if weakref_slot else ()
+ )
+ ):
+ doc = getattr(defined_fields.get(slot), 'doc', None)
+ if doc is not None:
+ seen_docs = True
+ slots.update({slot: doc})
+
+ # We only return dict if there's at least one doc member,
+ # otherwise we return tuple, which is the old default format.
+ if seen_docs:
+ return slots
+ return tuple(slots)
+
+
+def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# Need to create a new class, since we can't set __slots__ after a
# class has been created, and the @dataclass decorator is called
# after the class is created.
@@ -1255,17 +1283,9 @@ def _add_slots(cls, is_frozen, weakref_slot):
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)
- # The slots for our class. Remove slots from our base classes. Add
- # '__weakref__' if weakref_slot was given, unless it is already present.
- cls_dict["__slots__"] = tuple(
- itertools.filterfalse(
- inherited_slots.__contains__,
- itertools.chain(
- # gh-93521: '__weakref__' also needs to be filtered out if
- # already present in inherited_slots
- field_names, ('__weakref__',) if weakref_slot else ()
- )
- ),
+
+ cls_dict["__slots__"] = _create_slots(
+ defined_fields, inherited_slots, field_names, weakref_slot,
)
for field_name in field_names:
diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py
index bd2f878..2984f42 100644
--- a/Lib/test/test_dataclasses/__init__.py
+++ b/Lib/test/test_dataclasses/__init__.py
@@ -61,7 +61,7 @@ class TestCase(unittest.TestCase):
x: int = field(default=1, default_factory=int)
def test_field_repr(self):
- int_field = field(default=1, init=True, repr=False)
+ int_field = field(default=1, init=True, repr=False, doc='Docstring')
int_field.name = "id"
repr_output = repr(int_field)
expected_output = "Field(name='id',type=None," \
@@ -69,6 +69,7 @@ class TestCase(unittest.TestCase):
"init=True,repr=False,hash=None," \
"compare=True,metadata=mappingproxy({})," \
f"kw_only={MISSING!r}," \
+ "doc='Docstring'," \
"_field_type=None)"
self.assertEqual(repr_output, expected_output)
@@ -3304,7 +3305,7 @@ class TestSlots(unittest.TestCase):
j: str
h: str
- self.assertEqual(Base.__slots__, ('y', ))
+ self.assertEqual(Base.__slots__, ('y',))
@dataclass(slots=True)
class Derived(Base):
@@ -3314,7 +3315,7 @@ class TestSlots(unittest.TestCase):
k: str
h: str
- self.assertEqual(Derived.__slots__, ('z', ))
+ self.assertEqual(Derived.__slots__, ('z',))
@dataclass
class AnotherDerived(Base):
@@ -3322,6 +3323,24 @@ class TestSlots(unittest.TestCase):
self.assertNotIn('__slots__', AnotherDerived.__dict__)
+ def test_slots_with_docs(self):
+ class Root:
+ __slots__ = {'x': 'x'}
+
+ @dataclass(slots=True)
+ class Base(Root):
+ y1: int = field(doc='y1')
+ y2: int
+
+ self.assertEqual(Base.__slots__, {'y1': 'y1', 'y2': None})
+
+ @dataclass(slots=True)
+ class Child(Base):
+ z1: int = field(doc='z1')
+ z2: int
+
+ self.assertEqual(Child.__slots__, {'z1': 'z1', 'z2': None})
+
def test_cant_inherit_from_iterator_slots(self):
class Root:
diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py
index 2dba077..776e02f 100644
--- a/Lib/test/test_pydoc/test_pydoc.py
+++ b/Lib/test/test_pydoc/test_pydoc.py
@@ -463,6 +463,14 @@ class PydocDocTest(unittest.TestCase):
doc = pydoc.render_doc(BinaryInteger)
self.assertIn('BinaryInteger.zero', doc)
+ def test_slotted_dataclass_with_field_docs(self):
+ import dataclasses
+ @dataclasses.dataclass(slots=True)
+ class My:
+ x: int = dataclasses.field(doc='Docstring for x')
+ doc = pydoc.render_doc(My)
+ self.assertIn('Docstring for x', doc)
+
def test_mixed_case_module_names_are_lower_cased(self):
# issue16484
doc_link = get_pydoc_link(xml.etree.ElementTree)
diff --git a/Misc/NEWS.d/next/Library/2024-01-14-11-43-31.gh-issue-113878.dmEIN3.rst b/Misc/NEWS.d/next/Library/2024-01-14-11-43-31.gh-issue-113878.dmEIN3.rst
new file mode 100644
index 0000000..8e1937a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-01-14-11-43-31.gh-issue-113878.dmEIN3.rst
@@ -0,0 +1,9 @@
+Add *doc* parameter to :func:`dataclasses.field`, so it can be stored and
+shown as a documentation / metadata. If ``@dataclass(slots=True)`` is used,
+then the supplied string is availabl in the :attr:`~object.__slots__` dict.
+Otherwise, the supplied string is only available in the corresponding
+:class:`dataclasses.Field` object.
+
+In order to support this feature we are changing the ``__slots__`` format
+in dataclasses from :class:`tuple` to :class:`dict`
+when documentation / metadata is present.