Skip to content

Commit 6c71bd9

Browse files
authored
✨ feat: get_field (#39)
Signed-off-by: nstarman <nstarman@users.noreply.github.com>
1 parent 8ce498b commit 6c71bd9

File tree

5 files changed

+112
-5
lines changed

5 files changed

+112
-5
lines changed

src/dataclassish/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
# Submodules
99
"converters",
1010
"flags",
11-
# Dataclass API
12-
"DataclassInstance",
11+
# functions
1312
"replace",
1413
"fields",
1514
"asdict",
1615
"astuple",
17-
# Extensions
16+
"get_field",
1817
"field_keys",
1918
"field_values",
2019
"field_items",
20+
# Classes
21+
"DataclassInstance",
2122
"F",
2223
]
2324

@@ -29,6 +30,7 @@
2930
field_keys,
3031
field_values,
3132
fields,
33+
get_field,
3234
replace,
3335
)
3436
from ._src.types import DataclassInstance, F

src/dataclassish/_src/api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"asdict",
88
"astuple",
99
# Extensions
10+
"get_field",
1011
"field_keys",
1112
"field_values",
1213
"field_items",
@@ -50,6 +51,12 @@ def astuple(obj: Any, /) -> tuple[Any, ...]:
5051
# Extensions
5152

5253

54+
@dispatch.abstract # type: ignore[misc]
55+
def get_field(obj: Any, field_name: str, /) -> Any:
56+
"""Get the value of a field from an object."""
57+
raise NotImplementedError # pragma: no cover
58+
59+
5360
@dispatch.abstract # type: ignore[misc]
5461
def field_keys(obj: Any, /) -> tuple[str, ...]:
5562
"""Return the field names from the `dataclassish.fields`."""

src/dataclassish/_src/register_base.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,31 @@
1111
K = TypeVar("K")
1212
V = TypeVar("V")
1313

14+
# ===================================================================
15+
16+
17+
@dispatch # type: ignore[misc]
18+
def get_field(obj: Any, k: str, /) -> Any:
19+
"""Get a field of an object by name.
20+
21+
This default implementation is just to call `getattr`.
22+
23+
Examples
24+
--------
25+
>>> from dataclassish import get_field
26+
27+
>>> class Point:
28+
... def __init__(self, x, y):
29+
... self.x = x
30+
... self.y = y
31+
32+
>>> p = Point(1.0, 2.0)
33+
>>> get_field(p, "x")
34+
1.0
35+
36+
"""
37+
return getattr(obj, k)
38+
1439

1540
# ===================================================================
1641
# Field keys

src/dataclassish/_src/register_dataclass.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,31 @@
1616

1717
from .types import DataclassInstance, F
1818

19+
# ===================================================================
20+
21+
22+
@dispatch # type: ignore[misc]
23+
def get_field(obj: DataclassInstance, k: str, /) -> Any:
24+
"""Get a field of a dataclass instance by name.
25+
26+
Examples
27+
--------
28+
>>> from dataclasses import dataclass
29+
>>> from dataclassish import get_field
30+
31+
>>> @dataclass
32+
... class Point:
33+
... x: float
34+
... y: float
35+
36+
>>> p = Point(1.0, 2.0)
37+
>>> get_field(p, "x")
38+
1.0
39+
40+
"""
41+
return getattr(obj, k)
42+
43+
1944
# ===================================================================
2045
# Replace
2146

@@ -51,7 +76,7 @@ def _recursive_replace_dataclass_helper(
5176
if isinstance(v, F):
5277
out = v.value
5378
elif isinstance(v, Mapping):
54-
out = replace(getattr(obj, k), v)
79+
out = replace(get_field(obj, k), v)
5580
else:
5681
out = v
5782
return out
@@ -87,6 +112,23 @@ def replace(obj: DataclassInstance, fs: Mapping[str, Any], /) -> DataclassInstan
87112
PointofPoints(a=Point(x={'thing': 5.0}, y=2.0),
88113
b=Point(x=3.0, y=4.0))
89114
115+
This also works on mixed-type structures, e.g. a dictionary of dataclasses.
116+
117+
>>> p = {"a": Point(1.0, 2.0), "b": Point(3.0, 4.0)}
118+
>>> replace(p, {"a": {"x": 5.0}, "b": {"y": 6.0}})
119+
{'a': Point(x=5.0, y=2.0), 'b': Point(x=3.0, y=6.0)}
120+
121+
Or a dataclass of dictionaries.
122+
123+
>>> @dataclass
124+
... class Object:
125+
... a: dict[str, Any]
126+
... b: dict[str, Any]
127+
128+
>>> p = Object({"a": 1, "b": 2}, {"c": 3, "d": 4})
129+
>>> replace(p, {"a": {"b": 5}, "b": {"c": 6}})
130+
Object(a={'a': 1, 'b': 5}, b={'c': 6, 'd': 4})
131+
90132
"""
91133
kwargs = {k: _recursive_replace_dataclass_helper(obj, k, v) for k, v in fs.items()}
92134
return _dataclass_replace(obj, **kwargs)

src/dataclassish/_src/register_mapping.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@
1010

1111
from .types import F
1212

13+
# ===================================================================
14+
15+
16+
@dispatch(precedence=1) # type: ignore[misc]
17+
def get_field(obj: Mapping[Hashable, Any], k: Hashable, /) -> Any:
18+
"""Get a field of a mapping by key.
19+
20+
Examples
21+
--------
22+
>>> from dataclassish import get_field
23+
24+
>>> p = {"a": 1, "b": 2.0, "c": "3"}
25+
>>> get_field(p, "a")
26+
1
27+
28+
"""
29+
return obj[k]
30+
31+
1332
# ===================================================================
1433
# Replace
1534

@@ -54,7 +73,7 @@ def _recursive_replace_mapping_helper(
5473
if isinstance(v, F): # Field, stop here.
5574
out = v.value
5675
elif isinstance(v, Mapping): # more to replace, recurse.
57-
out = replace(obj[k], v)
76+
out = replace(get_field(obj, k), v)
5877
else: # nothing to replace, keep the value.
5978
out = v
6079
return out
@@ -82,6 +101,18 @@ def replace(
82101
>>> replace(p, {"c": F({"aa": 6, "bb": {"d": 7}})})
83102
{'a': 1, 'b': 2.0, 'c': {'aa': 6, 'bb': {'d': 7}}}
84103
104+
This also works on mixed-type structures, e.g. a dataclass of of dictionaries.
105+
106+
>>> from dataclasses import dataclass
107+
>>> @dataclass
108+
... class Object:
109+
... a: dict[str, Any]
110+
... b: dict[str, Any]
111+
112+
>>> p = Object({"a": 1, "b": 2}, {"c": 3, "d": 4})
113+
>>> replace(p, {"a": {"b": 5}, "b": {"c": 6}})
114+
Object(a={'a': 1, 'b': 5}, b={'c': 6, 'd': 4})
115+
85116
"""
86117
# Recursively replace the fields
87118
kwargs = {k: _recursive_replace_mapping_helper(obj, k, v) for k, v in fs.items()}

0 commit comments

Comments
 (0)