Coverage for archdocs/main.py: 100%
43 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-03 00:00 +0000
1import dataclasses
2import functools
3import pathlib
4import typing
5from concurrent import futures
7from archdocs import diagram_model, mermaid_syntax, settings
8from archdocs.mapping import (
9 MAPPING_OF_MANIFEST_PARSERS_AND_RENDERERS,
10 MAPPING_OF_PARSERS_AND_RENDERERS,
11 ManifestFeatureFunctions,
12)
15"""TODO:
17parsers from settings.py typical configuration
18parsers from docker-compose.yml?
19"""
22@typing.final
23@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
24class SettingsForArchdocs:
25 root_dir: str | pathlib.Path
26 service_name: str
27 kubernetes_dir: str | pathlib.Path | None = None
30type _ParsedManifests = tuple[tuple[ManifestFeatureFunctions, typing.Any], ...]
33def _parse_every_manifest(
34 root_path: pathlib.Path,
35 configured_manifest_dir: str | pathlib.Path | None,
36 /,
37) -> _ParsedManifests:
38 return tuple(
39 (
40 one_manifest_functions,
41 one_manifest_functions.parse_manifests(
42 one_manifest_functions.read_source(root_path, configured_manifest_dir),
43 ),
44 )
45 for one_manifest_functions in MAPPING_OF_MANIFEST_PARSERS_AND_RENDERERS.values()
46 )
49def _render_manifest_annotations(all_parsed_manifests: _ParsedManifests, /) -> tuple[str, ...]:
50 return tuple(
51 one_annotation
52 for one_manifest_functions, one_parsed_manifest in all_parsed_manifests
53 for one_annotation in one_manifest_functions.render_node_annotations(one_parsed_manifest)
54 )
57def _render_manifest_edges(
58 service_node: diagram_model.DiagramNode,
59 all_parsed_manifests: _ParsedManifests,
60 /,
61) -> tuple[diagram_model.DiagramEdge, ...]:
62 return tuple(
63 one_edge
64 for one_manifest_functions, one_parsed_manifest in all_parsed_manifests
65 for one_edge in one_manifest_functions.render_diagram(service_node, one_parsed_manifest)
66 )
69@typing.final
70@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
71class ArchitectureParserAndRenderer:
72 local_settings: SettingsForArchdocs
73 _rendered_diagram_cache: list[str] = dataclasses.field(default_factory=list)
75 def render_architecture_diagram(self) -> str:
76 # "why you doesnt use functools.cache lol"
77 # https://docs.astral.sh/ruff/rules/cached-instance-method/#cached-instance-method-b019
78 if self._rendered_diagram_cache:
79 return self._rendered_diagram_cache[0]
80 full_result: typing.Final = self._render_every_diagram_line()
81 self._rendered_diagram_cache.append(full_result)
82 return full_result
84 def _render_every_diagram_line(self) -> str:
85 root_path: typing.Final = pathlib.Path(self.local_settings.root_dir).resolve()
86 all_parsed_manifests: typing.Final = _parse_every_manifest(root_path, self.local_settings.kubernetes_dir)
87 service_node: typing.Final = diagram_model.build_service_node(
88 self.local_settings.service_name,
89 _render_manifest_annotations(all_parsed_manifests),
90 )
91 all_edges: typing.Final = (
92 *_render_manifest_edges(service_node, all_parsed_manifests),
93 *self._process_source_files(service_node, root_path),
94 )
95 return mermaid_syntax.MermaidDiagram(service_node=service_node, all_edges=all_edges).render_every_line()
97 def _process_source_files(
98 self,
99 service_node: diagram_model.DiagramNode,
100 root_path: pathlib.Path,
101 /,
102 ) -> tuple[diagram_model.DiagramEdge, ...]:
103 py_files: typing.Final = sorted(
104 one_src_file
105 for one_src_file in root_path.rglob(settings.FILES_SEARCH_PATTERN)
106 if settings.SKIPPED_DIR_NAMES.isdisjoint(one_src_file.relative_to(root_path).parts)
107 )
108 with futures.ThreadPoolExecutor(max_workers=settings.MAX_WORKERS) as executor:
109 all_rendered_files: typing.Final = executor.map(
110 functools.partial(self._process_one_file, service_node),
111 py_files,
112 )
113 return tuple(one_edge for one_file_edges in all_rendered_files for one_edge in one_file_edges)
115 def _process_one_file(
116 self,
117 service_node: diagram_model.DiagramNode,
118 one_src_file: pathlib.Path,
119 /,
120 ) -> tuple[diagram_model.DiagramEdge, ...]:
121 raw_file_source: typing.Final = one_src_file.read_text()
122 return tuple(
123 one_edge
124 for one_feature_functions in MAPPING_OF_PARSERS_AND_RENDERERS.values()
125 for one_edge in one_feature_functions.render_diagram(
126 service_node,
127 one_feature_functions.parse_source(raw_file_source),
128 )
129 )