A new dependency injection library for Python 3 with an original and clean design.
Project description
Wirinj
A new dependency injection library for Python 3 with an original and clean design.
Why another dependency injection library?
Working on a large project I tried out python-dependency-injector first. Then, I decided to switch to pinject. As none of them had the features I wanted, I finally decided to write my own library to meet my needs:
- Minimal boiler plate code.
- Easy refactorings.
- Friendly with IDE's code completion (e.g. with PyCharm).
- Using type annotations.
- Easy to define and use of factories.
- Autowiring option.
- Easy to fix dependency problems.
- Detailed injection reports.
- Powerful but simple wiring configuration.
- Avoiding name conventions.
- Avoiding dependencies to the injection library itself.
- Private injection.
- Open and extendable architecture.
Installation
python >= 3.5
$ pip install wirinj
Minimal example
from wirinj import Injector, Autowiring
class Cat:
pass
inj = Injector(Autowiring())
cat = inj.get(Cat)
print(cat)
Returns:
<__main__.Cat object at 0x7fbe537f52b0>
Basic usage
Example (basic_usage.py):
from wirinj import Injector, Definitions, Instance, Singleton, Factory
class Cat:
pass
class Dog:
pass
# Wiring definitions
defs = {
'sound': 'Meow',
Cat: Instance(),
Dog: Singleton(),
'cat_factory': Factory(Cat),
}
# Create injector
inj = Injector(Definitions(defs))
Get a 'sound'
:
sound = inj.get('sound')
print(sound)
Meow
Get a Cat
:
cat1 = inj.get(Cat)
cat2 = inj.get(Cat)
print(cat1)
print(cat2)
print(cat1 is cat2)
<__main__.Cat object at 0x7fab3ddc2208>
<__main__.Cat object at 0x7fab3c1fa438>
False
Cat
returns different instances each time because it is defined as an Instance()
.
Get the Dog
:
dog = inj.get(Dog)
dog2 = inj.get(Dog)
print(dog)
print(dog2)
print(dog is dog2)
<__main__.Dog object at 0x7fab3c1fa748>
<__main__.Dog object at 0x7fab3c1fa748>
True
Dog
always returns the same object because it is defined as a Singleton()
.
Get a cat factory
and use it to create a new Cat
:
cat_factory = inj.get('cat_factory')
cat3 = cat_factory()
print(cat3)
<__main__.Cat object at 0x7fc6a98119b0>
cat_factory
returns a factory because it is defined as a Factory(Cat)
.
The @inject decorator
If you are using an IDE such as PyCharm, you will notice that code completion do not work with the previous example.
The IDE cannot know the type of, for example, cat1
or dog
.
You can be IDE-friendly by decorating a function with @inject
:
Example (inject_function.py):
from wirinj import inject
defs = {
'sound': 'Meow',
Cat: Instance(),
Dog: Singleton(),
'cat_factory': Factory(Cat),
}
@inject(Definitions(defs))
def fn(cat1: Cat, cat2: Cat, dog: Dog, sound, cat_factory):
print('sound:', sound)
print('cat1:, cat1)
print('cat2:', cat2)
print('cat1 = cat2: ', cat1 is cat2)
print('dog:', dog)
cat3 = cat_factory()
print('cat3:', cat3)
fn()
Returns:
sound: Meow
cat1: <__main__.Cat object at 0x7f675ce5db38>
cat2: <__main__.Cat object at 0x7f675ce5df28>
cat1 = cat2: False
dog: <__main__.Dog object at 0x7f675ce5dac8>
cat3: <__main__.Cat object at 0x7f675ce72400>
@inject
inspects the arguments of the fn
function signature and inject the required dependencies.
You get all the needed dependencies through the function arguments.
You type less and it is clearer and IDE friendly.
As cat1
and cat2
arguments matches Cat: Instance()
configuration, two new Cat
instances are injected.
Simillary, dog
matches Dog: Singleton()
.
Therefore, any Dog
argument will be injected with the same single Dog
.
In contrast, for sound
and cat_factory
, the configuration entry is matched by name and not by class.
This is because, in the wiring configuration dict
, the key of 'sound': 'Meow'
and 'cat_factory': Factory(Cat)
are of type string
:
- When the key is a class, the injector will look at the type annotation of the argument.
- When the key is a string the injector will look at the name of the argument.
Factories
You can object that, in the previos example, the code completion will not work with cat_factory
.
That's true; fortunately, this can be easily fixed:
IDE friendly factories
Just add a type annotation of type Type[Cat]
to the factory argument:
Example (ide_friendly_factory.py):
from typing import Type
class Cat:
def __init__(self, sound):
pass
...
@inject(...)
def fn(cat_factory: Type[Cat]):
cat = cat_factory('Meow')
print('cat:', cat)
fn()
Now, it is recognized by the IDE:
Type[Cat]
, as explained in the typing module docs, represents the class Cat
or a subclass of it:
from typing import Type
class Cat:
pass
class BlackCat(Cat):
pass
def func(cat_class: Type[Cat]):
return cat_class()
print('Cat: ', func(Cat))
print('BlackCat: ', func(BlackCat))
Returns:
Cat: <__main__.Cat object at 0x7fc458bc7588>
BlackCat: <__main__.BlackCat object at 0x7fc458bc7588>
Defining the factory by type
To not be limited to a particular name, such as cat_factory
, we can define the factory dependency by type.
defs = {
Type[Cat]: Factory(),
}
This way you can name the argument as you like, as long as its type annotation is Type[Cat]
.
For example:
@inject(...)
def fn(cats: Type[Cat]):
cat = cats()
Dependency definitions
Definitions
class is a convenient way to configure the wiring of your classes according to a definition dict
.
Most of the previous examples have made use of a Definitions
object with a dict
as an argument.
Now I'm going to explain it in detail.
Definition format
One or several dict
define your wiring configuration.
Each key represents the argument to be injected.
The value represents how is it injected:
defs = {
Cat: Instance(),
'dog': Singleton(Dog),
}
inj = Injector(Definitions(defs))
dict keys
-
If the key is a
class
it will match the argument's type annotation. E.g.: the first key in the example above causes any argument of typeCat
, no matter its name, to be injected with a new instance ofCat
. -
If the key is a
string
it will match the argument name. E.g.: the second key causes any argument whose name is'dog'
to be injected with a uniqueDog
instance. Here, as the class can't be inferred from the key, you need to explicitly provide the class as an argument:'dog': Singleton(Dog)
.
dict values
Each value in the dict
can be:
- A literal value you want to be injected. E.g.
'db_name': 'my-db'
. Instance
: inject a new instance each time.Singleton
: inject the same single instance every time.Factory
: inject a factory object that can be called to create new objects dynamically.CustomInstance
: similar toInstance
but you provide a custom function which returns new instances.CustomSingleton
: similar toSingleton
but you provide a custom function which returns the singleton.- Other user defined subclasses of
DependencyBuilder
orDependency
.
Example (definition_types.py):
def dog_creator():
""" Custom instantiation """
...
return dog
defs = {
House: Singleton(),
Cat: Instance(),
Type[Cat]: Factory(),
Dog: CustomInstance(dog_creator),
Type[Dog]: Factory(),
}
@inject(Definitions(defs))
def fn(house: House, cat_factory: Type[Cat], dog_factory: Type[Dog]):
cat = cat_factory()
dog = dog_factory()
...
fn()
Providing a class
Instance
, Singleton
and Factory
accept an optional class argument to specify the class of the object to be created.
You pass the class in two use cases:
- The key is a
string
and therefore the dependency class is undefined. - The argument is annotated with a base class but you want to provide a subclass of it.
Example of both cases (explicit_type.py):
class Pet:
pass
class Cat(Pet):
pass
class Dog(Pet):
pass
defs = {
'cat': Singleton(Cat),
Pet: Singleton(Dog),
}
@inject(Definitions(defs))
def fn(cat, pet: Pet):
print('cat is a', cat.__class__.__name__)
print('pet is a', pet.__class__.__name__)
fn()
returns:
cat is a Cat
pet is a Dog
Creation path
class Nail:
pass
class Leg:
def __init__(self, nail: Nail):
pass
class Cat:
def __init__(self, leg: Leg):
pass
Imagine you ask for a Cat
which requires a Leg
which requires a Nail
.
The injector will gather:
- First, the
Nail
that has no dependencies. - Then, the
Leg
with theNail
as an argument. - Finally, the
Cat
with theLeg
as an argument.
We can think of this process as a path: Cat
-> nail:Nail
-> leg:Leg
.
I call this the creation path
.
You can explicitly specify a creation path
constraint in the definition dict
.
Example (creation_path.py):
class Animal:
def __init__(self, sound):
self.sound = sound
class Dog(Animal):
pass
class Cat(Animal):
pass
class Cow(Animal):
pass
defs = {
Dog: Instance(),
Cat: Instance(),
Cow: Instance(),
(Dog, 'sound'): 'woof',
(Cat, 'sound'): 'meow',
'sound': '?',
}
@inject(Definitions(defs))
def fn(cat: Cat, dog: Dog, cow: Cow):
print('Cat:', cat.sound)
print('Dog:', dog.sound)
print('Cow:', cow.sound)
fn()
Returns:
Cat: meow
Dog: woof
Cow: ?
To restrict a definition entry to a particular creation path
we use a tuple
in the key part.
This tuple
must match the last entries in the creation path
.
For each tuple
entry, a string
refers to the argument name and a class
refers to the argument type annotation.
If two entries match the required dependency, the more specific one will be chosen.
Custom dependencies
Instance
and Singleton
are used for simple class instantiation.
When a custom process is required to create or locate the dependency, use CustomInstance
or CustomSingleton
.
Both take a function
as an argument.
Example (custom_dependencies.py):
from random import randint
class Cat:
def __init__(self, color, weight):
self.color = color
self.weight = weight
def __str__(self):
return 'A {1} pounds {0} cat.'.format(self.color, self.weight)
def create_cat(color):
return Cat(color, randint(4, 20))
defs = {
'color': 'blue',
Cat: CustomInstance(create_cat),
}
@inject(Definitions(defs))
def fn(cat1: Cat, cat2: Cat, cat3: Cat):
print(cat1)
print(cat2)
print(cat3)
fn()
returns:
A 11 pounds blue cat.
A 5 pounds blue cat.
A 14 pounds blue cat.
Custom dependencies with arguments
In the previous example, the object is instantiated without arguments,
so all of __init__
's arguments are injected from dependencies.
If your class requires some arguments to be passed to be created (explicit arguments) in addition to the automatically injected dependencies (injection arguments), I recommend to follow these rules:
-
In the
__init__
method, place the explicit arguments in the first place and then, the injection arguments. This allow you to use positional arguments when you create the object. -
Set the default value of the injection arguments to
None
. This way the IDE code completion will not complain about missing arguments. Also, this is the only way you can have defaults in your explicit arguments if they are followed by injection arguments. -
About the creation function that you pass to
CustomInstance
, use the same name and position for the explicit arguments as you use in the__init__
method. The rest of the arguments don't have to be related at all to the__init__
arguments. Indeed, you can specify as many dependency arguments as you need to create the object. The injection process will inspect the function signature and will provide them.
Example (custom_dependencies_with_args.py):
class Cat:
def __init__(self, name, color=None, weight=None):
self.name = name
self.color = color
self.weight = weight
def __str__(self):
return '{0} is a {2} pounds {1} cat.'.format(self.name, self.color, self.weight)
def create_cat(name, color):
return Cat(name, color, randint(4, 20))
defs = {
'color': 'black',
Cat: CustomInstance(create_cat),
Type[Cat]: Factory(),
}
@inject(Definitions(defs))
def fn(factory: Type[Cat]):
cat = factory('Tom')
print(cat)
cat2 = factory('Sam')
print(cat2)
fn()
returns:
Tom is a 8 pounds black cat.
Sam is a 14 pounds black cat.
About the 3 arguments of Cat
.__init__
:
- One comes from calling the factory.
- Another one from the dependency configuration.
- The third is generated by the custom creation function.
Setting default to Injected
Instead of setting the default value of the injection arguments to None
, you can use the special value Injected
:
- Pros: if one argument is missing, a specific
MissingDependenciesError
is raised. Otherwise, the execution would continue withNone
default and fail later in an unpredictable way. - Cons: you add a dependency to
wirinj
.
from wirinj import Injected
class Cat:
def __init__(self, name, color=Injected, weight=Injected):
...
Splitting definitions
You can split the dependency configuration in several dict
definitions.
Example (splitted_definitions.py):
defs1 = {
Cat: Instance(),
Type[Cat]: Factory()
}
defs2 = {
Engine: Instance(),
Type[Engine]: Factory()
}
@inject(Definitions(defs1, defs2))
def fn(...):
...
fn()
Definitions
accepts any number of definition dict
s.
Autowiring
Dependency injection comes at the cost of having to maintain it.
The Autowiring
class makes this task lighter.
You add Autowiring
at the end of the wiring configurations as a last resort to provide a dependency on the fly when it is undefined.
The dependency type, Instance
, Singleton
or Factory
, is chosen by heuristic rules.
Let's view an example:
Example (autowiring.py):
class Cat:
pass
class Dog:
pass
class Horse:
pass
defs = {
Cat: Instance(),
}
@inject(Definitions(defs), Autowiring())
def fn(cat: Cat, dog: Dog, horse_factory: Type[Horse]):
print(cat.__class__.__name__)
print(dog.__class__.__name__)
horse = horse_factory()
print(horse.__class__.__name__)
fn()
Returns:
Cat
Dog
Horse
3 dependencies are automatically generated by Autowiring
:
- A
Dog
singleton. - A
Type[Horse]
factory. - A
Horse
instance.
Heuristic rules
Autowiring
works only for arguments that have a type annotation:
- If the type of the annotation is a
class
, as withdog: Dog
in the previous example, a singleton will be generated. - If the type is a
Type[class]
, as withhorse_factory: Type[Horse]
, a factory will be provided. - If the injection occurs in a factory, as when
horse_factory()
is called, an instance will be created.
Autowiring for production
In my opinion, this kind of magic should not be used in production environments; you should not take the risk to leave such important wiring decisions in the hands of a heuristic algorithm.
Fortunately, you can use AutowiringReport
class to easily convert the autowiring configuration into a regular dependency definition:
Autowiring report
It's quite simple to use; just pass an AutowiringReport
instance to Autowiring
:
Example (autowiring_report.py):
report = AutowiringReport()
@inject(Definitions(deps), Autowiring(report))
def fn(cat: Cat, dog: Dog, horse_factory: Type[Horse]):
...
fn()
print(report.get())
Returns:
...
--------------- wirinj ---------------
Autowiring report:
Definitions({
Dog: Singleton(),
Type[Horse]: Factory(),
Horse: Instance(),
}),
--------------------------------------
Call report.get()
to get the report.
Review and copy the definitions to your configuration file, remove Autowiring
, and you will be production ready.
No singletons option
You may set use_singletons
to False
to force all dependencies to be injected as an Instance
.
Autowiring(use_singletons=False)
Injection reports
During each injection process, a dependency tree is built with all the dependencies that are being gathered.
As you change your code, your dependency configuration can get out of sync.
wirinj
include reporting features that can help you to solve this dependency issues:
Debugging the injection
The injection process can be debugged to expose the creation order and the dependency tree.
Take this composition of classes (cat_example_classes.py) :
class Nail:
pass
class Leg:
def __init__(self, nail1: Nail, nail2: Nail, nail3: Nail, nail4: Nail, nail5: Nail):
pass
class Mouth:
pass
class Ear:
pass
class Eye:
pass
class Head:
def __init__(self, mouth: Mouth, ear1: Ear, ear2: Ear, eye1: Eye, eye2: Eye):
pass
class Body:
pass
class Tail:
pass
class Cat:
def __init__(self, head: Head, body: Body, tail: Tail, leg1: Leg, leg2: Leg, leg3: Leg, leg4: Leg):
pass
Now, we can debug the injection process just by setting the logging level to DEBUG
and then, requesting a Cat
from the Injector
:
Example (injection_debug_report.py):
import logging
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
inj = Injector(Autowiring(use_singletons=False))
cat = inj.get(Cat)
Note that we are replacing all the dependency definitions by a simple Autowiring()
.
We pass the argument use_singletons=False
to force all dependencies to be injected as an Instance
.
By default Autowiring
generates Singleton
dependencies and, in this case, we don't want all the legs of the Cat
to be the same.
The code above returns this:
--------------- wirinj ---------------
mouth:Mouth
ear1:Ear
ear2:Ear
eye1:Eye
eye2:Eye
head:Head
body:Body
tail:Tail
nail1:Nail
nail2:Nail
nail3:Nail
nail4:Nail
nail5:Nail
leg1:Leg
nail1:Nail
nail2:Nail
nail3:Nail
nail4:Nail
nail5:Nail
leg2:Leg
nail1:Nail
nail2:Nail
nail3:Nail
nail4:Nail
nail5:Nail
leg3:Leg
nail1:Nail
nail2:Nail
nail3:Nail
nail4:Nail
nail5:Nail
leg4:Leg
:Cat
--------------------------------------
You can see how all the dependencies are gathered, and in which order.
The final object is the requested Cat
object.
Missing dependencies
Injection doesn't stop when a dependency is missing. It continues building the dependency tree as far as it can. This makes it possible to fix several dependency issues in one shot.
If one ore more dependencies are missing, an ERROR
level report will be logged.
Therefore, you don't need to change the logging
level to get it; just look above the error traceback after a dependency exception.
Example (missing_dependencies_report.py):
from examples.report.cat_example_classes import Cat, Head
from wirinj import Injector, Definitions, Instance
inj = Injector(Definitions({
Cat: Instance(),
Head: Instance(),
}))
cat2 = inj.get(Cat)
In the example above, we only define the wiring for Cat
and Head
.
All the other dependencies, such as Mouth
, Ear
, Eye
, etc, are undefined.
After running the example, we get:
--------------- wirinj ---------------
Missing dependencies:
mouth:Mouth *** NotFound ***
ear1:Ear *** NotFound ***
ear2:Ear *** NotFound ***
eye1:Eye *** NotFound ***
eye2:Eye *** NotFound ***
head:Head
body:Body *** NotFound ***
tail:Tail *** NotFound ***
leg1:Leg *** NotFound ***
leg2:Leg *** NotFound ***
leg3:Leg *** NotFound ***
leg4:Leg *** NotFound ***
:Cat
--------------------------------------
Traceback (most recent call last):
...
wirinj.errors.MissingDependenciesError: Missing dependencies.
Notice that, although the first dependency, Mouth
, failed to be satisfied, the injection process continues in order to gather as much information as possible about the missing dependencies.
With this report, it becomes clear which classes are undefined, and what needs to be added in the injection configuration.
Instance error
If an exception is raised during the instantiation of any of the dependencies,
you will not get the dependency tree
logs as it happens when a dependency is missing.
You'll need to track the stack trace to fix the problem.
However, there is a task planned in the TO-DO list to log the dependency tree
in these cases too.
Private injection
You use dependency injection to decouple your class from its dependencies. A factory is a specific type of dependency which allows you to create objects dynamically.
Some factories require arguments to create the new object.
These arguments are passed to the __init__
method along with other required dependencies.
I call private injection to a special form of injection that separates the __init__
arguments in two groups:
- Public arguments: those you pass to the factory.
- Private arguments: other implementation dependencies.
Let's compare the two modes:
Regular injection
Some of the __init__
arguments come from the factory call;
the remaining ones are injected automatically.
Example (regular_injection.py):
class Cat:
def __init__(self, color, weight, feeder: Feeder = None):
...
@inject(...)
def fn(factory: Type[Cat]):
cat = factory('blue', 12)
...
While color
and weight
are passed to the factory, the feeder
argument is injected.
Private injection
This is the equivalent private form for the previous code:
Example (private_injection.py):
class Cat:
def __deps__(self, feeder: Feeder):
pass
@deps
def __init__(self, color, weight):
...
@inject(...)
def fn(factory: Type[Cat]):
cat = factory('blue', 12)
The @deps
decorator works this way:
- The
@deps
decorator on__init__
enables private injection. - The special method
__deps__
define which dependencies are required. - All the dependencies are located and injected before the object is initialized.
- Finally, the real
__init__
method is called.
Notice that __deps__
is a mock method, it is not ever called.
Its sole purpose is to define the dependencies through its argument signature.
Why using private injection
You split the initialization arguments in two parts:
__init__
takes only the public API arguments.- Private arguments are hidden and defined by the
__deps__
method.
pros:
- You keep your implementation dependencies apart of the public
__init__
interface. - Calling a factory is beautifully identical to instantiate a real class.
- You keep the same
__init__
signature on all subclasses. - Each subclass can have different dependencies.
- You don't have to code a new factory wrapper for each class to hide the implementation dependencies.
- You can use positional arguments to call the factory.
- You don't need to set the default values to
Null
for the dependency arguments. - You can freely use default values in
__init__
.
cons:
- Your class will be dependent on the
@deps
decorator.
@deps
decorator
In the previous sections, I have explained how this decorator is used. What I'm going to explain here is related only to the internal functioning of the feature.
When you wrap __init__
with the decorator @deps
, two simple things happen:
0. A hidden argument _dependencies
is added to __init__
waiting for a dict
to be passed.
0. The object is initialized with these dependencies before __init__
is called.
Notice that you can use this mechanism, even if you are not using other wirinj
features, by setting the _dependencies
argument manually.
By the time __init__
is called, your object dependencies will already be initialized.
Enabling code completion with __deps__
Of course, the IDE (e.g. PyCharm
) will not detect the dependencies defined by __deps__
:
class Cat:
def __deps__(self, feeder: Feeder):
pass
Ideally, an IDE plugin would detect the __deps__
signature, making the dependencies available in code completion.
As long as this plugin doesn't exist, you can achieve the same by adding some mock code to the __deps__
method:
class Cat:
def __deps__(self, feeder: Feeder):
self.feeder = feeder
It will never be executed, but now, the IDE will recognize the dependencies as member variables:
A full injection example
This silly example aims to illustrate several aspects of the wirinj
library.
The two main classes, Bob
and Mike
, extend PetDeliveryPerson
.
They are used to deliver pets to the client using one or more vehicles.
Whenever a Vehicle
is needed, it is built in advance.
While Bob
uses his only vehicle by repeating the route several times, Mike
builds a fleet of autonomous vehicles to deliver all the pets in one trip.
The classes (pet_delivery/classes.py):
class Pet:
def __deps__(self, sound: str, weight):
self.sound = sound
self.weight = weight
@deps
def __init__(self, gift_wrapped):
self.gift_wrapped = gift_wrapped
def cry(self):
return self.sound.lower() if self.gift_wrapped else self.sound.upper()
class Cat(Pet):
pass
class Dog(Pet):
pass
class Bird(Pet):
pass
class Part:
def __deps__(self, mount_sound):
self.mount_sound = mount_sound
@deps
def __init__(self):
pass
def mount(self):
return self.mount_sound
class Engine(Part):
pass
class Plate(Part):
pass
class Wheel(Part):
pass
class Container(Part):
pass
class VehicleBuilder:
def __deps__(self,
engine_factory: Type[Engine],
plate_factory: Type[Plate],
wheel_factory: Type[Wheel],
container_factory: Type[Container],
):
self.engine_factory = engine_factory
self.plate_factory = plate_factory
self.wheel_factory = wheel_factory
self.container_factory = container_factory
@deps
def __init__(self):
pass
def build(self, recipe: Dict):
parts = [] # type: List[Part]
parts += [self.engine_factory() for _ in range(recipe.get('engines', 0))]
parts += [self.plate_factory() for _ in range(recipe.get('plates', 0))]
parts += [self.wheel_factory() for _ in range(recipe.get('wheels', 0))]
parts += [self.container_factory() for _ in range(recipe.get('containers', 0))]
mounting = ''
for part in parts:
mounting += ' ' + part.mount()
return mounting
class Vehicle:
def __deps__(self, builder: VehicleBuilder, recipe: Dict, max_load_weight):
self.builder = builder
self.recipe = recipe
self.max_load_weight = max_load_weight
@deps
def __init__(self):
self.pets = []
self.build()
def go(self, miles):
logger.info('{} goes {} miles'.format(self.__class__.__name__, miles))
def come_back(self):
logger.info('{} commes back'.format(self.__class__.__name__))
def build(self):
logger.info('{} is built: {}'.format(
self.__class__.__name__,
self.builder.build(self.recipe)
))
def get_available_load(self):
return self.max_load_weight - sum(pet.weight for pet in self.pets)
class Car(Vehicle):
pass
class Van(Vehicle):
pass
class Truck(Vehicle):
pass
class PetLoader:
def upload(self, pets: List[Pet], vehicle: Vehicle):
info = 'Uploading to the {}:'.format(vehicle.__class__.__name__.lower())
while pets:
pet = pets.pop()
if vehicle.get_available_load() >= pet.weight:
vehicle.pets.append(pet)
info += ' ' + pet.__class__.__name__
else:
pets.append(pet)
break
logger.info(info)
def download(self, vehicle):
logger.info('{} pets delivered'.format(len(vehicle.pets)))
vehicle.pets = []
class PetPicker:
def __deps__(self, pet_store: Type[Pet]):
self.pet_store = pet_store
@deps
def __init__(self):
# raise Exception('HORROR!!!!')
pass
def pick(self, qty, gift_wrapped):
info = 'Picking pets up: '
pets = []
for _ in range(qty):
pet = self.pet_store(gift_wrapped)
info += ' ' + pet.cry()
pets.append(pet)
logger.info(info)
return pets
class PetDeliveryPerson:
@deps
def __init__(self):
pass
def deliver(self, pet_qty, miles, gift_wrapped):
pass
class Bob(PetDeliveryPerson):
"""Bob builds a car and deliver pets in his vehicle repeating the route several times."""
def __deps__(self, vehicle: Vehicle, pet_picker: PetPicker, pet_loader: PetLoader):
self.vehicle = vehicle
self.pet_picker = pet_picker
self.pet_loader = pet_loader
def deliver(self, pet_qty, miles, gift_wrapped):
# Pick up pets
pets = self.pet_picker.pick(pet_qty, gift_wrapped)
# Bob owns one vehicle only
while pets:
self.pet_loader.upload(pets, self.vehicle)
self.vehicle.go(miles)
self.pet_loader.download(self.vehicle)
self.vehicle.come_back()
class Mike(PetDeliveryPerson):
"""Mike builds several autonomous vehicles and use them to deliver the pets all together"""
def __deps__(self, vehicle_factory: Type[Vehicle], pet_picker: PetPicker, pet_loader: PetLoader):
self.vehicle_factory = vehicle_factory
self.pet_picker = pet_picker
self.pet_loader = pet_loader
@deps
def __init__(self):
super().__init__()
self.vehicles = [] # type: List[Vehicle]
def get_vehicle(self):
if self.vehicles:
return self.vehicles.pop()
else:
return self.vehicle_factory()
def park_vehicles(self, vehicles):
self.vehicles += vehicles
def deliver(self, pet_qty, miles, gift_wrapped):
# Pick up pets
pets = self.pet_picker.pick(pet_qty, gift_wrapped)
# Get vehicles and upload them
vehicles = []
while pets:
vehicle = self.get_vehicle()
vehicles.append(vehicle)
self.pet_loader.upload(pets, vehicle)
# Go
for vehicle in vehicles:
vehicle.go(miles)
# Deliver pets
for vehicle in vehicles:
self.pet_loader.download(vehicle)
# Come back
for vehicle in vehicles:
vehicle.come_back()
# Park
self.park_vehicles(vehicles)
Injection definitions (pet_delivery/defs.py):
pet_defs = {
Dog: Instance(),
Cat: Instance(),
Bird: Instance(),
(Dog, 'sound'): 'Woof',
(Dog, 'weight'): 10,
(Cat, 'sound'): 'Meow',
(Cat, 'weight'): 5,
(Bird, 'sound'): 'Chirp',
(Bird, 'weight'): 0.1,
}
vehicle_defs = {
Engine: Instance(),
Plate: Instance(),
Wheel: Instance(),
Container: Instance(),
Type[Engine]: Factory(),
Type[Plate]: Factory(),
Type[Wheel]: Factory(),
Type[Container]: Factory(),
VehicleBuilder: Singleton(),
(Engine, 'mount_sound'): 'RRRRoarrr',
(Plate, 'mount_sound'): 'plaf',
(Wheel, 'mount_sound'): 'pffff',
(Container, 'mount_sound'): 'BLOOOOM',
Car: Instance(),
(Car, 'max_load_weight'): 10,
(Car, 'recipe'): {
'engines': 1,
'plates': 6,
'wheels': 4,
},
Van: Instance(),
(Van, 'max_load_weight'): 50,
(Van, 'recipe'): {
'engines': 1,
'plates': 8,
'wheels': 4,
},
Truck: Instance(),
(Truck, 'max_load_weight'): 200,
(Truck, 'recipe'): {
'engines': 1,
'plates': 20,
'wheels': 12,
'container': 1,
},
}
common_defs = {
PetPicker: Singleton(),
PetLoader: Singleton(),
Bob: Singleton(),
Mike: Singleton(),
}
Running the app (pet_delivery/example_1.py):
world_one_defs = {
(Bob, Vehicle): Singleton(Car),
(Bob, PetPicker, Type[Pet]): Factory(Bird),
(Mike, Type[Vehicle]): Factory(Van),
(Mike, PetPicker, Type[Pet]): Factory(Cat),
}
world_one = Definitions(
pet_defs,
vehicle_defs,
common_defs,
world_one_defs,
)
logging.basicConfig(format='%(message)s', level=logging.INFO)
@inject(world_one)
def do(bob: Bob, mike: Mike):
bob.deliver(100, 5, False)
bob.deliver(50, 200, True)
mike.deliver(20, 1000, True)
do()
Running the same app with another wiring configuration (pet_delivery/example_2.py):
world_two_defs = {
(Bob, Vehicle): Singleton(Van),
(Bob, PetPicker, Type[Pet]): Factory(Cat),
(Mike, Type[Vehicle]): Factory(Truck),
(Mike, PetPicker, Type[Pet]): Factory(Dog),
}
world_two = Definitions(
pet_defs,
vehicle_defs,
common_defs,
world_two_defs,
)
logging.basicConfig(format='%(message)s', level=logging.INFO)
@inject(world_two)
def do(bob: Bob, mike: Mike):
bob.deliver(100, 5, False)
bob.deliver(50, 200, True)
mike.deliver(20, 1000, True)
do()
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.