From 742e9ca8ea98e1c23ac7bf25fbec85779c949c2b Mon Sep 17 00:00:00 2001 From: nstarman Date: Mon, 18 Nov 2024 15:27:04 -0500 Subject: [PATCH] refactor: use walled garden Signed-off-by: nstarman --- src/dataclassish/__init__.py | 4 +- src/dataclassish/_src/__init__.py | 3 + src/dataclassish/_src/converters.py | 141 ++++++++++++++++++++ src/dataclassish/{_core.py => _src/core.py} | 0 src/dataclassish/{_ext.py => _src/ext.py} | 2 +- src/dataclassish/converters.py | 131 +----------------- uv.lock | 4 +- 7 files changed, 150 insertions(+), 135 deletions(-) create mode 100644 src/dataclassish/_src/__init__.py create mode 100644 src/dataclassish/_src/converters.py rename src/dataclassish/{_core.py => _src/core.py} (100%) rename src/dataclassish/{_ext.py => _src/ext.py} (99%) diff --git a/src/dataclassish/__init__.py b/src/dataclassish/__init__.py index 70886cd..1b1e2a1 100644 --- a/src/dataclassish/__init__.py +++ b/src/dataclassish/__init__.py @@ -4,8 +4,8 @@ """ from . import converters -from ._core import DataclassInstance, F, asdict, astuple, fields, replace -from ._ext import field_items, field_keys, field_values +from ._src.core import DataclassInstance, F, asdict, astuple, fields, replace +from ._src.ext import field_items, field_keys, field_values from ._version import version as __version__ __all__ = [ diff --git a/src/dataclassish/_src/__init__.py b/src/dataclassish/_src/__init__.py new file mode 100644 index 0000000..95dceae --- /dev/null +++ b/src/dataclassish/_src/__init__.py @@ -0,0 +1,3 @@ +"""Dataclassish.""" + +__all__: list[str] = [] diff --git a/src/dataclassish/_src/converters.py b/src/dataclassish/_src/converters.py new file mode 100644 index 0000000..b976e43 --- /dev/null +++ b/src/dataclassish/_src/converters.py @@ -0,0 +1,141 @@ +"""Converters for dataclass fields. + +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``. This module provides a few useful converter +functions. If you need more, check out ``attrs``! + +""" +# ruff:noqa: N801 +# pylint: disable=C0103 + +__all__ = ["AbstractConverter", "Optional", "Unless"] + +import dataclasses +from abc import ABCMeta, abstractmethod +from collections.abc import Callable +from typing import Any, Generic, TypeVar, cast, overload + +ArgT = TypeVar("ArgT") # Input type +RetT = TypeVar("RetT") # Return type + + +class AbstractConverter(Generic[ArgT, RetT], metaclass=ABCMeta): + """Abstract converter class.""" + + converter: Callable[[ArgT], RetT] + """The converter to apply to the input value.""" + + @abstractmethod + def __call__(self, value: ArgT, /) -> Any: + """Convert the input value to the desired output type.""" + raise NotImplementedError # pragma: no cover + + +# ------------------------------------------------------------------- + + +@dataclasses.dataclass(frozen=True, slots=True, eq=False) +class Optional(AbstractConverter[ArgT, RetT]): + """Optional converter with a defined sentinel value. + + This converter allows for a field to be optional, i.e., it can be set to + `None`. This is useful when a field is required in some contexts but not in + others. + + This converter is based on ``attr.converters.optional``. If ``attrs`` ever + separates this out into its own package, so that other libraries, like + ``equinox``, can use the converter without depending on ``attrs``, then this + implementation will probably be removed. + + Examples + -------- + For this example we will use ``attrs`` as the dataclass library, but this + converter can be used with any dataclass-like library that supports + converters. + + >>> from attrs import define, field + >>> from dataclassish.converters import Optional + + >>> @define + ... class Class: + ... attr: int | None = field(default=None, converter=Optional(int)) + + >>> obj = Class() + >>> print(obj.attr) + None + + >>> obj = Class(1) + >>> obj.attr + 1 + + """ + + converter: Callable[[ArgT], RetT] + """The converter to apply to the input value.""" + + @overload + def __call__(self, value: None, /) -> None: ... + + @overload + def __call__(self, value: ArgT, /) -> RetT: ... + + def __call__(self, value: ArgT | None, /) -> RetT | None: + """Convert the input value to the output type, passing through `None`.""" + return None if value is None else self.converter(value) + + +# ------------------------------------------------------------------- + +PassThroughTs = TypeVar("PassThroughTs") + + +@dataclasses.dataclass(frozen=True, slots=True, eq=False) +class Unless(AbstractConverter[ArgT, RetT], Generic[ArgT, PassThroughTs, RetT]): + """Converter that is applied if the argument is NOT a specified type. + + This converter is useful when you want to pass through a value if it is of a + certain type, but convert it otherwise. + + Examples + -------- + For this example we will use ``attrs`` as the dataclass library, but this + converter can be used with any dataclass-like library that supports + converters. + + >>> from attrs import define, field + >>> from dataclassish.converters import Unless + + >>> @define + ... class Class: + ... attr: float | int = field(converter=Unless(int, converter=float)) + + >>> obj = Class(1) + >>> obj.attr + 1 + + >>> obj = Class("1") + >>> obj.attr + 1.0 + + """ + + unconverted_types: type[PassThroughTs] | tuple[type[PassThroughTs], ...] + """The types to pass through without conversion.""" + + converter: Callable[[ArgT], RetT] + """The converter to apply to the input value.""" + + @overload + def __call__(self, value: ArgT, /) -> RetT: ... + + @overload + def __call__(self, value: PassThroughTs, /) -> PassThroughTs: ... + + def __call__(self, value: ArgT | PassThroughTs, /) -> RetT | PassThroughTs: + """Pass through the input value.""" + return ( + cast(PassThroughTs, value) + if isinstance(value, self.unconverted_types) + else self.converter(cast(ArgT, value)) + ) diff --git a/src/dataclassish/_core.py b/src/dataclassish/_src/core.py similarity index 100% rename from src/dataclassish/_core.py rename to src/dataclassish/_src/core.py diff --git a/src/dataclassish/_ext.py b/src/dataclassish/_src/ext.py similarity index 99% rename from src/dataclassish/_ext.py rename to src/dataclassish/_src/ext.py index f9d32a8..29222a0 100644 --- a/src/dataclassish/_ext.py +++ b/src/dataclassish/_src/ext.py @@ -7,7 +7,7 @@ from plum import dispatch -from ._core import fields +from .core import fields K = TypeVar("K") V = TypeVar("V") diff --git a/src/dataclassish/converters.py b/src/dataclassish/converters.py index b976e43..7733d26 100644 --- a/src/dataclassish/converters.py +++ b/src/dataclassish/converters.py @@ -6,136 +6,7 @@ functions. If you need more, check out ``attrs``! """ -# ruff:noqa: N801 -# pylint: disable=C0103 __all__ = ["AbstractConverter", "Optional", "Unless"] -import dataclasses -from abc import ABCMeta, abstractmethod -from collections.abc import Callable -from typing import Any, Generic, TypeVar, cast, overload - -ArgT = TypeVar("ArgT") # Input type -RetT = TypeVar("RetT") # Return type - - -class AbstractConverter(Generic[ArgT, RetT], metaclass=ABCMeta): - """Abstract converter class.""" - - converter: Callable[[ArgT], RetT] - """The converter to apply to the input value.""" - - @abstractmethod - def __call__(self, value: ArgT, /) -> Any: - """Convert the input value to the desired output type.""" - raise NotImplementedError # pragma: no cover - - -# ------------------------------------------------------------------- - - -@dataclasses.dataclass(frozen=True, slots=True, eq=False) -class Optional(AbstractConverter[ArgT, RetT]): - """Optional converter with a defined sentinel value. - - This converter allows for a field to be optional, i.e., it can be set to - `None`. This is useful when a field is required in some contexts but not in - others. - - This converter is based on ``attr.converters.optional``. If ``attrs`` ever - separates this out into its own package, so that other libraries, like - ``equinox``, can use the converter without depending on ``attrs``, then this - implementation will probably be removed. - - Examples - -------- - For this example we will use ``attrs`` as the dataclass library, but this - converter can be used with any dataclass-like library that supports - converters. - - >>> from attrs import define, field - >>> from dataclassish.converters import Optional - - >>> @define - ... class Class: - ... attr: int | None = field(default=None, converter=Optional(int)) - - >>> obj = Class() - >>> print(obj.attr) - None - - >>> obj = Class(1) - >>> obj.attr - 1 - - """ - - converter: Callable[[ArgT], RetT] - """The converter to apply to the input value.""" - - @overload - def __call__(self, value: None, /) -> None: ... - - @overload - def __call__(self, value: ArgT, /) -> RetT: ... - - def __call__(self, value: ArgT | None, /) -> RetT | None: - """Convert the input value to the output type, passing through `None`.""" - return None if value is None else self.converter(value) - - -# ------------------------------------------------------------------- - -PassThroughTs = TypeVar("PassThroughTs") - - -@dataclasses.dataclass(frozen=True, slots=True, eq=False) -class Unless(AbstractConverter[ArgT, RetT], Generic[ArgT, PassThroughTs, RetT]): - """Converter that is applied if the argument is NOT a specified type. - - This converter is useful when you want to pass through a value if it is of a - certain type, but convert it otherwise. - - Examples - -------- - For this example we will use ``attrs`` as the dataclass library, but this - converter can be used with any dataclass-like library that supports - converters. - - >>> from attrs import define, field - >>> from dataclassish.converters import Unless - - >>> @define - ... class Class: - ... attr: float | int = field(converter=Unless(int, converter=float)) - - >>> obj = Class(1) - >>> obj.attr - 1 - - >>> obj = Class("1") - >>> obj.attr - 1.0 - - """ - - unconverted_types: type[PassThroughTs] | tuple[type[PassThroughTs], ...] - """The types to pass through without conversion.""" - - converter: Callable[[ArgT], RetT] - """The converter to apply to the input value.""" - - @overload - def __call__(self, value: ArgT, /) -> RetT: ... - - @overload - def __call__(self, value: PassThroughTs, /) -> PassThroughTs: ... - - def __call__(self, value: ArgT | PassThroughTs, /) -> RetT | PassThroughTs: - """Pass through the input value.""" - return ( - cast(PassThroughTs, value) - if isinstance(value, self.unconverted_types) - else self.converter(cast(ArgT, value)) - ) +from ._src.converters import AbstractConverter, Optional, Unless diff --git a/uv.lock b/uv.lock index 75f8b01..d7fe979 100644 --- a/uv.lock +++ b/uv.lock @@ -115,7 +115,7 @@ toml = [ [[package]] name = "dataclassish" -version = "0.3.2.dev10+g435a5aa.d20241118" +version = "0.3.2.dev5+gacc4eea.d20241118" source = { editable = "." } dependencies = [ { name = "plum-dispatch" }, @@ -143,7 +143,7 @@ test = [ [package.metadata] requires-dist = [ { name = "plum-dispatch", specifier = ">=2.5.1" }, - { name = "typing-extensions" }, + { name = "typing-extensions", specifier = ">=4.12.2" }, ] [package.metadata.requires-dev]