- 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.analyze
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.analyze(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.analyze(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.
- remove
LazyDependent
, sinceDependencyGraph
can inject itself to any dependencies it resolve,LazyDependent
can be easily achieved by injectingDependencyGraph
and lazy resolve
from ididi import ignore
class UserRepo:
def __init__(self, graph: DependencyGraph, db: Ignore[DataBase]=None):
self._graph = graph
self._db = db
@property
def db(self):
return self._graph.resolve(DataBase)
- performance boost
25% performance increase to DependencyGraph.resolve
compare to 1.2.4
Current implementation of dg.resolve is 17.356008 times slower than a hard-coded dependency construction. given how much extra work ididi does resolving dependency, our ultimate goal would be make dg.resolve < 10 times slower than a hard-coded solution.
- override class dependencies, entry dependencies
class Repo:
db: DataBase = use(db_factory)
def fake_db_factory()->DataBase:
return FakeDB()
dg.node(fake_db_factory)
def test_entry_replace():
dg = DependencyGraph()
def create_user(
user_name: str, user_email: str, service: UserService
) -> UserService:
return service
class FakeUserService(UserService): ...
create_user = dg.entry(reuse=False)(create_user)
create_user.replace(UserService, FakeUserService)
res = create_user("user", "user@email.com")
assert isinstance(res, FakeUserService)
class EvenFaker(UserService):
...
create_user.replace(service=EvenFaker)
create_user.replace(UserService, service=EvenFaker)
res = create_user("user", "user@email.com")
assert isinstance(res, EvenFaker)
400% performance improvements on Graph.resolve
, by apply cache whever possible,
In previous benchmark, 0.000558 seoncds to resolve 100 instances
ididi v1.3.0 0.000139 seoncds to resolve 100 instances
- rename
DependencyGraph
toGraph
- rename
DependencyGraph.static_resolve
toGraph.analyze
- rename
DependencyGraph.static_resolve_all
toGraph.analyze_nodes
Original names are kept as alias to new names to avoid breaking changes
- support Unpack
class DataBase: ...
class Cache: ...
class Config: ...
class KWARGS(TypedDict):
db: DataBase
cache: Cache
class Extra(KWARGS):
config: Config
class Repo:
def __init__(self, **kwargs: Unpack[KWARGS]) -> None:
self.db = kwargs["db"]
self.cache = kwargs["cache"]
class SubRepo(Repo):
def __init__(self, **kwargs: Unpack[Extra]):
super().__init__(db=kwargs["db"], cache=kwargs["cache"])
self.config = kwargs["config"]
def test_resolve_unpack():
dg = Graph()
repo = dg.resolve(Repo)
assert isinstance(repo.db, DataBase)
assert isinstance(repo.cache, Cache)
subrepo = dg.resolve(SubRepo)
assert isinstance(subrepo.config, Config)
- support Literal
def test_resolve_literal():
dg = Graph()
class User:
def __init__(self, name: Literal["user"] = "user"):
self.name = name
u = dg.resolve(User)
assert u.name == "user"
Graph.remove_singleton(self, dependent_type: type) -> None:
"Remove the registered singleton from current graph, return if not found"
Graph.remove_dependent(self, dependent_type: type) -> None
"Remove the dependent from current graph, return if not found"
Resolve NewType
from uuid import uuid4
UUID = NewType("UUID", str)
def uuid_factory() -> UUID:
return str(uuid4())
def user_factory(user_id: UUID) -> User:
return User(user_id=user_id)
Why not TypeAlias as well?
- It does not make sense, since TypeAlias is literally an alias
A = str
means A is the same as str, and it make no sense to resolve a str.
- Resolving TypeAlias is not possible in python 3.9
UUID = str
def uuid_factory() -> UUID:
return uuid4()
when we try to resolve uuid_factory, the return is a type `str`,
not a `TypeAlias`.
-
Now when override entry dependencies with
entryfunc.replace
, ididi will automatically analyze the override dependency -
NodeConfig
is now immutable and hashable -
rename
Graph._analyze_entry
toGraph.analyze_params
,
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.
- refactor
Scope
andGraph
to avoid circular reference,
- 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.
- Fix:
Graph.entry
no longer uses existing scope, instead, always create a new scope
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
.
we should take a smarter approach with ignore
, for node configured with ignore,
we don't put them in node's dependencies.
This would be more efficient and make more sense.
def test_ignore_dependences():
dg = DependencyGraph()
class User:
def __init__(self, name: Ignore[str]): ...
dg.node(User)
assert len(dg.nodes[User].dependencies) == 0
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
.
Improvements:
50% performance boost for Graph.resolve/aresolve/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.
- 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:
...
Graph.get
/Scope.get
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
- 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)
- make
Mark
public, user can use it withtyping.Annotated
Ignore = Annotated[T, IGNORE_PARAM_MARK]
so this works:
def get_conn(url: Annotated[str, IGNORE_PARAM_MARK]):
...
def get_repo(conn: Annotated[Connection, USE_FACTORY_MARK, get_conn, NodeConfig]):
...
- provide a more non-intrusive way to node config, where user only need to add new code, no modifying current code, not even decorator.
class AuthRepo: ...
class TokenRegistry: ...
def get_auth() -> AuthRepo: ...
def get_registry() -> TokenRegistry: ...
class AuthService:
def __init__(self, auth_repo: AuthRepo, token: TokenRegistry): ...
dg = Graph()
dg.add_nodes(
get_conn,
(get_auth, {"reuse": False, "ignore":"name"})
)
- make a re-use params version of
Graph.resolve
,
from typing import Any, NewType
class Request: ...
RequestParams = NewType("RequestParams", dict[str, Any])
async def test_resolve_request():
dg = Graph()
async def resolve_request(r: Request) -> RequestParams:
return RequestParams({"a": 1})
dg.node(resolve_request)
await dg.shared_resolve(resolve_request, r=Request())
-
consider switch to anyio[cancel]
-
rewrite ididi in cython
-
make ididi resolve overhead < 100%. make it as fast as hard-coded factories as possible.
-
support return dependency[cancel]
def json_encode(r: Any) -> bytes:
return orjson.dumps(r)
type Json[T] = Annotated[T, use(json_encode)]
async def create_user(user_id: str = "u") -> Json[User]:
return User(user_id=user_id)
assert isinstance(dg.resolve(create_user), bytes)
here we will serialize the result of create_user
we can also refactor how we deal with resource right now
by applying scope.enter_context
to result, so instead of
async def get_conn() -> AsyncGenerator[Connection, None, None]
...
user can write
def enter_ctx(scope: Scope):
await scope.resolve()
Scoped = Annotated[T, scope_factory]
from ididi import Scoped
class Connection:
...
async def aget_conn() -> Scoped[Connection]:
conn = Connection()
yield conn
def get_conn() -> Scoped[Connection]:
conn = Connection()
yield conn
- support extra params to dependencies and overrides-reuse, this allows us to do complex dependency resolution,
dg = Graph()
def dep3(e: int) -> Ignore[str]:
return "ok"
def dependency(a: int, a2: Annotated[str, use(dep3)]) -> Ignore[int]:
return a
def dep2(k: str, g: Annotated[int, use(dependency)]) -> Ignore[str]:
assert g == 1
return k
def main(
a: int,
b: int,
c: Annotated[int, use(dependency)],
d: Annotated[str, use(dep2)],
) -> Ignore[float]:
assert d == "f"
return a + b + c
assert dg.resolve(main, a=1, b=2, k="f", e="e") == 4
the downside is that now Graph.resolve
is 1.3x slower, we might find a way to reduce this in next few patches, but this feature is really needy right now.
Graph.entry
no longer ignore builtin-types by default, now user should specifically ignore builtin params. This is mainly for
- resolve and entry to have similar behaviors
- user might be confused by what will be automatically ignored, Ignore[T] is more explicit
-
rollback a behavior introduced in
version 1.4.3
, where Ignored params were not considered as dependencies, now they are dependencies again. -
Graph.resolve
no longer maintain ParamSpec, since for a factoryCallable[P, T]
,Graph.resolve
can accept either more params than P, less params P, or no params P at all, it does not make much sense to maintain paramspec anymore