Skip to content

Commit

Permalink
feat: field_keys and full docstrings
Browse files Browse the repository at this point in the history
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
  • Loading branch information
nstarman committed Jul 17, 2024
1 parent 6ee5e4a commit 03fd61e
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 28 deletions.
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,26 @@ src = ["src"]
extend-select = ["ALL"]
ignore = [
"ANN401", # Dynamically typed expressions are disallowed in `**kwargs`
"COM812", # For ruff.format
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the first line
"F811", # Redefinition of unused '...' (for plum-dispatch)
"FIX002", # Line contains TODO
"ISC001", # Conflicts with formatter
"PLR09", # Too many <...>
"PLR2004", # Magic value used in comparison
"ISC001", # Conflicts with formatter
"TD002", # Missing author in TODO
"TD003", # Missing issue link on the line following this TODO
"FIX002", # Line contains TODO
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ANN", "S101", "T20"]
"noxfile.py" = ["T20"]
"docs/conf.py" = ["INP001"]

[tool.ruff.lint.isort]
combine-as-imports = true


[tool.pylint]
py-version = "3.10"
Expand Down
3 changes: 2 additions & 1 deletion src/dataclasstools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

from ._core import DataclassInstance, asdict, astuple, fields, replace
from ._ext import field_items, field_values
from ._ext import field_items, field_keys, field_values
from ._version import version as __version__

__all__ = [
Expand All @@ -16,6 +16,7 @@
"asdict",
"astuple",
# ext
"field_keys",
"field_values",
"field_items",
]
214 changes: 199 additions & 15 deletions src/dataclasstools/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

__all__ = ["DataclassInstance", "replace", "fields", "asdict", "astuple"]

from collections.abc import Callable
from dataclasses import Field as _dataclass_Field
from dataclasses import asdict as _dataclass_asdict
from dataclasses import astuple as _dataclass_astuple
from dataclasses import fields as _dataclass_fields
from dataclasses import replace as _dataclass_replace
from collections.abc import Callable, Hashable, Mapping
from dataclasses import (
Field,
asdict as _dataclass_asdict,
astuple as _dataclass_astuple,
field,
fields as _dataclass_fields,
replace as _dataclass_replace,
)
from typing import Any, ClassVar, Protocol, runtime_checkable

from plum import dispatch
Expand All @@ -27,34 +30,215 @@ def __subclasshook__(cls: type, c: type) -> bool:
return hasattr(c, "__dataclass_fields__")


@dispatch # type: ignore[misc]
# ===================================================================
# Replace


@dispatch
def replace(obj: DataclassInstance, /, **kwargs: Any) -> DataclassInstance:
"""Replace the fields of a dataclass instance."""
"""Replace the fields of a dataclass instance.
Examples
--------
>>> from dataclasses import dataclass
>>> from dataclasstools import replace
>>> @dataclass
... class Point:
... x: float
... y: float
>>> p = Point(1.0, 2.0)
>>> p
Point(x=1.0, y=2.0)
>>> replace(p, x=3.0)
Point(x=3.0, y=2.0)
"""
return _dataclass_replace(obj, **kwargs)


@dispatch # type: ignore[misc]
def fields(obj: DataclassInstance) -> tuple[_dataclass_Field, ...]: # type: ignore[type-arg] # TODO: raise issue in beartype
"""Return the fields of a dataclass instance."""
@dispatch # type: ignore[no-redef]
def replace(obj: Mapping[Hashable, Any], /, **kwargs: Any) -> Mapping[Hashable, Any]:
"""Replace the fields of a mapping.
This operates similarly to `dict.update`, except that
the kwargs are checked against the keys of the mapping.
Examples
--------
>>> from dataclasses import dataclass
>>> from dataclasstools import replace
>>> p = {"a": 1, "b": 2, "c": 3}
>>> p
{'a': 1, 'b': 2, 'c': 3}
>>> replace(p, c=4.0)
{'a': 1, 'b': 2, 'c': 4.0}
"""
extra_keys = set(kwargs) - set(obj)
if extra_keys:
msg = f"invalid keys {extra_keys}."
raise ValueError(msg)

return type(obj)(**{**obj, **kwargs})


# ===================================================================
# Fields


@dispatch
def fields(obj: DataclassInstance, /) -> tuple[Field, ...]: # type: ignore[type-arg] # TODO: raise issue in beartype
"""Return the fields of a dataclass instance.
Examples
--------
>>> from dataclasses import dataclass
>>> from dataclasstools import fields
>>> @dataclass
... class Point:
... x: float
... y: float
>>> p = Point(1.0, 2.0)
>>> fields(p)
(Field(name='x',type=<class 'float'>,...),
Field(name='y',type=<class 'float'>,...))
"""
return _dataclass_fields(obj)


@dispatch # type: ignore[misc]
@dispatch # type: ignore[no-redef]
def fields(obj: Mapping[str, Any], /) -> tuple[Field, ...]: # type: ignore[type-arg] # TODO: raise issue in beartype
"""Return the mapping as a tuple of `dataclass.Field` objects.
Examples
--------
>>> from dataclasstools import fields
>>> p = {"a": 1, "b": 2.0, "c": "3"}
>>> fields(p)
(Field(name='a',type=<class 'int'>,...),
Field(name='b',type=<class 'float'>,...),
Field(name='c',type=<class 'str'>,...))
"""
fs = tuple(field(kw_only=True) for _ in obj) # pylint: ignore=invalid-field-call
for f, (k, v) in zip(fs, obj.items(), strict=True):
f.name = k
f.type = type(v)
return fs


# ===================================================================
# Asdict


@dispatch
def asdict(
obj: DataclassInstance,
/,
*,
dict_factory: Callable[[list[tuple[str, Any]]], dict[str, Any]] = dict,
) -> dict[str, Any]:
"""Return the fields of a dataclass instance as a dictionary."""
"""Return the fields of a dataclass instance as a dictionary.
Examples
--------
>>> from dataclasses import dataclass
>>> from dataclasstools import asdict
>>> @dataclass
... class Point:
... x: float
... y: float
>>> p = Point(1.0, 2.0)
>>> asdict(p)
{'x': 1.0, 'y': 2.0}
"""
return _dataclass_asdict(obj, dict_factory=dict_factory)


@dispatch # type: ignore[misc]
@dispatch # type: ignore[no-redef]
def asdict(
obj: Mapping[str, Any],
/,
*,
dict_factory: Callable[[list[tuple[str, Any]]], dict[str, Any]] = dict,
) -> dict[str, Any]:
"""Return the fields of a mapping as a dictionary.
Following the `asdict` API, the dictionary may be copied if ``dict_factory``
performs a copy when constructed from a :class:`~collections.abc.Mapping`.
Examples
--------
>>> from dataclasstools import asdict
>>> p = {"a": 1, "b": 2.0, "c": "3"}
>>> asdict(p)
{'a': 1, 'b': 2.0, 'c': '3'}
>>> asdict(p) is p
False
"""
return dict_factory(obj)


# ===================================================================
# Astuple


@dispatch
def astuple(
obj: DataclassInstance,
/,
tuple_factory: Callable[[Any], tuple[Any, ...]] = tuple,
) -> tuple[Any, ...]:
"""Return the fields of a dataclass instance as a tuple."""
"""Return the fields of a dataclass instance as a tuple.
Examples
--------
>>> from dataclasses import dataclass
>>> from dataclasstools import astuple
>>> @dataclass
... class Point:
... x: float
... y: float
>>> p = Point(1.0, 2.0)
>>> astuple(p)
(1.0, 2.0)
"""
return _dataclass_astuple(obj, tuple_factory=tuple_factory)


@dispatch # type: ignore[no-redef]
def astuple(
obj: Mapping[str, Any],
/,
tuple_factory: Callable[[Any], tuple[Any, ...]] = tuple,
) -> tuple[Any, ...]:
"""Return the fields of a mapping as a tuple.
Examples
--------
>>> from dataclasstools import astuple
>>> p = {"a": 1, "b": 2.0, "c": "3"}
>>> astuple(p)
(1, 2.0, '3')
"""
return tuple_factory(obj.values())
Loading

0 comments on commit 03fd61e

Please sign in to comment.