8000 gh-113878: Add `doc` parameter to `dataclasses.field` (gh-114051) · python/cpython@9c7657f · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9c7657f

Browse files
authored
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.
1 parent 0a3577b commit 9c7657f

File tree

5 files changed

+81
-21
lines changed

5 files changed

+81
-21
lines changed

Doc/library/dataclasses.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Module contents
231231
follows a field with a default value. This is true whether this
232232
occurs in a single class, or as a result of class inheritance.
233233

234-
.. function:: field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)
234+
.. function:: field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None)
235235

236236
For common and simple use cases, no other functionality is
237237
required. There are, however, some dataclass features that
@@ -300,6 +300,10 @@ Module contents
300300

301301
.. versionadded:: 3.10
302302

303+
- ``doc``: optional docstring for this field.
304+
305+
.. versionadded:: 3.13
306+
303307
If the default value of a field is specified by a call to
304308
:func:`!field`, then the class attribute for this field will be
305309
replaced by the specified *default* value. If *default* is not

Lib/dataclasses.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,12 @@ class Field:
283283
'compare',
284284
'metadata',
285285
'kw_only',
286+
'doc',
286287
'_field_type', # Private: not to be used by user code.
287288
)
288289

289290
def __init__(self, default, default_factory, init, repr, hash, compare,
290-
metadata, kw_only):
291+
metadata, kw_only, doc):
291292
self.name = None
292293
self.type = None
293294
self.default = default
@@ -300,6 +301,7 @@ def __init__(self, default, default_factory, init, repr, hash, compare,
300301
if metadata is None else
301302
types.MappingProxyType(metadata))
302303
self.kw_only = kw_only
304+
self.doc = doc
303305
self._field_type = None
304306

305307
@recursive_repr()
@@ -315,6 +317,7 @@ def __repr__(self):
315317
f'compare={self.compare!r},'
316318
f'metadata={self.metadata!r},'
317319
f'kw_only={self.kw_only!r},'
320+
f'doc={self.doc!r},'
318321
f'_field_type={self._field_type}'
319322
')')
320323

@@ -382,7 +385,7 @@ def __repr__(self):
382385
# so that a type checker can be told (via overloads) that this is a
383386
# function whose type depends on its parameters.
384387
def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
385-
hash=None, compare=True, metadata=None, kw_only=MISSING):
388+
hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None):
386389
"""Return an object to identify dataclass fields.
387390
388391
default is the default value of the field. default_factory is a
@@ -394,15 +397,15 @@ def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
394397
comparison functions. metadata, if specified, must be a mapping
395398
which is stored but not otherwise examined by dataclass. If kw_only
396399
is true, the field will become a keyword-only parameter to
397-
__init__().
400+
__init__(). doc is an optional docstring for this field.
398401
399402
It is an error to specify both default and default_factory.
400403
"""
401404

402405
if default is not MISSING and default_factory is not MISSING:
403406
raise ValueError('cannot specify both default and default_factory')
404407
return Field(default, default_factory, init, repr, hash, compare,
405-
metadata, kw_only)
408+
metadata, kw_only, doc)
406409

407410

408411
def _fields_in_init_order(fields):
@@ -1174,7 +1177,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
11741177
if weakref_slot and not slots:
11751178
raise TypeError('weakref_slot is True but slots is False')
11761179
if slots:
1177-
cls = _add_slots(cls, frozen, weakref_slot)
1180+
cls = _add_slots(cls, frozen, weakref_slot, fields)
11781181

11791182
abc.update_abstractmethods(cls)
11801183

@@ -1239,7 +1242,32 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
12391242
return False
12401243

12411244

1242-
def _add_slots(cls, is_frozen, weakref_slot):
1245+
def _create_slots(defined_fields, inherited_slots, field_names, weakref_slot):
1246+
# The slots for our class. Remove slots from our base classes. Add
1247+
# '__weakref__' if weakref_slot was given, unless it is already present.
1248+
seen_docs = False
1249+
slots = {}
1250+
for slot in itertools.filterfalse(
1251+
inherited_slots.__contains__,
1252+
itertools.chain(
1253+
# gh-93521: '__weakref__' also needs to be filtered out if
1254+
# already present in inherited_slots
1255+
field_names, ('__weakref__',) if weakref_slot else ()
1256+
)
1257+
):
1258+
doc = getattr(defined_fields.get(slot), 'doc', None)
1259+
if doc is not None:
1260+
seen_docs = True
1261+
slots.update({slot: doc})
1262+
1263+
# We only return dict if there's at least one doc member,
1264+
# otherwise we return tuple, which is the old default format.
1265+
if seen_docs:
1266+
return slots
1267+
return tuple(slots)
1268+
1269+
1270+
def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
12431271
# Need to create a new class, since we can't set __slots__ after a
12441272
# class has been created, and the @dataclass decorator is called
12451273
# after the class is created.
@@ -1255,17 +1283,9 @@ def _add_slots(cls, is_frozen, weakref_slot):
12551283
inherited_slots = set(
12561284
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
12571285
)
1258-
# The slots for our class. Remove slots from our base classes. Add
1259-
# '__weakref__' if weakref_slot was given, unless it is already present.
1260-
cls_dict["__slots__"] = tuple(
1261-
itertools.filterfalse(
1262-
inherited_slots.__contains__,
1263-
itertools.chain(
1264-
# gh-93521: '__weakref__' also needs to be filtered out if
1265-
# already present in inherited_slots
1266-
field_names, ('__weakref__',) if weakref_slot else ()
1267-
)
1268-
),
1286+
1287+
cls_dict["__slots__"] = _create_slots(
1288+
defined_fields, inherited_slots, field_names, weakref_slot,
12691289
)
12701290

12711291
for field_name in field_names:

Lib/test/test_dataclasses/__init__.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ class C:
6161
x: int = field(default=1, default_factory=int)
6262

6363
def test_field_repr(self):
64-
int_field = field(default=1, init=True, repr=False)
64+
int_field = field(default=1, init=True, repr=False, doc='Docstring')
6565
int_field.name = "id"
6666
repr_output = repr(int_field)
6767
expected_output = "Field(name='id',type=None," \
6868
f"default=1,default_factory={MISSING!r}," \
6969
"init=True,repr=False,hash=None," \
7070
"compare=True,metadata=mappingproxy({})," \
7171
f"kw_only={MISSING!r}," \
72+
"doc='Docstring'," \
7273
"_field_type=None)"
7374

7475
self.assertEqual(repr_output, expected_output)
@@ -3304,7 +3305,7 @@ class Base(Root4):
33043305
j: str
33053306
h: str
33063307

3307-
self.assertEqual(Base.__slots__, ('y', ))
3308+
self.assertEqual(Base.__slots__, ('y',))
33083309

33093310
@dataclass(slots=True)
33103311
class Derived(Base):
@@ -3314,14 +3315,32 @@ class Derived(Base):
33143315
k: str
33153316
h: str
33163317

3317-
self.assertEqual(Derived.__slots__, ('z', ))
3318+
self.assertEqual(Derived.__slots__, ('z',))
33183319

33193320
@dataclass
33203321
class AnotherDerived(Base):
33213322
z: int
33223323

33233324
self.assertNotIn('__slots__', AnotherDerived.__dict__)
33243325

3326+
def test_slots_with_docs(self):
3327+
class Root:
3328+
__slots__ = {'x': 'x'}
3329+
3330+
@dataclass(slots=True)
3331+
class Base(Root):
3332+
y1: int = field(doc='y1')
3333+
y2: int
3334+
3335+
self.assertEqual(Base.__slots__, {'y1': 'y1', 'y2': None})
3336+
3337+
@dataclass(slots=True)
3338+
class Child(Base):
3339+
z1: int = field(doc='z1')
3340+
z2: int
3341+
3342+
self.assertEqual(Child.__slots__, {'z1': 'z1', 'z2': None})
3343+
33253344
def test_cant_inherit_from_iterator_slots(self):
33263345

33273346
class Root:

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,14 @@ class BinaryInteger(enum.IntEnum):
463463
doc = pydoc.render_doc(BinaryInteger)
464464
self.assertIn('BinaryInteger.zero', doc)
465465

466+
def test_slotted_dataclass_with_field_docs(self):
467+
import dataclasses
468+
@dataclasses.dataclass(slots=True)
469+
class My:
470+
x: int = dataclasses.field(doc='Docstring for x')
471+
doc = pydoc.render_doc(My)
472+
self.assertIn('Docstring for x', doc)
473+
466474
def test_mixed_case_module_names_are_lower_cased(self):
467475
# issue16484
468476
doc_link = get_pydoc_link(xml.etree.ElementTree)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Add *doc* parameter to :func:`dataclasses.field`, so it can be stored and
2+
shown as a documentation / metadata. If ``@dataclass(slots=True)`` is used,
3+
then the supplied string is availabl in the :attr:`~object.__slots__` dict.
4+
Otherwise, the supplied string is only available in the corresponding
5+
:class:`dataclasses.Field` object.
6+
7+
In order to support this feature we are changing the ``__slots__`` format
8+
in dataclasses from :class:`tuple` to :class:`dict`
9+
when documentation / metadata is present.

0 commit comments

Comments
 (0)
0