Small DI
The smallest dependency injection mechanism possible in python. SmallDI provides you with an intuitive and simple interface for doing dependency injections in your project.
SmallDI is built around four features: singletons, containers, interfaces and contexts.
Singletons
A singleton is a class with exactly one instance, created on first use and passed to every function that
asks for it with Provide[T].
Example
Meowing and purring services
import random
from smalldi import Injector, Provide
# Lets create some service
@Injector.singleton
class MeowService:
_MEOWS = ["Meow", "Meow-meow", "Meowwwwww", "Mrromeowww", "Meeeeoooow"]
def __init__(self):
print("Meow-meow! Meowing service is initialized")
def meow(self):
print(random.choice(self._MEOWS))
# Now lets make purring service.
# But cats do not purr without telling meow!(at least in this test)
# So its time to inject dependency
@Injector.singleton
class PurrService:
_PURRS = ["Purrrrr", "Purr-purr"]
@Injector.inject
def __init__(self, meow_service: Provide[MeowService]):
self.meow_service = meow_service
print("Purr-purr! Purring service is initialized")
def purr(self):
self.meow_service.meow()
print(random.choice(self._PURRS))
# Now lets put it all together.
# Ask our services to meow and then purr
@Injector.inject
def main(meow_service: Provide[MeowService], purr_service: Provide[PurrService]):
meow_service.meow()
purr_service.purr()
if __name__ == '__main__':
main()
Injector
Injector is a static class(i.e., one that should never be instantiated) which is the main (and currently the only) DI container inside the library. Injector provides these decorators:
@Injector.singletonregisters a class whose instance may further be injected in functions@Injector.interfacemarks an abstract class as an interface singletons may implement@Injector.implements(...)declares which interface a singleton implements@Injector.override(...)makes a singleton injected instead of an interface's implementation or another singleton@Injector.injectreplaces parameters annotated with typeProvide[Singleton](orProvide[Interface]) with actual instances of Singleton. EveryProvide[]dependency must already be registered when the function is decorated, otherwiseTypeErroris raised.
Besides the decorators, Injector.get_instance(T) returns the instance for a registered singleton or interface,
creating it if needed, and Injector.context(kind) opens a context in which registrations, overrides and
instances are scoped to a with block.
How singletons work
Singletons are classes having a single instance. In smalldi singletons may not take constructor(__init__) other
than annotated with Provide[] type. Only singletons may be decorated with @Injector.singleton. As a consequence,
only singleton classes, and interfaces they implement, may be injected at the current state of library development.
Abstract classes can't be singletons.
Singletons are lazy: registering a class doesn't instantiate it. The instance is created, exactly once and in a
thread-safe way, the first time it is needed, that is when an @Injector.inject-decorated function is called or
Injector.get_instance is called. The instance belongs to the context that is current at that moment:
a RESET context gets its own instances, created again on first use inside it.
Once an instance was requested, a singleton is frozen and can no longer be overridden, even if
creating the instance failed.
A singleton whose constructor requires itself, directly or through its dependencies, raises RuntimeError
(circular dependency). Cycles that span several threads, or a constructor waiting for a thread that needs the
singleton being constructed, deadlock instead and aren't detected.
Provide
Provide[T] is an annotation for injector telling it that instead of this argument
instance of T should be passed. Caller of function with Provide[T] may explicitly
override argument annotated with Provide[T] by directly passing annotated_di_arg=my_value.
Only keyword arguments are recognized: passing the dependency positionally makes the call raise TypeError
for a duplicate argument.
@Injector.inject on an async def function returns a coroutine function. Its Provide[T] parameters are resolved
when the coroutine starts running, not when the function is called.
Injector.singletons_available (deprecated)
Instances are meant to be reached through injection only. Injector.singletons_available, a read-only mapping of
every registered singleton class to its instance, is kept for compatibility and emits a DeprecationWarning when
read; it is planned to be removed in 1.0.0. Reading it creates every singleton that doesn't exist yet, so it freezes
the whole registry. Assigning to an item of the mapping raises TypeError.
Containers
A container is a singleton that collects related classes or functions registered with @MyContainer.component.
Example
Catnip flavours
import sys
from abc import ABC, abstractmethod
from smalldi.container import Container
from smalldi import Injector, Provide
# Define interface for catnip
class Catnip(ABC):
@property
@abstractmethod
def flavour(self):
pass
# Create a container to store different flavours of catnip
@Injector.singleton
class CatnipContainer(Container):
def get_all_flavours(self) -> list[str]:
flavours = list()
for catnip in self._get_components():
flavours.append(catnip().flavour)
return flavours
@CatnipContainer.component
class PlainCatnip(Catnip):
@property
def flavour(self):
return "plain"
@CatnipContainer.component
class ChocolateCatnip(Catnip):
@property
def flavour(self):
return "chocolate"
@CatnipContainer.component
class StrawberryCatnip(Catnip):
@property
def flavour(self):
return "strawberry"
@Injector.inject
def main(catnip_container: Provide[CatnipContainer]) -> int:
print(catnip_container.get_all_flavours())
return 0
if __name__ == '__main__':
sys.exit(main())
How containers work
Container helps to collect classes and potential functions into a single unit.
All containers must be singletons inherited from Container class.
To create a container, write and inheritor of Container class and annotate it with @Injector.singleton.
Then you may register components in the container by annotating them with @MyContainer.component.
Additionally, @MyContainer.component may be called with () in order to provide metadata about the component.
Registering the first component creates the container instance, so a container can only be
overridden before any component is registered in it. Components registered afterwards through
@MyContainer.component go to the overriding container's instance.
Container._get_components
The container exposes protected method _get_components which returns an iterator over all classes and functions
registered in the container, in registration order. Their metadata is kept in registrations.
Container._on_component_register
The container has protected method _on_component_register(registration) which is called with the
registration every time a new component is registered in the container. Override it to react
to registrations; the default does nothing.
ComponentRegistration
ComponentRegistration is a dataclass which holds information about registered component which
consists of:
component: an actual component typeargs: arguments passed to@MyContainer.componentduring registrationkwargs: keyword arguments passed to@MyContainer.componentduring registration
Interfaces
An interface is an abstract class that code depends on instead of a concrete singleton; a singleton implements it, and an override can replace that implementation without changing the code that injects it.
Example
Cohee-Neko, Kansai-Neko and Ootani in disguise
import abc
import dataclasses
import datetime
import enum
import random
import sys
from smalldi import Injector, Provide
def ootani_is_on_patrol() -> bool:
# Otani tries to save his apartment complex as well as the world's strategic coffee reserves from Cohee-Neko
# Though he is also very busy with mopping the yard and watering cabbages
hour = datetime.datetime.now().hour
return hour >= 22 or hour < 6 or random.random() < 0.1
class BeanKind(enum.Enum):
REAL_BEAN = enum.auto()
DECAF_BEAN = enum.auto()
@dataclasses.dataclass
class Bean:
kind: BeanKind
price: float
@Injector.interface
class CoffeeDealer(abc.ABC):
@property
@abc.abstractmethod
def price(self) -> float:
pass
@abc.abstractmethod
def buy(self, suggested_price: float) -> Bean:
pass
# Kansai Neko doesn't care about apartment or coffee reserves
@Injector.implements(CoffeeDealer)
@Injector.singleton
class KansaiNekoCoffeeDealer(CoffeeDealer):
def __init__(self):
self.supply_remaining = 3401
@property
def price(self) -> float:
return 1
def buy(self, suggested_price: float) -> Bean:
if self.supply_remaining == 0:
raise ValueError("No more coffee. I've bean better.")
if suggested_price - self.price >= 0:
self.supply_remaining -= 1
return Bean(BeanKind.REAL_BEAN, suggested_price)
raise ValueError("No money, no ~~honey~~ coffee")
@Injector.override(CoffeeDealer, when=ootani_is_on_patrol)
@Injector.singleton
class OotaniDisguisedAsCoffeeDealer(CoffeeDealer):
def __init__(self):
self.supply_remaining = 3301
@property
def price(self) -> float:
return 0.5
def buy(self, suggested_price: float) -> Bean:
if self.supply_remaining == 0:
raise ValueError("No more coffee")
if suggested_price - self.price >= 0:
self.supply_remaining -= 1
return Bean(BeanKind.DECAF_BEAN, suggested_price)
raise ValueError("Oh...Cohee-Neko would be awake whole night again...")
# She starts doing anything after a "one more" cup of coffee
@Injector.singleton
class CoheeNeko:
@Injector.inject
def __init__(self, coffee_dealer: Provide[CoffeeDealer]):
print("Fueeeelllling with CoFfEE")
self.money = 0x29A01 # She spends THAT much on coffee?! What does she even do to get THIS amount of money?
self.beans = 0
while self.money > 0:
bean = coffee_dealer.buy(1)
self.money -= 1
if bean.kind is BeanKind.DECAF_BEAN:
print("Ewww...Decaf...*throws away*")
break
self.beans += 1
@Injector.inject
def main(cohee_neko: Provide[CoheeNeko]) -> int:
print("Cohee Neko is ready for next cup of coffee!")
if cohee_neko.beans > 0:
print(f"She has {cohee_neko.money}K Yens and drank Doppio++ from {cohee_neko.beans} beans already!")
elif cohee_neko.beans == 0:
print(f"She has {cohee_neko.money}K Yens and extremely strong desire to find next cup of coffee")
else:
print(f"WAD? Who gave her loan in COFFEE beans?")
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as e:
# Cohee-Neko is injected before main() runs, so Kansai-Neko's empty stock surfaces here
print(f"{type(e).__name__}: {e}")
sys.exit(1)
How interfaces work
An interface lets code depend on an abstraction instead of a concrete singleton. Mark the abstraction with
@Injector.interface, then declare the implementing singleton with @Injector.implements, applied above
@Injector.singleton:
Coffee machine with a caffeine computer
import sys
import dataclasses
import abc
import enum
from smalldi import Injector, Provide
class CoffeeKind(enum.Enum):
NORMAL = enum.auto()
DECAF = enum.auto()
@Injector.interface
class CaffeineComputer(abc.ABC):
@abc.abstractmethod
def compute_caffeine(self, kind: CoffeeKind, amount_ml: float) -> float:
"""Computes the amount of caffeine in a given amount of coffee, assuming espresso benchmark."""
pass
@abc.abstractmethod
def compute_volume_for_caffeine_dose(self, kind: CoffeeKind, target_caffeine_mg: float) -> float:
"""Computes the amount of coffee needed to reach a given caffeine dose."""
pass
@Injector.implements(CaffeineComputer)
@Injector.singleton
class DefaultCaffeineComputer(CaffeineComputer):
_ESPRESSO_CAFFEINE_MG_PER_ML = 2.0
def compute_caffeine(self, kind: CoffeeKind, amount_ml: float) -> float:
if kind is CoffeeKind.DECAF:
return 0.0
return self._ESPRESSO_CAFFEINE_MG_PER_ML * amount_ml
def compute_volume_for_caffeine_dose(self, kind: CoffeeKind, target_caffeine_mg: float) -> float:
if kind is CoffeeKind.DECAF:
return 0.0
return target_caffeine_mg / self._ESPRESSO_CAFFEINE_MG_PER_ML
@dataclasses.dataclass
class CoffeeCup:
kind: CoffeeKind
amount_ml: float
caffeine_mg: float
def __str__(self) -> str:
return f"{self.kind.name} cup with {self.amount_ml} ml and {self.caffeine_mg} mg of caffeine"
@Injector.singleton
class CoffeeMachine:
@Injector.inject
def __init__(self, caffeine_computer: Provide[CaffeineComputer]):
self._computer = caffeine_computer
def brew(self, requested_coffee_kind: CoffeeKind, requested_caffeine_mg: float) -> CoffeeCup:
if requested_caffeine_mg < 0:
return CoffeeCup(requested_coffee_kind, 0, 0) # Don't need caffeine? Why are you even near coffee machine?
# Well it is definitely strange coffee machine, but aren't caffeine-addicted catgirls strange too?
if requested_coffee_kind is CoffeeKind.DECAF:
return CoffeeCup(requested_coffee_kind, 330, 0) # You can drink decaf Espresso as water, can't you?
computed_ml = self._computer.compute_volume_for_caffeine_dose(requested_coffee_kind, requested_caffeine_mg)
# Who said computer is fair?
computed_real_dose = self._computer.compute_caffeine(requested_coffee_kind, computed_ml)
return CoffeeCup(requested_coffee_kind, computed_ml, computed_real_dose)
@Injector.inject
def main(machine: Provide[CoffeeMachine]) -> int:
target_caffeine_amount = float(input("How many milligrams of caffeine do you want today? "))
want_decaf = input("Do you want decaf? (y/n) ").lower().startswith('y')
if want_decaf:
coffee_kind = CoffeeKind.DECAF
else:
coffee_kind = CoffeeKind.NORMAL
print("Here is your espresso cup:", machine.brew(coffee_kind, target_caffeine_amount))
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as e:
# Raised by a caffeine computer that refuses to serve the requested coffee
print(f"{type(e).__name__}: {e}")
sys.exit(1)
Rules:
- Each interface has at most one implementation; declaring a second one raises
RuntimeError. @Injector.implementstakes one interface. A singleton implementing several interfaces stacks the decorator, once per interface, above@Injector.singleton.- The singleton must be a subclass of every interface it implements (virtual subclasses registered with
ABC.registercount). Provide[Interface]receives the same instance asProvide[Implementation].- A function may be decorated with
@Injector.injectbefore the interface is implemented, as long as the interface itself is registered. Calling it while the interface has neither an implementation nor an override raisesRuntimeError. - Interfaces must be abstract classes (with at least one abstract method), so a class can't be both an interface and a singleton.
- Interfaces aren't listed in
Injector.singletons_available, only their implementations are. - Like singletons, an interface is registered once: registering the same class again raises
TypeError. Repeating@Injector.implementsraisesRuntimeErrorlike any second implementation, even with the same singleton.
Overrides
Overrides apply to interfaces and to singletons alike.
@Injector.override makes another singleton injected instead of an interface's implementation or instead of another
singleton, for example to swap in a fake in tests or a platform-specific implementation. Like @Injector.implements,
apply it above @Injector.singleton. An override declared inside a context is gone once the context is
left, which is how a fake is scoped to one test.
Continuing the coffee machine example, add these classes right before the
if __name__ == '__main__': block:
Cohee-Neko's caffeine computer and Ootani's coffee machine
@Injector.override(CaffeineComputer) # an interface
@Injector.singleton
class CoheeNekoXX71(CaffeineComputer):
"""An "improved" caffeine computer by Cohee Neko."""
def compute_caffeine(self, kind: CoffeeKind, amount_ml: float) -> float:
if kind is CoffeeKind.DECAF:
raise ValueError("No one gets decaf!")
return 2.0 * amount_ml
def compute_volume_for_caffeine_dose(self, kind: CoffeeKind, target_caffeine_mg: float) -> float:
if kind is CoffeeKind.DECAF:
raise ValueError("No one gets decaf!")
return target_caffeine_mg / 0.000001 # Sure, there is almost no caffeine in real coffee, this is why Cohee-Neko disabled decaf
@Injector.override(CoffeeMachine) # a singleton
@Injector.singleton
class OotaniCoffeeMachine(CoffeeMachine):
"""A coffee machine by Ootani. No more caffeine in apartment complex."""
def brew(self, requested_coffee_kind: CoffeeKind, requested_caffeine_mg: float) -> CoffeeCup:
volume_ml = self._computer.compute_volume_for_caffeine_dose(requested_coffee_kind, requested_caffeine_mg)
return CoffeeCup(CoffeeKind.DECAF, volume_ml, 0)
Provide[CaffeineComputer] then receives the CoheeNekoXX71 instance and Provide[CoffeeMachine] the
OotaniCoffeeMachine instance: the same objects Provide[CoheeNekoXX71] and Provide[OotaniCoffeeMachine] receive.
An override may be conditional: @Injector.override(what, when=condition) takes a callable with no arguments.
condition is called once, when the decorator is applied, that is at import time. If it returns False, the
decorator does nothing and the class stays a plain singleton. The Interfaces example uses it to put
Ootani on patrol.
Rules:
-
The overriding class must be a singleton and a subclass of its target; a class can't override itself.
@Injector.overridetakes one target. -
An overridden singleton is never instantiated through the injector, and
Injector.singletons_availablemaps it to its override's instance. -
An interface override wins whatever the import order: it may be declared before or after
@Injector.implements, and an implementation declared later doesn't replace it. An interface with only an override is injectable too. The implementation stays registered, soProvide[DefaultCaffeineComputer]still receives aDefaultCaffeineComputer, unlessDefaultCaffeineComputeris overridden as well. An interface whose implementation is overridden as a singleton resolves to that override. -
Overrides are not transitive: an override can't be overridden, and an overridden singleton can't become an override. Both raise
TypeError. -
Each interface or singleton may have only one override, so it is unambiguous what gets injected: a second override of an interface raises
TypeError, a second override of a singleton raisesRuntimeError, even when it repeats the same class. -
Targets are frozen once an instance was requested through them, even if creating it failed, and overriding a frozen target raises
SingletonFrozenError:- an interface the first time
Provide[Interface]is resolved, or whenInjector.singletons_availableis read. Injecting the implementation class directly (Provide[DefaultCaffeineComputer]) doesn't freeze its interfaces; - a singleton once it is injected (directly, through an interface it implements, or through a singleton it
overrides), when
Injector.singletons_availableis read, or, for containers, when the first component is registered.
So declare overrides before anything injects their targets, or declare them inside a
RESETorSTACKcontext, whose singletons and interfaces start unfrozen. Frozen means frozen: nothing rebinds a frozen target, the only binding still accepted is the first@Injector.implementsof an interface that already has an override, since the override keeps winning. - an interface the first time
-
A singleton target may be overridden by a class that was already injected: the target then shares that instance. If the overriding class was requested but has no instance, because its constructor is still running or raised, the override raises
SingletonFrozenError.
SingletonFrozenError can be imported from smalldi.
Contexts
A context is a registry of singletons and interfaces that a with Injector.context(kind): block makes current for
the code running inside it, so registrations, overrides and instances can be scoped to the block, for example to one
test.
Example
Ryoga's bath
import abc
import enum
import threading
from smalldi import Injector, Provide
from smalldi.context import InjectionSubcontextType
from smalldi.thread import ContextAwareThread
class Temperature(enum.Enum):
COLD = enum.auto()
HOT = enum.auto()
@Injector.interface
class Water(abc.ABC):
@abc.abstractmethod
def temperature(self) -> Temperature: ...
@abc.abstractmethod
def warm_up(self) -> None: ...
@Injector.implements(Water)
@Injector.singleton
class TapWater(Water):
def __init__(self):
self._temperature = Temperature.COLD
def temperature(self) -> Temperature:
return self._temperature
def warm_up(self) -> None:
self._temperature = Temperature.HOT
@Injector.singleton
class Ryoga:
@Injector.inject
def __init__(self, water: Provide[Water]):
self._water = water
def __str__(self) -> str:
return "P-chan" if self._water.temperature() is Temperature.COLD else "Ryoga Hibiki"
@Injector.inject
def take_a_bath(water: Provide[Water]) -> None:
water.warm_up()
@Injector.inject
def meet(ryoga: Provide[Ryoga]) -> str:
return str(ryoga)
def meet_in_another_thread(thread_cls: type[threading.Thread]) -> str:
seen = []
thread = thread_cls(target=lambda: seen.append(meet()))
thread.start()
thread.join()
return seen[0]
def main() -> None:
print("Before the bath:", meet()) # P-chan
# RESET keeps the blueprints (singletons, implementations, overrides)
# but builds fresh instances: this bath warms up a different Water
with Injector.context(InjectionSubcontextType.RESET):
take_a_bath()
print("In the bath:", meet()) # Ryoga Hibiki
print("Akane, invited along:", meet_in_another_thread(ContextAwareThread)) # Ryoga Hibiki
print("Ranma, outside the door:", meet_in_another_thread(threading.Thread)) # P-chan
print("After the bath:", meet()) # P-chan
# A context is also where test doubles live: an override registered inside
# it disappears together with the context
with Injector.context(InjectionSubcontextType.RESET):
@Injector.override(Water)
@Injector.singleton
class CursedSpring(TapWater):
def warm_up(self) -> None:
pass # Jusenkyo water stays cold
take_a_bath()
print("In the cursed spring:", meet()) # P-chan
with Injector.context(InjectionSubcontextType.RESET):
take_a_bath()
print("Back home, plain tap water:", meet()) # Ryoga Hibiki
if __name__ == "__main__":
main()
The example shows, in order:
- An interface and its implementation.
Wateris registered with@Injector.interface,TapWateris its implementation, andProvide[Water]receives theTapWaterinstance of the current context. @Injector.injecton__init__.Ryogagets itsWaterwhen it is instantiated, that is the first timemeet()runs in a context. Each context that creates aRyogagives it that context'sWater.RESET. The firstwithblock keeps every registration and binding of the root context but gives every singleton a fresh, unfrozen slot without an instance.take_a_bath()warms up the block's ownTapWater, andmeet()builds the block's ownRyoga, who sees hot water. After the block the root instances are untouched: the rootTapWaterwas never warmed, so the rootRyogais stillP-chan.ContextAwareThreadversusthreading.Thread. Akane runs in aContextAwareThreadcreated inside the block, which captures the block's context and enters it for the duration of itsrun(), so she meetsRyoga Hibiki. Ranma runs in a plainthreading.Thread, which starts in the root context whatever its creator had open, so he meets the rootRyoganext to the cold rootTapWater:P-chan.- An override as a test double scoped to a context.
CursedSpringis registered, and overridesWater, inside the secondRESETblock. The rootWaterwas frozen by the firstmeet(), but the block's copy starts unfrozen, so the override is accepted. Both the registration and the override disappear with the block: the third block resolvesWaterto plainTapWateragain, and nobody outside the block ever sawCursedSpring.
Context kinds
Injector.context(kind) takes a member of smalldi.context.InjectionSubcontextType; RESET is the default.
| Kind | What the new context shares with its parent |
|---|---|
PRESERVE |
The same InjectionContext object as the current one: nothing is copied. |
RESET |
The same registrations, implementations and overrides; every singleton is a fresh, unfrozen LazySingleton without an instance. Classes sharing one singleton through an override keep sharing one, and interfaces are rebound to the new singletons. The default kind. |
EMPTY |
Nothing: no singleton and no interface is registered. |
SHALLOW |
A new registry dict holding the same LazySingleton objects: instances and frozen state are shared with the parent. Singletons registered inside are invisible to the parent. Overriding an inherited singleton inside the block mutates the shared object and is visible in the parent. |
STACK |
Each singleton's effective class (its override if it has one, else its own class) becomes the base of a fresh singleton with no override; classes that shared one singleton keep sharing one. An interface's override becomes its implementation, so it can be overridden again. |
Rules:
- Contexts are tracked per execution slice: the running
asyncio.Taskif code runs inside a Task, otherwise the currentthreading.Threadobject. A slice with nothing pushed sees the root context.Injector.get_context()returns the current slice's context. - A
threading.Threadstarts in the root context, regardless of the context of the thread that created it.smalldi.thread.ContextAwareThreadcaptures the creator's currentInjectionContextin__init__(the object itself, not a copy) and enters it for the duration ofrun(). - An
asyncio.Taskstarts in the root context. Opening injection contexts inside coroutines is not supported.@Injector.injecton anasync defreturns a coroutine function and resolvesProvide[T]when the coroutine starts running. Injector.context(kind)andInjector.foreign_context(ctx)return a single-use context manager. Nothing is pushed until__enter__; the child context is built from the context that is current at__enter__time, and__enter__returns theInjectionContext.__exit__pops exactly the context this manager pushed: exiting out of order raisesRuntimeErrorand leaves the stack unchanged, and entering the same manager twice raisesRuntimeError.- If a slice ends with contexts still open, they are removed and a
ResourceWarningis emitted. - A registration goes into the context that is current when the decorator runs. Python caches modules: a module
first imported inside
with Injector.context(...)keeps its registrations in that context only, and importing it again at the root does nothing. Import the modules that register singletons at the root. - A request for an instance freezes what it looks up in the current context. A
RESETorSTACKchild starts with everything unfrozen; aSHALLOWchild shares the parent's frozen state, andPRESERVEis the parent.
The docs have diagrams of what each kind shares with its parent and of the per-slice context stacks (contexts), the objects behind the registry (architecture) and the locking model (concurrency).
Utilities
Concurrency
smalldi.concurrency provides two decorators for serializing calls between threads:
-
@synchronizedguards a function with its own reentrant lock, shared by every caller. -
@threadsafepicks the lock from where the method is defined:- instance methods lock per instance,
@classmethods lock per class (subclasses get their own lock),@staticmethods lock per function, like@synchronized.
Applied outside a class body, it warns and falls back to
@synchronized.
Limitations
- Async functions (
async defand async generators) aren't supported yet: decorating one raisesNotImplementedError. - Plain generator functions are accepted, but the lock is held only while the generator object is created, not while it is iterated. Don't rely on either decorator to protect a generator's body.
@threadsafestores the instance lock as__instance_mutex__in the object's__dict__the first time a guarded method runs. Classes using__slots__must list__instance_mutex__in their slots.- Once that lock exists, the object can't be pickled or deep-copied (
TypeError: cannot pickle '_thread.RLock' object), andcopy.copymakes the copy share the original's lock. If you need copying, drop the lock from the state, and a fresh one is created on the next call:
def __getstate__(self):
state = self.__dict__.copy()
state.pop("__instance_mutex__", None)
return state
Collector
Collector is a class which imports all modules in order execute decorators.
Collector.collect_from_package
This method imports all modules in a given package.
Metadata
Release files for smalldi 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| smalldi-0.4.0.tar.gz | 80.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| smalldi-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.1 kB
Release files / smalldi-0.4.0.tar.gz
| Download URL | smalldi-0.4.0.tar.gz |
|---|---|
| Size | 80.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0e49ff07dd19fd0c1f4aa4c1b10daea4a3f47534cf63931fccdab43c20c5c490
|
|
BLAKE2b-256 checksum How to use checksums |
8ba032f6e8c5ffb7e8527d038293b968321d4bc094aad70090a1fa5bf11e8c62
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / smalldi-0.4.0-py3-none-any.whl
| Download URL | smalldi-0.4.0-py3-none-any.whl |
|---|---|
| Size | 41.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7c89903b78ea3d63f37a8ab167277ae346ce7ae2fce22a98406e68eb027edddc
|
|
BLAKE2b-256 checksum How to use checksums |
6fb221b55a4ab439f7a8f58a6d71983680dcaa8ad0ef96f61a9597b16d145146
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log