Simple python package to solve dependency injections
Project description
Pieceful
Description
Pieceful is a Python package that provides a collection of utility functions for working with dependency injection.
Installation
Install with
pip install pieceful
API reference
- Piece
- PieceFactory
- get_piece
- get_piece_by_name
- get_piece_by_supertype
- register_piece
- register_piece_factory
- PieceException
- PieceNotFound
- ParameterNotAnnotatedException
- AmbiguousPieceException
- PieceIncorrectUseException
- InitStrategy
- Scope
Tutorial
In this tutorial we explain basic usage of pieceful library on simple example.
Let's describe composition problem on abstraction level.
When we have a car instance that has it's driver and engine.
Car is an abstract vehicle concept that also depends on abstact driver and abstract engine.
First perform necessary import:
from typing import Annotated
from pieceful import Piece, PieceFactor, get_piece
Note:
pieceful's dependency injection specification relies ontyping.Annotatedannotation.
Abstraction can look like this:
from abc import ABC, abstractmethod
class AbstractEngine(ABC):
@abstractmethod
def run(self) -> None:
...
class AbstractDriver(ABC):
@abstractmethod
def drive(self) -> None:
...
class AbstractVehicle(ABC):
engine: AbstractEngine
driver: AbstractDriver
@abstractmethod
def start(self) -> None:
...
Then we can define implementations and decorate them as dependencies with the @Piece of @PieceFactory decorator.
This way pieces are added to the library registry.
@Piece("engine")
class PowerfulEngine(AbstractEngine):
def run(self):
print("Powerful engine is running and ready to go.")
class ResponsibleDriver(AbstractDriver):
def drive(self):
print("Responsible driver starts driving.")
@PieceFactory("reponsible_driver")
def driver_factory() -> ResponsibleDriver:
return ResponsibleDriver()
@Piece("car")
class Car(AbstractVehicle):
def __init__(
self,
engine: Annotated[AbstractEngine, "engine"],
driver: Annotated[AbstractDriver, "responsible_driver"]
):
self.engine = engine
self.driver = driver
def start(self) -> None:
self.engine.run()
self.driver.drive()
See that we are defining
nameof dependency in@Pieceor@PieceFactorydecorator.
When using
@PieceFactoryname is optional, when not specified, decorated function's name is used.
When using
@PieceFactoryfactory function must declare a return type, otherwise exception is thrown.
Now components can be injected to other components (like AbstractEngine -> Car) by using typing.Annotated or they can be directly obtained with get_piece function.
Example of get_piece function usage:
def main():
car = get_piece("car", AbstractVehicle)
Notice that
Cardepends onengineand adriver, that are injected in a constructor.
To tell the framework what dependencies we want to inject to our
Car, we usetyping.Annotated, where first argument has a meaning oftypeof dependency and second representsnameof ourPiece(Annotated[piece_type: Type[Any], piece_name: str]). This way, framework will recognize what to inject.
Notice that
mainfunction does not need to know anything about specific car implementation. Function depends only on abstract concept and dependecy inversion principle is followed this way. Also see that, functionget_piececan retrieve required dependency based on abstract type and dependency name. This framework also helps you following dependency inversion principle.
Now let's assume, that we want to use other driver dependency in our Car definition. Another driver type must be registered as dependency. When done, all it takes is to change dependency name in Car's constructor ("responsible_driver" -> "impetuous_driver"):
@Piece("impetuous_driver")
class ImpetuousDriver(AbstractDriver):
def drive(self):
print("Impetuous driver starts driving, be careful!")
@Piece("car")
class Car(AbstractVehicle):
def __init__(
self,
wheels: int,
engine: t.Annotated[AbstractEngine, "engine"],
driver: t.Annotated[AbstractDriver, "impetuous_driver"],
) -> None:
...
To repeat again,
Cardepends on abstract concepts, so bothResponsibleDriverandImpetuousDrivermatch typeAbstractDriverand can be injected as adriverparameter toCarconstructor. Dependencies are resolved by their name and type (or super-type of any level).
Other ways to register pieces
Registration is also possible through functions register_piece and register_piece_factory.
from pieceful import register_piece, register_piece_factory
class OtherCar(AbstractVehicle):
... # omitted code
# first option
register_piece(OtherCar, "other_car")
# other option
def other_car_factory() -> AbstractVehicle:
return OtherCar()
register_piece_factory(other_car_factory, "other_car")
Other ways to obtain pieces
Besides typing.Annotated and get_piece function, registered dependencies could be retrieved in groups by specifiing dependency name pattern (regex pattern) or dependency supertype.
For example:
from pieceful import get_pieces_by_name
get_pieces_by_name(".*driver$")
returns iterator of all registered dependencies that's name end with "driver" and calling function get_pieces_by_supertype:
from pieceful import get_pieces_by_supertype
get_pieces_by_supertype(AbstractDriver)
returns all registered pieces that's supertype is AbstractDriver.
Tip: call
get_pieces_by_supertype(object)to get all registered pieces.
Eager vs. Lazy initialization
Library allows to choose from two strategies of object initialization. Strategy can be specified when decorating class with @Piece or @PieceFactory with help of enum type: InitStrategy.
from pieceful import Piece, InitStrategy
@Piece("foo", strategy=InitStrategy.EAGER)
class Foo:
pass
@Piece("bar", strategy=InitStrategy.LAZY)
class Bar:
pass
InitStrategy.LAZY
Object is initialized just when its needed for the first time. That means object is obtained by any get function (e. g. get_piece) or is injected to the component that is being initialized. This approach is default.
InitStrategy.EAGER
Object is initialized at the same time interpreter reaches the registration. This approach is not recommended, because it's more tricky to understand when object is created inside library and depends on the order of imports.
Imagine importing some module in other python file, code of whole module is executed and this way also
@Pieceobject is created in library storage. This can lead to possible complications.
When registered many dependencies with EAGER strategies, all initializations may have impact on performance, because dependencies are created usually at application startup (usually, because for example with importlib behavior can be different).
Scope
Framework provides Scope enum, that is used when registering dependencies.
from pieceful import Piece, Scope
@Piece("baz", scope=Scope.UNIVERSAL)
class Baz:
pass
@Piece("qux", scope=Scope.ORIGINAL)
class Qux:
pass
Scope.UNIVERSAL
Takes care of creating one instance of piece and injection references to the same object where requested.
assert get_piece("baz", Baz) is get_piece("baz", Baz)
Scope.ORIGINAL
Creates new instance for every place dependency is requested.
assert get_piece("qux", Qux) is not get_piece("qux", Qux)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pieceful-0.4.1.tar.gz.
File metadata
- Download URL: pieceful-0.4.1.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.0.1 CPython/3.12.3 Linux/6.8.0-52-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9a221a0fefbdab4492af32ce9f8a090f60079c0030a76518fa7c25c062bb724
|
|
| MD5 |
163310e4256f9727f15ce412ecbc5b5f
|
|
| BLAKE2b-256 |
3dc981c98ee8a378e487bea2a8f642e3209e2e10f94710b26caccb279a20b4dd
|
File details
Details for the file pieceful-0.4.1-py3-none-any.whl.
File metadata
- Download URL: pieceful-0.4.1-py3-none-any.whl
- Upload date:
- Size: 9.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.0.1 CPython/3.12.3 Linux/6.8.0-52-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a7ae6e0cdd65e2e02482228958da427e09ba88aeb0d81ff948418c4374d0b20
|
|
| MD5 |
459fe0413e5434af2d9639d5dae48435
|
|
| BLAKE2b-256 |
c997b0270c23e1985f21cb44e251c4498f1015bd112d19434e2c5fa3cebe784e
|