Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,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
Expand Down Expand Up @@ -292,6 +292,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 no ``default`` is
Expand Down
24 changes: 14 additions & 10 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,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
Expand All @@ -320,6 +321,7 @@ def __init__(self, default, default_factory, init, repr, hash, compare,
if metadata is None else
types.MappingProxyType(metadata))
self.kw_only = kw_only
self.doc = doc
self._field_type = None

@_recursive_repr
Expand All @@ -335,6 +337,7 @@ def __repr__(self):
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}'
')')

Expand Down Expand Up @@ -402,7 +405,7 @@ def __repr__(self):
# 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
Expand All @@ -414,15 +417,15 @@ 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.
"""

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):
Expand Down Expand Up @@ -1156,7 +1159,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)

Expand Down Expand Up @@ -1191,7 +1194,7 @@ def _get_slots(cls):
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")


def _add_slots(cls, is_frozen, weakref_slot):
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.

Expand All @@ -1208,16 +1211,17 @@ def _add_slots(cls, is_frozen, 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.
cls_dict["__slots__"] = tuple(
itertools.filterfalse(
cls_dict["__slots__"] = {
slot: getattr(defined_fields.get(slot), 'doc', None)
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 ()
)
),
)
)
}

for field_name in field_names:
# Remove our attributes, if present. They'll still be
Expand Down
28 changes: 24 additions & 4 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,15 @@ class C:
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," \
f"default=1,default_factory={MISSING!r}," \
"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)
Expand Down Expand Up @@ -3211,7 +3212,7 @@ class Base(Root4):
j: str
h: str

self.assertEqual(Base.__slots__, ('y', ))
self.assertEqual(Base.__slots__, {'y': None})

@dataclass(slots=True)
class Derived(Base):
Expand All @@ -3221,14 +3222,32 @@ class Derived(Base):
k: str
h: str

self.assertEqual(Derived.__slots__, ('z', ))
self.assertEqual(Derived.__slots__, {'z': None})

@dataclass
class AnotherDerived(Base):
z: int

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:
Expand Down Expand Up @@ -3269,7 +3288,8 @@ class FrozenWithoutSlotsClass:
def test_frozen_pickle(self):
# bpo-43999

self.assertEqual(self.FrozenSlotsClass.__slots__, ("foo", "bar"))
self.assertEqual(self.FrozenSlotsClass.__slots__,
{"foo": None, "bar": None})
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
obj = self.FrozenSlotsClass("a", 1)
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_pydoc/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,14 @@ class BinaryInteger(enum.IntEnum):
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``doc`` parameter to :func:`dataclasses.field`, so it can be stored and
shown as a documentation / metadata.