Skip to content

Releases: raceychan/ididi

Release v1.5.0

17 Feb 18:29
Compare
Choose a tag to compare

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 with typing.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

11 Feb 14:19
Compare
Choose a tag to compare

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

09 Feb 15:18
Compare
Choose a tag to compare

version 1.4.3

Improvements:

50% performance boost for Graph.resolve/Graph.aresolve/Graph.entry

Features:

  • Graph now receives workers: concurrent.futures.ThreadPoolExecutor in constructor

  • send entering and exiting of contextmanager in a diffrent thread when using AsyncScope, 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 to Graph.scope and Graph.ascope.
  • Deprecate DependencyGraph, DependencyGraph.static_resolve, DependencyGraph.static_resolve_all
  • variadic arguments, such as *args or **kwargs without typing.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

05 Feb 12:24
Compare
Choose a tag to compare

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

04 Feb 10:44
Compare
Choose a tag to compare

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

03 Feb 09:25
Compare
Choose a tag to compare

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

03 Feb 07:57
Compare
Choose a tag to compare

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

03 Feb 05:26
Compare
Choose a tag to compare

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

02 Feb 02:18
Compare
Choose a tag to compare

version 1.3.4

This patch mainly focuses on code maintainence and reduce memory usage & performance boost.

  • refactor Scope and Graph to avoid circular reference, Scope not longer depends on Graph
  • Graph.resolve is approxiamately 17%-20% faster

Release v1.3.3

29 Jan 00:23
Compare
Choose a tag to compare

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.