mirror of https://github.com/python/cpython
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.
This commit is contained in:
parent
0a3577bdfc
commit
9c7657f099
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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…
Reference in New Issue