Skip to content
Open
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
17 changes: 15 additions & 2 deletions devtools/prettier.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,21 @@ def _format_dataclass(self, value: Any, _: str, indent_current: int, indent_new:
try:
field_items = value.__dict__.items()
except AttributeError:
# slots
field_items = ((f, getattr(value, f)) for f in value.__slots__)
from dataclasses import fields

slots = value.__slots__
slot_names = (slots,) if isinstance(slots, str) else tuple(slots)
# Keep explicitly declared slots in their existing order, including
# auxiliary values that are not dataclass fields.
field_names = dict.fromkeys(
[field.name for field in fields(value) if field.name not in slot_names] + list(slot_names)
)
missing = object()
field_items = (
(name, field_value)
for name in field_names
if (field_value := getattr(value, name, missing)) is not missing
)
self._format_fields(value, field_items, indent_current, indent_new)

def _format_sqlalchemy_class(self, value: Any, _: str, indent_current: int, indent_new: int) -> None:
Expand Down
193 changes: 191 additions & 2 deletions tests/test_prettier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import string
from collections import Counter, OrderedDict
from collections.abc import Iterator
from dataclasses import dataclass
from dataclasses import dataclass, field
from importlib.util import find_spec
from typing import Any, ClassVar, NamedTuple, cast
from unittest.mock import MagicMock

import pytest

from devtools.ansi import strip_ansi
from devtools.prettier import PrettyFormat, pformat, pprint
from devtools.prettier import MISSING, PrettyFormat, pformat, pprint


class Foobar(NamedTuple):
Expand Down Expand Up @@ -289,6 +289,195 @@ class FooDataclass:
)


def test_inherited_dataclass_slots():
@dataclass(slots=True)
class Base:
first: int

@dataclass(slots=True)
class Child(Base):
second: str

assert (
pformat(Child(123, 'value'))
== """\
Child(
first=123,
second='value',
)"""
)


def test_dataclass_with_only_inherited_slots():
@dataclass(slots=True)
class Base:
first: int

@dataclass(slots=True)
class Child(Base):
pass

assert (
pformat(Child(123))
== """\
Child(
first=123,
)"""
)


def test_dataclass_slots_with_overridden_inherited_field():
@dataclass(slots=True)
class Base:
first: int

@dataclass(slots=True)
class Child(Base):
first: int
second: str

@dataclass(slots=True)
class Grandchild(Child):
third: bool

assert (
pformat(Grandchild(123, 'value', True))
== """\
Grandchild(
first=123,
second='value',
third=True,
)"""
)


@pytest.mark.parametrize(
('slots', 'expected_fields'),
[
(('first', 'extra'), " first=123,\n extra='value',\n"),
(('extra', 'first'), " extra='value',\n first=123,\n"),
],
)
def test_manual_dataclass_slots_keep_auxiliary_values_and_order(slots: tuple[str, ...], expected_fields: str):
@dataclass
class ManualSlots:
__slots__ = slots
first: int

value = ManualSlots(123)
cast(Any, value).extra = 'value'
assert pformat(value) == f'ManualSlots(\n{expected_fields})'


def test_undecorated_dataclass_subclass_keeps_auxiliary_slots():
@dataclass(slots=True)
class Base:
first: int

class Child(Base):
__slots__ = ('extra',)

def __init__(self, first: int, extra: str) -> None:
super().__init__(first)
self.extra = extra

assert (
pformat(Child(123, 'value'))
== """\
Child(
first=123,
extra='value',
)"""
)


def test_dataclass_string_slots():
@dataclass
class StringSlots:
__slots__ = 'long_name' # noqa: PLC0205 - exercise Python's valid single-slot string form
long_name: int

assert (
pformat(StringSlots(123))
== """\
StringSlots(
long_name=123,
)"""
)


def test_dataclass_keeps_explicit_weakref_slot():
@dataclass
class WeakSlots:
__slots__ = ('__weakref__', 'first')
first: int

assert (
pformat(WeakSlots(123))
== """\
WeakSlots(
__weakref__=None,
first=123,
)"""
)


def test_slotted_dataclass_unassigned_inherited_field():
@dataclass(slots=True)
class Base:
pending: int = field(init=False, repr=False)

@dataclass(slots=True)
class Child(Base):
ready: int = field(default=42, repr=False)

value = Child()
assert pformat(value) == 'Child(\n ready=42,\n)'
value.pending = 7
assert pformat(value) == 'Child(\n pending=7,\n ready=42,\n)'


def test_slotted_dataclass_unassigned_leaf_field():
@dataclass(slots=True)
class Value:
pending: int = field(init=False, repr=False)
ready: int = field(default=42, repr=False)

assert pformat(Value()) == 'Value(\n ready=42,\n)'


def test_slotted_dataclass_stored_missing_sentinel():
@dataclass(slots=True)
class Base:
first: Any

@dataclass(slots=True)
class Child(Base):
second: Any

assert pformat(Child(MISSING, MISSING)) == f'Child(\n first={MISSING!r},\n second={MISSING!r},\n)'


def test_slotted_dataclass_field_read_once():
reads: list[str] = []

@dataclass(slots=True)
class Base:
pending: int = field(init=False, repr=False)

def __getattribute__(self, name: str) -> Any:
if name in {'pending', 'ready'}:
reads.append(name)
return object.__getattribute__(self, name)

@dataclass(slots=True)
class Child(Base):
ready: int = field(default=42, repr=False)

assert pformat(Child()) == 'Child(\n ready=42,\n)'
assert reads == ['pending', 'ready']


@pytest.mark.skipif(find_spec('numpy') is None, reason='numpy not installed')
def test_indent_numpy():
import numpy
Expand Down
Loading