From 631c4167234f0ba99b0688aeb81d5249a6b40029 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Mon, 24 Nov 2025 14:54:46 +0000 Subject: [PATCH 01/13] types: add py.typed file --- py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 py.typed diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 -- 2.52.0 From af4974075c2c139b0adb59120970b3a59d029844 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Mon, 24 Nov 2025 14:55:29 +0000 Subject: [PATCH 02/13] fix: sync license in pyproject.toml with LICENSE file --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d9fa955..9d48e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "types-networkx>=3.5.0.20251106", "typing-extensions>=4.15.0", ] -license = "CC0-1.0" +license = "Unlicense" license-files = ["LICENSE"] [project.urls] -- 2.52.0 From 34b5a1272bc80bcb9ca3415dfa0e29aef76e00bf Mon Sep 17 00:00:00 2001 From: rus07tam Date: Mon, 24 Nov 2025 15:01:44 +0000 Subject: [PATCH 03/13] refactor: small changes --- src/snakia/core/rx/base_bindable.py | 3 +-- src/snakia/core/rx/combines.py | 4 ++-- src/snakia/types/marker.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/snakia/core/rx/base_bindable.py b/src/snakia/core/rx/base_bindable.py index b229821..bd8c73b 100644 --- a/src/snakia/core/rx/base_bindable.py +++ b/src/snakia/core/rx/base_bindable.py @@ -29,8 +29,7 @@ class BaseBindable(Generic[T]): def value(self) -> T: if self.has_value: return self.__value - else: - return self.default_value + return self.default_value @property def has_value(self) -> bool: diff --git a/src/snakia/core/rx/combines.py b/src/snakia/core/rx/combines.py index 345d0a7..23c6675 100644 --- a/src/snakia/core/rx/combines.py +++ b/src/snakia/core/rx/combines.py @@ -95,7 +95,7 @@ def combine( ) def subscriber(_: ValueChanged[Any]) -> None: - combined.set(combiner(*[*map(lambda s: s.value, sources)])) + combined.set(combiner(*map(lambda s: s.value, sources))) for source in sources: if isinstance(source, Bindable): @@ -185,7 +185,7 @@ def async_combine( ) async def subscriber(_: ValueChanged[Any]) -> None: - result = await combiner(*[*map(lambda s: s.value, sources)]) + result = await combiner(*map(lambda s: s.value, sources)) await combined.set(result) for source in sources: diff --git a/src/snakia/types/marker.py b/src/snakia/types/marker.py index 002e2ec..3024fb9 100644 --- a/src/snakia/types/marker.py +++ b/src/snakia/types/marker.py @@ -12,7 +12,7 @@ MARKERS_ATTR = "__snakia_markers__" def _get_all_markers(obj: Any) -> dict[type["Marker"], "Marker"]: - return get_or_set_attr(obj, MARKERS_ATTR, dict[type[Marker], Marker]()) + return get_or_set_attr(obj, MARKERS_ATTR, dict()) class Marker: -- 2.52.0 From e61dd387d4246668448c28c89681628093633766 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Mon, 24 Nov 2025 15:05:07 +0000 Subject: [PATCH 04/13] feat!: remove broken plugin system --- examples/health_plugin.py | 88 ---------------------- src/snakia/core/engine.py | 7 +- src/snakia/core/loader/__init__.py | 6 -- src/snakia/core/loader/loadable.py | 18 ----- src/snakia/core/loader/loader.py | 26 ------- src/snakia/core/loader/meta.py | 35 --------- src/snakia/core/loader/plugin.py | 72 ------------------ src/snakia/core/loader/plugin_processor.py | 14 ---- 8 files changed, 3 insertions(+), 263 deletions(-) delete mode 100644 examples/health_plugin.py delete mode 100644 src/snakia/core/loader/__init__.py delete mode 100644 src/snakia/core/loader/loadable.py delete mode 100644 src/snakia/core/loader/loader.py delete mode 100644 src/snakia/core/loader/meta.py delete mode 100644 src/snakia/core/loader/plugin.py delete mode 100644 src/snakia/core/loader/plugin_processor.py diff --git a/examples/health_plugin.py b/examples/health_plugin.py deleted file mode 100644 index a795d33..0000000 --- a/examples/health_plugin.py +++ /dev/null @@ -1,88 +0,0 @@ -from typing import final - -from pydantic import Field - -from snakia.core.ecs import Component -from snakia.core.ecs.system import System -from snakia.core.engine import Engine -from snakia.core.es import Event -from snakia.core.loader import Meta, Plugin, PluginProcessor -from snakia.types import Version - - -class HealthComponent(Component): - max_value: int = Field(default=100, ge=0) - value: int = Field(default=100, ge=0) - - -class DamageComponent(Component): - damage: int = Field(ge=0) - ticks: int = Field(default=1, ge=0) - - -class HealComponent(Component): - heal: int = Field(ge=0) - ticks: int = Field(default=1, ge=0) - - -class DeathEvent(Event): - entity: int = Field() - - -class HealthProcessor(PluginProcessor): - def process(self, system: System) -> None: - for entity, (heal, health) in system.get_components( - HealComponent, HealthComponent - ): - health.value += heal.heal - heal.ticks -= 1 - if heal.ticks <= 0: - system.remove_component(entity, HealComponent) - for entity, (damage, health) in system.get_components( - DamageComponent, HealthComponent - ): - health.value -= damage.damage - damage.ticks -= 1 - if damage.ticks <= 0: - system.remove_component(entity, DamageComponent) - if health.value <= 0: - system.remove_component(entity, HealthComponent) - self.plugin.dispatcher.publish(DeathEvent(entity=entity)) - - -@final -class HealthPlugin( - Plugin, - meta=Meta( - name="health", - author="snakia", - version=Version.from_args(1, 0, 0), - subscribers=(), - processors=(HealthProcessor,), - ), -): - def on_load(self) -> None: - pass - - def on_unload(self) -> None: - pass - - -def main() -> None: - engine = Engine() - engine.loader.register(HealthPlugin) - engine.loader.load_all() - - @engine.dispatcher.on(DeathEvent) - def on_death(event: DeathEvent) -> None: - print(f"Entity: {event.entity} is death!") - - player = engine.system.create_entity() - engine.system.add_component(player, HealthComponent()) - engine.system.add_component(player, DamageComponent(damage=10, ticks=10)) - - engine.start() - - -if __name__ == "__main__": - main() diff --git a/src/snakia/core/engine.py b/src/snakia/core/engine.py index 047c26e..a4f47b2 100644 --- a/src/snakia/core/engine.py +++ b/src/snakia/core/engine.py @@ -3,14 +3,12 @@ from typing import Final from .ecs import System from .es import Dispatcher -from .loader.loader import Loader class Engine: __slots__ = ( "system", "dispatcher", - "loader", "__system_thread", "__dispatcher_thread", ) @@ -18,12 +16,13 @@ class Engine: def __init__(self) -> None: self.system: Final = System() self.dispatcher: Final = Dispatcher() - self.loader: Final = Loader(self) self.__system_thread: threading.Thread | None = None self.__dispatcher_thread: threading.Thread | None = None def start(self) -> None: - self.__system_thread = threading.Thread(target=self.system.start, daemon=False) + self.__system_thread = threading.Thread( + target=self.system.start, daemon=False + ) self.__dispatcher_thread = threading.Thread( target=self.dispatcher.start, daemon=False ) diff --git a/src/snakia/core/loader/__init__.py b/src/snakia/core/loader/__init__.py deleted file mode 100644 index 9e099c5..0000000 --- a/src/snakia/core/loader/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .loadable import Loadable -from .meta import Meta -from .plugin import Plugin -from .plugin_processor import PluginProcessor - -__all__ = ["Loadable", "Meta", "Plugin", "PluginProcessor"] diff --git a/src/snakia/core/loader/loadable.py b/src/snakia/core/loader/loadable.py deleted file mode 100644 index 51e75d0..0000000 --- a/src/snakia/core/loader/loadable.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from snakia.core.engine import Engine - - -class Loadable(ABC): - @abstractmethod - def __init__(self, engine: Engine) -> None: ... - - @abstractmethod - def load(self) -> None: ... - - @abstractmethod - def unload(self) -> None: ... diff --git a/src/snakia/core/loader/loader.py b/src/snakia/core/loader/loader.py deleted file mode 100644 index 0dcf807..0000000 --- a/src/snakia/core/loader/loader.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Callable, Final - -if TYPE_CHECKING: - from snakia.core.engine import Engine - from snakia.core.loader import Loadable - - -class Loader: - __slots__ = ("__engine", "__loadables") - - def __init__(self, engine: Engine) -> None: - self.__engine: Final = engine - self.__loadables: Final[list[Loadable]] = [] - - def register(self, loadable: Callable[[Engine], Loadable]) -> None: - self.__loadables.append(loadable(self.__engine)) - - def load_all(self) -> None: - for loadable in self.__loadables: - loadable.load() - - def unload_all(self) -> None: - for loadable in reversed(self.__loadables): - loadable.unload() diff --git a/src/snakia/core/loader/meta.py b/src/snakia/core/loader/meta.py deleted file mode 100644 index 429d4b4..0000000 --- a/src/snakia/core/loader/meta.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -from pydantic import BaseModel, ConfigDict, Field - -from snakia.core.es import Event, Subscriber -from snakia.types import Version - -from .plugin_processor import PluginProcessor - - -class Meta(BaseModel): - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - - name: str = Field( - default="unknown", - min_length=4, - max_length=32, - pattern="^[a-z0-9_]{4,32}$", - ) - author: str = Field( - default="unknown", - min_length=4, - max_length=32, - pattern="^[a-z0-9_]{4,32}$", - ) - version: Version = Field(default_factory=lambda: Version(major=1, minor=0, patch=0)) - - subscribers: tuple[tuple[type[Event], Subscriber[Event]], ...] = Field( - default_factory=tuple - ) - processors: tuple[type[PluginProcessor], ...] = Field(default_factory=tuple) - - @property - def id(self) -> str: - return f"{self.author}.{self.name}" diff --git a/src/snakia/core/loader/plugin.py b/src/snakia/core/loader/plugin.py deleted file mode 100644 index f8abfa5..0000000 --- a/src/snakia/core/loader/plugin.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from typing import TYPE_CHECKING, ClassVar, Final, final - -from snakia.core.ecs import System -from snakia.core.es import Dispatcher - -from .loadable import Loadable -from .meta import Meta - -if TYPE_CHECKING: - from snakia.core.engine import Engine - - -class Plugin(Loadable): - __meta: ClassVar[Meta] - - @final - def __init__(self, engine: Engine) -> None: - self.__engine: Final = engine - - @final - @property - def meta(self) -> Meta: - """The plugin's metadata.""" - return self.__meta - - @final - @property - def dispatcher(self) -> Dispatcher: - return self.__engine.dispatcher - - @final - @property - def system(self) -> System: - return self.__engine.system - - @final - def load(self) -> None: - for processor in self.meta.processors: - self.__engine.system.add_processor(processor(self)) - for event_type, subscriber in self.meta.subscribers: - self.__engine.dispatcher.subscribe(event_type, subscriber) - self.on_load() - - @final - def unload(self) -> None: - for processor in self.meta.processors: - self.__engine.system.remove_processor(processor) - for event_type, subscriber in self.meta.subscribers: - self.__engine.dispatcher.unsubscribe(event_type, subscriber) - self.on_unload() - - @abstractmethod - def on_load(self) -> None: - pass - - @abstractmethod - def on_unload(self) -> None: - pass - - if TYPE_CHECKING: - - @final - def __init_subclass__(cls, meta: Meta) -> None: - pass - - else: - - def __init_subclass__(cls, meta: Meta) -> None: - cls.meta = meta diff --git a/src/snakia/core/loader/plugin_processor.py b/src/snakia/core/loader/plugin_processor.py deleted file mode 100644 index f959327..0000000 --- a/src/snakia/core/loader/plugin_processor.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Final, final - -from snakia.core.ecs import Processor - -if TYPE_CHECKING: - from .plugin import Plugin - - -class PluginProcessor(Processor): - @final - def __init__(self, plugin: Plugin) -> None: - self.plugin: Final = plugin -- 2.52.0 From 12c0f42f996e141f8b15e9b3f9e0cda16302e258 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Mon, 24 Nov 2025 15:24:45 +0000 Subject: [PATCH 05/13] fix: renaming methods to more logical ones --- src/snakia/types/unique.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/snakia/types/unique.py b/src/snakia/types/unique.py index 3c149fd..24ad86e 100644 --- a/src/snakia/types/unique.py +++ b/src/snakia/types/unique.py @@ -52,21 +52,21 @@ class UniqueType(type): def map( cls: type[T], value: V | type[T] | T, - and_then: Callable[[V], R], - or_else: Callable[[type[T]], R], + or_else: Callable[[V], R], + and_then: Callable[[type[T]], R], ) -> R: if value is cls or isinstance(value, cls): - return or_else(cls) - return and_then(value) # type: ignore + return and_then(cls) + return or_else(value) # type: ignore - def and_then( + def or_else( cls: type[T], value: V | type[T] | T, func: Callable[[V], R] ) -> type[T] | R: if value is cls or isinstance(value, cls): return cls return func(value) # type: ignore - def or_else( + def and_then( cls: type[T], value: V | type[T] | T, func: Callable[[type[T]], R] ) -> R | V: if value is cls or isinstance(value, cls): -- 2.52.0 From 1e82a457ace343c15b481d08bf3f1d30d34f80de Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 13:26:21 +0000 Subject: [PATCH 06/13] feat: add default_factory in PrivProperty --- src/snakia/field/field.py | 21 ++++++++++++------ src/snakia/property/priv_property.py | 33 +++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/snakia/field/field.py b/src/snakia/field/field.py index 628b7c7..57e9de6 100644 --- a/src/snakia/field/field.py +++ b/src/snakia/field/field.py @@ -1,7 +1,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Final, Generic, TypeVar, final +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + TypeVar, + final, +) from snakia.property.priv_property import PrivProperty from snakia.utils import inherit @@ -11,10 +18,6 @@ R = TypeVar("R") class Field(ABC, PrivProperty[T], Generic[T]): - def __init__(self, default_value: T) -> None: - self.default_value: Final[T] = default_value - super().__init__(default_value) - @abstractmethod def serialize(self, value: T, /) -> bytes: """Serialize a value @@ -42,14 +45,18 @@ class Field(ABC, PrivProperty[T], Generic[T]): serialize: Callable[[Field[R], R], bytes], deserialize: Callable[[Field[R], bytes], R], ) -> type[Field[R]]: - return inherit(cls, {"serialize": serialize, "deserialize": deserialize}) + return inherit( + cls, {"serialize": serialize, "deserialize": deserialize} + ) @final @staticmethod def get_fields(class_: type[Any] | Any, /) -> dict[str, Field[Any]]: if not isinstance(class_, type): class_ = class_.__class__ - return {k: v for k, v in class_.__dict__.items() if isinstance(v, Field)} + return { + k: v for k, v in class_.__dict__.items() if isinstance(v, Field) + } if TYPE_CHECKING: diff --git a/src/snakia/property/priv_property.py b/src/snakia/property/priv_property.py index 1a85e09..705a2e6 100644 --- a/src/snakia/property/priv_property.py +++ b/src/snakia/property/priv_property.py @@ -1,22 +1,43 @@ -from typing import Any, Generic, TypeVar +from typing import Any, Callable, Final, Generic, TypeVar, overload +from typing_extensions import Self T = TypeVar("T") class PrivProperty(Generic[T]): - __slots__ = "__name", "__default_value" + __slots__ = "__name", "__default_value", "__default_factory" __name: str - def __init__(self, default_value: T | None = None) -> None: - self.__default_value: T | None = default_value + @overload + def __init__(self) -> None: ... + @overload + def __init__(self, default_value: T) -> None: ... + @overload + def __init__(self, *, default_factory: Callable[[Self], T]) -> None: ... + def __init__( + self, + default_value: T | None = None, + default_factory: Callable[[Self], T] | None = None, + ) -> None: + self.__default_value: Final[T | None] = default_value + self.__default_factory: Final[Callable[[Self], T] | None] = ( + default_factory + ) + + def _get_default(self: Self) -> T: + if self.__default_value is not None: + return self.__default_value + if self.__default_factory is not None: + return self.__default_factory(self) + raise ValueError("Either default_value or default_factory must be set") def __set_name__(self, owner: type, name: str) -> None: self.__name = f"_{owner.__name__}__{name}" def __get__(self, instance: Any, owner: type | None = None, /) -> T: - if self.__default_value: - return getattr(instance, self.__name, self.__default_value) + if not hasattr(instance, self.__name): + setattr(instance, self.__name, self._get_default()) return getattr(instance, self.__name) # type: ignore def __set__(self, instance: Any, value: T, /) -> None: -- 2.52.0 From 420f7626fbd6e81fe21db45c18f0b8b0b485d7f4 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 13:33:16 +0000 Subject: [PATCH 07/13] feat: add support default_factory in AutoField --- src/snakia/field/auto.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/snakia/field/auto.py b/src/snakia/field/auto.py index 1216a30..12e7d25 100644 --- a/src/snakia/field/auto.py +++ b/src/snakia/field/auto.py @@ -1,5 +1,7 @@ import pickle -from typing import Final, Generic, TypeVar +from typing import Callable, Final, Generic, TypeVar, overload + +from typing_extensions import Self from .field import Field @@ -9,8 +11,28 @@ T = TypeVar("T") class AutoField(Field[T], Generic[T]): __slots__ = ("__target_type",) - def __init__(self, default_value: T, *, target_type: type[T] | None = None) -> None: - super().__init__(default_value) + @overload + def __init__( + self, default_value: T, *, target_type: type[T] | None = None + ) -> None: ... + @overload + def __init__( + self, + *, + default_factory: Callable[[Self], T], + target_type: type[T] | None = None, + ) -> None: ... + def __init__( + self, + default_value: T | None = None, + *, + default_factory: Callable[[Self], T] | None = None, + target_type: type[T] | None = None, + ) -> None: + if default_factory is not None and default_value is None: + super().__init__(default_factory=default_factory) + elif default_value is not None and default_factory is None: + super().__init__(default_value) self.__target_type: Final = target_type def serialize(self, value: T, /) -> bytes: @@ -19,5 +41,5 @@ class AutoField(Field[T], Generic[T]): def deserialize(self, serialized: bytes, /) -> T: value = pickle.loads(serialized) if not isinstance(value, self.__target_type or object): - return self.default_value + return self._get_default() return value # type: ignore -- 2.52.0 From 3d132251fce8ff938be568cc8e01170a05000d4e Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 13:46:49 +0000 Subject: [PATCH 08/13] refactor: merge side.py and calls.py to funcs.py --- src/snakia/utils/__init__.py | 3 +-- src/snakia/utils/{calls.py => funcs.py} | 10 +++++++++- src/snakia/utils/side.py | 11 ----------- 3 files changed, 10 insertions(+), 14 deletions(-) rename src/snakia/utils/{calls.py => funcs.py} (55%) delete mode 100644 src/snakia/utils/side.py diff --git a/src/snakia/utils/__init__.py b/src/snakia/utils/__init__.py index 353803d..70ae023 100644 --- a/src/snakia/utils/__init__.py +++ b/src/snakia/utils/__init__.py @@ -1,10 +1,9 @@ from .attrs import get_attrs, get_or_set_attr -from .calls import call, caller +from .funcs import call, caller, side, side_func from .exceptions import catch, throw from .frames import frame from .gil import GIL_ENABLED, nolock from .inherit import inherit -from .side import side, side_func from .this import this from .to_async import to_async diff --git a/src/snakia/utils/calls.py b/src/snakia/utils/funcs.py similarity index 55% rename from src/snakia/utils/calls.py rename to src/snakia/utils/funcs.py index eeabd5f..528cf1e 100644 --- a/src/snakia/utils/calls.py +++ b/src/snakia/utils/funcs.py @@ -1,4 +1,4 @@ -from typing import Callable, ParamSpec, TypeVar +from typing import Any, Callable, ParamSpec, TypeVar P = ParamSpec("P") T = TypeVar("T") @@ -10,3 +10,11 @@ def call(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: def caller(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[..., T]: return lambda *_, **__: f(*args, **kwargs) + + +def side(value: T, *_: Any, **__: Any) -> T: + return value + + +def side_func(value: T, *_: Any, **__: Any) -> Callable[..., T]: + return lambda *_, **__: value diff --git a/src/snakia/utils/side.py b/src/snakia/utils/side.py deleted file mode 100644 index 985e919..0000000 --- a/src/snakia/utils/side.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Any, Callable, TypeVar - -T = TypeVar("T") - - -def side(value: T, *_: Any, **__: Any) -> T: - return value - - -def side_func(value: T, *_: Any, **__: Any) -> Callable[..., T]: - return lambda *_, **__: value -- 2.52.0 From 2cc15bf2508c95f49a4c92b125acc7ac093419d9 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 13:48:31 +0000 Subject: [PATCH 09/13] feat: add ret() --- src/snakia/utils/__init__.py | 3 ++- src/snakia/utils/funcs.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/snakia/utils/__init__.py b/src/snakia/utils/__init__.py index 70ae023..ad6100d 100644 --- a/src/snakia/utils/__init__.py +++ b/src/snakia/utils/__init__.py @@ -1,7 +1,7 @@ from .attrs import get_attrs, get_or_set_attr -from .funcs import call, caller, side, side_func from .exceptions import catch, throw from .frames import frame +from .funcs import call, caller, ret, side, side_func from .gil import GIL_ENABLED, nolock from .inherit import inherit from .this import this @@ -16,6 +16,7 @@ __all__ = [ "frame", "inherit", "nolock", + "ret", "side", "side_func", "this", diff --git a/src/snakia/utils/funcs.py b/src/snakia/utils/funcs.py index 528cf1e..94cdad4 100644 --- a/src/snakia/utils/funcs.py +++ b/src/snakia/utils/funcs.py @@ -18,3 +18,7 @@ def side(value: T, *_: Any, **__: Any) -> T: def side_func(value: T, *_: Any, **__: Any) -> Callable[..., T]: return lambda *_, **__: value + + +def ret() -> Callable[[T], T]: + return lambda x: x -- 2.52.0 From fee08f360985554de8375018643ad62993984560 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 13:58:06 +0000 Subject: [PATCH 10/13] feat: add UniqueType.unwrap_or() --- src/snakia/types/unique.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/snakia/types/unique.py b/src/snakia/types/unique.py index 24ad86e..2f66e0f 100644 --- a/src/snakia/types/unique.py +++ b/src/snakia/types/unique.py @@ -49,6 +49,11 @@ class UniqueType(type): raise TypeError(f"{cls} not unwrapped") return value # type: ignore + def unwrap_or(cls: type[T], value: V | type[T] | T, default: R, /) -> V | R: + if value is cls or isinstance(value, cls): + return default + return value # type: ignore + def map( cls: type[T], value: V | type[T] | T, -- 2.52.0 From 2149fcf3082540f501a6a0ab5ee58bab1b8694be Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 14:00:22 +0000 Subject: [PATCH 11/13] refactor: add Unset annotation --- src/snakia/field/auto.py | 29 +++++++++++++++++----------- src/snakia/property/priv_property.py | 23 +++++++++++----------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/snakia/field/auto.py b/src/snakia/field/auto.py index 12e7d25..f456a1d 100644 --- a/src/snakia/field/auto.py +++ b/src/snakia/field/auto.py @@ -3,6 +3,8 @@ from typing import Callable, Final, Generic, TypeVar, overload from typing_extensions import Self +from snakia.types import Unset + from .field import Field T = TypeVar("T") @@ -13,33 +15,38 @@ class AutoField(Field[T], Generic[T]): @overload def __init__( - self, default_value: T, *, target_type: type[T] | None = None + self, default_value: T, *, target_type: type[T] | Unset = Unset() ) -> None: ... + @overload def __init__( self, *, default_factory: Callable[[Self], T], - target_type: type[T] | None = None, + target_type: type[T] | Unset = Unset(), ) -> None: ... + def __init__( self, - default_value: T | None = None, + default_value: T | Unset = Unset(), *, - default_factory: Callable[[Self], T] | None = None, - target_type: type[T] | None = None, + default_factory: Callable[[Self], T] | Unset = Unset(), + target_type: type[T] | Unset = Unset(), ) -> None: - if default_factory is not None and default_value is None: - super().__init__(default_factory=default_factory) - elif default_value is not None and default_factory is None: - super().__init__(default_value) - self.__target_type: Final = target_type + if not Unset.itis(default_factory): + super().__init__(default_factory=Unset.unwrap(default_factory)) + elif not Unset.itis(default_value): + super().__init__(Unset.unwrap(default_value)) + else: + super().__init__() + self.__target_type: Final[type] = Unset.unwrap_or(target_type, object) def serialize(self, value: T, /) -> bytes: return pickle.dumps(value) def deserialize(self, serialized: bytes, /) -> T: value = pickle.loads(serialized) - if not isinstance(value, self.__target_type or object): + + if not isinstance(value, self.__target_type): return self._get_default() return value # type: ignore diff --git a/src/snakia/property/priv_property.py b/src/snakia/property/priv_property.py index 705a2e6..fba63da 100644 --- a/src/snakia/property/priv_property.py +++ b/src/snakia/property/priv_property.py @@ -1,6 +1,9 @@ from typing import Any, Callable, Final, Generic, TypeVar, overload + from typing_extensions import Self +from snakia.types import Unset + T = TypeVar("T") @@ -17,20 +20,18 @@ class PrivProperty(Generic[T]): def __init__(self, *, default_factory: Callable[[Self], T]) -> None: ... def __init__( self, - default_value: T | None = None, - default_factory: Callable[[Self], T] | None = None, + default_value: T | Unset = Unset(), + default_factory: Callable[[Self], T] | Unset = Unset(), ) -> None: - self.__default_value: Final[T | None] = default_value - self.__default_factory: Final[Callable[[Self], T] | None] = ( - default_factory - ) + self.__default_value: Final[T | Unset] = default_value + self.__default_factory: Final[Callable[[Self], T] | Unset] = default_factory def _get_default(self: Self) -> T: - if self.__default_value is not None: - return self.__default_value - if self.__default_factory is not None: - return self.__default_factory(self) - raise ValueError("Either default_value or default_factory must be set") + return Unset.map( + self.__default_factory, + lambda f: f(self), + lambda _: Unset.unwrap(self.__default_value), + ) def __set_name__(self, owner: type, name: str) -> None: self.__name = f"_{owner.__name__}__{name}" -- 2.52.0 From 7e8b57793f18cda69936825cf69703763cabd24b Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 14:02:48 +0000 Subject: [PATCH 12/13] feat: add ListField --- src/snakia/field/__init__.py | 2 ++ src/snakia/field/list.py | 38 ++++++++++++++++++++++++++++++++++++ src/snakia/field/t.py | 2 ++ 3 files changed, 42 insertions(+) create mode 100644 src/snakia/field/list.py diff --git a/src/snakia/field/__init__.py b/src/snakia/field/__init__.py index 654fdd9..752b06e 100644 --- a/src/snakia/field/__init__.py +++ b/src/snakia/field/__init__.py @@ -3,6 +3,7 @@ from .bool import BoolField from .field import Field from .float import FloatField from .int import IntField +from .list import ListField from .str import StrField __all__ = [ @@ -11,5 +12,6 @@ __all__ = [ "BoolField", "FloatField", "IntField", + "ListField", "StrField", ] diff --git a/src/snakia/field/list.py b/src/snakia/field/list.py new file mode 100644 index 0000000..f2671da --- /dev/null +++ b/src/snakia/field/list.py @@ -0,0 +1,38 @@ +from typing import Callable, Final, Iterable, TypeVar + +from typing_extensions import Self + +from .field import Field + +T = TypeVar("T") + + +class ListField(Field[list[T]]): + def __init__( + self, + field: Field[T], + *, + length_size: int = 1, + default_factory: Callable[[Self], Iterable[T]] = lambda _: (), + ) -> None: + self.length_size: Final[int] = length_size + self.field: Final = field + super().__init__(default_factory=lambda s: [*default_factory(s)]) + + def serialize(self, items: list[T], /) -> bytes: + result = b"" + for item in items: + value = self.field.serialize(item) + length_prefix = len(value).to_bytes(self.length_size, "big") + result += length_prefix + value + return result + + def deserialize(self, serialized: bytes, /) -> list[T]: + result = [] + while serialized: + length = int.from_bytes(serialized[: self.length_size], "big") + serialized = serialized[self.length_size :] + item = self.field.deserialize(serialized[:length]) + serialized = serialized[length:] + result.append(item) + return result diff --git a/src/snakia/field/t.py b/src/snakia/field/t.py index f535738..64cf5a7 100644 --- a/src/snakia/field/t.py +++ b/src/snakia/field/t.py @@ -4,6 +4,7 @@ from .bool import BoolField as bool from .field import Field as field from .float import FloatField as float from .int import IntField as int +from .list import ListField as list from .str import StrField as str __all__ = [ @@ -12,5 +13,6 @@ __all__ = [ "field", "float", "int", + "list", "str", ] -- 2.52.0 From 36d9da10bcaea9a54b48b318859e939f5408ae45 Mon Sep 17 00:00:00 2001 From: rus07tam Date: Wed, 26 Nov 2025 14:06:10 +0000 Subject: [PATCH 13/13] feat: add OptionalField --- src/snakia/field/__init__.py | 2 ++ src/snakia/field/optional.py | 28 ++++++++++++++++++++++++++++ src/snakia/field/t.py | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 src/snakia/field/optional.py diff --git a/src/snakia/field/__init__.py b/src/snakia/field/__init__.py index 752b06e..e95ce46 100644 --- a/src/snakia/field/__init__.py +++ b/src/snakia/field/__init__.py @@ -4,6 +4,7 @@ from .field import Field from .float import FloatField from .int import IntField from .list import ListField +from .optional import OptionalField from .str import StrField __all__ = [ @@ -13,5 +14,6 @@ __all__ = [ "FloatField", "IntField", "ListField", + "OptionalField", "StrField", ] diff --git a/src/snakia/field/optional.py b/src/snakia/field/optional.py new file mode 100644 index 0000000..eb437c0 --- /dev/null +++ b/src/snakia/field/optional.py @@ -0,0 +1,28 @@ +from typing import Final, TypeVar + +from .field import Field + +T = TypeVar("T") + + +class OptionalField(Field[T | None]): + + def __init__( + self, + field: Field[T], + *, + none_value: bytes = b"", + ) -> None: + super().__init__(None) + self.none_value: Final = none_value + self.field: Final = field + + def serialize(self, value: T | None, /) -> bytes: + if value is None: + return self.none_value + return self.field.serialize(value) + + def deserialize(self, serialized: bytes, /) -> T | None: + if serialized == self.none_value: + return None + return self.field.deserialize(serialized) diff --git a/src/snakia/field/t.py b/src/snakia/field/t.py index 64cf5a7..abb404b 100644 --- a/src/snakia/field/t.py +++ b/src/snakia/field/t.py @@ -5,6 +5,7 @@ from .field import Field as field from .float import FloatField as float from .int import IntField as int from .list import ListField as list +from .optional import OptionalField as optional from .str import StrField as str __all__ = [ @@ -14,5 +15,6 @@ __all__ = [ "float", "int", "list", + "optional", "str", ] -- 2.52.0