diff --git a/pyproject.toml b/pyproject.toml index 8def9fb..fe95cd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "haiway" description = "Framework for dependency injection and state management within structured concurrency model." -version = "0.9.1" +version = "0.10.0" readme = "README.md" maintainers = [ { name = "Kacper KaliƄski", email = "kacper.kalinski@miquido.com" }, diff --git a/src/haiway/__init__.py b/src/haiway/__init__.py index 14dce46..0bdd178 100644 --- a/src/haiway/__init__.py +++ b/src/haiway/__init__.py @@ -28,6 +28,8 @@ from haiway.state import AttributePath, AttributeRequirement, State from haiway.types import ( MISSING, + Default, + DefaultValue, Missing, frozenlist, is_missing, @@ -39,6 +41,7 @@ always, as_dict, as_list, + as_set, as_tuple, async_always, async_noop, @@ -59,6 +62,8 @@ "AsyncQueue", "AttributePath", "AttributeRequirement", + "Default", + "DefaultValue", "Disposable", "Disposables", "MetricsContext", @@ -78,6 +83,7 @@ "always", "as_dict", "as_list", + "as_set", "as_tuple", "async_always", "async_noop", diff --git a/src/haiway/state/structure.py b/src/haiway/state/structure.py index 0a0f4b6..271d106 100644 --- a/src/haiway/state/structure.py +++ b/src/haiway/state/structure.py @@ -1,5 +1,5 @@ import typing -from collections.abc import Mapping +from collections.abc import Callable, Mapping from types import EllipsisType, GenericAlias from typing import ( Any, @@ -10,34 +10,55 @@ cast, dataclass_transform, final, + overload, ) from weakref import WeakValueDictionary from haiway.state.attributes import AttributeAnnotation, attribute_annotations from haiway.state.path import AttributePath -from haiway.state.validation import ( - AttributeValidation, - AttributeValidator, -) -from haiway.types import MISSING, Missing, not_missing +from haiway.state.validation import AttributeValidation, AttributeValidator +from haiway.types import MISSING, DefaultValue, Missing, not_missing __all__ = [ "State", ] +@overload +def Default[Value]( + value: Value, + /, +) -> Value: ... + + +@overload +def Default[Value]( + *, + factory: Callable[[], Value], +) -> Value: ... + + +def Default[Value]( + value: Value | Missing = MISSING, + /, + *, + factory: Callable[[], Value] | Missing = MISSING, +) -> Value: # it is actually a DefaultValue, but type checker has to be fooled + return cast(Value, DefaultValue(value, factory=factory)) + + @final class StateAttribute[Value]: def __init__( self, name: str, annotation: AttributeAnnotation, - default: Value | Missing, + default: DefaultValue[Value], validator: AttributeValidation[Value], ) -> None: self.name: str = name self.annotation: AttributeAnnotation = annotation - self.default: Value | Missing = default + self.default: DefaultValue[Value] = default self.validator: AttributeValidation[Value] = validator def validated( @@ -45,13 +66,13 @@ def validated( value: Any | Missing, /, ) -> Value: - return self.validator(self.default if value is MISSING else value) + return self.validator(self.default() if value is MISSING else value) @dataclass_transform( kw_only_default=True, frozen_default=True, - field_specifiers=(), + field_specifiers=(DefaultValue,), ) class StateMeta(type): def __new__( @@ -81,7 +102,7 @@ def __new__( attributes[key] = StateAttribute( name=key, annotation=annotation.update_required(default is MISSING), - default=default, + default=_resolve_default(default), validator=AttributeValidator.of( annotation, recursion_guard={ @@ -187,6 +208,18 @@ def __subclasscheck__( # noqa: C901, PLR0911, PLR0912 return False # we have different base / comparing to not parametrized +def _resolve_default[Value]( + value: DefaultValue[Value] | Value | Missing, +) -> DefaultValue[Value]: + if isinstance(value, DefaultValue): + return cast(DefaultValue[Value], value) + + return DefaultValue[Value]( + value, + factory=MISSING, + ) + + _types_cache: WeakValueDictionary[ tuple[ Any, diff --git a/src/haiway/types/__init__.py b/src/haiway/types/__init__.py index 1fa58a8..ef73bab 100644 --- a/src/haiway/types/__init__.py +++ b/src/haiway/types/__init__.py @@ -1,8 +1,11 @@ +from haiway.types.default import Default, DefaultValue from haiway.types.frozen import frozenlist from haiway.types.missing import MISSING, Missing, is_missing, not_missing, when_missing __all__ = [ "MISSING", + "Default", + "DefaultValue", "Missing", "frozenlist", "is_missing", diff --git a/src/haiway/types/default.py b/src/haiway/types/default.py new file mode 100644 index 0000000..e9a5bde --- /dev/null +++ b/src/haiway/types/default.py @@ -0,0 +1,108 @@ +from collections.abc import Callable +from typing import Any, cast, final, overload + +from haiway.types.missing import MISSING, Missing, not_missing +from haiway.utils.always import always + +__all__ = [ + "Default", + "DefaultValue", +] + + +@final +class DefaultValue[Value]: + @overload + def __init__( + self, + value: Value, + /, + ) -> None: ... + + @overload + def __init__( + self, + /, + *, + factory: Callable[[], Value], + ) -> None: ... + + @overload + def __init__( + self, + value: Value | Missing, + /, + *, + factory: Callable[[], Value] | Missing, + ) -> None: ... + + def __init__( + self, + value: Value | Missing = MISSING, + /, + *, + factory: Callable[[], Value] | Missing = MISSING, + ) -> None: + assert ( # nosec: B101 + value is MISSING or factory is MISSING + ), "Can't specify both default value and factory" + + self._value: Callable[[], Value | Missing] + if not_missing(factory): + object.__setattr__( + self, + "_value", + factory, + ) + + else: + object.__setattr__( + self, + "_value", + always(value), + ) + + def __call__(self) -> Value | Missing: + return self._value() + + def __setattr__( + self, + __name: str, + __value: Any, + ) -> None: + raise AttributeError("Missing can't be modified") + + def __delattr__( + self, + __name: str, + ) -> None: + raise AttributeError("Missing can't be modified") + + +@overload +def Default[Value]( + value: Value, + /, +) -> Value: ... + + +@overload +def Default[Value]( + *, + factory: Callable[[], Value], +) -> Value: ... + + +def Default[Value]( + value: Value | Missing = MISSING, + /, + *, + factory: Callable[[], Value] | Missing = MISSING, +) -> Value: # it is actually a DefaultValue, but type checker has to be fooled most some cases + return cast( + Value, + DefaultValue( + value, + factory=factory, + ), + ) diff --git a/src/haiway/types/missing.py b/src/haiway/types/missing.py index ad53361..50f6a1f 100644 --- a/src/haiway/types/missing.py +++ b/src/haiway/types/missing.py @@ -85,6 +85,7 @@ def not_missing[Value]( def when_missing[Value]( check: Value | Missing, /, + *, value: Value, ) -> Value: if check is MISSING: diff --git a/src/haiway/utils/__init__.py b/src/haiway/utils/__init__.py index 13435a1..f0cf2d7 100644 --- a/src/haiway/utils/__init__.py +++ b/src/haiway/utils/__init__.py @@ -1,18 +1,18 @@ from haiway.utils.always import always, async_always +from haiway.utils.collections import as_dict, as_list, as_set, as_tuple from haiway.utils.env import getenv_bool, getenv_float, getenv_int, getenv_str, load_env -from haiway.utils.immutable import freeze +from haiway.utils.freezing import freeze from haiway.utils.logs import setup_logging -from haiway.utils.mappings import as_dict from haiway.utils.mimic import mimic_function from haiway.utils.noop import async_noop, noop from haiway.utils.queue import AsyncQueue -from haiway.utils.sequences import as_list, as_tuple __all__ = [ "AsyncQueue", "always", "as_dict", "as_list", + "as_set", "as_tuple", "async_always", "async_noop", diff --git a/src/haiway/utils/collections.py b/src/haiway/utils/collections.py new file mode 100644 index 0000000..368cb13 --- /dev/null +++ b/src/haiway/utils/collections.py @@ -0,0 +1,108 @@ +from collections.abc import Mapping, Sequence, Set + +__all__ = [ + "as_dict", + "as_list", + "as_set", + "as_tuple", +] + + +def as_list[T]( + collection: Sequence[T], + /, +) -> list[T]: + """ + Converts any given Sequence into a list. + + Parameters + ---------- + collection : Sequence[T] + The input collection to be converted. + + Returns + ------- + list[T] + A new list containing all elements of the input collection, + or the original list if it was already one. + """ + if isinstance(collection, list): + return collection + + else: + return list(collection) + + +def as_tuple[T]( + collection: Sequence[T], + /, +) -> tuple[T, ...]: + """ + Converts any given Sequence into a tuple. + + Parameters + ---------- + collection : Sequence[T] + The input collection to be converted. + + Returns + ------- + tuple[T] + A new tuple containing all elements of the input collection, + or the original tuple if it was already one. + """ + if isinstance(collection, tuple): + return collection + + else: + return tuple(collection) + + +def as_set[T]( + collection: Set[T], + /, +) -> set[T]: + """ + Converts any given Set into a set. + + Parameters + ---------- + collection : Set[T] + The input collection to be converted. + + Returns + ------- + set[T] + A new set containing all elements of the input collection, + or the original set if it was already one. + """ + if isinstance(collection, set): + return collection + + else: + return set(collection) + + +def as_dict[K, V]( + collection: Mapping[K, V], + /, +) -> dict[K, V]: + """ + Converts any given Mapping into a dict. + + Parameters + ---------- + collection : Mapping[K, V] + The input collection to be converted. + + Returns + ------- + dict[K, V] + A new dict containing all elements of the input collection, + or the original dict if it was already one. + """ + if isinstance(collection, dict): + return collection + + else: + return dict(collection) diff --git a/src/haiway/utils/immutable.py b/src/haiway/utils/freezing.py similarity index 100% rename from src/haiway/utils/immutable.py rename to src/haiway/utils/freezing.py diff --git a/src/haiway/utils/mappings.py b/src/haiway/utils/mappings.py deleted file mode 100644 index cfc48c3..0000000 --- a/src/haiway/utils/mappings.py +++ /dev/null @@ -1,30 +0,0 @@ -from collections.abc import Mapping - -__all__ = [ - "as_dict", -] - - -def as_dict[K, V]( - mapping: Mapping[K, V], - /, -) -> dict[K, V]: - """ - Converts any given mapping into a dict. - - Parameters - ---------- - mapping : Mapping[K, V] - The input mapping to be converted. - - Returns - ------- - dict[K, V] - A new dict containing all elements of the input mapping, - or the original dict if it was already one. - """ - if isinstance(mapping, dict): - return mapping - - else: - return dict(mapping) diff --git a/src/haiway/utils/sequences.py b/src/haiway/utils/sequences.py deleted file mode 100644 index acb1a7f..0000000 --- a/src/haiway/utils/sequences.py +++ /dev/null @@ -1,56 +0,0 @@ -from collections.abc import Sequence - -__all__ = [ - "as_list", - "as_tuple", -] - - -def as_list[T]( - sequence: Sequence[T], - /, -) -> list[T]: - """ - Converts any given sequence into a list. - - Parameters - ---------- - sequence : Sequence[T] - The input sequence to be converted. - - Returns - ------- - list[T] - A new list containing all elements of the input sequence, - or the original list if it was already one. - """ - if isinstance(sequence, list): - return sequence - - else: - return list(sequence) - - -def as_tuple[T]( - sequence: Sequence[T], - /, -) -> tuple[T, ...]: - """ - Converts any given sequence into a tuple. - - Parameters - ---------- - sequence : Sequence[T] - The input sequence to be converted. - - Returns - ------- - tuple[T] - A new tuple containing all elements of the input sequence, - or the original tuple if it was already one. - """ - if isinstance(sequence, tuple): - return sequence - - else: - return tuple(sequence) diff --git a/tests/test_state.py b/tests/test_state.py index 44c1ef8..d37fc45 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -7,7 +7,7 @@ from pytest import raises -from haiway import MISSING, Missing, State, frozenlist +from haiway import MISSING, Default, Missing, State, frozenlist def test_basic_initializes_with_arguments() -> None: @@ -92,11 +92,15 @@ class Basics(State): string: str = "" integer: int = 0 optional: str | None = None + unique: UUID = Default(factory=uuid4) + same: UUID = Default(uuid4()) basic = Basics() assert basic.string == "" assert basic.integer == 0 assert basic.optional is None + assert basic.unique is not Basics().unique + assert basic.same is Basics().same def test_basic_equals_checks_properties() -> None: