Releases: raceychan/ididi
Release v1.5.0
version 1.5.0
improvements
ididi is optimzed using cython,
resolve
is now 4x fasters than previous patch, and is as fast as hard-coded factories.
entry
is now 50% faster.
ididi has gotten 40x faster since v1.0.0 , it is as fast as plain menual factories, user can expect no overhead using ididi to resolve dependencies.
Features
- Re-using overrides
class Username:
def __init__(self, name: str):
self.name = name
class User:
def __init__(self, name: str, uname: Username):
self.name = name
self.uname = uname
dg = Graph()
user = dg.resolve(User, name="uuu")
assert user.name == user.uname.name == "uuu"
reuse-overrides with function dependency is also supported
def dependency(a: int) -> Ignore[int]:
return a
def main(a: int, b: int, c: Annotated[int, use(dependency)]) -> Ignore[float]:
return a + b + c
assert dg.resolve(main, a=1, b=2) == 4
- support resolve from class method
class Book:
@classmethod
def from_article(cls, a: str) -> "Book":
return Book()
@pytest.mark.debug
def test_resolve_classmethod():
dg = Graph()
b = dg.resolve(Book.from_article, a="5")
assert isinstance(b, Book)
- adding multiple nodes at once via
Graph.add_nodes
def add_nodes(
self, *nodes: Union[IDependent[T], tuple[IDependent[T], INodeConfig]]
) -> None: ...
class AuthRepo: ...
class Connection: ...
def get_auth() -> AuthRepo: ...
def get_conn() -> Generator[Connection, None, None]: ...
dg = Graph()
dg.add_nodes(
get_conn,
(get_auth, {"reuse": False, "ignore":"name"})
)
-
Mark
now public, user can use it withtyping.Annotated
Ignore = Annotated[T, IGNORE_PARAM_MARK]
so this would work now:
def get_conn(url: Annotated[str, IGNORE_PARAM_MARK]):
...
def get_repo(conn: Annotated[Connection, USE_FACTORY_MARK, get_conn, NodeConfig]):
...
Release v1.4.4
version 1.4.4
- refactor scope, now user can create a new scope using
scope.scope
def test_scope_graph_share_methods():
dg = Graph()
with dg.scope() as s1:
with s1.scope() as s2:
...
create scope from graph vs crate scope from scope
the created scope will inherit
resolved instances and registered singletons from its creator, thus
-
when create from graph, scope inherit resolved instances and registered singletons from graph
-
when create from scope, scope inherit resolved instances and registered singletons from the parent scope.
Example
dg = Graph()
class User: ...
class Cache: ...
dg_u = dg.resolve(User)
with dg.scope() as s1:
s1_u = s1.resolve(User)
assert s1_u is dg_u
s1_cache = s1.resolve(Cache)
dg_cache = dg.resolve(Cache)
assert s1_cache is not dg_cache
with s1.scope() as s12:
assert s12.resolve(User) is dg_u
s12_cache = s12.resolve(Cache)
assert s12_cache is s1_cache
assert s12_cache is not dg_cache
with dg.scope() as s2:
s2_u = s2.resolve(User)
assert s2_u is dg_u
s2_cache = s2.resolve(Cache)
assert s2_cache is not s1_cache
Graph.get
/Scope.get
def get(self, dep: INode[P, T]) -> Union[DependentNode[T], None]:
a helper function that works like Graph.nodes.get
Release v1.4.3
version 1.4.3
Improvements:
50% performance boost for Graph.resolve/Graph.aresolve/Graph.entry
Features:
-
Graph
now receivesworkers: concurrent.futures.ThreadPoolExecutor
in constructor -
send entering and exiting of
contextmanager
in a diffrent thread when usingAsyncScope
, to avoid blocking.
def get_session() -> Generator[Session, None, None]:
session = Session()
with session.begin():
yield session
async with dg.ascope() as scope:
ss = await scope.resolve(get_session)
Here, entering and exiting get_session
will be executed in a different thread.
Graph
now create a default scope,Graph.use_scope
should always returns a scope.- Split
Graph.scope
toGraph.scope
andGraph.ascope
. - Deprecate
DependencyGraph
,DependencyGraph.static_resolve
,DependencyGraph.static_resolve_all
- variadic arguments, such as
*args
or**kwargs
withouttyping.UnPack
, are no longer considered as dependencies. - Builtin types with provided default are no longer considered as dependencies.
Dependency.unresolvabale
is now an attribute, instead of a property.
Release v1.4.2
version 1.4.2
Function as Dependency
Declear a function as dependency via annotating its return type with Ignore
.
@dataclass
class User:
name: str
role: str
def get_user(config: Config) -> Ignore[User]:
assert isinstance(config, Config)
return User("user", "admin")
def validate_admin(
user: Annotated[User, get_user], service: UserService
) -> Ignore[str]:
assert user.role == "admin"
assert isinstance(service, UserService)
return "ok"
assert dg.resolve(validate_admin) == "ok"
Note that since get_user
returns Ignore[User]
instead of User
, it won't be used as factory to resolve User
.
Release v1.4.1
version 1.4.1
- Ignored dependencies are not longer considered dependencies to ididi.
def test_ignore_dependences():
dg = DependencyGraph()
class User:
def __init__(self, name: Ignore[str]): ...
dg.node(User)
assert len(dg.nodes[User].dependencies) == 0
In theory resolve time would be faster, but this should not have noticeable impact unless you use Ignore extensively.
Release v1.4.0
version 1.4.0
This minor focus on a small refactor on Scope
, we like the idea that Scope
is a temporary view of its parent Graph
, so following change is made:
- both resource and non-resource instances created in scope will stay in the scope
- when a graph create a scope, it shares a copy of its resolved singletons and registered singletons, scope can read them, but can not modify them.
This gives a better separation between Graph
and Scope
.
Release v1.3.6
version 1.3.6
this patch brings a quick fix to Graph.entry
- Fix:
Graph.entry
no longer uses existing scope, instead, always create a new scope
Release v1.3.5
version 1.3.5
- a quick bug fix, where in 1.3.4, registered_singleton is not shared between graph and scope.
The general rule is that scope can access registered singletons and resolved instances but not vice versa.
class Service:
def __init__(self, name: str = "s", age: int = 1): ...
async def test_shared_registered_singleton():
dg = Graph()
singleton = Service("service", 1)
dg.register_singleton(singleton)
scope_mng = dg.scope()
with scope_mng as scope:
service1 = scope.resolve(Service)
async with scope_mng as ascope:
service2 = await ascope.resolve(Service)
assert service1 is singleton
assert service2 is singleton
- scope should be able to access singletons registered in graph
async def test_no_reversed_share():
dg = Graph()
service = Service("1", 2)
with dg.scope() as scope:
scope.register_singleton(service)
assert scope.resolve(Service) is service
assert not dg.is_registered_singleton(Service)
s2 = dg.resolve(Service)
assert s2 is not service
- graph should not be able to access singleton registered in scope
Release v1.3.4
version 1.3.4
This patch mainly focuses on code maintainence and reduce memory usage & performance boost.
- refactor
Scope
andGraph
to avoid circular reference,Scope
not longer depends onGraph
Graph.resolve
is approxiamately 17%-20% faster
Release v1.3.3
version 1.3.3
Features
Graph.search_node
, search node by name, O(n) complexity
This is mainly for debugging purpose, sometimes you don't have access to the dependent type, but do know the name of it. example
class User: ...
dg = Graph()
dg.node(User)
assert dg.search_node("User").dependent_type is User
This is particularly useful for type defined by NewType
UserId = NewType("UserId", str)
assert dg.search_node("UserId")
Graph.override
a helper function to override dependent within the graph
def override(self, old_dep: INode[P, T], new_dep: INode[P, T]) -> None:
dg = DependencyGraph()
@dg.entry
async def create_user(
user_name: str, user_email: str, service: UserService
) -> UserService:
return service
@dg.node
def user_factory() -> UserService:
return UserService("1", 2)
class FakeUserService(UserService): ...
dg.override(UserService, FakeUserService)
service_res = await create_user("1", "2")
assert isinstance(service_res, FakeUserService)
Note that, if you only want to override dependency for create_user
you can still just use create_user.replace(UserService, FakeUserService)
,
and such override won't affect others.