- Support for Generic types
- Support for Forward references
- Circular dependencies detection(beta)
- Factory return generic types
- (beta) Visualizer to visualize the dependency graph
- support more resource types
- support more builtin types
- bettr typing support
- node config
- static resolve
- async context manager with graph
- fix: when exit DependencyGraph, iterate over all nodes would cause rror when builtin types are involved
- feat: DependencyGraph.factory would statically resolve the dependent and return a factory
-
fix: when a dependency is a union type, the dependency resolver would not be able to identify the dependency
-
fix: variadic arguments would cause UnsolvableDependencyError, instead of injection an empty tuple or dict
- feat: lazy dependent
- fix: static resolve at aenter would cause dict changed during iteration error
- feat: adding py.typed file
-
fix: change implementation of DependencyGraph.factory to be compatible with fastapi.Depends
-
fix: fix a bug with subclass of ty.Protocol
-
feat: dg.resolve depends on static resolve
-
feat: node.build now support nested, partial override
e.g.
class DataBase:
def __init__(self, engine: str = "mysql", /, driver: str = "aiomysql"):
self.engine = engine
self.driver = driver
class Repository:
def __init__(self, db: DataBase):
self.db = db
class UserService:
def __init__(self, repository: Repository):
self.repository = repository
node = DependentNode.from_node(UserService)
instance = node.build(repository=dict(db=dict(engine="sqlite")))
assert isinstance(instance, UserService)
assert isinstance(instance.repository, Repository)
assert isinstance(instance.repository.db, DataBase)
assert instance.repository.db.engine == "sqlite"
assert instance.repository.db.driver == "aiomysql"
- feat: detect unsolveable dependency with static resolve
Features:
- feat: support for async entry
- feat: adding
solve
as a top level api
Improvements:
- resolve / static resolve now supports factory
- better typing support for DependentNode.resolve
- dag.reset now supports clear resolved nodes
Improvements:
- better error message for node creation error NOTE: this would leads to most exception be just NodeCreationError, and the real root cause would be wrapped in NodeCreationError.error.
This is because we build the DependencyNode recursively, and if we only raise the root cause exception without putting it in a error chain, it would be hard to debug and notice the root cause.
e.g.
.pixi/envs/test/lib/python3.12/site-packages/ididi/graph.py:416: in node
raise NodeCreationError(factory_or_class, "", e, form_message=True) from e
E ididi.errors.NodeCreationError: token_bucket_factory()
E -> TokenBucketFactory(aiocache: Cache)
E -> RedisCache(redis: Redis)
E -> Redis(connection_pool: ConnectionPool)
E -> ConnectionPool(connection_class: idid.Missing)
E -> MissingAnnotationError: Unable to resolve dependency for parameter: args in <class 'type'>, annotation for `args` must be provided
Features:
Feature:
- you can now use async/sync generator to create a resource that needs to be closed
from ididi import DependencyGraph
dg = DependencyGraph()
@dg.node
async def get_client() -> ty.AsyncGenerator[Client, None]:
client = Client()
try:
yield client
finally:
await client.close()
@dg.node
async def get_db(client: Client) -> ty.AsyncGenerator[DataBase, None]:
db = DataBase(client)
try:
yield db
finally:
await db.close()
@dg.entry_node
async def main(db: DataBase):
assert not db.is_closed
Improvements:
- support sync resource in async dependent
- better error message for async resource in sync function
- resource shared within the same scope, destroyed when scope is exited
FIX:
Adding a temporary fix to missing annotation error, which would be removed in the future.
Example:
class Config:
def __init__(self, a):
self.a = a
@dag.node
def config_factory() -> Config:
return Config(a=1)
class Service:
def __init__(self, config: Config):
self.config = config
dg.resolve(Service)
This would previously raise NodeCreationError
, with root cause being MissingAnnotationErro
, now being fixed.
API changes:
ididi.solve
renamed to ididi.resolve
for better consistency with naming style.
ididi.entry
no longer accepts INodeConfig
as kwargs.
Feat:
DependencyGraph.use_scope to indirectly retrive nearest local
raise OutOfScopeError
, when not within any scope.
Fix:
- now correctly recoganize if a class is async closable or not
- resolve factory with forward ref as return type.
Improvements:
-
raise PositionalOverrideError when User use
dg.resolve
with any positional arguments. -
test coverages raised from 95% to 99%, where the only misses only exists in visual.py, and are import error when graphviz not installed, and graph persist logic.
-
user can directly call
Visualizer.save
without first callingVisualizer.make_graph
.
Improvements
- performance boost on dg.static_resolve
Features
- named scope, user can now call
dg.use_scope(name)
to get a specific parent scope in current context, this is particularly useful in scenario where you have tree-structured scopes, like app-route-request.
Fix:
- now config of
entry
node is default to reuse=False, which means the result will no longer be cached. - better typing with
entry
improvements: refactor visitor
improvements:
DependencyGraph now supports __contains__
operator, a in dg
is the equivalent to a in dg.nodes
features:
DependencyGraph now supports a merge
operator, example:
from app.features.users import user_dg
dg = DependencyGraph()
dg.merge(user_dg)
you might choose to merge one or more DependencyGraph into the main graph, so that you don't have to import a single dg to register all your classes.
adding support for python 3.13
improvements:
now ididi will look for conflict in reusability,
For example,
when a dependency with reuse=False
has a dependent with reuse=True
, ididi would raise ReusabilityConflictError when statically resolve the nodes.
ididi.errors.ReusabilityConflictError: Transient dependency `Database` with reuse dependents
make sure each of AuthService -> Repository is configured as `reuse=False`
-
remove
import typing as ty
,import typing_extensions as tyex
to reduce global lookup -
add an
ignore
field to node config, where these ignored param names or types will be ignored at statical resolve / resolve phase
@dg.node(ignore=("name", int))
class User:
name: str
age: int
dg = DependencyGraph()
dg.node(ignore=("name", int))(User)
with pytest.raises(TypeError):
dg.resolve(User)
n = dg.resolve(User, name="test", age=3)
assert n.name == "test"
assert n.age == 3
-
fix a potential bug where when resolve dependent return None or False it could be re-resolved
-
rasie error when a factory returns a builtin type
@dg.node
def create_user() -> str:
...
- fix a bug where error would happen when user use
dg.resolve
to resolve a resource factor, example:
def user_factory() -> ty.Generator[User, None, None]:
u = User(1, "test")
yield u
dg.static_resolve(user_factory)
with dg.scope() as scope:
# this would fail before 1.0.10
u = scope.resolve(user_factory)
improvement improve typing support for scope.resolve, now it recognize resource better.
Feat:
- add a special mark so that users don't need to specifically mark
dg.node
async def create_user(user_repo: inject(repo_factory)):
...
# so that user does not have to explicitly declear repo_factory as a node
dg = DependencyGraph()
@dg.node
def repo_factory():
...
-
support config to entry
-
support DependencyGraph as dependency
def get_user_service(dg: DependencyGraph) -> UserService:
return dg.resolve(UserService)
dg.resolve(get_user_service)
# this would pass the same graph into function
- remove
static_resolve
config from DependencyGraph - remove
factory
method frmo DependencyGraph - add a new
self_inejct
config to DependencyGraph - add
register_dependent
config to DependencyGraph, SyncScope, AsyncScope - inject now supports both as annotated annotation and as default value, as well as nested annotated annotation
class APP:
def __init__(self, graph: DependencyGraph):
self._graph = graph
self._graph.register_dependent(self)
dg = DependencygGraph()
app = APP(graph)
@app.register
async def login(app: APP):
assert app is app
if we need a Context object in a non context manner, e.g.
class Service:
async def __aenter__(self): ...
async def __aexit__(self, *args): ...
def get_service() -> Service:
return Service()
dg.resolve(get_service)
- factory_type
- factory override order
improvements on entry
entry
now supports positional argument.entry
now would use current scope- 100% performance boost on
entry
- separate
INodeConfig
andIEntryConfig
Improvements:
-
rename
inject
touse
, to avoid the implication ofinject
as if it was to say only param annotated withinject
will be injected. whereas what it really means is which factory to use -
further optimize
entry
, now when the decorated function does not depends on resource, it will not create a scope.
In 1.1.2
0.089221 seoncds to call call regular function create_user 100000 times
0.412887 seoncds to call entry version of create_user 100000 times
now at 1.1.3
0.099846 seoncds to call regular function create_user 100000 times
0.104534 seoncds to call entry version of create_user 100000 times
This make functions that does not depends on resource 4-5 times faster than 1.1.2 Fix:
- ✨ fix a bug where when a dependent with is registered with
DependencyGraph.register_dependnet
, it will still be statically resolved.
Improvements:
- Add
DependencyGraph.should_be_scoped
api to check if a dependent type contains any resource dependency, and thus should be scoped. this is particularly useful when user needs to (dynamically) decide whether they should create the resource in a scope.
Fix:
- previously entry only check if any of its direct dependency is rousource or not, this will cause bug when any of its indirect dependencies is a resource, raise OutOfScope Exception
Fix:
- previously only resource itself will be managed by scope, now if a dependnet depends on a resource, it will also be managed by scope.
Fix:
- fix a bug where if user menually decorate its async generator / sync factory with contextlib.asynccontextmanager / contextmanager,
DependencyNode.factory_type
would generated asfunction
.
.e.g:
from typing import AsyncGenerator
from contextlib import asynccontextmanager
@asynccontextmanager
async def get_client() -> AsyncGenerator[Client, None]:
client = Client()
try:
yield client
finally:
await client.close()
-
improvement
-
use a dedicate ds to hold singleton dependent,
-
improve error message for missing annotation / unresolvable dependency
improvements
- factory have higher priority than default value
now, for a non-builtin type, ididi will try to resolve it even with default value, this is because for classses that needs dependency injection, it does not make sense to have a default value in most cases, and in cases where it does, it most likely be like this
class UserRepos:
def __init__(self, db: Optional[Database] = None):
self.db = db
and it make more sense to inject Database
than to ignore it.
- global ignore with
DependencyGraph(ignore=(type))
class Timer:
def __init__(self, d: datetime):
self._d = d
dg = DependencyGraph(ignore=datetime)
with pytest.raises(TypeError):
dg.resolve(Timer)
improvements
-
change
Scope.register_dependent
toScope.register_singleton
to match the change made in 1.1.7 onDependencyGraph
. -
positional ignore for node
def test_ignore():
dg = DependencyGraph()
class Item:
def __init__(self, a: int, b: str, c: int):
self.a = a
self.b = b
self.c = c
with pytest.raises(UnsolvableDependencyError):
dg.resolve(Item)
dg.node(ignore=(0, str, "c"))
with pytest.raises(TypeError):
dg.resolve(Item)
>>> TypeError: __init__() missing 3 required positional arguments: 'a', 'b', and 'c'
- partial resolve
previously, if a dependent requires a unresolvable dependency without default value, it would raise UnsolvableDependencyError
,
this might be a problem when user wants to resolve the dependent by providing the unresolvable dependency with dg.resolve
.
now DependencyGraph
accepts a partial_resolve
parameter, which would allow user to resolve a dependent with missing unsolvable-dependency.
def test_graph_partial_resolve_with_dep():
dg = DependencyGraph(partial_resolve=True)
class Database: ...
DATABASE = Database()
def db_factory() -> Database:
return DATABASE
class Data:
def __init__(self, name: str, db: Database = use(db_factory)):
self.name = name
self.db = db
dg.node(Data)
dg.static_resolve(Data)
with pytest.raises(UnsolvableDependencyError):
data = dg.resolve(Data)
data = dg.resolve(Data, name="data")
assert data.name == "data"
assert data.db is DATABASE
a quick bug fix
from ididi import DependencyGraph
class Time:
def __init__(self, name: str):
self.name = name
def test_registered_singleton():
"""
previously
dg.should_be_scoped(dep_type) would analyze dependencies of the
dep_type, and if it contains resolvable dependencies exception would be raised
since registered_singleton is managed by user,
dg.should_be_scoped(registered_singleton) should always be False.
"""
timer = Time("1")
dg = DependencyGraph()
dg.register_singleton(timer)
assert dg.should_be_scoped(Time) is False
NodeConfig.ignore
&GraphConfig.ignore
now supportsTypeAliasType
only affect type checker
-
remove
EntryConfig
, nowentry
share the same config asNodeConfig
, the only difference is nowdg.entry(reuse=True)
is possible, whereas it used to be always False, which does not really make sense, user can share the same dependencies across different calls to the entry function. -
dg._reigster_node
,dg._remove_node
, are now private,dg.replace_node
is removed. these three methods require user provide aDependencyNode
, but we want to avoid user having to interact directly withDependencyNode
to lower complexity.
-
change
_itypes
tointerfaces
, make it public, since it is unlikely to cause breaking change -
remove
partial_resolve
from DependencyGraph
partial_resolve
was introduced in 1.2.0
. it is not a good design,
it is like a global try except
, which hides problems instead of solving them.
Ignore
Annotation
from ididi import Ignore
class User:
def __init__(self, name: Ignore[str]):
self.name = name
which is equivalent to
DependencyGraph().node(ignore="name")(User)
both use
and Ignore
are designed to be user friendly alternative to DependencyGraph.node
Improvements:
- minor performance gain, 60% faster for instance resolve, should resolve 0.2 million instances in 1 second, although still 40 times slower than hard-coded factory, we tried replace recursive appraoch with iterative, no luck, only tiny improvements, so we stay at recursive appraoch
Fix: in previous patch
def service_factory(
*, db: Database = use(db_fact), auth: AuthenticationService, name: str
) -> UserService:
return UserService(db=db, auth=auth)
def test_resolve():
dg = DependencyGraph()
dg.resolve(service_factory, name="aloha")
woud raise UnsolvableDependencyError
, because name
is a str without default values,
when when we resolve it, we will first static_resolve it, which would raise error,
now if dependencies that are provided with overrides won't be statically resolved.