diff --git a/.gitignore b/.gitignore
index b9c190e..ab34020 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,6 @@ wheels/
.direnv
.mypy_cache
.python-version
+.pytest_cache
.vscode
_autosummary
\ No newline at end of file
diff --git a/README.md b/README.md
index a2a7a91..234dea7 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,41 @@
+
+
# 🐍 Snakia Framework
+
+
+
+
+
+
+[API Reference](https://ruject.github.io/snakia/)
+ •
+[Telegram Chat](https://t.me/RuJect_Community)
+
+
+
**Snakia** is a modern Python framework for creating applications with Entity-Component-System (ECS) architecture, event system, and reactive programming. Built with performance (maybe) and modularity in mind, Snakia provides a clean API for developing complex applications ranging from games to terminal user interfaces.
## 📋 Table of Contents
-- [🎯 Roadmap & TODO](#-roadmap--todo)
-- [🚀 Installation](#-installation)
-- [🚀 Quick Start](#-quick-start)
-- [🏗️ Architecture](#️-architecture)
-- [⚙️ Core](#️-core)
-- [🎯 ECS System](#-ecs-system)
-- [📡 Event System (ES)](#-event-system-es)
-- [🔌 Plugin System](#-plugin-system)
-- [🎨 TUI System](#-tui-system)
-- [⚡ Reactive Programming (RX)](#-reactive-programming-rx)
-- [🛠️ Utilities](#️-utilities)
-- [🎭 Decorators](#-decorators)
-- [🏷️ Properties](#-properties)
-- [🌐 Platform Abstraction](#-platform-abstraction)
-- [📦 Examples](#-examples)
-- [🤝 Contributing](#-contributing)
-- [🆘 Support](#-support)
-- [📄 License](#-license)
+- [🐍 Snakia Framework](#-snakia-framework)
+ - [📋 Table of Contents](#-table-of-contents)
+ - [✨ Key Features](#-key-features)
+ - [🚀 Installation](#-installation)
+ - [Prerequisites](#prerequisites)
+ - [Install from PyPi (recommended)](#install-from-pypi-recommended)
+ - [Install from Source](#install-from-source)
+ - [🎯 Roadmap \& TODO](#-roadmap--todo)
+ - [🚀 Quick Start](#-quick-start)
+ - [🏗️ Architecture](#️-architecture)
+ - [📦 Examples](#-examples)
+ - [Health System](#health-system)
+ - [TUI Application](#tui-application)
+ - [🤝 Contributing](#-contributing)
+ - [How to Contribute](#how-to-contribute)
+ - [Development Guidelines](#development-guidelines)
-### ✨ Key Features
+## ✨ Key Features
- 🏗️ **ECS Architecture** - Flexible entity-component-system for scalable game/app logic
- 📡 **Event System** - Asynchronous event handling with filters and priorities
@@ -52,15 +64,9 @@ pip install snakia
### Install from Source
```bash
-# Clone the repository
git clone https://github.com/RuJect/Snakia.git
cd Snakia
-
-# Install with pip
pip install -e .
-
-# Or with uv (recommended)
-uv sync
```
## 🎯 Roadmap & TODO
@@ -133,616 +139,6 @@ Snakia/
└── types/ # Special types
```
-## ⚙️ Core
-
-### Engine
-
-The central component of the framework that coordinates all systems:
-
-```python
-from snakia.core.engine import Engine
-
-engine = Engine()
-# Systems:
-# - engine.system - ECS system
-# - engine.dispatcher - Event system
-# - engine.loader - Plugin loader
-
-engine.start() # Start all systems
-engine.stop() # Stop all systems
-engine.update() # Update systems
-```
-
-## 🎯 ECS System
-
-Entity-Component-System architecture for creating flexible and performant applications.
-
-### Component
-
-Base class for all components:
-
-```python
-from snakia.core.ecs import Component
-from pydantic import Field
-
-class PositionComponent(Component):
- x: float = Field(default=0.0)
- y: float = Field(default=0.0)
-
-class VelocityComponent(Component):
- vx: float = Field(default=0.0)
- vy: float = Field(default=0.0)
-```
-
-### Processor
-
-Processors handle components in the system:
-
-```python
-from snakia.core.ecs import Processor, System
-
-class MovementProcessor(Processor):
- def process(self, system: System) -> None:
- # Get all entities with Position and Velocity
- for entity, (pos, vel) in system.get_components(
- PositionComponent, VelocityComponent
- ):
- pos.x += vel.vx
- pos.y += vel.vy
-```
-
-### System
-
-Entity and component management:
-
-```python
-# Creating an entity with components
-entity = system.create_entity(
- PositionComponent(x=10, y=20),
- VelocityComponent(vx=1, vy=0)
-)
-
-# Adding a component to an existing entity
-system.add_component(entity, HealthComponent(value=100))
-
-# Getting entity components
-pos, vel = system.get_components_of_entity(
- entity, PositionComponent, VelocityComponent
-)
-
-# Checking for components
-if system.has_components(entity, PositionComponent, VelocityComponent):
- print("Entity has position and velocity")
-
-# Removing a component
-system.remove_component(entity, VelocityComponent)
-
-# Deleting an entity
-system.delete_entity(entity)
-```
-
-## 📡 Event System (ES)
-
-Asynchronous event system with filter and priority support.
-
-### Event
-
-Base class for events:
-
-```python
-from snakia.core.es import Event
-from pydantic import Field
-
-class PlayerDiedEvent(Event):
- player_id: int = Field()
- cause: str = Field(default="unknown")
- ttl: int = Field(default=10) # Event lifetime
-```
-
-### Handler
-
-Event handlers:
-
-```python
-from snakia.core.es import Handler, Action
-
-def on_player_died(event: PlayerDiedEvent) -> Action | None:
- print(f"Player {event.player_id} died from {event.cause}")
- return Action.move(1) # Move to next handler
-```
-
-### Filter
-
-Event filters:
-
-```python
-from snakia.core.es import Filter
-
-def only_important_deaths(event: PlayerDiedEvent) -> bool:
- return event.cause in ["boss", "pvp"]
-
-# Using a filter
-@dispatcher.on(PlayerDiedEvent, filter=only_important_deaths)
-def handle_important_death(event: PlayerDiedEvent):
- print("Important death occurred!")
-```
-
-### Dispatcher
-
-Central event dispatcher:
-
-```python
-from snakia.core.es import Dispatcher, Subscriber
-
-dispatcher = Dispatcher()
-
-# Subscribing to an event
-dispatcher.subscribe(PlayerDiedEvent, Subscriber(
- handler=on_player_died,
- filter=only_important_deaths,
- priority=10
-))
-
-# Decorator for subscription
-@dispatcher.on(PlayerDiedEvent, priority=5)
-def handle_death(event: PlayerDiedEvent):
- print("Death handled!")
-
-# Publishing an event
-dispatcher.publish(PlayerDiedEvent(player_id=123, cause="boss"))
-```
-
-## 🔌 Plugin System
-
-Modular system for loading and managing plugins.
-
-### Plugin
-
-Base class for plugins:
-
-```python
-from snakia.core.loader import Meta, Plugin, PluginProcessor
-from snakia.types import Version
-
-class MyProcessor(PluginProcessor):
- def process(self, system):
- # Processor logic
- pass
-
-class MyPlugin(Plugin, meta=Meta(
- name="my_plugin",
- author="developer",
- version=Version.from_args(1, 0, 0),
- processors=(MyProcessor,),
- subscribers=()
-)):
- def on_load(self):
- print("Plugin loaded!")
-
- def on_unload(self):
- print("Plugin unloaded!")
-```
-
-### Meta
-
-Plugin metadata:
-
-```python
-from snakia.core.loader import Meta
-from snakia.core.es import Subscriber
-
-meta = Meta(
- name="plugin_name",
- author="author_name",
- version=Version.from_args(1, 0, 0),
- processors=(Processor1, Processor2),
- subscribers=(
- (EventType, Subscriber(handler, filter, priority)),
- )
-)
-```
-
-### Loader
-
-Plugin loader:
-
-```python
-from snakia.core.loader import Loader
-
-loader = Loader(engine)
-
-# Registering a plugin
-loader.register(MyPlugin)
-
-# Loading all plugins
-loader.load_all()
-
-# Unloading all plugins
-loader.unload_all()
-```
-
-## 🎨 TUI System
-
-System for creating text-based user interfaces.
-
-### Widget
-
-Base class for widgets:
-
-```python
-from snakia.core.tui import Widget, Canvas, CanvasChar
-from snakia.core.rx import Bindable
-
-class MyWidget(Widget):
- def __init__(self):
- super().__init__()
- self.text = self.state("Hello World")
- self.color = self.state(CanvasChar(fg_color="red"))
-
- def on_render(self) -> Canvas:
- canvas = Canvas(20, 5)
- canvas.draw_text(0, 0, self.text.value, self.color.value)
- return canvas
-```
-
-### Canvas
-
-Drawing canvas:
-
-```python
-from snakia.core.tui import Canvas, CanvasChar
-
-canvas = Canvas(80, 24)
-
-# Drawing text
-canvas.draw_text(10, 5, "Hello", CanvasChar(fg_color="blue"))
-
-# Drawing rectangle
-canvas.draw_rect(0, 0, 20, 10, CanvasChar("█", fg_color="green"))
-
-# Filling area
-canvas.draw_filled_rect(5, 5, 10, 5, CanvasChar(" ", bg_color="red"))
-
-# Lines
-canvas.draw_line_h(0, 0, 20, CanvasChar("-"))
-canvas.draw_line_v(0, 0, 10, CanvasChar("|"))
-```
-
-### CanvasChar
-
-Character with attributes:
-
-```python
-from snakia.core.tui import CanvasChar
-
-char = CanvasChar(
- char="A",
- fg_color="red", # Text color
- bg_color="blue", # Background color
- bold=True, # Bold
- italic=False, # Italic
- underline=True # Underline
-)
-```
-
-### Renderer
-
-Screen rendering:
-
-```python
-from snakia.core.tui import RenderContext
-from snakia.core.tui.render import ANSIRenderer
-import sys
-
-class StdoutTarget:
- def write(self, text: str): sys.stdout.write(text)
- def flush(self): sys.stdout.flush()
-
-renderer = ANSIRenderer(StdoutTarget())
-
-with RenderContext(renderer) as ctx:
- ctx.render(widget.render())
-```
-
-### Ready-made Widgets
-
-```python
-from snakia.core.tui.widgets import (
- TextWidget, BoxWidget,
- HorizontalSplitWidget, VerticalSplitWidget
-)
-
-# Text widget
-text = TextWidget("Hello", CanvasChar(fg_color="red", bold=True))
-
-# Box widget
-box = BoxWidget(10, 5, CanvasChar("█", fg_color="yellow"))
-
-# Splitters
-h_split = HorizontalSplitWidget([text1, text2], "|")
-v_split = VerticalSplitWidget([h_split, box], "-")
-```
-
-## ⚡ Reactive Programming (RX)
-
-Reactive programming system for creating responsive interfaces.
-
-### Bindable
-
-Reactive variables:
-
-```python
-from snakia.core.rx import Bindable, ValueChanged
-
-# Creating a reactive variable
-counter = Bindable(0)
-
-# Subscribing to changes
-def on_change(event: ValueChanged[int]):
- print(f"Counter changed from {event.old_value} to {event.new_value}")
-
-counter.subscribe(on_change)
-
-# Changing value
-counter.set(5) # Will call on_change
-counter(10) # Alternative syntax
-```
-
-### AsyncBindable
-
-Asynchronous reactive variables:
-
-```python
-from snakia.core.rx import AsyncBindable
-
-async_counter = AsyncBindable(0)
-
-async def async_handler(event: ValueChanged[int]):
- print(f"Async counter: {event.new_value}")
-
-await async_counter.subscribe(async_handler, run_now=True)
-await async_counter.set(42)
-```
-
-### Operators
-
-```python
-from snakia.core.rx import map, filter, combine, merge
-
-# Transformation
-doubled = map(counter, lambda x: x * 2)
-
-# Filtering
-even_only = filter(counter, lambda x: x % 2 == 0)
-
-# Combining
-combined = combine(counter, doubled, lambda a, b: a + b)
-
-# Merging streams
-merged = merge(counter, async_counter)
-```
-
-## 🛠️ Utilities
-
-### to_async
-
-Converting synchronous functions to asynchronous:
-
-```python
-from snakia.utils import to_async
-
-def sync_function(x):
- return x * 2
-
-async_function = to_async(sync_function)
-result = await async_function(5)
-```
-
-### nolock
-
-Performance optimization:
-
-```python
-from snakia.utils import nolock
-
-def busy_loop():
- while running:
- # Work
- nolock() # Release GIL
-```
-
-### inherit
-
-Simplified inheritance:
-
-```python
-from snakia.utils import inherit
-
-class Base:
- def method(self): pass
-
-class Derived(inherit(Base)):
- def method(self):
- super().method()
- # Additional logic
-```
-
-### this
-
-Reference to current object:
-
-```python
-from snakia.utils import this
-
-def func():
- return this() # Returns ``
-```
-
-### throw
-
-Throwing exceptions:
-
-```python
-from snakia.utils import throw
-
-def validate(value):
- if value < 0:
- throw(ValueError("Value must be positive"))
-```
-
-### frame
-
-Working with frames:
-
-```python
-from snakia.utils import frame
-
-def process_frame():
- current_frame = frame()
- # Process frame
-```
-
-## 🎭 Decorators
-
-### inject_replace
-
-Method replacement:
-
-```python
-from snakia.decorators import inject_replace
-
-class Original:
- def method(self): return "original"
-
-@inject_replace(Original, "method")
-def new_method(self): return "replaced"
-```
-
-### inject_before / inject_after
-
-Hooks before and after execution:
-
-```python
-from snakia.decorators import inject_before, inject_after
-
-@inject_before(MyClass, "method")
-def before_hook(self): print("Before method")
-
-@inject_after(MyClass, "method")
-def after_hook(self): print("After method")
-```
-
-### singleton
-
-Singleton pattern:
-
-```python
-from snakia.decorators import singleton
-
-@singleton
-class Database:
- def __init__(self):
- self.connection = "connected"
-```
-
-### pass_exceptions
-
-Exception handling:
-
-```python
-from snakia.decorators import pass_exceptions
-
-@pass_exceptions(ValueError, TypeError)
-def risky_function():
- # Code that might throw exceptions
- pass
-```
-
-## 🏷️ Properties
-
-### readonly
-
-Read-only property:
-
-```python
-from snakia.property import readonly
-
-
-class Currency:
- @readonly
- def rate(self) -> int:
- return 100
-
-
-currency = Currency()
-currency.rate = 200
-print(currency.rate) # Output: 100
-```
-
-### initonly
-
-Initialization-only property:
-
-```python
-from snakia.property import initonly
-
-
-class Person:
- name = initonly[str]("name")
-
-
-bob = Person()
-bob.name = "Bob"
-print(bob.name) # Output: "Bob"
-bob.name = "not bob"
-print(bob.name) # Output: "Bob"
-```
-
-### 🏛️ classproperty
-
-Class property:
-
-```python
-from snakia.property import classproperty
-
-class MyClass:
- @classproperty
- def class_value(cls):
- return "class_value"
-```
-
-## 🌐 Platform Abstraction
-
-### 🖥️ PlatformOS
-
-Operating system abstraction:
-
-```python
-from snakia.platform import PlatformOS, OS
-
-# Detecting current OS
-current_os = OS.current()
-
-if current_os == PlatformOS.LINUX:
- print("Running on Linux")
-elif current_os == PlatformOS.ANDROID:
- print("Running on Android")
-```
-
-### 🏗️ PlatformLayer
-
-Platform layers:
-
-```python
-from snakia.platform import LinuxLayer, AndroidLayer
-
-# Linux layer
-linux_layer = LinuxLayer()
-
-# Android layer
-android_layer = AndroidLayer()
-```
-
## 📦 Examples
### Health System
@@ -856,16 +252,4 @@ We welcome contributions to Snakia development! Whether you're fixing bugs, addi
- Add type hints to all new code
- Write clear commit messages
- Update documentation for new features
-- Test your changes thoroughly
-
-## 🆘 Support
-
-Need help? We're here to assist you!
-
-- 🐛 **Bug Reports** - [GitHub Issues](https://github.com/RuJect/Snakia/issues)
-- 💬 **Community Chat** - [RuJect Community Telegram](https://t.me/RuJect_Community)
-- 📧 **Direct Contact** - mailto:rus07tam.uwu@gmail.com
-
-## 📄 License
-
-See the `LICENSE` file for details.
+- Test your changes thoroughly
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 13fdbbc..c781094 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "snakia"
-version = "0.4.0"
+version = "0.4.1"
description = "Modern python framework"
readme = "README.md"
authors = [
@@ -37,4 +37,5 @@ init-hook = "import sys; sys.path.append('.venv/lib/python3.12/site-packages')"
disable = ["C0114", "C0115", "C0116", "R0801"]
max-args = 8
max-positional-arguments = 7
-min-public-methods = 1
\ No newline at end of file
+min-public-methods = 1
+fail-on = "error"
\ No newline at end of file
diff --git a/src/snakia/core/rx/__init__.py b/src/snakia/core/rx/__init__.py
index 995ffa5..3c3d499 100644
--- a/src/snakia/core/rx/__init__.py
+++ b/src/snakia/core/rx/__init__.py
@@ -4,6 +4,7 @@ from .bindable import Bindable
from .chain import chain
from .combine import combine
from .concat import concat
+from .cond import cond
from .const import const
from .filter import filter # noqa: W0622 # pylint: disable=W0622
from .map import map # noqa: W0622 # pylint: disable=W0622
@@ -18,6 +19,7 @@ __all__ = [
"chain",
"combine",
"concat",
+ "cond",
"const",
"filter",
"map",
diff --git a/src/snakia/core/rx/async_bindable.py b/src/snakia/core/rx/async_bindable.py
index 178ba18..a31c2df 100644
--- a/src/snakia/core/rx/async_bindable.py
+++ b/src/snakia/core/rx/async_bindable.py
@@ -12,10 +12,6 @@ class AsyncBindable[T: Any](BaseBindable[T]):
super().__init__(default_value)
self.__subscribers: list[BindableSubscriber[T, Awaitable[Any]]] = []
- @property
- def value(self) -> T:
- return self.__value
-
@property
def subscribers(
self,
@@ -25,8 +21,8 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def set(self, value: T) -> None:
"""Set the value."""
- e = ValueChanged(self.__value, value)
- self.__value = value
+ e = ValueChanged(self.value, value)
+ self.set_silent(value)
for subscriber in self.__subscribers:
await subscriber(e)
@@ -58,7 +54,7 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def _run() -> None:
await subscriber(
- ValueChanged(self.__default_value, self.__value)
+ ValueChanged(self.__default_value, self.value)
)
return _run()
@@ -96,7 +92,7 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def _run() -> None:
await subscriber(
- ValueChanged(self.__default_value, self.__value)
+ ValueChanged(self.__default_value, self.value)
)
return _run()
diff --git a/src/snakia/core/rx/bindable.py b/src/snakia/core/rx/bindable.py
index be66139..fe2a448 100644
--- a/src/snakia/core/rx/bindable.py
+++ b/src/snakia/core/rx/bindable.py
@@ -19,7 +19,7 @@ class Bindable[T: Any](BaseBindable[T]):
def set(self, value: T) -> None:
"""Set the value."""
- e = ValueChanged(self.__value, value)
+ e = ValueChanged(self.value, value)
self.set_silent(value)
for subscriber in self.__subscribers:
subscriber(e)
diff --git a/src/snakia/core/rx/cond.py b/src/snakia/core/rx/cond.py
new file mode 100644
index 0000000..fce6cdb
--- /dev/null
+++ b/src/snakia/core/rx/cond.py
@@ -0,0 +1,13 @@
+from typing import Callable
+
+
+def cond[**P, T, F](
+ condition: Callable[P, bool],
+ if_true: Callable[P, T],
+ if_false: Callable[P, F],
+) -> Callable[P, T | F]:
+ return lambda *args, **kw: (
+ if_true(*args, **kw)
+ if condition(*args, **kw)
+ else if_false(*args, **kw)
+ )
diff --git a/src/snakia/field/field.py b/src/snakia/field/field.py
index c59c8ae..4fdc9d6 100644
--- a/src/snakia/field/field.py
+++ b/src/snakia/field/field.py
@@ -36,14 +36,24 @@ class Field[T: Any](ABC, PrivProperty[T]):
@classmethod
def custom[R](
cls: type[Field[Any]],
- serialize: Callable[[R], str],
- deserialize: Callable[[str], R],
+ serialize: Callable[[Field[R], R], bytes],
+ deserialize: Callable[[Field[R], bytes], R],
) -> type[Field[R]]:
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)
+ }
+
if TYPE_CHECKING:
+ @final
@classmethod
def type(cls) -> type[T]: ...
diff --git a/src/snakia/property/__init__.py b/src/snakia/property/__init__.py
index 91e4ec1..6af10c9 100644
--- a/src/snakia/property/__init__.py
+++ b/src/snakia/property/__init__.py
@@ -1,5 +1,5 @@
from .cell_property import CellProperty
-from .classproperty import ClassProperty
+from .classproperty import ClassProperty, classproperty
from .hook_property import HookProperty
from .initonly import Initonly, initonly
from .priv_property import PrivProperty
@@ -9,6 +9,7 @@ from .readonly import Readonly, readonly
__all__ = [
"CellProperty",
"ClassProperty",
+ "classproperty",
"HookProperty",
"Initonly",
"initonly",
diff --git a/src/snakia/property/classproperty.py b/src/snakia/property/classproperty.py
index 48691ed..a9b2413 100644
--- a/src/snakia/property/classproperty.py
+++ b/src/snakia/property/classproperty.py
@@ -48,7 +48,7 @@ class ClassProperty[T]:
def classproperty[T](
- fget: Callable[[Any], T] = empty.func,
+ fget: Callable[[Any], T],
fset: Callable[[Any, T], None] = empty.func,
fdel: Callable[[Any], None] = empty.func,
) -> ClassProperty[T]:
diff --git a/src/snakia/property/priv_property.py b/src/snakia/property/priv_property.py
index 4203503..2628ec9 100644
--- a/src/snakia/property/priv_property.py
+++ b/src/snakia/property/priv_property.py
@@ -15,7 +15,7 @@ class PrivProperty[T]:
def __get__(self, instance: Any, owner: type | None = None, /) -> T:
if self.__default_value:
return getattr(instance, self.__name, self.__default_value)
- return getattr(instance, self.__name)
+ return getattr(instance, self.__name) # type: ignore
def __set__(self, instance: Any, value: T, /) -> None:
setattr(instance, self.__name, value)
diff --git a/src/snakia/property/property.py b/src/snakia/property/property.py
index 97e8b29..53b30bc 100644
--- a/src/snakia/property/property.py
+++ b/src/snakia/property/property.py
@@ -5,7 +5,12 @@ from snakia.types import empty
class Property[T]:
"""
- A property that can be set, get, and deleted."""
+ A property that can be set, get, and deleted.
+ """
+
+ __slots__ = "__fget", "__fset", "__fdel", "__name"
+
+ __name: str
def __init__(
self,
@@ -17,6 +22,9 @@ class Property[T]:
self.__fset = fset
self.__fdel = fdel
+ def __set_name__(self, owner: type, name: str) -> None:
+ self.__name = name
+
def __get__(self, instance: Any, owner: type | None = None, /) -> T:
return self.__fget(instance)
@@ -40,3 +48,7 @@ class Property[T]:
"""Descriptor deleter."""
self.__fdel = fdel
return self
+
+ @property
+ def name(self) -> str:
+ return self.__name
diff --git a/src/snakia/property/readonly.py b/src/snakia/property/readonly.py
index 0f6f2cf..d9e8943 100644
--- a/src/snakia/property/readonly.py
+++ b/src/snakia/property/readonly.py
@@ -1,27 +1,32 @@
from typing import Any, Callable
+from snakia.utils import throw
-class Readonly[T]:
+from .property import Property
+
+
+class Readonly[T](Property[T]):
"""
Readonly property.
"""
- __slots__ = ("__fget",)
-
def __init__(
self,
fget: Callable[[Any], T],
+ *,
+ strict: bool = False,
) -> None:
- self.__fget = fget
-
- def __get__(self, instance: Any, owner: type | None = None, /) -> T:
- return self.__fget(instance)
-
- def __set__(self, instance: Any, value: T, /) -> None:
- pass
+ super().__init__(
+ fget=fget,
+ fset=(
+ (lambda *_: throw(TypeError("Cannot set readonly property")))
+ if strict
+ else lambda *_: None
+ ),
+ )
-def readonly[T](value: T) -> Readonly[T]:
+def readonly[T](value: T, *, strict: bool = False) -> Readonly[T]:
"""Create a readonly property with the given value.
Args:
@@ -30,4 +35,4 @@ def readonly[T](value: T) -> Readonly[T]:
Returns:
Readonly[T]: The readonly property.
"""
- return Readonly(lambda _: value)
+ return Readonly(lambda _: value, strict=strict)
diff --git a/uv.lock b/uv.lock
index d894a9f..b6deda9 100644
--- a/uv.lock
+++ b/uv.lock
@@ -104,7 +104,7 @@ wheels = [
[[package]]
name = "snakia"
-version = "0.4.0"
+version = "0.4.1"
source = { editable = "." }
dependencies = [
{ name = "networkx" },