modwire 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {modwire-2.0.0 → modwire-2.1.0}/PKG-INFO +1 -1
  2. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/__init__.py +10 -0
  3. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/_version.py +3 -3
  4. modwire-2.1.0/src/modwire/callables.py +162 -0
  5. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/definitions.py +64 -0
  6. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/models.py +49 -1
  7. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/serialization.py +3 -2
  8. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/base.py +3 -0
  9. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/scripts/php_extractor.php +459 -5
  10. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/scripts/python_extractor.py +393 -2
  11. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/scripts/typescript_extractor.js +380 -2
  12. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/testing/factories.py +109 -0
  13. {modwire-2.0.0 → modwire-2.1.0}/src/modwire.egg-info/PKG-INFO +1 -1
  14. {modwire-2.0.0 → modwire-2.1.0}/src/modwire.egg-info/SOURCES.txt +1 -0
  15. {modwire-2.0.0 → modwire-2.1.0}/tests/test_api.py +211 -1
  16. {modwire-2.0.0 → modwire-2.1.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  17. {modwire-2.0.0 → modwire-2.1.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  18. {modwire-2.0.0 → modwire-2.1.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  19. {modwire-2.0.0 → modwire-2.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {modwire-2.0.0 → modwire-2.1.0}/.github/workflows/ci.yml +0 -0
  21. {modwire-2.0.0 → modwire-2.1.0}/.github/workflows/release.yml +0 -0
  22. {modwire-2.0.0 → modwire-2.1.0}/.gitignore +0 -0
  23. {modwire-2.0.0 → modwire-2.1.0}/CONTRIBUTING.md +0 -0
  24. {modwire-2.0.0 → modwire-2.1.0}/LICENSE +0 -0
  25. {modwire-2.0.0 → modwire-2.1.0}/README.md +0 -0
  26. {modwire-2.0.0 → modwire-2.1.0}/docs/wiki/Development-checks.md +0 -0
  27. {modwire-2.0.0 → modwire-2.1.0}/docs/wiki/Home.md +0 -0
  28. {modwire-2.0.0 → modwire-2.1.0}/docs/wiki/Reporting-bugs.md +0 -0
  29. {modwire-2.0.0 → modwire-2.1.0}/docs/wiki/Requesting-features.md +0 -0
  30. {modwire-2.0.0 → modwire-2.1.0}/pyproject.toml +0 -0
  31. {modwire-2.0.0 → modwire-2.1.0}/setup.cfg +0 -0
  32. {modwire-2.0.0 → modwire-2.1.0}/show_test_source_files.py +0 -0
  33. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/__init__.py +0 -0
  34. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/analyzers.py +0 -0
  35. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/config.py +0 -0
  36. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/insights.py +0 -0
  37. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/matching.py +0 -0
  38. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/policy.py +0 -0
  39. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/render.py +0 -0
  40. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/architecture/violations.py +0 -0
  41. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/exports.py +0 -0
  42. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/__init__.py +0 -0
  43. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/cache.py +0 -0
  44. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/manifest.py +0 -0
  45. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/roots.py +0 -0
  46. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extraction/service.py +0 -0
  47. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/__init__.py +0 -0
  48. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/loader.py +0 -0
  49. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/php.py +0 -0
  50. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/python.py +0 -0
  51. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/extractors/typescript.py +0 -0
  52. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/graph.py +0 -0
  53. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/metadata.py +0 -0
  54. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/shape/__init__.py +0 -0
  55. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/shape/config.py +0 -0
  56. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/shape/evaluator.py +0 -0
  57. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/shape/rules.py +0 -0
  58. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/shape/violations.py +0 -0
  59. {modwire-2.0.0 → modwire-2.1.0}/src/modwire/testing/__init__.py +0 -0
  60. {modwire-2.0.0 → modwire-2.1.0}/src/modwire.egg-info/dependency_links.txt +0 -0
  61. {modwire-2.0.0 → modwire-2.1.0}/src/modwire.egg-info/requires.txt +0 -0
  62. {modwire-2.0.0 → modwire-2.1.0}/src/modwire.egg-info/top_level.txt +0 -0
  63. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/php/ignored/generated.php +0 -0
  64. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/php/src/application/use_cases/activate.php +0 -0
  65. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/php/src/domain/model/user.php +0 -0
  66. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/php/src/domain/services/policy.php +0 -0
  67. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/php/src/interfaces/http/controller.php +0 -0
  68. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/python/ignored/generated.py +0 -0
  69. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/python/src/application/use_cases/activate.py +0 -0
  70. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/python/src/domain/model/user.py +0 -0
  71. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/python/src/domain/services/policy.py +0 -0
  72. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/python/src/interfaces/http/controller.py +0 -0
  73. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/ignored/generated.ts +0 -0
  74. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/application/use_cases/activate.ts +0 -0
  75. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/domain/model/profile.tsx +0 -0
  76. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/domain/model/user.ts +0 -0
  77. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/domain/services/audit.js +0 -0
  78. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/domain/services/policy.ts +0 -0
  79. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/interfaces/http/controller.ts +0 -0
  80. {modwire-2.0.0 → modwire-2.1.0}/tests/apps/typescript/src/interfaces/http/view.jsx +0 -0
  81. {modwire-2.0.0 → modwire-2.1.0}/tests/test_architecture_api.py +0 -0
  82. {modwire-2.0.0 → modwire-2.1.0}/tests/test_standalone.py +0 -0
  83. {modwire-2.0.0 → modwire-2.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modwire
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Extract source-code dependencies and build dependency graphs.
5
5
  Author: Tomasz Szpak
6
6
  License-Expression: MIT
@@ -1,4 +1,10 @@
1
1
  from ._version import __version__
2
+ from .callables import (
3
+ CallableReportEntry,
4
+ callable_report_entries,
5
+ render_callable_report,
6
+ structured_callable_report,
7
+ )
2
8
  from .extraction import (
3
9
  CodeMap,
4
10
  CodeMapSerializationError,
@@ -46,6 +52,7 @@ from .shape import (
46
52
  __all__ = [
47
53
  "CodeMap",
48
54
  "CodeMapSerializationError",
55
+ "CallableReportEntry",
49
56
  "DependencyGraph",
50
57
  "Edge",
51
58
  "EXTRACTION_SCHEMA_VERSION",
@@ -69,6 +76,7 @@ __all__ = [
69
76
  "UnusedExport",
70
77
  "__version__",
71
78
  "build_dependency_graph",
79
+ "callable_report_entries",
72
80
  "deserialize_code_map",
73
81
  "discover_sources",
74
82
  "evaluate_shape",
@@ -79,8 +87,10 @@ __all__ = [
79
87
  "languages",
80
88
  "normalize_source_id",
81
89
  "require_runtime",
90
+ "render_callable_report",
82
91
  "runtime_diagnostics",
83
92
  "serialize_code_map",
93
+ "structured_callable_report",
84
94
  "supported_languages",
85
95
  "validate_shape_config",
86
96
  ]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.0'
22
- __version_tuple__ = version_tuple = (2, 0, 0)
21
+ __version__ = version = '2.1.0'
22
+ __version_tuple__ = version_tuple = (2, 1, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gb985fe227'
24
+ __commit_id__ = commit_id = 'g54f33f5a3'
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable
5
+
6
+ from .definitions import SourceCall, SourceCallable
7
+ from .extraction import CodeMap
8
+
9
+
10
+ PathDisplay = Callable[[str], str]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CallableReportEntry:
15
+ source_callable: SourceCallable
16
+ calls: tuple[SourceCall, ...]
17
+ callers: tuple[SourceCall, ...]
18
+
19
+ def to_dict(self) -> dict[str, object]:
20
+ return {
21
+ "callable": self.source_callable.model_dump(mode="json"),
22
+ "calls": [source_call.model_dump(mode="json") for source_call in self.calls],
23
+ "callers": [
24
+ source_call.model_dump(mode="json") for source_call in self.callers
25
+ ],
26
+ }
27
+
28
+
29
+ def callable_report_entries(code_map: CodeMap) -> tuple[CallableReportEntry, ...]:
30
+ return tuple(
31
+ CallableReportEntry(
32
+ source_callable=source_callable,
33
+ calls=tuple(
34
+ sorted(
35
+ code_map.calls_from(source_callable.id),
36
+ key=lambda source_call: (
37
+ source_call.line,
38
+ source_call.target_name,
39
+ source_call.expression,
40
+ ),
41
+ )
42
+ ),
43
+ callers=tuple(
44
+ sorted(
45
+ code_map.calls_to(source_callable.id),
46
+ key=lambda source_call: (
47
+ source_call.source_id,
48
+ source_call.line,
49
+ source_call.source_callable_id,
50
+ ),
51
+ )
52
+ ),
53
+ )
54
+ for source_callable in sorted(
55
+ (
56
+ source_callable
57
+ for source_file in code_map.extraction_result.files.values()
58
+ for source_callable in source_file.callables
59
+ ),
60
+ key=lambda source_callable: (
61
+ source_callable.source_id,
62
+ source_callable.line_start,
63
+ source_callable.qualified_name,
64
+ ),
65
+ )
66
+ )
67
+
68
+
69
+ def structured_callable_report(code_map: CodeMap) -> tuple[dict[str, object], ...]:
70
+ return tuple(entry.to_dict() for entry in callable_report_entries(code_map))
71
+
72
+
73
+ def render_callable_report(
74
+ code_map: CodeMap,
75
+ *,
76
+ path_display: PathDisplay = str,
77
+ ) -> str:
78
+ entries = callable_report_entries(code_map)
79
+ if not entries:
80
+ return "Callable Report\n\nNo callables found."
81
+
82
+ callable_names = {
83
+ entry.source_callable.id: entry.source_callable.qualified_name
84
+ for entry in entries
85
+ }
86
+ lines = ["Callable Report", ""]
87
+ current_source_id = ""
88
+ for entry in entries:
89
+ source_callable = entry.source_callable
90
+ if source_callable.source_id != current_source_id:
91
+ current_source_id = source_callable.source_id
92
+ lines.extend([f"## {path_display(current_source_id)}", ""])
93
+ lines.append(
94
+ "- "
95
+ f"{source_callable.qualified_name} "
96
+ f"[{source_callable.kind}] "
97
+ f"lines {source_callable.line_start}-{source_callable.line_end}"
98
+ )
99
+ lines.append(" Calls:")
100
+ lines.extend(
101
+ _call_lines(
102
+ entry.calls,
103
+ callable_names=callable_names,
104
+ direction="outgoing",
105
+ path_display=path_display,
106
+ )
107
+ )
108
+ lines.append(" Called by:")
109
+ lines.extend(
110
+ _call_lines(
111
+ entry.callers,
112
+ callable_names=callable_names,
113
+ direction="incoming",
114
+ path_display=path_display,
115
+ )
116
+ )
117
+ lines.append("")
118
+ return "\n".join(lines).rstrip()
119
+
120
+
121
+ def _call_lines(
122
+ calls: tuple[SourceCall, ...],
123
+ *,
124
+ callable_names: dict[str, str],
125
+ direction: str,
126
+ path_display: PathDisplay,
127
+ ) -> list[str]:
128
+ if not calls:
129
+ return [" - none"]
130
+
131
+ rendered = []
132
+ for source_call in calls:
133
+ if direction == "incoming":
134
+ caller_name = callable_names.get(
135
+ source_call.source_callable_id,
136
+ source_call.source_callable_id,
137
+ )
138
+ rendered.append(
139
+ f" - {caller_name} "
140
+ f"at {path_display(source_call.source_id)}:{source_call.line}"
141
+ )
142
+ continue
143
+
144
+ target = (
145
+ callable_names[source_call.target_callable_id]
146
+ if source_call.target_callable_id in callable_names
147
+ else source_call.target_name
148
+ )
149
+ rendered.append(
150
+ f" - {source_call.expression} -> {target} "
151
+ f"at {path_display(source_call.source_id)}:{source_call.line} "
152
+ f"({source_call.resolution})"
153
+ )
154
+ return rendered
155
+
156
+
157
+ __all__ = [
158
+ "CallableReportEntry",
159
+ "callable_report_entries",
160
+ "render_callable_report",
161
+ "structured_callable_report",
162
+ ]
@@ -6,6 +6,19 @@ from pydantic import BaseModel, Field
6
6
  ImportCrossingType = Literal["module", "symbol"]
7
7
  SourceVisibility = Literal["public", "protected", "private"]
8
8
  SourceSignatureKind = Literal["call", "construct", "index"]
9
+ SourceValueDeclarationKind = Literal["assignment", "constant", "property", "unknown"]
10
+ SourceValueKind = Literal["callable", "class", "literal", "object", "unknown"]
11
+ SourceParameterKind = Literal["positional", "vararg", "keyword_only", "kwarg"]
12
+ SourceCallableKind = Literal[
13
+ "function",
14
+ "method",
15
+ "classmethod",
16
+ "staticmethod",
17
+ "constructor",
18
+ "callable_value",
19
+ "anonymous",
20
+ ]
21
+ SourceCallResolution = Literal["resolved", "unresolved", "external", "dynamic"]
9
22
  SourceExportKind = Literal[
10
23
  "module",
11
24
  "class",
@@ -64,6 +77,54 @@ class SourceFunction(BaseModel):
64
77
  optional_args: int
65
78
 
66
79
 
80
+ class SourceValue(BaseModel):
81
+ name: str
82
+ visibility: SourceVisibility
83
+ visibility_intent: SourceVisibility
84
+ line_count: int
85
+ declaration_kind: SourceValueDeclarationKind
86
+ value_kind: SourceValueKind
87
+ declared_args: int = 0
88
+ optional_args: int = 0
89
+
90
+
91
+ class SourceParameter(BaseModel):
92
+ name: str
93
+ annotation: str = ""
94
+ kind: SourceParameterKind
95
+ has_default: bool = False
96
+
97
+
98
+ class SourceCallable(BaseModel):
99
+ id: str
100
+ source_id: str
101
+ name: str
102
+ qualified_name: str
103
+ owner_name: str = ""
104
+ kind: SourceCallableKind
105
+ visibility: SourceVisibility
106
+ visibility_intent: SourceVisibility
107
+ line_start: int
108
+ line_end: int
109
+ line_count: int
110
+ parameters: list[SourceParameter] = Field(default_factory=list)
111
+ declared_args: int = 0
112
+ optional_args: int = 0
113
+ return_annotation: str = ""
114
+ decorators: list[str] = Field(default_factory=list)
115
+ docstring: str = ""
116
+
117
+
118
+ class SourceCall(BaseModel):
119
+ source_callable_id: str
120
+ target_callable_id: str = ""
121
+ source_id: str
122
+ line: int
123
+ expression: str
124
+ resolution: SourceCallResolution
125
+ target_name: str
126
+
127
+
67
128
  class SourceClassMethod(BaseModel):
68
129
  name: str
69
130
  visibility: SourceVisibility
@@ -131,6 +192,9 @@ class SourceFile(BaseModel):
131
192
  types: list[SourceType] = Field(default_factory=list)
132
193
  abstract_classes: list[SourceAbstractClass] = Field(default_factory=list)
133
194
  functions: list[SourceFunction]
195
+ values: list[SourceValue] = Field(default_factory=list)
196
+ callables: list[SourceCallable] = Field(default_factory=list)
197
+ calls: list[SourceCall] = Field(default_factory=list)
134
198
  line_count: int
135
199
  code_line_count: int
136
200
  public_symbol_count: int
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
- from ..definitions import SourceFile
8
+ from ..definitions import SourceCall, SourceCallable, SourceFile
9
9
  from ..graph import DependencyGraph, Edge
10
10
  from .roots import SourceIdMode
11
11
 
@@ -58,6 +58,54 @@ class CodeMap:
58
58
  def external_edges(self) -> tuple[Edge, ...]:
59
59
  return self.graph.external_edges(self.source_ids())
60
60
 
61
+ def callable_ids(self) -> tuple[str, ...]:
62
+ return tuple(
63
+ source_callable.id
64
+ for source_file in self.extraction_result.files.values()
65
+ for source_callable in source_file.callables
66
+ )
67
+
68
+ def callable(self, callable_id: str) -> SourceCallable:
69
+ for source_file in self.extraction_result.files.values():
70
+ for source_callable in source_file.callables:
71
+ if source_callable.id == callable_id:
72
+ return source_callable
73
+ raise KeyError(callable_id)
74
+
75
+ def calls_from(self, callable_id: str) -> tuple[SourceCall, ...]:
76
+ return tuple(
77
+ source_call
78
+ for source_file in self.extraction_result.files.values()
79
+ for source_call in source_file.calls
80
+ if source_call.source_callable_id == callable_id
81
+ )
82
+
83
+ def calls_to(self, callable_id: str) -> tuple[SourceCall, ...]:
84
+ return tuple(
85
+ source_call
86
+ for source_file in self.extraction_result.files.values()
87
+ for source_call in source_file.calls
88
+ if source_call.target_callable_id == callable_id
89
+ )
90
+
91
+ def callable_graph(self) -> DependencyGraph:
92
+ graph = DependencyGraph()
93
+ callable_ids = set(self.callable_ids())
94
+ for callable_id in sorted(callable_ids):
95
+ graph.add_node(callable_id, kind="callable")
96
+ for source_file in self.extraction_result.files.values():
97
+ for source_call in source_file.calls:
98
+ if (
99
+ source_call.source_callable_id in callable_ids
100
+ and source_call.target_callable_id in callable_ids
101
+ ):
102
+ graph.add_edge(
103
+ source_call.source_callable_id,
104
+ source_call.target_callable_id,
105
+ kind="call",
106
+ )
107
+ return graph
108
+
61
109
  def tracked_only(self) -> CodeMap:
62
110
  return self.subgraph(self.source_ids())
63
111
 
@@ -7,7 +7,8 @@ from ..graph import DependencyGraph, Edge, Node
7
7
  from .models import CodeMap, ExtractionResult, ExtractionSummary
8
8
 
9
9
 
10
- CODE_MAP_SCHEMA_VERSION = 1
10
+ CODE_MAP_SCHEMA_VERSION = 2
11
+ SUPPORTED_CODE_MAP_SCHEMA_VERSIONS = {1, CODE_MAP_SCHEMA_VERSION}
11
12
 
12
13
 
13
14
  class CodeMapSerializationError(ValueError):
@@ -55,7 +56,7 @@ def deserialize_code_map(
55
56
  cache_key: str = "",
56
57
  ) -> CodeMap:
57
58
  try:
58
- if payload["schema_version"] != CODE_MAP_SCHEMA_VERSION:
59
+ if payload["schema_version"] not in SUPPORTED_CODE_MAP_SCHEMA_VERSIONS:
59
60
  raise CodeMapSerializationError(
60
61
  f"Unsupported CodeMap schema version: {payload['schema_version']}"
61
62
  )
@@ -151,6 +151,9 @@ class SourceExtractor(Protocol):
151
151
  types=source_file.types,
152
152
  abstract_classes=source_file.abstract_classes,
153
153
  functions=source_file.functions,
154
+ values=source_file.values,
155
+ callables=source_file.callables,
156
+ calls=source_file.calls,
154
157
  line_count=source_file.line_count,
155
158
  code_line_count=source_file.code_line_count,
156
159
  public_symbol_count=source_file.public_symbol_count,