dataclass.pyPython 3.7
"""
Data classes (PEP 557), introduced in Python 3.7, provide a declarative syntax
for classes that primarily store state.

Before dataclasses, developers wrote repetitive boilerplate for `__init__`,
`__repr__`, and `__eq__` methods. The `@dataclass` decorator inspects type
annotations on class attributes to automatically generate these standard methods.

Advanced capabilities include:
- `field(default_factory=...)` to avoid mutable default argument pitfalls
- `frozen=True` for immutable instances that can be hashed and stored in sets
- `kw_only=True` to enforce keyword-only arguments during initialization
- `__post_init__` for validation and derived attribute computation
"""

from dataclasses import FrozenInstanceError, asdict, dataclass, field


@dataclass
class Point:
    """Simple 2D coordinate with auto-generated constructor and equality."""

    x: float
    y: float


@dataclass
class UserProfile:
    """User profile demonstrating default values and default factories."""

    username: str
    email: str
    tags: list[str] = field(default_factory=list)
    active: bool = True


@dataclass(frozen=True)
class ServerConfig:
    """Immutable server configuration that is safely hashable."""

    host: str
    port: int


@dataclass(kw_only=True)
class InventoryItem:
    """Item demonstrating keyword-only parameters and post-initialization."""

    name: str
    unit_price: float
    quantity: int = 1
    total_cost: float = field(init=False)

    def __post_init__(self) -> None:
        if self.unit_price < 0 or self.quantity < 0:
            raise ValueError("Price and quantity must be non-negative")
        self.total_cost = round(self.unit_price * self.quantity, 2)


def main() -> None:
    # 1. Automatic __init__, __repr__, and __eq__
    p1 = Point(10.0, 20.0)
    p2 = Point(10.0, 20.0)
    p3 = Point(5.0, 15.0)

    assert p1 == p2
    assert p1 != p3
    assert repr(p1) == "Point(x=10.0, y=20.0)"

    # 2. Default factories prevent shared mutable state across instances
    user_a = UserProfile("alice", "[email protected]")
    user_b = UserProfile("bob", "[email protected]")
    user_a.tags.append("admin")

    assert user_a.tags == ["admin"]
    assert user_b.tags == []
    assert user_a.tags is not user_b.tags

    # Convert dataclass instance directly to a dictionary
    assert asdict(user_a) == {
        "username": "alice",
        "email": "[email protected]",
        "tags": ["admin"],
        "active": True,
    }

    # 3. Frozen dataclasses guarantee immutability and allow hashing
    config = ServerConfig("127.0.0.1", 8080)
    mutation_failed = False
    try:
        config.port = 9090  # type: ignore[misc]
    except FrozenInstanceError:
        mutation_failed = True
    assert mutation_failed is True

    # Because config is immutable, it can be used in sets and as dictionary keys
    config_set = {config, ServerConfig("127.0.0.1", 8080)}
    assert len(config_set) == 1

    # 4. Keyword-only instantiation and __post_init__ computation
    item = InventoryItem(name="Mechanical Keyboard", unit_price=99.99, quantity=2)
    assert item.total_cost == 199.98

    # Keyword-only enforcement ensures clear call sites
    positional_failed = False
    try:
        InventoryItem("Monitor", 299.99)  # type: ignore[misc]
    except TypeError:
        positional_failed = True
    assert positional_failed is True

    # Post-init validation prevents invalid state
    invalid_item_failed = False
    try:
        InventoryItem(name="Faulty Item", unit_price=-10.0)
    except ValueError as e:
        invalid_item_failed = True
        assert str(e) == "Price and quantity must be non-negative"
    assert invalid_item_failed is True


if __name__ == "__main__":
    main()