diff --git a/README.md b/README.md index 3ded85c..3a174ee 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ from ...plugin.....other_product import OtherProduct @product( module=SomeModule, # Declares the module or plugin this component belongs to imports=[SomeService, ...], # List of dependencies (components) that this product needs - provider=providers.Singleton, # Provider type (Singleton, Factory, Resource) + provider=providers.Factory, # Provider type (Singleton, Factory, Resource) ) class SomeProduct(Interface, Product): """This is the product class. This class will check for its dependencies. @@ -216,32 +216,28 @@ class SomeProduct(Interface, Product): ## Important Notes -- Declare all the dependencies (components) on Instances and Products to avoid injection issues. +- Remember to declare all the dependencies you need in the `imports` parameter of the `@instance` or `@product` decorator. - Read the documentation carefully and refer to the examples to understand the framework's behavior. ## Usage Examples This repository includes a practical example demonstrating how to use the framework. You can find this example in the `example` directory. It showcases the implementation of the core components and how they interact to manage dependencies effectively in a sample application. -This example requires the `module-injection` package to be installed and the `library` folder to be present in the project root. - ## Future Work This project is a work in progress, and there are several improvements and enhancements planned for the future. Some planned features are: +- Add pre-defined components for common patterns and use cases +- Dependency CLI support for easier interaction with the framework +- Pytest testing framework integration for better test management + +Some new improvements that has been recently added: - Enhance documentation and examples for better understanding - Implement framework API and extension points for customization - Improve injection resolution and initialization process -- Testing framework integration for better test coverage - Visualization tools for dependency graphs and relationships -Some of the areas that will be explored in the future include: -- Add some basic components and plugins for common use cases -- Dependency CLI support for easier interaction with the framework -- Explore more advanced dependency injection patterns and use cases -- Improve testing and validation for projects using this framework - Pending issues that eventually will be addressed: - Migration guide from previous versions (some breaking changes were introduced) diff --git a/pyproject.toml b/pyproject.toml index 4ddcb4e..31b10d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "pytest-asyncio", "pytest-cov", "pytest-xdist", + "uvloop", ] [tool.hatch.envs.build.env-vars] @@ -68,13 +69,12 @@ testpaths = ["tests"] [project] name = "module_dependency" -version = "1.1.5" +version = "1.1.6" dependencies = [ "dependency_injector", "jinja2", "pydantic", "pydantic-settings", - "uvloop", ] requires-python = ">=3.11" authors = [ diff --git a/src/dependency/library/graph/generate.py b/src/dependency/library/graph/generate.py index 6469501..f887f1e 100644 --- a/src/dependency/library/graph/generate.py +++ b/src/dependency/library/graph/generate.py @@ -1,6 +1,6 @@ from dependency.core import Registry -from dependency.library.graph.models import Graph -from dependency.library.graph.process import process_container +from dependency.core.injection import ContainerInjection, ProviderInjection +from dependency.library.graph.models import Graph, Cluster, Node, Edge def generate_graph( output: str = "build/output", @@ -8,16 +8,49 @@ def generate_graph( ) -> None: """Generate a graph visualization of the registered containers and providers. - This method is intended for debugging and documentation purposes, allowing - developers to visualize the structure of their dependency graph. It uses - the graphviz library to create a visual representation of the nodes and - their relationships. - """ + This method allows you to visualize the structure of your dependency graph, including the + containers (modules) and providers (components/products) and their relationships. The generated + graph can be used for debugging, documentation, or simply to understand the structure of your + dependency graph. The output will be saved as an SVG file at the specified location. + Args: + output: The output path for the generated graph. + ignore_modules: A set of module names to ignore during graph generation. + """ graph: Graph = Graph(name="Dependency Graph") for container in Registry.containers: if container.is_root: - process_container(graph, container, ignore_modules) + graph.drawable.append(process_container(graph, container, ignore_modules)) digraph = graph.draw() digraph.render(filename=output, format="svg") # type: ignore + +def process_container( + graph: Graph, + container: ContainerInjection, + ignore_modules: set[str] = {"BasePlugin"}, +) -> Cluster: + cluster = Cluster(name=container.name) + for child in container.childs: + if isinstance(child, ContainerInjection): + cluster.childs.append(process_container(graph, child, ignore_modules)) + elif isinstance(child, ProviderInjection): + cluster.childs.append(process_provider(graph, child, ignore_modules)) + return cluster + +def process_provider( + graph: Graph, + provider: ProviderInjection, + ignore_modules: set[str] = {"BasePlugin"}, +) -> Node: + if provider.parent is not None and str(provider.parent) in ignore_modules: + return Node(name=provider.name) + + for dependent in provider.injectable.dependent: + source: str = provider.injectable.interface_cls.__name__ + target: str = dependent.interface_cls.__name__ + edge: Edge = Edge(source=source, target=target) + graph.edges.append(edge) + + in_degree: int = provider.injectable.weight() + return Node(name=provider.name, in_degree=in_degree) diff --git a/src/dependency/library/graph/models.py b/src/dependency/library/graph/models.py index dbccab3..4c3afc5 100644 --- a/src/dependency/library/graph/models.py +++ b/src/dependency/library/graph/models.py @@ -1,12 +1,13 @@ from abc import ABC, abstractmethod -from itertools import pairwise +from itertools import groupby, pairwise from graphviz import Digraph from pydantic import BaseModel -from typing import Optional + +GROUP_SIZE: int = 2 class Graph(BaseModel): name: str = "Dependency Graph" - drawable: dict[str, Drawable] = {} + drawable: list[Drawable] = [] edges: list[Edge] = [] def draw(self) -> Digraph: @@ -14,7 +15,7 @@ def draw(self) -> Digraph: graph.attr(rankdir="TB", newrank="true", ordering="in", overlap="false", splines="true", nodesep="1.0", ranksep="1.0") graph.attr("node", fontname="Helvetica", fontsize="12", margin="0.2", style="invis") - for drawable in self.drawable.values(): + for drawable in self.drawable: drawable.draw(graph) for edge in self.edges: edge.draw(graph) @@ -22,7 +23,6 @@ def draw(self) -> Digraph: class Drawable(BaseModel, ABC): name: str - label: Optional[str] = None in_degree: int = 0 @abstractmethod @@ -31,7 +31,6 @@ def draw(self, parent: Digraph) -> None: class Cluster(Drawable): childs: list[Drawable] = [] - include_modules: bool = True style: dict[str, str] = { "style": "rounded,filled", "fillcolor": "lightyellow", @@ -40,17 +39,26 @@ class Cluster(Drawable): } def draw(self, parent: Digraph) -> None: - name: str = f"cluster_{self.name}" if self.include_modules else self.name - with parent.subgraph(name=name) as c: + with parent.subgraph(name=f"cluster_{self.name}") as c: c.attr(label=self.name, **self.style) + # Agrupar por profundidad y ordenar por in_degree dentro de cada grupo + def bucket(x: Drawable): return x.in_degree // GROUP_SIZE childs: list[Drawable] = sorted(self.childs, key=lambda c: c.in_degree) - for child in childs: - child.draw(c) + groups = [list(g) for _, g in groupby(childs, key=bucket)] + + for group in groups: + for child in group: + child.draw(c) + + # Arista invisible entre nodos del mismo grupo para mantenerlos juntos + for i, (n1, n2) in enumerate(pairwise(group)): + if isinstance(n2, Node) and i % min(2, max(1, len(group) // GROUP_SIZE)) != 0: + c.edge(n1.name, n2.name, style="invis", weight="1") - # Encadenar nodos verticalmente con aristas invisibles - for i, j in pairwise(childs): - c.edge(i.name, j.name, style="invis", weight="10") + # Arista invisible solo entre representantes de grupos consecutivos + for (g1, g2) in pairwise(groups): + c.edge(g1[0].name, g2[0].name, style="invis", weight="1") class Node(Drawable): style: dict[str, str] = { @@ -60,15 +68,12 @@ class Node(Drawable): } def draw(self, parent: Digraph) -> None: - parent.node(self.name, label=self.label, **self.style) + parent.node(self.name, **self.style) class Edge(BaseModel): source: str target: str - same_cluster: bool = False def draw(self, parent: Digraph) -> None: kwargs: dict[str, str] = {} - if self.same_cluster: - kwargs["constraint"] = "false" - parent.edge(self.source, self.target, **kwargs) + parent.edge(self.source, self.target, weight="5", minlen="1", **kwargs) diff --git a/src/dependency/library/graph/process.py b/src/dependency/library/graph/process.py deleted file mode 100644 index 7501c63..0000000 --- a/src/dependency/library/graph/process.py +++ /dev/null @@ -1,33 +0,0 @@ -from dependency.core.injection import ContainerInjection, ProviderInjection -from dependency.library.graph.models import Graph, Drawable, Cluster, Node, Edge - -def process_container( - graph: Graph, - container: ContainerInjection, - ignore_modules: set[str] = {"BasePlugin"}, -) -> Cluster: - cluster = Cluster(name=container.name) - for child in container.childs: - if isinstance(child, ContainerInjection): - cluster.childs.append(process_container(graph, child, ignore_modules)) - elif isinstance(child, ProviderInjection): - cluster.childs.append(process_provider(graph, child, ignore_modules)) - graph.drawable[container.name] = cluster - return cluster - -def process_provider( - graph: Graph, - provider: ProviderInjection, - ignore_modules: set[str] = {"BasePlugin"}, -) -> Node: - if provider.parent is not None and str(provider.parent) in ignore_modules: - return Node(name=provider.name) - - for dependent in provider.injectable.dependent: - source: str = provider.injectable.interface_cls.__name__ - target: str = dependent.interface_cls.__name__ - edge: Edge = Edge(source=source, target=target) - graph.edges.append(edge) - - in_degree: int = provider.injectable.weight() - return Node(name=provider.name, in_degree=in_degree) diff --git a/src/dependency/library/graph/utils.py b/src/dependency/library/graph/utils.py deleted file mode 100644 index 457c50d..0000000 --- a/src/dependency/library/graph/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -from dependency.core.injection import Injectable, ProviderInjection - -def find_first_parent( - provider: ProviderInjection, - visited: set[ProviderInjection] | None = None, - injectable_to_provider: dict[Injectable, ProviderInjection] = {}, -) -> str: - if visited is None: - visited = set() - if provider in visited: - return "fallback" - visited.add(provider) - - if provider.parent is not None: - if provider.parent.name not in ("RootInternal", "FallbackInternal"): - return provider.parent.name - - scores: dict[str, int] = {} - for dependent in provider.injectable.dependent: - dep_provider = injectable_to_provider.get(dependent) - if dep_provider is None: - continue - result = find_first_parent(dep_provider, visited, injectable_to_provider) - if result != "fallback": - scores[result] = scores.get(result, 0) + 1 - - if scores: - return max(scores, key=lambda k: scores[k]) - return "fallback" diff --git a/src/example/diagram.svg b/src/example/diagram.svg index 531e49d..6188d08 100644 --- a/src/example/diagram.svg +++ b/src/example/diagram.svg @@ -1,149 +1,143 @@ - - - - + + + -cluster_BasePlugin - -BasePlugin - - cluster_ReporterPlugin - -ReporterPlugin + +ReporterPlugin cluster_HardwarePlugin - -HardwarePlugin - - - -NumberService - -NumberService + +HardwarePlugin - - -StringService - -StringService + +cluster_BasePlugin + +BasePlugin - - - -DeferredService - -DeferredService + + +ReporterA + +ReporterA - - + ReporterFactory - -ReporterFactory + +ReporterFactory + + + +ReporterA->ReporterFactory + + - + ReportFacade - -ReportFacade + +ReportFacade + - - + ReporterFactory->ReportFacade - - - - - -ReporterA - -ReporterA - - - -ReporterA->ReporterFactory - - + + - - - -HardwareAbstraction - -HardwareAbstraction - - - -HardwareAbstraction->ReportFacade - - - - - -HardwareA - -HardwareA - - - + HardwareObserver - -HardwareObserver - - - - -HardwareFactory - -HardwareFactory - - - -HardwareA->HardwareFactory - - + +HardwareObserver - + HardwareObserver->ReporterA - - + + - - -HardwareB - -HardwareB + + +HardwareFactory + +HardwareFactory - - + + HardwareObserver->HardwareFactory - - + + + + + +HardwareB + +HardwareB - - + HardwareB->HardwareFactory - - + + + + + +HardwareA + +HardwareA + + + +HardwareA->HardwareFactory + + + + + +HardwareAbstraction + +HardwareAbstraction - + HardwareFactory->HardwareAbstraction - - + + + + + +HardwareAbstraction->ReportFacade + + + + + +NumberService + +NumberService + + + +DeferredService + +DeferredService + + + +StringService + +StringService diff --git a/stubs/dependency/library/graph/generate.pyi b/stubs/dependency/library/graph/generate.pyi index 5d6fa13..b587b16 100644 --- a/stubs/dependency/library/graph/generate.pyi +++ b/stubs/dependency/library/graph/generate.pyi @@ -1,6 +1,6 @@ from dependency.core import Registry as Registry -from dependency.library.graph.models import Graph as Graph -from dependency.library.graph.process import process_container as process_container +from dependency.core.injection import ContainerInjection as ContainerInjection, ProviderInjection as ProviderInjection +from dependency.library.graph.models import Cluster as Cluster, Edge as Edge, Graph as Graph, Node as Node def generate_graph(output: str = 'build/output', ignore_modules: set[str] = {'BasePlugin'}) -> None: """Generate a graph visualization of the registered containers and providers. @@ -10,3 +10,5 @@ def generate_graph(output: str = 'build/output', ignore_modules: set[str] = {'Ba the graphviz library to create a visual representation of the nodes and their relationships. """ +def process_container(graph: Graph, container: ContainerInjection, ignore_modules: set[str] = {'BasePlugin'}) -> Cluster: ... +def process_provider(graph: Graph, provider: ProviderInjection, ignore_modules: set[str] = {'BasePlugin'}) -> Node: ... diff --git a/stubs/dependency/library/graph/models.pyi b/stubs/dependency/library/graph/models.pyi index 2f6f7c2..3483369 100644 --- a/stubs/dependency/library/graph/models.pyi +++ b/stubs/dependency/library/graph/models.pyi @@ -3,24 +3,23 @@ from abc import ABC, abstractmethod from graphviz import Digraph from pydantic import BaseModel +GROUP_SIZE: int + class Graph(BaseModel): name: str - drawable: dict[str, Drawable] + drawable: list[Drawable] edges: list[Edge] def draw(self) -> Digraph: ... class Drawable(BaseModel, ABC, metaclass=abc.ABCMeta): name: str - label: str | None in_degree: int @abstractmethod def draw(self, parent: Digraph) -> None: ... class Cluster(Drawable): childs: list[Drawable] - include_modules: bool style: dict[str, str] - def get_childs(self) -> list[Drawable]: ... def draw(self, parent: Digraph) -> None: ... class Node(Drawable): @@ -30,5 +29,4 @@ class Node(Drawable): class Edge(BaseModel): source: str target: str - same_cluster: bool def draw(self, parent: Digraph) -> None: ... diff --git a/stubs/dependency/library/graph/process.pyi b/stubs/dependency/library/graph/process.pyi deleted file mode 100644 index 670f006..0000000 --- a/stubs/dependency/library/graph/process.pyi +++ /dev/null @@ -1,5 +0,0 @@ -from dependency.core.injection import ContainerInjection as ContainerInjection, ProviderInjection as ProviderInjection -from dependency.library.graph.models import Cluster as Cluster, Drawable as Drawable, Edge as Edge, Graph as Graph, Node as Node - -def process_container(graph: Graph, container: ContainerInjection, ignore_modules: set[str] = {'BasePlugin'}) -> Cluster: ... -def process_provider(graph: Graph, provider: ProviderInjection, ignore_modules: set[str] = {'BasePlugin'}) -> Node: ...