Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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
Expand Down Expand Up @@ -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
Expand Down
54 changes: 37 additions & 17 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -300,6 +301,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 @@ -315,6 +317,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 @@ -382,7 +385,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 @@ -394,15 +397,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 @@ -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)

Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down
25 changes: 22 additions & 3 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 @@ -3261,7 +3262,7 @@ class Base(Root4):
j: str
h: str

self.assertEqual(Base.__slots__, ('y', ))
self.assertEqual(Base.__slots__, ('y',))

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

self.assertEqual(Derived.__slots__, ('z', ))
self.assertEqual(Derived.__slots__, ('z',))

@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
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 @@ -463,6 +463,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,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.
Loading