Releases: raceychan/ididi
Release v1.8.4
Release v1.8.3
version 1.8.3
Fixes:
fix a bug where function dependency won't be properly parsed and cause return type to be treated as factory
e.g.
def get_user(user_id: str) -> Ignore[User]:
...in 1.8.2 get_user will be treated as factory of User whereas it should be treated as a function dep.
Release v1.8.2
version 1.8.2
Highlights
NodeMetaanduseare now public exports, so applications can declare factories or metadata without reaching into private modules.resolve_metanow preservesignoreflags and infers factories fromAnnotated[Service, use()], keeping ignored dependencies and default factories consistent during analysis and scope checks.
Details
ididi.__init__re-exportsNodeMetaandusealongside other public helpers.Graphanalysis andshould_be_scopedrespectNodeMeta(ignore=True)in nestedAnnotatedmetadata, so ignored parameters stay skipped andUnsolvableNodeError.__notes__still include dependent/parameter context when resolution fails.use()markers without explicit factories now default to the annotated type, matching earlier behavior while keeping the new metadata path intact.
Release v1.8.1
version 1.8.1
Highlights
Ignore[...]is now implemented viaNodeMeta(ignore=True), so the legacyIGNORE_PARAM_MARK
sentinel is gone. Anytyping.Annotatedblocks that previously used the constant can switch to
ididi.Ignore(or embed their ownNodeMeta(ignore=True)marker) with no behavioral change.NodeMetagained the newignoreflag, and alluse(...)helpers / graph analysis logic now honor
it when deciding whether a parameter should participate in dependency resolution.
Details
- Removed
IGNORE_PARAM_MARKfrom the public config surface and runtime checks. - Updated
Ignorehelper to expand toAnnotated[T, NodeMeta(ignore=True)]. resolve_type_from_meta,Dependency.should_ignore, and_from_functionnow look for the new
metadata instead of literal sentinels, so nestedAnnotatedstacks keep functioning.- Tests and documentation were refreshed to showcase the new marker form.
Release v1.8.0
version 1.8.0
Minor typing improvements:
- now
use()without any arguments won't cause typing issue flatten_annotatedis a public method fromutils.typing_utils
Release v1.7.9
fix
fix a bug where union of unsolveable type is not treated as unsolvable type
def test_is_unsolvable_type():
assert is_unsolvable_type(str)
assert is_unsolvable_type(int)
assert is_unsolvable_type(list[str])
assert is_unsolvable_type(dict[int, float])
assert is_unsolvable_type(tuple[int, ...])
assert is_unsolvable_type(set[bytes])
assert is_unsolvable_type(Union[str, None])
assert not is_unsolvable_type(User)Release v1.7.8
Release v1.7.7
version 1.7.7
Highlights
Dependency.annotationnow stores the exact annotation object (includingtyping.Annotatedmetadata), so analysis tooling can interrogate user-defined markers without recomputing them.DependencyandDependentNodeare now slot-based data classes—no__dict__—which shrinks each node and makes accidental attribute writes fail fast.
Details
- Annotation preservation (
tests/versions/test_v1_7_7.py:20-tests/versions/test_v1_7_7.py:30): runninggraph.analyze(get_user)keeps the originalAnnotated[str, meta(...)]object onnode.dependencies[0].annotation, letting advanced callers recover the metadata that produced the dependency. - Slot-only nodes (
tests/versions/test_v1_7_7.py:31-tests/versions/test_v1_7_7.py:35,ididi/_node.py): bothDependencyandDependentNodenow declare__slots__, so reaching for__dict__raisesAttributeErrorand per-node memory drops because Python no longer allocates dictionaries for them.
from dataclasses import dataclass
from typing import Annotated
import pytest
from ididi import Graph, Ignore
@dataclass
class Meta:
name: str
version: str
def meta(name: str, version: str) -> Meta:
return Meta(name=name, version=version)
def get_user(query: Annotated[str, meta("query_param", "v1")]) -> Ignore[str]:
return f"User({query})"
graph = Graph()
node = graph.analyze(get_user)
dependency = node.dependencies[0]
assert dependency.annotation == Annotated[str, meta("query_param", "v1")]
with pytest.raises(AttributeError):
dependency.__dict__
with pytest.raises(AttributeError):
node.__dict__The preserved annotation makes it trivial to read custom metadata back out of analyzed dependencies, while __slots__ keeps heavily-used graph structures lean and predictable in large applications.
Release v1.7.6
version 1.7.6
Highlights
- Node reuse now starts as "unspecified", giving decorator registration and
use()hints a chance to agree before conflicts are raised. - Parameter-level reuse clashes now surface as
ParamReusabilityConflictError, pointing directly at the factory parameter that introduced the mismatch. use()return annotations propagate reuse flags to their nodes, so factory overrides can mark themselves reusable without extra bookkeeping.
Details
- Missing-as-default reuse (
tests/versions/test_v1_7_6.py:22): analyzing a dependency that usesuse(get_user, reuse=False)flips the previously unspecified node to non-reusable, and a conflictingreuse=Truehint now fails fast withParamReusabilityConflictError. - Return annotations drive node config (
tests/versions/test_v1_7_6.py:35): factories that returnAnnotated[T, use(reuse=True)]mark the target reusable immediately; later attempts to re-register them withreuse=FalseraiseConfigConflictError. - Explicit overrides stay authoritative (
tests/versions/test_v1_7_6.py:50,tests/versions/test_v1_7_6.py:66): decorator-specified reuse continues to win, and any subsequent override that disagrees is rejected withConfigConflictError. - Public
is_providedhelper (ididi/__init__.py:34): exposeis_providedso callers can tell whether a node's reuse flag has been set before deciding on overrides.
Behavior Change
Nodes now keep their reuse flag unset until code explicitly opts in, because _reuse starts as the sentinel MISSING during node construction (ididi/_node.py:349-ididi/_node.py:392). When the same dependent is included again we run through DependentNode.update_reusability; any side that still has _reuse equal to MISSING simply adopts the provided value, so ConfigConflictError only fires when both registrations made an explicit, conflicting choice (ididi/graph.py:700-ididi/graph.py:708). use(...) markers participate in the same flow: they publish their reuse preference while analysis walks the parameter graph, and we only escalate to ParamReusabilityConflictError when an earlier declaration disagrees (ididi/graph.py:551-ididi/graph.py:563, tests/versions/test_v1_7_6.py:22-tests/versions/test_v1_7_6.py:47). The public is_provided helper lets applications check whether a node already committed to a reuse value before attempting overrides (ididi/__init__.py:34, tests/versions/test_v1_7_6.py:26, tests/versions/test_v1_7_6.py:72).
from typing import Annotated
import pytest
from ididi import Graph, use, is_provided
from ididi.errors import ConfigConflictError, ParamReusabilityConflictError
graph = Graph()
class User: ...
# First registration – leaves reuse as the sentinel MISSING
graph.node(User)
assert not is_provided(graph.nodes[User].reuse)
# A handler pins the dependency to reuse=False; this now succeeds
class LoginHandler:
def __init__(self, user: Annotated[User, use(reuse=False)]):
self.user = user
graph.analyze(LoginHandler)
assert graph.nodes[User].reuse is False # value captured only now
# A conflicting handler tries to demand reuse=True; we now get a targeted error
class ProfileHandler:
def __init__(self, user: Annotated[User, use(reuse=True)]):
self.user = user
with pytest.raises(ParamReusabilityConflictError):
graph.analyze(ProfileHandler)
# Direct overrides behave the same way: once reuse is explicit, any opposite
# declaration fails fast with ConfigConflictError.
with pytest.raises(ConfigConflictError):
graph.node(User, reuse=True)
# If you really need an opposite policy, supply it before anyone else fixes reuse:
graph2 = Graph()
graph2.node(User, reuse=True) # explicit upfront
class AuditHandler:
def __init__(self, user: User): ...
graph2.analyze(AuditHandler) # fine, reuse already decidedPreviously graph.node(User) defaulted to a boolean reuse flag, so any later use(reuse=True)/use(reuse=False) annotation collided instantly. The sentinel-driven workflow leaves the decision open until a caller makes an explicit choice, keeping decorator registration, annotations, and overrides in sync and ensuring conflicts only occur when two intentional policies disagree.
Release v1.7.5
version 1.7.5
A quick patch to improve error message when reusability conflicts
dg = Graph()
class User: ...
@dg.node
def user_factory() -> Annotated[User, use(reuse=True)]:
return User()
class UserManager:
def __init__(self, user: User):
self.user = user
assert dg.nodes[User].reuse
@dg.node
def user_manager_factory(user: Annotated[User, use(user_factory, reuse=False)]) -> UserManager:
return UserManager(user)
with pytest.raises(ParamReusabilityConflictError):
dg.analyze(user_manager_factory)This would show error message
E ididi.errors.ParamReusabilityConflictError:
E user_manager_factory(Param[user: typing.Annotated[tests.test_graph.test_graph_analyze_reuse_dependentcy.<locals>.User, typing.Annotated[~T, '__ididi_use_factory__', <function test_graph_analyze_reuse_dependentcy.<locals>.user_factory at 0x7061322e2b80>, False]]]) has param conflict:
E Existing node DependentNode(type: <class 'tests.test_graph.test_graph_analyze_reuse_dependentcy.<locals>.User'>, factory: <function test_graph_analyze_reuse_dependentcy.<locals>.user_factory at 0x7061322e2b80>, function_dependent: False, reuse: True) has config conflicts with DependentNode(type: <class 'tests.test_graph.test_graph_analyze_reuse_dependentcy.<locals>.User'>, factory: <function test_graph_analyze_reuse_dependentcy.<locals>.user_factory at 0x7061322e2b80>, function_dependent: False, reuse: False)