Skip to main content

Simple python package to solve dependency injections

Project description

Pieceful

License Python Version

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
  • provide
  • get_piece_by_name
  • get_piece_by_supertype
  • register_piece
  • register_piece_factory
  • PieceException
  • PieceNotFound
  • ParameterNotAnnotatedException
  • AmbiguousPieceException
  • PieceIncorrectUseException
  • InitStrategy
  • Scope

Register piece

This library provides 4 ways to register a dependency. Decorator versions are just wrappers and use register_piece or register_piece_factory internally.

@Piece() # piece name autodetected by class name: "Car"
class Car:
    pass
class Car:
    pass

@PieceFactory() # piece name autodetected by factory function name: "car"
def car() -> Car:
    return Car()
class Car:
    pass

register_piece(Car) # piece name autodetected
class Car:
    pass

def car() -> Car:
    return Car()

register_piece_factory(car) # piece name autodetected

Piece that depends on something else

Class or factory function that define a depencency (piece) can require arguments, when every satisfies one of following conditions. Argument satisfies when:

  • has a default value (arg=1) (if specified takes priority over other possibilities). In that case default value is used within dependency creation.
@Piece()
class Car:
    def __init__(self, wheels=4): ...
  • is typed with typing.Annotated[t, name] when name is the str specifying other registered dependency name.
@Piece()
class Car:
    def __init__(
        self,
        engine: Annotated[Engine, "engine"], # target other piece
    ): ...
  • is typed with typing.Annotated[t, factory] when factory is a function with zero args that provide value for argument.
@Piece()
class Car:
    def __init__(
        self,
        fuel_percent: Annotated[int, lambda: random.randint(0, 100)]
    ): ...
  • is typed (arg: OtherPiece) when OtherPiece is type of other registered dependency with name "OtherPiece" (autodetected).
@Piece()
class Car:
    def __init__(
        self,
        engine: Engine, # other piece with name "Engine"
    ): ...

Retrieve piece

Package provides several options to retrieve a registered piece.

provide(Car, "my_car")
provide(Car) # get piece with name "Car" (autodetected)
get_piece("my_car", Car) # deprecated
get_pieces_by_supertype(Car) # get all pieces with Car supertype
get_pieces_by_name("^my_.*") # get all pieces with name matching pattern

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 on typing.Annotated annotation.

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 name of dependency in @Piece or @PieceFactory decorator.

When using @PieceFactory name is optional, when not specified, decorated function's name is used.

When using @PieceFactory factory 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 = provide(AbstractVehicle, "car")

Notice that Car depends on engine and a driver, that are injected in a constructor.

To tell the framework what dependencies we want to inject to our Car, we use typing.Annotated, where first argument has a meaning of type of dependency and second represents name of our Piece (Annotated[piece_type: Type[Any], piece_name: str]). This way, framework will recognize what to inject.

Notice that main function 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, function get_piece can 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,
        engine: t.Annotated[AbstractEngine, "engine"],
        driver: t.Annotated[AbstractDriver, "impetuous_driver"],
        wheels: int = 4,
    ) -> None:
        ...

To repeat again, Car depends on abstract concepts, so both ResponsibleDriver and ImpetuousDriver match type AbstractDriver and can be injected as a driver parameter to Car constructor. 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 @Piece object 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pieceful-1.0.0.tar.gz (11.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pieceful-1.0.0-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

File details

Details for the file pieceful-1.0.0.tar.gz.

File metadata

  • Download URL: pieceful-1.0.0.tar.gz
  • Upload date:
  • Size: 11.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.0.1 CPython/3.12.3 Linux/6.8.0-55-generic

File hashes

Hashes for pieceful-1.0.0.tar.gz
Algorithm Hash digest
SHA256 f0b4af530c9dc7d063eea87d6544665032ca65a959b520692c063e9a5ad840e8
MD5 8b3de8daba9ffac499b7b4f696405e32
BLAKE2b-256 7e8a49c1a16c8238c71a61217fcc48fc3a97eadccb892ce9b46479c0082e665c

See more details on using hashes here.

File details

Details for the file pieceful-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pieceful-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 11.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.0.1 CPython/3.12.3 Linux/6.8.0-55-generic

File hashes

Hashes for pieceful-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 26fe427b9658357cd902cf43e689a818f492f8b63adfce42cb16919738884c03
MD5 9acafea29046a0b9527e389d2129e870
BLAKE2b-256 050d5b44ae2aa5972c43a01f9ec5bb052c08be20647bbf3f66d7f9d3663abb47

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page