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))
    """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

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.4.0.tar.gz (28.9 kB view details)

Uploaded Source

Built Distribution

dataclassish-0.4.0-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

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

Hashes for dataclassish-0.4.0.tar.gz
Algorithm Hash digest
SHA256 9bb875d5525d601265c9184c59c9829f518dca6b2e90d622aa21b3a21ebc87a5
MD5 563b86fd0eb2c4c74c72401099558e0c
BLAKE2b-256 e2ca43f3a6b2de1cb62217771d71b9531890d41e53e16486d506bd167103a78b

See more details on using hashes here.

Provenance

The following attestation bundles were made for dataclassish-0.4.0.tar.gz:

Publisher: cd.yml on GalacticDynamics/dataclassish

Attestations:

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

Hashes for dataclassish-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 16bea5f72a80133897715524edee6bfc65125f8f0cb519598614a0baa6d7ba7d
MD5 53fd7890bcc1960881964dea72f8328b
BLAKE2b-256 b663a7378ed17163b044ff1b51828a052098bbc9697589b0b473adc806e8cc2d

See more details on using hashes here.

Provenance

The following attestation bundles were made for dataclassish-0.4.0-py3-none-any.whl:

Publisher: cd.yml on GalacticDynamics/dataclassish

Attestations:

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