From 339d17e51d203f58761ef2f005bd08e77ad1aef8 Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 13:57:51 -0500 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=93=9D=20docs(readme):=20document=20f?= =?UTF-8?q?lags,=20multiple-dispatch,=20and=20more=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 368 +++++++++++++++++++++++++++++++------------------ pyproject.toml | 2 +- 2 files changed, 233 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 1c89bf1..93f84b2 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,10 @@

PyPI: dataclassish PyPI versions: dataclassish - ReadTheDocs dataclassish license

CI status - ReadTheDocs codecov ruff ruff @@ -26,7 +24,7 @@ interface for object manipulation. 🕶️ For example, -```python +```{code-block} python from dataclassish import replace # New object, replacing select fields d1 = {"a": 1, "b": 2.0, "c": "3"} @@ -46,144 +44,208 @@ pip install dataclassish ## Documentation -[![Documentation Status][rtd-badge]][rtd-link] - -WIP. But if you've worked with a -[`dataclass`](https://docs.python.org/3/library/dataclasses.html) then you -basically already know everything you need to know. +- [Installation](#installation) +- [Documentation](#documentation) + - [Getting Started](#getting-started) + - [Working with a `@dataclass`](#working-with-a-dataclass) + - [Working with a `dict`](#working-with-a-dict) + - [The `__replace__` Method](#the-__replace__-method) + - [Registering in a Custom Type](#registering-in-a-custom-type) + - [Adding a Second Argument](#adding-a-second-argument) + - [dataclass tools](#dataclass-tools) + - [More tools](#more-tools) + - [Converters](#converters) + - [Flags](#flags) +- [Citation](#citation) +- [Development](#development) ### Getting Started +#### Working with a `@dataclass` + In this example we'll show how `dataclassish` works exactly the same as [`dataclasses`][dataclasses-link] when working with a `@dataclass` object. -```python -from dataclassish import replace -from dataclasses import dataclass +```{code-block} python +>>> from dataclassish import replace +>>> from dataclasses import dataclass +>>> @dataclass(frozen=True) +... class Point: +... x: float | int +... y: float | int -@dataclass -class Point: - x: float - y: float +>>> p = Point(1.0, 2.0) +>>> p +Point(x=1.0, y=2.0) -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) +>>> p2 = replace(p, x=3.0) +>>> p2 +Point(x=3.0, y=2.0) ``` -Now we'll work with a [`dict`][dict-link] object. Note that you cannot use tools -from [`dataclasses`][dataclasses-link] with [`dict`][dict-link] objects. +#### Working with a `dict` + +Now we'll work with a [`dict`][dict-link] object. Note that +[`dataclasses`][dataclasses-link] does _not_ work with [`dict`][dict-link] +objects, but with `dataclassish` it's easy! -```python -from dataclassish import replace +```{code-block} python +>>> from dataclassish import replace -p = {"x": 1, "y": 2.0} -print(p) -# {'x': 1, 'y': 2.0} +>>> p = {"x": 1, "y": 2.0} +>>> p +{'x': 1, 'y': 2.0} -p2 = replace(p, x=3.0) -print(p2) -# {'x': 3.0, 'y': 2.0} +>>> p2 = replace(p, x=3.0) +>>> 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'}. +>>> 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. +#### The `__replace__` Method + +In Python 3.13+ objects can implement the `__replace__` method to define how +`copy.replace` should operate on them. This was directly inspired by +`dataclass.replace`, and is a nice generalization to more general Python +objects. `dataclassish` too supports this method. + +```{code-block} python +>>> class HasReplace: +... def __init__(self, a, b): +... self.a = a +... self.b = b +... def __repr__(self) -> str: +... return f"HasReplace(a={self.a},b={self.b})" +... def __replace__(self, **changes): +... return type(self)(**(self.__dict__ | changes)) + +>>> obj = HasReplace(1, 2) +>>> obj +HasReplace(a=1,b=2) -```python -from typing import Any -from plum import dispatch +>>> obj2 = replace(obj, b=3) +>>> obj2 +HasReplace(a=1,b=3) +``` + +#### Registering in a Custom Type -class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c +Let's say there's a custom object that we want to use `replace` on, but which +doesn't have a `__replace__` method (or which we want more control over using a +second argument, discussed later). Registering in a custom type is very easy! +Let's make a custom object and define how `replace` will operate on it. - def __repr__(self) -> str: - return f"MyClass(a={self.a},b={self.b},c={self.c})" +```{code-block} python +>>> 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) +>>> @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) +>>> obj = MyClass(1, 2, 3) +>>> obj +MyClass(a=1,b=2,c=3) + +>>> obj2 = replace(obj, c=4.0) +>>> 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: +specifying a nested replacement. For example consider the following dict of +Point objects: -```python -p = {"a": {"a1": 1, "a2": 2}, "b": {"b1": 3, "b2": 4}, "c": {"c1": 5, "c2": 6}} +```{code-block} python +>>> p = {"a": Point(1, 2), "b": Point(3, 4), "c": Point(5, 6)} ``` -With `replace` the sub-dicts can be updated via: +With `replace` the nested structure can be updated via: -```python -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}} +```{code-block} python +>>> replace(p, {"a": {"x": 1.5}, "b": {"y": 4.5}, "c": {"x": 5.5}}) +{'a': Point(x=1.5, y=2), 'b': Point(x=3, y=4.5), 'c': Point(x=5.5, y=6)} ``` -In contrast in pure Python this would be: +In contrast in pure Python this would be very challenging. Expand the example +below to see how this might be done. + +

+Expand for detailed example -```python -from copy import deepcopy +This is a bad approach, updating the frozen dataclasses in place: -newp = deepcopy(p) -newp["a"]["a1"] = 1.5 -newp["b"]["b2"] = 4.5 -newp["c"]["c1"] = 5.5 +```{code-block} python +>>> from copy import deepcopy + +>>> newp = deepcopy(p) +>>> object.__setattr__(newp["a"], "x", 1.5) +>>> object.__setattr__(newp["b"], "y", 4.5) +>>> object.__setattr__(newp["c"], "x", 5.5) ``` -And this is the simplest case, where the mutability of a [`dict`][dict-link] -allows us to copy the full object and update it after. Note that we had to use +A better way might be to create an entirely new object! + +```{code-block} python +>>> newp = {"a": Point(1.5, p["a"].y), +... "b": Point(p["b"].x, 4.5), +... "c": Point(5.5, p["c"].y)} +``` + +This isn't so good either. + +
+ +`dataclassish.replace` is a one-liner that can work on any object (if it has a +registered means to do so), regardless of mutability or nesting. Consider this +fully immutable structure: And this is the simplest case, where the mutability +of a [`dict`][dict-link] allows us to copy the full object and update it after. +Note that we had to use [`deepcopy`](https://docs.python.org/3/library/copy.html#copy.deepcopy) to avoid mutating the sub-dicts. So what if the objects are immutable? -```python -@dataclass(frozen=True) -class Object: - x: float | dict - y: float +```{code-block} python +>>> @dataclass(frozen=True) +... class Object: +... x: float | dict +... y: float -@dataclass(frozen=True) -class Collection: - a: Object - b: Object +>>> @dataclass(frozen=True) +... class Collection: +... a: Object +... b: Object -p = Collection(Object(1.0, 2.0), Object(3.0, 4.0)) -print(p) +>>> p = Collection(Object(1.0, 2.0), Object(3.0, 4.0)) +>>> 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)) +>>> 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, @@ -191,12 +253,12 @@ regardless of nesting. To disambiguate dictionary fields from nested structures, use the `F` marker. -```python -from dataclassish import F +```{code-block} python +>>> 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)) +>>> 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 @@ -205,78 +267,113 @@ replace(p, {"a": {"x": F({"thing": 5.0})}}) `replace`: `fields`, `asdict`, and `astuple`. `dataclassish` supports of all these functions. -```python -from dataclassish import fields, asdict, astuple +```{code-block} python +>>> from dataclassish import fields, asdict, astuple -p = Point(1.0, 2.0) +>>> p = Point(1.0, 2.0) -print(fields(p)) -# (Field(name='x',...), Field(name='y',...)) +>>> fields(p) +(Field(name='x',...), Field(name='y',...)) -print(asdict(p)) -# {'x': 1.0, 'y': 2.0} +>>> asdict(p) +{'x': 1.0, 'y': 2.0} -print(astuple(p)) -# (1.0, 2.0) +>>> astuple(p) +(1.0, 2.0) ``` `dataclassish` extends these functions to [`dict`][dict-link]'s: -```python -p = {"x": 1, "y": 2.0} +```{code-block} python +>>> p = {"x": 1, "y": 2.0} -print(fields(p)) -# (Field(name='x',...), Field(name='y',...)) +>>> fields(p) +(Field(name='x',...), Field(name='y',...)) -print(asdict(p)) -# {'x': 1.0, 'y': 2.0} +>>> asdict(p) +{'x': 1, 'y': 2.0} -print(astuple(p)) -# (1.0, 2.0) +>>> astuple(p) +(1, 2.0) ``` Support for custom objects can be implemented similarly to `replace`. -### converters +### More tools + +In addition to the `dataclasses` tools, `dataclassish` provides a few more +utilities. + +- `get_field` returns the field of an object by name. +- `field_keys` returns the names of an object's fields. +- `field_values` returns the values of an object's fields. +- `field_items` returns the names and values of an object's fields. + +### 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`! -```python -from attrs import define, field -from dataclassish.converters import Optional, Unless +```{code-block} python +>>> 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.""" -@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() -print(obj.attr) -# None +>>> obj = Class1(attr=1.0) +>>> obj.attr +1 -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) +>>> 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") +>>> obj.attr +1.0 +``` + +### Flags + +`dataclassish` provides flags for customizing the behavior of functions. For +example, the [`coordinax`](https://pypi.org/project/coordinax/) package, which +depends on `dataclassish`, uses a flag `AttrFilter` to filter out fields from +consideration by the functions in `dataclassish`. + +`dataclassish` provides a few built-in flags and flag-related utilities. + +```{code-block} python +>>> from dataclassish import flags +>>> flags.__all__ +['FlagConstructionError', 'AbstractFlag', 'NoFlag'] +``` +Where `AbstractFlag` is the base class for flags, and `NoFlag` is a flag that +does nothing. `FlagConstructionError` is an error that is raised when a flag is +constructed incorrectly. -obj = Class2(1) -print(obj.attr) -# 1 +As a quick example, we'll show how to use `NoFlag`. -obj = Class2("1") -print(obj.attr) -# 1.0 +```{code-block} python +>>> from dataclassish import field_keys +>>> field_keys(flags.NoFlag, p) +dict_keys(['x', 'y']) ``` ## Citation @@ -289,7 +386,6 @@ then click the link above. ## Development [![Actions Status][actions-badge]][actions-link] -[![Documentation Status][rtd-badge]][rtd-link] [![codecov][codecov-badge]][codecov-link] [![SPEC 0 — Minimum Supported Dependencies][spec0-badge]][spec0-link] [![pre-commit][pre-commit-badge]][pre-commit-link] diff --git a/pyproject.toml b/pyproject.toml index b6fd29a..a4e9649 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ filterwarnings = [ ] log_cli_level = "INFO" testpaths = [ - "src", "tests", + "README.md", "src", "tests", ] From c2652ec8442ef2cb1f3114de7d20adec98e49e15 Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 14:10:23 -0500 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=8E=A8=20style(toc):=20only=20for=20t?= =?UTF-8?q?he=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 93f84b2..d24e1c3 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,16 @@ pip install dataclassish ## Documentation -- [Installation](#installation) -- [Documentation](#documentation) - - [Getting Started](#getting-started) - - [Working with a `@dataclass`](#working-with-a-dataclass) - - [Working with a `dict`](#working-with-a-dict) - - [The `__replace__` Method](#the-__replace__-method) - - [Registering in a Custom Type](#registering-in-a-custom-type) - - [Adding a Second Argument](#adding-a-second-argument) - - [dataclass tools](#dataclass-tools) - - [More tools](#more-tools) - - [Converters](#converters) - - [Flags](#flags) -- [Citation](#citation) -- [Development](#development) +- [Getting Started](#getting-started) + - [Working with a `@dataclass`](#working-with-a-dataclass) + - [Working with a `dict`](#working-with-a-dict) + - [The `__replace__` Method](#the-__replace__-method) + - [Registering in a Custom Type](#registering-in-a-custom-type) +- [Adding a Second Argument](#adding-a-second-argument) +- [dataclass tools](#dataclass-tools) +- [More tools](#more-tools) +- [Converters](#converters) +- [Flags](#flags) ### Getting Started From 7358e7260e48ce4818ace0c221ec9c140502f85f Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 14:17:05 -0500 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20more=20on=20ex?= =?UTF-8?q?t=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d24e1c3..91a57e3 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ pip install dataclassish ### Getting Started -#### Working with a `@dataclass` +#### Replacing a `@dataclass` In this example we'll show how `dataclassish` works exactly the same as [`dataclasses`][dataclasses-link] when working with a `@dataclass` object. @@ -81,7 +81,7 @@ Point(x=1.0, y=2.0) Point(x=3.0, y=2.0) ``` -#### Working with a `dict` +#### Replacing a `dict` Now we'll work with a [`dict`][dict-link] object. Note that [`dataclasses`][dataclasses-link] does _not_ work with [`dict`][dict-link] @@ -106,7 +106,7 @@ objects, but with `dataclassish` it's easy! invalid keys {'z'}. ``` -#### The `__replace__` Method +#### Replacing via the `__replace__` Method In Python 3.13+ objects can implement the `__replace__` method to define how `copy.replace` should operate on them. This was directly inspired by @@ -133,7 +133,7 @@ HasReplace(a=1,b=3) ``` -#### Registering in a Custom Type +#### Replacing a Custom Type Let's say there's a custom object that we want to use `replace` on, but which doesn't have a `__replace__` method (or which we want more control over using a @@ -169,7 +169,7 @@ MyClass(a=1,b=2,c=3) MyClass(a=1,b=2,c=4.0) ``` -### Adding a Second Argument +### Nested Replacement `replace` can also accept a second positional argument which is a dictionary specifying a nested replacement. For example consider the following dict of @@ -305,6 +305,43 @@ utilities. - `field_values` returns the values of an object's fields. - `field_items` returns the names and values of an object's fields. +```{code-block} python +>>> from dataclassish import get_field, field_keys, field_values, field_items + +>>> p = Point(1.0, 2.0) + +>>> get_field(p, "x") +1.0 + +>>> field_keys(p) +('x', 'y') + +>>> field_values(p) +(1.0, 2.0) + +>>> field_items(p) +(('x', 1.0), ('y', 2.0)) +``` + +These functions work on any object that has been registered in, not just +`@dataclass` objects. + +```{code-block} python +>>> p = {"x": 1, "y": 2.0} + +>>> get_field(p, "x") +1 + +>>> field_keys(p) +dict_keys(['x', 'y']) + +>>> field_values(p) +dict_values([1, 2.0]) + +>>> field_items(p) +dict_items([('x', 1), ('y', 2.0)]) +``` + ### Converters While `dataclasses.field` itself does not allow for converters (See PEP 712) @@ -368,8 +405,8 @@ As a quick example, we'll show how to use `NoFlag`. ```{code-block} python >>> from dataclassish import field_keys ->>> field_keys(flags.NoFlag, p) -dict_keys(['x', 'y']) +>>> tuple(field_keys(flags.NoFlag, p)) +('x', 'y') ``` ## Citation From cedab96b4104bf2703dba85d59bd998a56a8256b Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 14:41:17 -0500 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=8E=A8=20style:=20use=20pycon=20marke?= =?UTF-8?q?rs=20for=20GH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nstarman --- .pre-commit-config.yaml | 1 + README.md | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 558b9a6..20bb558 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,7 @@ repos: hooks: - id: blacken-docs additional_dependencies: [black==24.*] + exclude: README.md - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v5.0.0" diff --git a/README.md b/README.md index 91a57e3..c73a07c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ interface for object manipulation. 🕶️ For example, -```{code-block} python +```pycon from dataclassish import replace # New object, replacing select fields d1 = {"a": 1, "b": 2.0, "c": "3"} @@ -62,7 +62,7 @@ pip install dataclassish In this example we'll show how `dataclassish` works exactly the same as [`dataclasses`][dataclasses-link] when working with a `@dataclass` object. -```{code-block} python +```pycon >>> from dataclassish import replace >>> from dataclasses import dataclass @@ -87,7 +87,7 @@ Now we'll work with a [`dict`][dict-link] object. Note that [`dataclasses`][dataclasses-link] does _not_ work with [`dict`][dict-link] objects, but with `dataclassish` it's easy! -```{code-block} python +```pycon >>> from dataclassish import replace >>> p = {"x": 1, "y": 2.0} @@ -113,7 +113,7 @@ In Python 3.13+ objects can implement the `__replace__` method to define how `dataclass.replace`, and is a nice generalization to more general Python objects. `dataclassish` too supports this method. -```{code-block} python +```pycon >>> class HasReplace: ... def __init__(self, a, b): ... self.a = a @@ -140,7 +140,7 @@ doesn't have a `__replace__` method (or which we want more control over using a second argument, discussed later). Registering in a custom type is very easy! Let's make a custom object and define how `replace` will operate on it. -```{code-block} python +```pycon >>> from typing import Any >>> from plum import dispatch @@ -175,13 +175,13 @@ MyClass(a=1,b=2,c=4.0) specifying a nested replacement. For example consider the following dict of Point objects: -```{code-block} python +```pycon >>> p = {"a": Point(1, 2), "b": Point(3, 4), "c": Point(5, 6)} ``` With `replace` the nested structure can be updated via: -```{code-block} python +```pycon >>> replace(p, {"a": {"x": 1.5}, "b": {"y": 4.5}, "c": {"x": 5.5}}) {'a': Point(x=1.5, y=2), 'b': Point(x=3, y=4.5), 'c': Point(x=5.5, y=6)} ``` @@ -194,7 +194,7 @@ below to see how this might be done. This is a bad approach, updating the frozen dataclasses in place: -```{code-block} python +```pycon >>> from copy import deepcopy >>> newp = deepcopy(p) @@ -205,7 +205,7 @@ This is a bad approach, updating the frozen dataclasses in place: A better way might be to create an entirely new object! -```{code-block} python +```pycon >>> newp = {"a": Point(1.5, p["a"].y), ... "b": Point(p["b"].x, 4.5), ... "c": Point(5.5, p["c"].y)} @@ -223,7 +223,7 @@ Note that we had to use [`deepcopy`](https://docs.python.org/3/library/copy.html#copy.deepcopy) to avoid mutating the sub-dicts. So what if the objects are immutable? -```{code-block} python +```pycon >>> @dataclass(frozen=True) ... class Object: ... x: float | dict @@ -249,7 +249,7 @@ regardless of nesting. To disambiguate dictionary fields from nested structures, use the `F` marker. -```{code-block} python +```pycon >>> from dataclassish import F >>> replace(p, {"a": {"x": F({"thing": 5.0})}}) @@ -263,7 +263,7 @@ Collection(a=Object(x={'thing': 5.0}, y=2.0), `replace`: `fields`, `asdict`, and `astuple`. `dataclassish` supports of all these functions. -```{code-block} python +```pycon >>> from dataclassish import fields, asdict, astuple >>> p = Point(1.0, 2.0) @@ -280,7 +280,7 @@ these functions. `dataclassish` extends these functions to [`dict`][dict-link]'s: -```{code-block} python +```pycon >>> p = {"x": 1, "y": 2.0} >>> fields(p) @@ -305,7 +305,7 @@ utilities. - `field_values` returns the values of an object's fields. - `field_items` returns the names and values of an object's fields. -```{code-block} python +```pycon >>> from dataclassish import get_field, field_keys, field_values, field_items >>> p = Point(1.0, 2.0) @@ -326,7 +326,7 @@ utilities. These functions work on any object that has been registered in, not just `@dataclass` objects. -```{code-block} python +```pycon >>> p = {"x": 1, "y": 2.0} >>> get_field(p, "x") @@ -349,7 +349,7 @@ 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`! -```{code-block} python +```pycon >>> from attrs import define, field >>> from dataclassish.converters import Optional, Unless @@ -391,7 +391,7 @@ consideration by the functions in `dataclassish`. `dataclassish` provides a few built-in flags and flag-related utilities. -```{code-block} python +```pycon >>> from dataclassish import flags >>> flags.__all__ ['FlagConstructionError', 'AbstractFlag', 'NoFlag'] @@ -403,7 +403,7 @@ constructed incorrectly. As a quick example, we'll show how to use `NoFlag`. -```{code-block} python +```pycon >>> from dataclassish import field_keys >>> tuple(field_keys(flags.NoFlag, p)) ('x', 'y') From 95f3bb566b79b6d7e3371c14d18d8336b462ad71 Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 15:11:59 -0500 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=8E=A8=20style:=20use=20pycon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++++++++++++++- conftest.py | 21 ++++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c73a07c..05700ae 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ interface for object manipulation. 🕶️ For example, -```pycon +```python from dataclassish import replace # New object, replacing select fields d1 = {"a": 1, "b": 2.0, "c": "3"} @@ -79,6 +79,7 @@ Point(x=1.0, y=2.0) >>> p2 = replace(p, x=3.0) >>> p2 Point(x=3.0, y=2.0) + ``` #### Replacing a `dict` @@ -104,6 +105,7 @@ objects, but with `dataclassish` it's easy! ... except ValueError as e: ... print(e) invalid keys {'z'}. + ``` #### Replacing via the `__replace__` Method @@ -167,6 +169,7 @@ MyClass(a=1,b=2,c=3) >>> obj2 = replace(obj, c=4.0) >>> obj2 MyClass(a=1,b=2,c=4.0) + ``` ### Nested Replacement @@ -177,6 +180,7 @@ Point objects: ```pycon >>> p = {"a": Point(1, 2), "b": Point(3, 4), "c": Point(5, 6)} + ``` With `replace` the nested structure can be updated via: @@ -184,6 +188,7 @@ With `replace` the nested structure can be updated via: ```pycon >>> replace(p, {"a": {"x": 1.5}, "b": {"y": 4.5}, "c": {"x": 5.5}}) {'a': Point(x=1.5, y=2), 'b': Point(x=3, y=4.5), 'c': Point(x=5.5, y=6)} + ``` In contrast in pure Python this would be very challenging. Expand the example @@ -201,6 +206,7 @@ This is a bad approach, updating the frozen dataclasses in place: >>> object.__setattr__(newp["a"], "x", 1.5) >>> object.__setattr__(newp["b"], "y", 4.5) >>> object.__setattr__(newp["c"], "x", 5.5) + ``` A better way might be to create an entirely new object! @@ -209,6 +215,7 @@ A better way might be to create an entirely new object! >>> newp = {"a": Point(1.5, p["a"].y), ... "b": Point(p["b"].x, 4.5), ... "c": Point(5.5, p["c"].y)} + ``` This isn't so good either. @@ -242,6 +249,7 @@ 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, @@ -255,6 +263,7 @@ To disambiguate dictionary fields from nested structures, use the `F` marker. >>> 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 @@ -276,6 +285,7 @@ these functions. >>> astuple(p) (1.0, 2.0) + ``` `dataclassish` extends these functions to [`dict`][dict-link]'s: @@ -291,6 +301,7 @@ these functions. >>> astuple(p) (1, 2.0) + ``` Support for custom objects can be implemented similarly to `replace`. @@ -321,6 +332,7 @@ utilities. >>> field_items(p) (('x', 1.0), ('y', 2.0)) + ``` These functions work on any object that has been registered in, not just @@ -340,6 +352,7 @@ dict_values([1, 2.0]) >>> field_items(p) dict_items([('x', 1), ('y', 2.0)]) + ``` ### Converters @@ -380,6 +393,7 @@ None >>> obj = Class2("1") >>> obj.attr 1.0 + ``` ### Flags @@ -395,6 +409,7 @@ consideration by the functions in `dataclassish`. >>> from dataclassish import flags >>> flags.__all__ ['FlagConstructionError', 'AbstractFlag', 'NoFlag'] + ``` Where `AbstractFlag` is the base class for flags, and `NoFlag` is a flag that @@ -407,6 +422,7 @@ As a quick example, we'll show how to use `NoFlag`. >>> from dataclassish import field_keys >>> tuple(field_keys(flags.NoFlag, p)) ('x', 'y') + ``` ## Citation diff --git a/conftest.py b/conftest.py index 8af1483..75a7fb4 100644 --- a/conftest.py +++ b/conftest.py @@ -5,23 +5,30 @@ from sybil import Document, Region, Sybil from sybil.parsers.myst import ( - DocTestDirectiveParser as MarkdownDocTestDirectiveParser, - PythonCodeBlockParser as MarkdownPythonCodeBlockParser, - SkipParser as MarkdownSkipParser, + DocTestDirectiveParser as MystDocTestDirectiveParser, + PythonCodeBlockParser as MystPythonCodeBlockParser, + SkipParser as MystSkipParser, ) from sybil.parsers.rest import DocTestParser as ReSTDocTestParser +from sybil.sybil import SybilCollection optionflags = ELLIPSIS | NORMALIZE_WHITESPACE parsers: Sequence[Callable[[Document], Iterable[Region]]] = [ - MarkdownDocTestDirectiveParser(optionflags=optionflags), - MarkdownPythonCodeBlockParser(doctest_optionflags=optionflags), - MarkdownSkipParser(), + MystDocTestDirectiveParser(optionflags=optionflags), + MystPythonCodeBlockParser(doctest_optionflags=optionflags), + MystSkipParser(), ] +# TODO: figure out native parser for `pycon` that doesn't require a new line at +# the end. +readme = Sybil( + parsers=[ReSTDocTestParser(optionflags=optionflags)], + patterns=["README.md"], +) docs = Sybil(parsers=parsers, patterns=["*.md"]) python = Sybil( parsers=[ReSTDocTestParser(optionflags=optionflags), *parsers], patterns=["*.py"] ) -pytest_collect_file = (docs + python).pytest() +pytest_collect_file = SybilCollection([docs, readme, python]).pytest() From 2ece60da64eb39ccea75e573ea532cc70e83a37e Mon Sep 17 00:00:00 2001 From: nstarman Date: Wed, 4 Dec 2024 15:15:49 -0500 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20typo:=20remove=20repea?= =?UTF-8?q?t=20sentences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 05700ae..ba91135 100644 --- a/README.md +++ b/README.md @@ -224,11 +224,7 @@ This isn't so good either. `dataclassish.replace` is a one-liner that can work on any object (if it has a registered means to do so), regardless of mutability or nesting. Consider this -fully immutable structure: And this is the simplest case, where the mutability -of a [`dict`][dict-link] allows us to copy the full object and update it after. -Note that we had to use -[`deepcopy`](https://docs.python.org/3/library/copy.html#copy.deepcopy) to avoid -mutating the sub-dicts. So what if the objects are immutable? +fully immutable structure: ```pycon >>> @dataclass(frozen=True)