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


@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=Unless(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.1.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

dataclassish-0.3.1-py3-none-any.whl (11.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dataclassish-0.3.1.tar.gz
  • Upload date:
  • Size: 15.9 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.1.tar.gz
Algorithm Hash digest
SHA256 8740823800e06d5ed6b838e955033411f618c2aca75315c5d09d18f94b74a97f
MD5 2e882475a9e8cd75601b80d9059c0a13
BLAKE2b-256 88dfbe7db25c9703f7831acb49de69fcbf2f6e39492e27e26609507da8727bc4

See more details on using hashes here.

File details

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

File metadata

  • Download URL: dataclassish-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 11.5 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b6457368d1624550131934a5b991b3c388fcd70b34fc23d725ea6add69dc05de
MD5 33be75081b5b332a90c80cd4a35bc91b
BLAKE2b-256 3ef58013dd25702bc1f5a8656788891523534ab1d2eae83249b13bc34aeaa36d

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