Welcome to haps documentation!¶
Haps [χaps] is a simple DI library, with IoC container included. It is written in pure Python with no external dependencies.
Look how easy it is to use:
from haps import Container as IoC, Inject, inject
# import interfaces
from my_application.core import IDatabase, IUserService
class MyApp:
db: IDatabase = Inject() # dependency as a property
@inject # or passed to the constructor
def __init__(self, user_service: IUserService) -> None:
self.user_service = user_service
IoC.autodiscover('my_application') # find all interfaces and implementations
if __name__ == '__main__':
app = MyApp()
assert isinstance(app.db, IDatabase)
assert isinstance(app.user_service, IUserService)
Features¶
- IoC container
- No XML/JSON/YAML - pure python configuration
- No dependencies
- Based on the Python 3.6+ annotation system
Contribute¶
- Issue Tracker: github.com/ekiro/haps/issues
- Source Code: github.com/ekiro/haps
Changelog¶
1.1.1 (2018-07-27)¶
- Fix bug with optional arguments for functions decorated with
@inject
1.1.0 (2018-07-26)¶
- Add configuration module
- Add application class and runner
- Add profiles
- Minor fixes
1.0.5 (2018-07-12)¶
@egg
decorator can be used without function invocation
1.0.4 (2018-06-30)¶
- Add support for python 3.7
- Fix autodiscover sample
Support¶
If you are having issues, ask a question on projects issue tracker.
License¶
The project is licensed under the MIT license.
QuickStart¶
Here’s a simple tutorial on how to write your first application using haps. Assuming you have already created an environment with python 3.6+ and haps installed, you can start writing some juicy code.
Application layout¶
Since haps doesn’t enforce any project/code design (you can use it even as an addition to your existing Django or flask application!), this is just an example layout. You are going to create a simple user registration system.
quickstart/
├── setup.py
└── user_module/
├── app.py
├── core
│ ├── implementations/
│ │ ├── __init__.py
│ │ ├── db.py
│ │ └── others.py
│ ├── __init__.py
│ └── interfaces.py
└── __init__.py
Interfaces¶
Let’s start with creating some interfaces, so we can keep our code clean and readable:
# quickstart/user_module/core/interfaces.py
from haps import base
@base
class IUserService:
def create_user(self, username: str) -> bool:
raise NotImplementedError
def delete_user(self, username: str) -> bool:
raise NotImplementedError
@base
class IDatabase:
def add_object(self, bucket: str, name: str, data: dict) -> bool:
raise NotImplementedError
def delete_object(self, bucket: str, name) -> bool:
raise NotImplementedError
@base
class IMailer:
def send(self, email: str, message: str) -> None:
raise NotImplementedError
There are three interfaces:
IUserService
: High-level interface with methods to create and delete usersIDatabase
: Low-level data repositoryIMailer
: One-method interface for mailing integration
You need to tell haps about your interfaces by using @base
class decorator,
so it can resolve dependencies correctly.
Note
Be aware that you don’t have to create a fully-featured interface, instead you can just define a base type, that’s enough for haps:
@base
class IUserService:
pass
However, it’s a good practice to do so.
Implementations¶
Every interface should have at least one implementation. So, we will start with UserService and Mailer implementation.
# quickstart/user_module/core/implementations/others.py
from haps import egg, Inject
from user_module.core.interfaces import IDatabase, IMailer, IUserService
@egg
class DummyMailer(IMailer):
def send(self, email: str, message: str) -> None:
print(f'Mail to {email}: {message}')
@egg
class UserService(IUserService):
db: IDatabase = Inject()
mailer: IMailer = Inject()
_bucket = 'users'
def create_user(self, username: str) -> bool:
email = f'{username}@my-service.com'
created = self.db.add_object(self._bucket, username, {
'email': email
})
if created:
self.mailer.send(email, f'Hello {username}!')
return created
def delete_user(self, username: str) -> bool:
return self.db.delete_object(self._bucket, username)
There are two classes, and the first one is quite simple, it inherits from
IMailer
and implements its only method send
. The only new
thing here is the @egg
decorator. You can use it to tell haps about any
callable (a class is also a callable) that returns the implementation of a base type.
Now you can probably guess how haps can resolve right dependencies - it looks into
inheritance chain.
The UserService
implementation is a way more interesting. Besides the parts
we’ve already seen in the DummyMailer
implementation, it uses the
Inject
descriptor to provide
dependencies. Yes, it’s that simple. You only need to define class-level field Inject
with proper annotation, and haps will take care of everything else. It means
creating and binding the proper instance.
Warning
With this method, the instance of an injected class, e.g., DummyMailer, is
created (or fetched from the container) at the time of first property access,
and then is assigned to the current UserService
instance.
So:
us = UserService()
assert us.mailer is us.mailer # it's always true
# but
assert us.mailer is UserService().mailer # not necessarily
# (but it can, as you will see later)
Now let’s move to our repository. We need to implement some data storage for our project. For now, it’ll be in-memory storage, but, thanks to haps, you can quickly switch between many implementations. Creation of the database repository may be more complicated, so we’ll use a factory function.
# quickstart/user_module/core/implementations/db.py
from collections import defaultdict
from haps import egg, scope, SINGLETON_SCOPE
from user_module.core.interfaces import IDatabase
class InMemoryDb(IDatabase):
storage: dict
def __init__(self):
self.storage = defaultdict(dict)
def add_object(self, bucket: str, name: str, data: dict) -> bool:
if name in self.storage[bucket]:
return False
else:
self.storage[bucket][name] = data
return True
def delete_object(self, bucket: str, name) -> bool:
try:
del self.storage[bucket][name]
except KeyError:
return False
else:
return True
@egg
@scope(SINGLETON_SCOPE)
def database_factory() -> IDatabase:
db = InMemoryDb()
# Maybe do some stuff, like reading configuration
# or create some kind of db-session.
return db
InMemoryDb
is a simple implementation of IDatabase
that uses
defaultdict to store users. It could be file-based storage or even SQL storage.
However, notice there’s no @egg
decorator on this implementation. Instead,
we’ve created a function decorated with it which have IDatabase
declared as the return type.
In this case, when injecting, haps calls database_factory
function
and injects the result.
Warning
Be aware that haps by design WILL NOT validate function output in any way. So if your function returns a type that’s not compatible with declared one, it could lead to hard to catch errors.
Scope¶
As you can see in the previous file, database_factory
function
is also decorated with scope
decorator.
A scope in haps determines object life-cycle. The default scope is INSTANCE_SCOPE
,
and you don’t have to declare it explicitly. There are also two scopes that ships with
haps, SINGLETON_SCOPE
, and THREAD_SCOPE
. You can also create your own
scopes. You can read about scopes in another chapter, but for the clarity:
SINGLETON_SCOPE
means that haps creates only one instance, and injects
the same object every time. On the other hand, dependencies with
INSTANCE_SCOPE
(which is default), are instantiated on every injection.
Run the code!¶
Now we have configured our interfaces and dependencies, and we’re ready to run our application:
# quickstart/user_module/app.py
from haps import Container as IoC, inject
from user_module.core.interfaces import IUserService
class UserModule:
@inject
def __init__(self, user_service: IUserService) -> None:
self.user_service = user_service
def register_user(self, username: str) -> None:
if self.user_service.create_user(username):
print(f'User {username} created!')
else:
print(f'User {username} already exists!')
def delete_user(self, username: str) -> None:
if self.user_service.delete_user(username):
print(f'User {username} deleted!')
else:
print(f'User {username} does not exists!')
IoC.autodiscover(['user_module.core'])
if __name__ == '__main__':
um = UserModule()
um.register_user('Kiro')
um.register_user('John')
um.register_user('Kiro')
um.delete_user('Kiro')
um.delete_user('Kiro')
another_um_instance = UserModule()
another_um_instance.register_user('John')
The main class UserModule
takes IUserService
in the constructor,
and thanks to the @inject
decorator, haps will create and
pass UserService
instance to it.
After that, we have to call autodiscover
method from haps, which
scans all modules under given path and configures all dependencies.
Running our application should give following output:
Mail to Kiro@my-service.com: Hello Kiro!
User Kiro created!
Mail to John@my-service.com: Hello John!
User John created!
User Kiro already exists!
User Kiro deleted!
User Kiro does not exists!
User John already exists!
Profiles¶
Haps allows you to attach dependencies to configuration profile. It helps
with development, testing, and some other stuff. You can set active profiles
using Configuration
.
Example¶
One of many good use cases for profiles is mailing. Imagine you have to implement mailer class. Your production environment uses AWS SES, stage uses an internal SMTP system, on your local env every mail is printed to stdout, and mailer for tests do nothing. You may ask, how to implement this without nasty ifs? Well, it’s quite easy with profiles:
from haps import base, egg
@base
class IMailer
def send(self, to: str, message: str) -> None:
raise NotImplementedError
@egg(profile='production')
class SESMailer(IMailer):
def send(self, to: str, message: str) -> None:
# SES implementation
@egg(profile='stage')
class SMTPMailer(IMailer):
def send(self, to: str, message: str) -> None:
# SMTP implementation
@egg(profile='tests')
class DummyMailer(IMailer):
def send(self, to: str, message: str) -> None:
pass
@egg # missing profile means default
class LogMailer(IMailer):
def send(self, to: str, message: str) -> None:
print(f"Mail to {to}: {message})
And that’s it. Now you only need to run your app with HAPS_PROFILES=production
(or any other profile) and haps will choose proper dependency. You can set more than
one profile separating them by a comma: HAPS_PROFILES=stage,local-static,sqlite
If there is more than one egg for the given profiles list, the order decides about
priority. e.g. for HAPS_PROFILES=tests,production
the DummyMailer
class is chosen.
Profiles can be configured programmatically, before Container configuration:
from haps import PROFILES
from haps.config import Configuration
Configuration().set(PROFILES, ('tests', 'tmp-static', 'sqlite'))
# Container config / autodiscover
Note
Profiles that are set directly by Configuration
overrides profiles from the environment variable.
API¶
Container¶
Container is a heart of haps. For now, its implemented as a singleton that can only be used after one-time configuration.
from haps import Container
Container.autodiscover(['my.package']) # configuration, once in the app lifetime
Container().some_method() # Call method on the instance
That means, you can create instances of classes that use injections, only after haps is properly configured.
-
classmethod
Container.
autodiscover
(module_paths: List[str], subclass: Optional[haps.container.Container] = None) → None[source]¶ Load all modules automatically and find bases and eggs.
Parameters: - module_paths – List of paths that should be discovered
- subclass – Optional Container subclass that should be used
-
static
Container.
configure
(config: List[haps.container.Egg], subclass: Optional[haps.container.Container] = None) → None[source]¶ Configure haps manually, an alternative to
autodiscover()
Parameters: - config – List of configured Eggs
- subclass – Optional Container subclass that should be used
Egg¶
-
class
haps.
Egg
(base_: Optional[Type], type_: Type, qualifier: Optional[str], egg_: Callable, profile: str = None)[source]¶ Configuration primitive. Can be used to configure haps manually.
-
Egg.
__init__
(base_: Optional[Type], type_: Type, qualifier: Optional[str], egg_: Callable, profile: str = None) → None[source]¶ Parameters: - base – base of dependency, used to retrieve object
- type – type of dependency (for functions it’s a return type)
- qualifier – extra qualifier for dependency. Can be used to register more than one type for one base.
- egg – any callable that returns an instance of dependency, can be a class or a function
- profile – dependency profile name
Injection¶
-
class
haps.
Inject
(qualifier: str = None)[source]¶ A descriptor for injecting dependencies as properties
class SomeClass: my_dep: DepType = Inject()
Important
Dependency is injected (created/fetched) at the moment of accessing the attribute, not at the moment of instance creation. So, even if you create an instance of SomeClass, the instance of DepType may never be created.
-
haps.
inject
(fun: Callable) → Callable[source]¶ A decorator for injection dependencies into functions/methods, based on their type annotations.
class SomeClass: @inject def __init__(self, my_dep: DepType) -> None: self.my_dep = my_dep
Important
On the opposite to
Inject
, dependency is injected at the moment of method invocation. In case of decorating __init__, dependency is injected when SomeClass instance is created.Parameters: fun – callable with annotated parameters Returns: decorated callable
Dependencies¶
-
haps.
base
(cls: T) → T[source]¶ A class decorator that marks class as a base type.
Parameters: cls – Some base type Returns: Not modified cls
-
haps.
egg
(qualifier: Union[str, Type] = '', profile: str = None)[source]¶ A function that returns a decorator (or acts like a decorator) that marks class or function as a source of base.
If a class is decorated, it should inherit from base type.
If a function is decorated, it declared return type should inherit from some base type, or it should be the base type.
@egg class DepImpl(DepType): pass @egg(profile='test') class TestDepImpl(DepType): pass @egg(qualifier='special_dep') def dep_factory() -> DepType: return SomeDepImpl()
Parameters: - qualifier – extra qualifier for dependency. Can be used to register more than one type for one base. If non-string argument is passed, it’ll act like a decorator.
- profile – An optional profile within this dependency should be used
Returns: decorator
Configuration¶
-
class
haps.config.
Configuration
[source]¶ Configuration container, a simple object to manage application config variables. Variables can be set manually, from the environment, or resolved via custom function.
-
Configuration.
get_var
(var_name: str, default: Optional[Any] = <object object>) → Any[source]¶ Get a config variable. If a variable is not set, a resolver is not set, and no default is given
UnknownConfigVariable
is raised.Parameters: - var_name – Name of variable
- default – Default value
Returns: Value of config variable
-
classmethod
Configuration.
resolver
(var_name: str) → function[source]¶ Variable resolver decorator. Function or method decorated with it is used to resolve the config variable.
Note
Variable is resolved only once. Next gets are returned from the cache.
Parameters: var_name – Variable name Returns: Function decorator
-
classmethod
Configuration.
env_resolver
(var_name: str, env_name: str = None, default: Any = <object object>) → haps.config.Configuration[source]¶ Method for configuring environment resolver.
Parameters: - var_name – Variable name
- env_name – An optional environment variable name. If not set haps looks for HAPS_var_name
- default – Default value for variable. If it’s a callable, it is called before return. If not provided
UnknownConfigVariable
is raised
Returns: Configuration
instance for easy chaining
-
classmethod
Configuration.
set
(var_name: str, value: Any) → haps.config.Configuration[source]¶ Set the variable
Parameters: - var_name – Variable name
- value – Value of variable
Returns: Configuration
instance for easy chaining
-
class
haps.config.
Config
(var_name: str = None, default=<object object>)[source]¶ Descriptor providing config variables as a class properties.
class SomeClass: my_var: VarType = Config() custom_property_name: VarType = Config('var_name')
-
Config.
__init__
(var_name: str = None, default=<object object>) → None[source]¶ Parameters: - var_name – An optional variable name. If not set the property name is used.
- default – Default value for variable. If it’s a callable, it is called before return. If not provided
UnknownConfigVariable
is raised
Scopes¶
A scope is a special object that controls dependency creation. It decides if new dependency instance should be created, or some cached instance should be returned.
By default, there are two scopes registered in haps:
InstanceScope
and SingletonScope
as haps.INSTANCE_SCOPE
and haps.SINGLETON_SCOPE
.
The haps.INSTANCE_SCOPE
is used as a default.
You can register any other scope by calling
haps.Container.register_scope()
. New scopes should be a subclass
of haps.scopes.Scope
.
-
class
haps.scopes.instance.
InstanceScope
[source]¶ Dependencies within InstanceScope are created at every injection.