Merge pull request #12 from rus07tam/release/v0.6.0

Release/v0.6.0
This commit is contained in:
rus07tam 2025-11-26 17:17:42 +03:00 committed by GitHub
commit 006c0a494d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 196 additions and 319 deletions

View file

@ -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()

0
py.typed Normal file
View file

View file

@ -26,7 +26,7 @@ dependencies = [
"types-networkx>=3.5.0.20251106", "types-networkx>=3.5.0.20251106",
"typing-extensions>=4.15.0", "typing-extensions>=4.15.0",
] ]
license = "CC0-1.0" license = "Unlicense"
license-files = ["LICENSE"] license-files = ["LICENSE"]
[project.urls] [project.urls]

View file

@ -3,14 +3,12 @@ from typing import Final
from .ecs import System from .ecs import System
from .es import Dispatcher from .es import Dispatcher
from .loader.loader import Loader
class Engine: class Engine:
__slots__ = ( __slots__ = (
"system", "system",
"dispatcher", "dispatcher",
"loader",
"__system_thread", "__system_thread",
"__dispatcher_thread", "__dispatcher_thread",
) )
@ -18,12 +16,13 @@ class Engine:
def __init__(self) -> None: def __init__(self) -> None:
self.system: Final = System() self.system: Final = System()
self.dispatcher: Final = Dispatcher() self.dispatcher: Final = Dispatcher()
self.loader: Final = Loader(self)
self.__system_thread: threading.Thread | None = None self.__system_thread: threading.Thread | None = None
self.__dispatcher_thread: threading.Thread | None = None self.__dispatcher_thread: threading.Thread | None = None
def start(self) -> 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( self.__dispatcher_thread = threading.Thread(
target=self.dispatcher.start, daemon=False target=self.dispatcher.start, daemon=False
) )

View file

@ -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"]

View file

@ -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: ...

View file

@ -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()

View file

@ -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}"

View file

@ -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

View file

@ -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

View file

@ -29,8 +29,7 @@ class BaseBindable(Generic[T]):
def value(self) -> T: def value(self) -> T:
if self.has_value: if self.has_value:
return self.__value return self.__value
else: return self.default_value
return self.default_value
@property @property
def has_value(self) -> bool: def has_value(self) -> bool:

View file

@ -95,7 +95,7 @@ def combine(
) )
def subscriber(_: ValueChanged[Any]) -> None: 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: for source in sources:
if isinstance(source, Bindable): if isinstance(source, Bindable):
@ -185,7 +185,7 @@ def async_combine(
) )
async def subscriber(_: ValueChanged[Any]) -> None: 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) await combined.set(result)
for source in sources: for source in sources:

View file

@ -3,6 +3,8 @@ from .bool import BoolField
from .field import Field from .field import Field
from .float import FloatField from .float import FloatField
from .int import IntField from .int import IntField
from .list import ListField
from .optional import OptionalField
from .str import StrField from .str import StrField
__all__ = [ __all__ = [
@ -11,5 +13,7 @@ __all__ = [
"BoolField", "BoolField",
"FloatField", "FloatField",
"IntField", "IntField",
"ListField",
"OptionalField",
"StrField", "StrField",
] ]

View file

@ -1,5 +1,9 @@
import pickle import pickle
from typing import Final, Generic, TypeVar from typing import Callable, Final, Generic, TypeVar, overload
from typing_extensions import Self
from snakia.types import Unset
from .field import Field from .field import Field
@ -9,15 +13,40 @@ T = TypeVar("T")
class AutoField(Field[T], Generic[T]): class AutoField(Field[T], Generic[T]):
__slots__ = ("__target_type",) __slots__ = ("__target_type",)
def __init__(self, default_value: T, *, target_type: type[T] | None = None) -> None: @overload
super().__init__(default_value) def __init__(
self.__target_type: Final = target_type self, default_value: T, *, target_type: type[T] | Unset = Unset()
) -> None: ...
@overload
def __init__(
self,
*,
default_factory: Callable[[Self], T],
target_type: type[T] | Unset = Unset(),
) -> None: ...
def __init__(
self,
default_value: T | Unset = Unset(),
*,
default_factory: Callable[[Self], T] | Unset = Unset(),
target_type: type[T] | Unset = Unset(),
) -> None:
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: def serialize(self, value: T, /) -> bytes:
return pickle.dumps(value) return pickle.dumps(value)
def deserialize(self, serialized: bytes, /) -> T: def deserialize(self, serialized: bytes, /) -> T:
value = pickle.loads(serialized) value = pickle.loads(serialized)
if not isinstance(value, self.__target_type or object):
return self.default_value if not isinstance(value, self.__target_type):
return self._get_default()
return value # type: ignore return value # type: ignore

View file

@ -1,7 +1,14 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod 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.property.priv_property import PrivProperty
from snakia.utils import inherit from snakia.utils import inherit
@ -11,10 +18,6 @@ R = TypeVar("R")
class Field(ABC, PrivProperty[T], Generic[T]): 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 @abstractmethod
def serialize(self, value: T, /) -> bytes: def serialize(self, value: T, /) -> bytes:
"""Serialize a value """Serialize a value
@ -42,14 +45,18 @@ class Field(ABC, PrivProperty[T], Generic[T]):
serialize: Callable[[Field[R], R], bytes], serialize: Callable[[Field[R], R], bytes],
deserialize: Callable[[Field[R], bytes], R], deserialize: Callable[[Field[R], bytes], R],
) -> type[Field[R]]: ) -> type[Field[R]]:
return inherit(cls, {"serialize": serialize, "deserialize": deserialize}) return inherit(
cls, {"serialize": serialize, "deserialize": deserialize}
)
@final @final
@staticmethod @staticmethod
def get_fields(class_: type[Any] | Any, /) -> dict[str, Field[Any]]: def get_fields(class_: type[Any] | Any, /) -> dict[str, Field[Any]]:
if not isinstance(class_, type): if not isinstance(class_, type):
class_ = class_.__class__ 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: if TYPE_CHECKING:

38
src/snakia/field/list.py Normal file
View file

@ -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

View file

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

View file

@ -4,6 +4,8 @@ from .bool import BoolField as bool
from .field import Field as field from .field import Field as field
from .float import FloatField as float from .float import FloatField as float
from .int import IntField as int from .int import IntField as int
from .list import ListField as list
from .optional import OptionalField as optional
from .str import StrField as str from .str import StrField as str
__all__ = [ __all__ = [
@ -12,5 +14,7 @@ __all__ = [
"field", "field",
"float", "float",
"int", "int",
"list",
"optional",
"str", "str",
] ]

View file

@ -1,22 +1,44 @@
from typing import Any, Generic, TypeVar from typing import Any, Callable, Final, Generic, TypeVar, overload
from typing_extensions import Self
from snakia.types import Unset
T = TypeVar("T") T = TypeVar("T")
class PrivProperty(Generic[T]): class PrivProperty(Generic[T]):
__slots__ = "__name", "__default_value" __slots__ = "__name", "__default_value", "__default_factory"
__name: str __name: str
def __init__(self, default_value: T | None = None) -> None: @overload
self.__default_value: T | None = default_value 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 | Unset = Unset(),
default_factory: Callable[[Self], T] | Unset = Unset(),
) -> None:
self.__default_value: Final[T | Unset] = default_value
self.__default_factory: Final[Callable[[Self], T] | Unset] = default_factory
def _get_default(self: Self) -> T:
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: def __set_name__(self, owner: type, name: str) -> None:
self.__name = f"_{owner.__name__}__{name}" self.__name = f"_{owner.__name__}__{name}"
def __get__(self, instance: Any, owner: type | None = None, /) -> T: def __get__(self, instance: Any, owner: type | None = None, /) -> T:
if self.__default_value: if not hasattr(instance, self.__name):
return getattr(instance, self.__name, self.__default_value) setattr(instance, self.__name, self._get_default())
return getattr(instance, self.__name) # type: ignore return getattr(instance, self.__name) # type: ignore
def __set__(self, instance: Any, value: T, /) -> None: def __set__(self, instance: Any, value: T, /) -> None:

View file

@ -12,7 +12,7 @@ MARKERS_ATTR = "__snakia_markers__"
def _get_all_markers(obj: Any) -> dict[type["Marker"], "Marker"]: 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: class Marker:

View file

@ -49,24 +49,29 @@ class UniqueType(type):
raise TypeError(f"{cls} not unwrapped") raise TypeError(f"{cls} not unwrapped")
return value # type: ignore 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( def map(
cls: type[T], cls: type[T],
value: V | type[T] | T, value: V | type[T] | T,
and_then: Callable[[V], R], or_else: Callable[[V], R],
or_else: Callable[[type[T]], R], and_then: Callable[[type[T]], R],
) -> R: ) -> R:
if value is cls or isinstance(value, cls): if value is cls or isinstance(value, cls):
return or_else(cls) return and_then(cls)
return and_then(value) # type: ignore return or_else(value) # type: ignore
def and_then( def or_else(
cls: type[T], value: V | type[T] | T, func: Callable[[V], R] cls: type[T], value: V | type[T] | T, func: Callable[[V], R]
) -> type[T] | R: ) -> type[T] | R:
if value is cls or isinstance(value, cls): if value is cls or isinstance(value, cls):
return cls return cls
return func(value) # type: ignore return func(value) # type: ignore
def or_else( def and_then(
cls: type[T], value: V | type[T] | T, func: Callable[[type[T]], R] cls: type[T], value: V | type[T] | T, func: Callable[[type[T]], R]
) -> R | V: ) -> R | V:
if value is cls or isinstance(value, cls): if value is cls or isinstance(value, cls):

View file

@ -1,10 +1,9 @@
from .attrs import get_attrs, get_or_set_attr from .attrs import get_attrs, get_or_set_attr
from .calls import call, caller
from .exceptions import catch, throw from .exceptions import catch, throw
from .frames import frame from .frames import frame
from .funcs import call, caller, ret, side, side_func
from .gil import GIL_ENABLED, nolock from .gil import GIL_ENABLED, nolock
from .inherit import inherit from .inherit import inherit
from .side import side, side_func
from .this import this from .this import this
from .to_async import to_async from .to_async import to_async
@ -17,6 +16,7 @@ __all__ = [
"frame", "frame",
"inherit", "inherit",
"nolock", "nolock",
"ret",
"side", "side",
"side_func", "side_func",
"this", "this",

View file

@ -1,12 +0,0 @@
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def call(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
return f(*args, **kwargs)
def caller(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> Callable[..., T]:
return lambda *_, **__: f(*args, **kwargs)

24
src/snakia/utils/funcs.py Normal file
View file

@ -0,0 +1,24 @@
from typing import Any, Callable, ParamSpec, TypeVar
P = ParamSpec("P")
T = TypeVar("T")
def call(f: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
return f(*args, **kwargs)
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
def ret() -> Callable[[T], T]:
return lambda x: x

View file

@ -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