Recipes#
Real-world, copy-pasteable patterns that combine several decorators. Every snippet on this page is executed as-is by the test suite, so it runs exactly as shown. For a single-decorator tour see Quick start; for the standalone runnable scripts see Examples.
Immutable value object#
Use @Value for small, comparable, hashable values that should never change
after construction — money, coordinates, identifiers. It is immutable on its
own, with no @dataclass(frozen=True) needed.
from dataclasses import FrozenInstanceError
from inito import Value
@Value
class Money:
amount: int
currency: str = "USD"
price = Money(500)
print(price) # Money(amount=500, currency='USD')
print(price == Money(500, "USD")) # True
print(price.get_currency()) # USD (getters, but never setters)
try:
price.amount = 0 # immutable
except FrozenInstanceError:
print("cannot mutate a @Value")
# Hashable, so it works as a dict key or set member:
totals = {Money(500): "five dollars"}
Configuration object with defaults and a mutable default#
Required fields first, defaulted fields after — the same ordering Python
uses. For a mutable default (a list, dict, set), stack @dataclass so
inito picks up field(default_factory=...); each instance then gets its own
fresh container.
from dataclasses import dataclass, field
from inito import Data
@Data
@dataclass
class ServerConfig:
host: str = "localhost"
port: int = 8080
tags: list = field(default_factory=list)
config = ServerConfig(port=9000)
print(config) # ServerConfig(host='localhost', port=9000, tags=[])
ServerConfig().tags.append("a")
print(ServerConfig().tags) # [] — a fresh list per instance, not shared
Fluent builder for a request/response model#
@Builder shines when a type has several optional fields and you want
readable, order-independent construction. to_builder=True also adds a
.to_builder() for deriving a modified copy.
from dataclasses import dataclass
from inito import Builder
@Builder(to_builder=True)
@dataclass
class HttpRequest:
method: str
url: str
timeout: float = 30.0
retries: int = 0
request = (
HttpRequest.builder()
.method("GET")
.url("/users")
.timeout(5.0)
.build()
)
print(request.timeout) # 5.0
# Derive a variant without mutating the original:
with_retries = request.to_builder().retries(3).build()
print(with_retries.retries, request.retries) # 3 0
Service layer with dependency injection#
@Singleton/@Service register classes; @Inject fills a function’s
annotated parameters from the container. A parameter is autowired only if its
type is a registered service — plain values (page_size: int) keep their own
defaults. Combine with @RequiredArgsConstructor so a field annotation is
all you need, no hand-written __init__.
from inito import Service, Singleton, Inject, RequiredArgsConstructor, default_container
@Singleton
class Database:
def __init__(self) -> None:
self.rows = {1: "Ada", 2: "Linus"}
@Service
@RequiredArgsConstructor
class UserRepository:
db: Database # autowired
def name(self, user_id: int) -> str:
return self.db.rows[user_id]
@Service
@RequiredArgsConstructor
class Greeter:
repo: UserRepository # autowired, transitively
def greet(self, user_id: int) -> str:
return f"Hello, {self.repo.name(user_id)}"
@Inject
def handle(greeter: Greeter) -> None:
print(greeter.greet(1)) # Hello, Ada
handle()
# The same graph, resolved explicitly:
print(default_container.get(Greeter).greet(2)) # Hello, Linus
# Every @Service class is still an ordinary class you can build by hand:
manual = Greeter(UserRepository(Database()))
Transient scope and an isolated container#
Singletons are cached; Scope.TRANSIENT rebuilds on every resolution. Use a
dedicated Container (instead of the shared default_container) to keep a
subsystem’s registrations isolated — handy in tests.
from inito import Service, Scope
from inito.di import Container
container = Container()
@Service(scope=Scope.TRANSIENT, container=container)
class RequestContext:
def __init__(self) -> None:
self.token = object()
a = container.get(RequestContext)
b = container.get(RequestContext)
print(a is b) # False — a fresh instance each time
print(container.is_registered(RequestContext)) # True
Compose exactly the capabilities you want#
@Data is a bundle; when you want less, stack the atomic decorators. This
class gets a repr, value equality, and read-only accessors — but no
constructor and no setters.
from inito import ToString, EqualsAndHashCode, Getter
@ToString
@EqualsAndHashCode
@Getter
class Point:
x: int
y: int
p = Point()
p.x, p.y = 1, 2
print(p) # Point(x=1, y=2)
print(p.get_x()) # 1
print(p == p) # True
Inheritance#
Fields accumulate across the MRO, base-class fields first — so a subclass constructor takes the base’s fields followed by its own.
from inito import Data
@Data
class Animal:
name: str
@Data
class Dog(Animal):
breed: str
rex = Dog("Rex", "Labrador")
print(rex) # Dog(name='Rex', breed='Labrador')
print(rex.name, rex.breed) # Rex Labrador
Add your own methods#
inito only attaches the members each decorator owns; anything else you write is left untouched, so decorated classes are just normal classes.
from inito import Data
@Data
class Temperature:
celsius: float
def to_fahrenheit(self) -> float:
return self.celsius * 9 / 5 + 32
t = Temperature(20)
print(t.to_fahrenheit()) # 68.0
print(t) # Temperature(celsius=20)