Skip to main content

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

PyPI platforms PyPI version

pip install dataclassish

Documentation

Documentation Status

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, ifnotisinstance


@define
class Class1:
    attr: int | None = field(default=None, converter=optional(int))


obj = Class1()
print(obj.attr)
# None

obj = Class1(a=1)
print(obj.attr)
# 1


@define
class Class2:
    attr: float | int = field(converter=ifnotisinstance((int,), converter=float))


obj = Class2(1)
print(obj.attr)
# 1

obj = Class2("1")
print(obj.attr)
# 1.0

Citation

DOI

If you enjoyed using this library and would like to cite the software you use then click the link above.

Development

Actions Status

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

dataclassish-0.3.0.tar.gz (16.3 kB view details)

Uploaded Source

Built Distribution

dataclassish-0.3.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

Details for the file dataclassish-0.3.0.tar.gz.

File metadata

  • Download URL: dataclassish-0.3.0.tar.gz
  • Upload date:
  • Size: 16.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.5

File hashes

Hashes for dataclassish-0.3.0.tar.gz
Algorithm Hash digest
SHA256 427b996292f68b614dec76ff0085e08ef24babca92c6689a23bd376c53249d0d
MD5 42d2faff8607cee961419cf1ef3bf2fb
BLAKE2b-256 bc6bce998641397ddbb154d8b522148edfeeefb0a643ed54056cd3364d22b18b

See more details on using hashes here.

File details

Details for the file dataclassish-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: dataclassish-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 11.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.0 CPython/3.12.5

File hashes

Hashes for dataclassish-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0a9998126f3f3474dbe519f0a5cdc0f159e099236324e6753f96277802cf70af
MD5 aca8efcc7a64a347281aaa8c604a125e
BLAKE2b-256 211fb3841f2208bd0ffda194e849ae828cacc0244c3296234e83fcc371cab64a

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page