dataclass tools, extended by multiple dispatch
Project description
dataclassish
Tools from dataclasses
, extended to all of Python
Python's dataclasses
provides tools for working with
objects, but only compatible @dataclass
objects. 😢
This repository is a
superset of those tools and extends them to work on ANY Python object you want!
🎉
You can easily register in object-specific methods and use a unified
interface for object manipulation. 🕶️
For example,
from dataclassish import replace # New object, replacing select fields
d1 = {"a": 1, "b": 2.0, "c": "3"}
d2 = replace(d1, c=3 + 0j)
print(d2)
# {'a': 1, 'b': 2.0, 'c': (3+0j)}
Installation
pip install dataclassish
Documentation
WIP. But if you've worked with a
dataclass
then you
basically already know everything you need to know.
Getting Started
In this example we'll show how dataclassish
works exactly the same as
dataclasses
when working with a @dataclass
object.
from dataclassish import replace
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p)
# Point(x=1.0, y=2.0)
p2 = replace(p, x=3.0)
print(p2)
# Point(x=3.0, y=2.0)
Now we'll work with a dict
object. Note that you cannot use tools
from dataclasses
with dict
objects.
from dataclassish import replace
p = {"x": 1, "y": 2.0}
print(p)
# {'x': 1, 'y': 2.0}
p2 = replace(p, x=3.0)
print(p2)
# {'x': 3.0, 'y': 2.0}
# If we try to `replace` a value that isn't in the dict, we'll get an error
try:
replace(p, z=None)
except ValueError as e:
print(e)
# invalid keys {'z'}.
Registering in a custom type is very easy! Let's make a custom object and define
how replace
will operate on it.
from typing import Any
from plum import dispatch
class MyClass:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __repr__(self) -> str:
return f"MyClass(a={self.a},b={self.b},c={self.c})"
@dispatch
def replace(obj: MyClass, **changes: Any) -> MyClass:
current_args = {k: getattr(obj, k) for k in "abc"}
updated_args = current_args | changes
return MyClass(**updated_args)
obj = MyClass(1, 2, 3)
print(obj)
# MyClass(a=1,b=2,c=3)
obj2 = replace(obj, c=4.0)
print(obj2)
# MyClass(a=1,b=2,c=4.0)
Adding a Second Argument
replace
can also accept a second positional argument which is a dictionary
specifying a nested replacement. For example consider the following dict:
p = {"a": {"a1": 1, "a2": 2}, "b": {"b1": 3, "b2": 4}, "c": {"c1": 5, "c2": 6}}
With replace
the sub-dicts can be updated via:
replace(p, {"a": {"a1": 1.5}, "b": {"b2": 4.5}, "c": {"c1": 5.5}})
# {'a': {'a1': 1.5, 'a2': 2}, 'b': {'b1': 3, 'b2': 4.5}, 'c': {'c1': 5.5, 'c2': 6}}
In contrast in pure Python this would be:
from copy import deepcopy
newp = deepcopy(p)
newp["a"]["a1"] = 1.5
newp["b"]["b2"] = 4.5
newp["c"]["c1"] = 5.5
And this is the simplest case, where the mutability of a dict
allows us to copy the full object and update it after. Note that we had to use
deepcopy
to avoid
mutating the sub-dicts. So what if the objects are immutable?
@dataclass(frozen=True)
class Object:
x: float | dict
y: float
@dataclass(frozen=True)
class Collection:
a: Object
b: Object
p = Collection(Object(1.0, 2.0), Object(3.0, 4.0))
print(p)
Collection(a=Object(x=1.0, y=2.0), b=Object(x=3.0, y=4.0))
replace(p, {"a": {"x": 5.0}, "b": {"y": 6.0}})
# Collection(a=Object(x=5.0, y=2.0), b=Object(x=3.0, y=6.0))
With replace
this remains a one-liner. Replace pieces of any structure,
regardless of nesting.
To disambiguate dictionary fields from nested structures, use the F
marker.
from dataclassish import F
replace(p, {"a": {"x": F({"thing": 5.0})}})
# Collection(a=Object(x={'thing': 5.0}, y=2.0),
# b=Object(x=3.0, y=4.0))
dataclass tools
dataclasses
has a number of utility functions beyond
replace
: fields
, asdict
, and astuple
. dataclassish
supports of all
these functions.
from dataclassish import fields, asdict, astuple
p = Point(1.0, 2.0)
print(fields(p))
# (Field(name='x',...), Field(name='y',...))
print(asdict(p))
# {'x': 1.0, 'y': 2.0}
print(astuple(p))
# (1.0, 2.0)
dataclassish
extends these functions to dict
's:
p = {"x": 1, "y": 2.0}
print(fields(p))
# (Field(name='x',...), Field(name='y',...))
print(asdict(p))
# {'x': 1.0, 'y': 2.0}
print(astuple(p))
# (1.0, 2.0)
Support for custom objects can be implemented similarly to replace
.
converters
While dataclasses.field
itself does not allow for converters (See PEP 712)
many dataclasses-like libraries do. A very short, very non-exhaustive list
includes: attrs
and equinox
. The module dataclassish.converters
provides a
few useful converter functions. If you need more, check out attrs
!
from attrs import define, field
from dataclassish.converters import Optional, Unless
@define
class Class1:
attr: int | None = field(default=None, converter=Optional(int))
"""attr is converted to an int or kept as None."""
obj = Class1()
print(obj.attr)
# None
obj = Class1(a=1.0)
print(obj.attr)
# 1
@define
class Class2:
attr: float | int = field(converter=Unless(int, converter=float))
"""attr is converted to a float, unless it's an int."""
obj = Class2(1)
print(obj.attr)
# 1
obj = Class2("1")
print(obj.attr)
# 1.0
Citation
If you enjoyed using this library and would like to cite the software you use then click the link above.
Development
We welcome contributions!
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file dataclassish-0.4.0.tar.gz
.
File metadata
- Download URL: dataclassish-0.4.0.tar.gz
- Upload date:
- Size: 28.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.1 CPython/3.12.7
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9bb875d5525d601265c9184c59c9829f518dca6b2e90d622aa21b3a21ebc87a5 |
|
MD5 | 563b86fd0eb2c4c74c72401099558e0c |
|
BLAKE2b-256 | e2ca43f3a6b2de1cb62217771d71b9531890d41e53e16486d506bd167103a78b |
Provenance
The following attestation bundles were made for dataclassish-0.4.0.tar.gz
:
Publisher:
cd.yml
on GalacticDynamics/dataclassish
-
Statement type:
https://in-toto.io/Statement/v1
- Predicate type:
https://docs.pypi.org/attestations/publish/v1
- Subject name:
dataclassish-0.4.0.tar.gz
- Subject digest:
9bb875d5525d601265c9184c59c9829f518dca6b2e90d622aa21b3a21ebc87a5
- Sigstore transparency entry: 149625535
- Sigstore integration time:
- Predicate type:
File details
Details for the file dataclassish-0.4.0-py3-none-any.whl
.
File metadata
- Download URL: dataclassish-0.4.0-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/5.1.1 CPython/3.12.7
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 16bea5f72a80133897715524edee6bfc65125f8f0cb519598614a0baa6d7ba7d |
|
MD5 | 53fd7890bcc1960881964dea72f8328b |
|
BLAKE2b-256 | b663a7378ed17163b044ff1b51828a052098bbc9697589b0b473adc806e8cc2d |
Provenance
The following attestation bundles were made for dataclassish-0.4.0-py3-none-any.whl
:
Publisher:
cd.yml
on GalacticDynamics/dataclassish
-
Statement type:
https://in-toto.io/Statement/v1
- Predicate type:
https://docs.pypi.org/attestations/publish/v1
- Subject name:
dataclassish-0.4.0-py3-none-any.whl
- Subject digest:
16bea5f72a80133897715524edee6bfc65125f8f0cb519598614a0baa6d7ba7d
- Sigstore transparency entry: 149625537
- Sigstore integration time:
- Predicate type: