modwire 2.0.0__tar.gz → 2.2.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 (92) hide show
  1. modwire-2.2.0/.github/workflows/large-repo-fixtures.yml +88 -0
  2. {modwire-2.0.0 → modwire-2.2.0}/PKG-INFO +1 -1
  3. {modwire-2.0.0 → modwire-2.2.0}/pyproject.toml +3 -2
  4. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/__init__.py +10 -0
  5. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/_version.py +3 -3
  6. modwire-2.2.0/src/modwire/callables.py +162 -0
  7. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/definitions.py +64 -0
  8. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/manifest.py +5 -4
  9. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/models.py +49 -1
  10. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/serialization.py +3 -2
  11. modwire-2.2.0/src/modwire/extractors/base.py +781 -0
  12. modwire-2.2.0/src/modwire/extractors/php.py +348 -0
  13. modwire-2.2.0/src/modwire/extractors/python.py +472 -0
  14. modwire-2.2.0/src/modwire/extractors/resources.py +29 -0
  15. modwire-2.2.0/src/modwire/extractors/scripts/__init__.py +2 -0
  16. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/scripts/php_extractor.php +483 -5
  17. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/scripts/python_extractor.py +393 -2
  18. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/scripts/typescript_extractor.js +409 -5
  19. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/typescript.py +27 -6
  20. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/metadata.py +16 -20
  21. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/testing/factories.py +109 -0
  22. {modwire-2.0.0 → modwire-2.2.0}/src/modwire.egg-info/PKG-INFO +1 -1
  23. {modwire-2.0.0 → modwire-2.2.0}/src/modwire.egg-info/SOURCES.txt +8 -1
  24. modwire-2.2.0/tests/large_projects/README.md +47 -0
  25. modwire-2.2.0/tests/large_projects/fixtures.json +242 -0
  26. modwire-2.2.0/tests/large_projects/run_large_project_fixtures.py +534 -0
  27. {modwire-2.0.0 → modwire-2.2.0}/tests/test_api.py +415 -35
  28. modwire-2.0.0/src/modwire/extractors/base.py +0 -353
  29. modwire-2.0.0/src/modwire/extractors/php.py +0 -199
  30. modwire-2.0.0/src/modwire/extractors/python.py +0 -150
  31. {modwire-2.0.0 → modwire-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  32. {modwire-2.0.0 → modwire-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  33. {modwire-2.0.0 → modwire-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  34. {modwire-2.0.0 → modwire-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  35. {modwire-2.0.0 → modwire-2.2.0}/.github/workflows/ci.yml +0 -0
  36. {modwire-2.0.0 → modwire-2.2.0}/.github/workflows/release.yml +0 -0
  37. {modwire-2.0.0 → modwire-2.2.0}/.gitignore +0 -0
  38. {modwire-2.0.0 → modwire-2.2.0}/CONTRIBUTING.md +0 -0
  39. {modwire-2.0.0 → modwire-2.2.0}/LICENSE +0 -0
  40. {modwire-2.0.0 → modwire-2.2.0}/README.md +0 -0
  41. {modwire-2.0.0 → modwire-2.2.0}/docs/wiki/Development-checks.md +0 -0
  42. {modwire-2.0.0 → modwire-2.2.0}/docs/wiki/Home.md +0 -0
  43. {modwire-2.0.0 → modwire-2.2.0}/docs/wiki/Reporting-bugs.md +0 -0
  44. {modwire-2.0.0 → modwire-2.2.0}/docs/wiki/Requesting-features.md +0 -0
  45. {modwire-2.0.0 → modwire-2.2.0}/setup.cfg +0 -0
  46. {modwire-2.0.0 → modwire-2.2.0}/show_test_source_files.py +0 -0
  47. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/__init__.py +0 -0
  48. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/analyzers.py +0 -0
  49. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/config.py +0 -0
  50. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/insights.py +0 -0
  51. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/matching.py +0 -0
  52. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/policy.py +0 -0
  53. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/render.py +0 -0
  54. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/architecture/violations.py +0 -0
  55. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/exports.py +0 -0
  56. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/__init__.py +0 -0
  57. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/cache.py +0 -0
  58. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/roots.py +0 -0
  59. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extraction/service.py +0 -0
  60. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/__init__.py +0 -0
  61. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/extractors/loader.py +0 -0
  62. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/graph.py +0 -0
  63. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/shape/__init__.py +0 -0
  64. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/shape/config.py +0 -0
  65. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/shape/evaluator.py +0 -0
  66. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/shape/rules.py +0 -0
  67. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/shape/violations.py +0 -0
  68. {modwire-2.0.0 → modwire-2.2.0}/src/modwire/testing/__init__.py +0 -0
  69. {modwire-2.0.0 → modwire-2.2.0}/src/modwire.egg-info/dependency_links.txt +0 -0
  70. {modwire-2.0.0 → modwire-2.2.0}/src/modwire.egg-info/requires.txt +0 -0
  71. {modwire-2.0.0 → modwire-2.2.0}/src/modwire.egg-info/top_level.txt +0 -0
  72. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/php/ignored/generated.php +0 -0
  73. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/php/src/application/use_cases/activate.php +0 -0
  74. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/php/src/domain/model/user.php +0 -0
  75. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/php/src/domain/services/policy.php +0 -0
  76. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/php/src/interfaces/http/controller.php +0 -0
  77. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/python/ignored/generated.py +0 -0
  78. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/python/src/application/use_cases/activate.py +0 -0
  79. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/python/src/domain/model/user.py +0 -0
  80. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/python/src/domain/services/policy.py +0 -0
  81. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/python/src/interfaces/http/controller.py +0 -0
  82. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/ignored/generated.ts +0 -0
  83. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/application/use_cases/activate.ts +0 -0
  84. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/domain/model/profile.tsx +0 -0
  85. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/domain/model/user.ts +0 -0
  86. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/domain/services/audit.js +0 -0
  87. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/domain/services/policy.ts +0 -0
  88. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/interfaces/http/controller.ts +0 -0
  89. {modwire-2.0.0 → modwire-2.2.0}/tests/apps/typescript/src/interfaces/http/view.jsx +0 -0
  90. {modwire-2.0.0 → modwire-2.2.0}/tests/test_architecture_api.py +0 -0
  91. {modwire-2.0.0 → modwire-2.2.0}/tests/test_standalone.py +0 -0
  92. {modwire-2.0.0 → modwire-2.2.0}/uv.lock +0 -0
@@ -0,0 +1,88 @@
1
+ name: Large Repository Fixtures
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ fixture:
7
+ description: "Fixture id to run, or all."
8
+ required: true
9
+ default: "all"
10
+ type: choice
11
+ options:
12
+ - all
13
+ - django
14
+ - ansible
15
+ - salt
16
+ - vscode
17
+ - angular
18
+ - typescript
19
+ - wordpress
20
+ - drupal
21
+ - moodle
22
+ mode:
23
+ description: "Fixture execution mode."
24
+ required: true
25
+ default: "enclosure"
26
+ type: choice
27
+ options:
28
+ - enclosure
29
+ - modwire
30
+ - both
31
+ command-timeout:
32
+ description: "Seconds allowed for each enclosure command."
33
+ required: true
34
+ default: "60"
35
+ schedule:
36
+ - cron: "17 3 * * 0"
37
+
38
+ jobs:
39
+ large-fixture:
40
+ name: ${{ matrix.fixture }}
41
+ runs-on: ubuntu-latest
42
+ timeout-minutes: 60
43
+ if: github.event_name != 'workflow_dispatch' || inputs.fixture == 'all' || inputs.fixture == matrix.fixture
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ fixture:
48
+ - django
49
+ - ansible
50
+ - salt
51
+ - vscode
52
+ - angular
53
+ - typescript
54
+ - wordpress
55
+ - drupal
56
+ - moodle
57
+
58
+ steps:
59
+ - name: Check out repository
60
+ uses: actions/checkout@v4
61
+
62
+ - name: Set up Python
63
+ uses: actions/setup-python@v5
64
+ with:
65
+ python-version: "3.13"
66
+
67
+ - name: Set up Node.js
68
+ uses: actions/setup-node@v4
69
+ with:
70
+ node-version: "20"
71
+
72
+ - name: Set up PHP
73
+ uses: shivammathur/setup-php@v2
74
+ with:
75
+ php-version: "8.3"
76
+
77
+ - name: Install enclosure and local modwire
78
+ run: |
79
+ python -m pip install --upgrade pip
80
+ python -m pip install "enclosure @ git+https://github.com/9orky/enclosure.git"
81
+ python -m pip install -e .
82
+
83
+ - name: Run large fixture
84
+ run: >
85
+ python tests/large_projects/run_large_project_fixtures.py
86
+ --fixture "${{ matrix.fixture }}"
87
+ --mode "${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'enclosure' }}"
88
+ --command-timeout "${{ github.event_name == 'workflow_dispatch' && inputs['command-timeout'] || '60' }}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modwire
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Extract source-code dependencies and build dependency graphs.
5
5
  Author: Tomasz Szpak
6
6
  License-Expression: MIT
@@ -72,8 +72,9 @@ include-package-data = true
72
72
  where = ["src"]
73
73
 
74
74
  [tool.setuptools.package-data]
75
- modwire = [
76
- "extractors/scripts/*",
75
+ "modwire.extractors.scripts" = [
76
+ "*.js",
77
+ "*.php",
77
78
  ]
78
79
 
79
80
  [tool.setuptools_scm]
@@ -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.2.0'
22
+ __version_tuple__ = version_tuple = (2, 2, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gb985fe227'
24
+ __commit_id__ = commit_id = 'gc116aabfa'
@@ -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
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from .._version import __version__
9
9
  from ..extractors import load_extractor
10
10
  from ..extractors.base import _collect_extraction_targets
11
+ from ..extractors.resources import extractor_script_path
11
12
  from .models import SourceManifest, SourceManifestEntry
12
13
  from .roots import (
13
14
  DEFAULT_SOURCE_ROOTS,
@@ -57,14 +58,14 @@ def discover_sources(
57
58
  mtime_ns=stat.st_mtime_ns,
58
59
  )
59
60
  )
60
- extractor_path = (
61
- Path(__file__).parents[1] / "extractors" / "scripts" / extractor.extractor_file
62
- ).resolve()
63
61
  runtime_path = shutil.which(extractor.command) or ""
64
62
  runtime_mtime_ns = 0
65
63
  if runtime_path != "":
66
64
  runtime_mtime_ns = Path(runtime_path).stat().st_mtime_ns
67
65
 
66
+ with extractor_script_path(extractor.extractor_file) as extractor_path:
67
+ extractor_mtime_ns = extractor_path.stat().st_mtime_ns
68
+
68
69
  return SourceManifest(
69
70
  language=language,
70
71
  workspace_root=(
@@ -83,7 +84,7 @@ def discover_sources(
83
84
  runtime_mtime_ns=runtime_mtime_ns,
84
85
  extractor_file=extractor.extractor_file,
85
86
  extractor_path=extractor_path,
86
- extractor_mtime_ns=extractor_path.stat().st_mtime_ns,
87
+ extractor_mtime_ns=extractor_mtime_ns,
87
88
  modwire_version=__version__,
88
89
  files_found=files_found,
89
90
  files_checked=len(entries),
@@ -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
  )