codemarp 0.1.0__py3-none-any.whl

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.
codemarp/__init__.py ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,77 @@
1
+ from codemarp.graph.models import Edge, ModuleNode
2
+ from codemarp.parser.python_parser import ParsedPythonModule
3
+
4
+
5
+ def build_high_level_edges(
6
+ parsed_modules: list[ParsedPythonModule],
7
+ modules: list[ModuleNode],
8
+ ) -> tuple:
9
+ module_to_group = {module.id: aggregate_module_id(module.id) for module in modules}
10
+ group_ids = sorted(set(module_to_group.values()))
11
+
12
+ known_module_ids = set(module_to_group.keys())
13
+ edges: list[Edge] = []
14
+
15
+ for parsed in parsed_modules:
16
+ source_group = module_to_group.get(
17
+ parsed.module_id, aggregate_module_id(parsed.module_id)
18
+ )
19
+
20
+ for imported in parsed.imports:
21
+ target_module = _resolve_local_import(imported, known_module_ids)
22
+ if not target_module:
23
+ continue
24
+
25
+ target_group = module_to_group.get(
26
+ target_module, aggregate_module_id(target_module)
27
+ )
28
+
29
+ if source_group != target_group:
30
+ edges.append(
31
+ Edge(
32
+ source=source_group,
33
+ target=target_group,
34
+ kind="imports",
35
+ label="imports",
36
+ )
37
+ )
38
+
39
+ return group_ids, _dedupe_edges(edges)
40
+
41
+
42
+ def aggregate_module_id(module_id: str) -> str:
43
+ """
44
+ Collapse deep module paths for the high-level graph.
45
+
46
+ - 3+ segments collapse to the first 2 segments:
47
+ codemarp.views.trace -> codemarp.views
48
+ - 1–2 segments stay as-is:
49
+ codemarp.errors -> codemarp.errors
50
+ codemarp.cli -> codemarp.cli
51
+ """
52
+ segments = module_id.split(".")
53
+ if len(segments) >= 3:
54
+ return ".".join(segments[:2])
55
+ return module_id
56
+
57
+
58
+ def _resolve_local_import(import_name: str, known_module_ids: set[str]) -> str | None:
59
+ if import_name in known_module_ids:
60
+ return import_name
61
+ for module_id in sorted(known_module_ids, key=len, reverse=True):
62
+ if import_name.startswith(module_id + "."):
63
+ return module_id
64
+ if module_id.startswith(import_name + "."):
65
+ return module_id
66
+ return None
67
+
68
+
69
+ def _dedupe_edges(edges: list[Edge]) -> list[Edge]:
70
+ seen = set()
71
+ out = []
72
+ for edge in edges:
73
+ key = (edge.source, edge.target, edge.kind)
74
+ if key not in seen:
75
+ seen.add(key)
76
+ out.append(edge)
77
+ return out
@@ -0,0 +1,272 @@
1
+ import ast
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from codemarp.graph.models import ControlFlowNode, Edge
6
+ from codemarp.parser.python_parser import find_function_node
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class LowLevelResult:
11
+ function_id: str
12
+ nodes: list[ControlFlowNode]
13
+ edges: list[Edge]
14
+
15
+
16
+ def build_low_level_view(root: str | Path, focus: str) -> LowLevelResult:
17
+ function_id, function_node = find_function_node(Path(root), focus)
18
+ builder = ControlFlowBuilder()
19
+ nodes, edges = builder.build_for_function(function_node)
20
+ return LowLevelResult(function_id=function_id, nodes=nodes, edges=edges)
21
+
22
+
23
+ class ControlFlowBuilder:
24
+ def __init__(self) -> None:
25
+ self.nodes: list[ControlFlowNode] = []
26
+ self.edges: list[Edge] = []
27
+ self._counter = 0
28
+
29
+ def build_for_function(
30
+ self,
31
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
32
+ ) -> tuple[list[ControlFlowNode], list[Edge]]:
33
+ start = self._new_node("Start", "start", lineno=function_node.lineno)
34
+ exits = self._walk_statements(function_node.body, [start])
35
+ end = self._new_node(
36
+ "End",
37
+ "end",
38
+ lineno=getattr(function_node, "end_lineno", function_node.lineno),
39
+ )
40
+
41
+ for exit_node in exits:
42
+ self._add_edge(exit_node, end)
43
+
44
+ return self.nodes, self.edges
45
+
46
+ def _walk_statements(
47
+ self,
48
+ statements: list[ast.stmt],
49
+ incoming: list[str],
50
+ ) -> list[str]:
51
+ exits = incoming
52
+ for statement in statements:
53
+ exits = self._handle_statement(statement, exits)
54
+ return exits
55
+
56
+ def _handle_statement(
57
+ self, statement: ast.stmt, incoming: list[str], edge_label: str | None = None
58
+ ) -> list[str]:
59
+ if isinstance(statement, ast.If):
60
+ return self._handle_if(statement, incoming)
61
+
62
+ if isinstance(statement, (ast.For, ast.AsyncFor, ast.While)):
63
+ return self._handle_loop(statement, incoming)
64
+
65
+ if isinstance(statement, ast.Try):
66
+ node = self._new_node("Try/Except", "statement", lineno=statement.lineno)
67
+ for index, source in enumerate(incoming):
68
+ self._add_edge(source, node, label=edge_label if index == 0 else None)
69
+ return [node]
70
+
71
+ if isinstance(statement, ast.Return):
72
+ node = self._new_node("Return", "terminal", lineno=statement.lineno)
73
+ for index, source in enumerate(incoming):
74
+ self._add_edge(source, node, label=edge_label if index == 0 else None)
75
+ return [node]
76
+
77
+ if isinstance(statement, ast.Raise):
78
+ node = self._new_node("Raise", "terminal", lineno=statement.lineno)
79
+ for index, source in enumerate(incoming):
80
+ self._add_edge(source, node, label=edge_label if index == 0 else None)
81
+ return [node]
82
+
83
+ label = self._statement_label(statement)
84
+ node = self._new_node(label, "statement", lineno=statement.lineno)
85
+ for index, source in enumerate(incoming):
86
+ self._add_edge(source, node, label=edge_label if index == 0 else None)
87
+ return [node]
88
+
89
+ def _walk_branch_statements(
90
+ self,
91
+ statements: list[ast.stmt],
92
+ condition_node: str,
93
+ *,
94
+ branch_label: str,
95
+ ) -> list[str]:
96
+ # if not statements:
97
+ # empty_branch = self._new_node(branch_label, "branch")
98
+ # self._add_edge(condition_node, empty_branch, label=branch_label)
99
+ # return [empty_branch]
100
+
101
+ if not statements:
102
+ return [condition_node]
103
+
104
+ first_exits = self._handle_statement(
105
+ statements[0], [condition_node], edge_label=branch_label
106
+ )
107
+ if len(statements) == 1:
108
+ return first_exits
109
+ return self._walk_statements(statements[1:], first_exits)
110
+
111
+ def _handle_if(self, statement: ast.If, incoming: list[str]) -> list[str]:
112
+ condition = self._new_node(
113
+ self._expr_label(statement.test),
114
+ "decision",
115
+ lineno=statement.lineno,
116
+ )
117
+ for source in incoming:
118
+ self._add_edge(source, condition)
119
+
120
+ # then_entry = self._new_node("Then", "branch", lineno=statement.lineno)
121
+ # else_entry = self._new_node("Else", "branch", lineno=statement.lineno)
122
+
123
+ # self._add_edge(condition, then_entry, label="True")
124
+ # self._add_edge(condition, else_entry, label="False")
125
+
126
+ then_exits = self._walk_branch_statements(
127
+ statement.body, condition, branch_label="True"
128
+ )
129
+
130
+ else_exits = self._walk_branch_statements(
131
+ statement.orelse, condition, branch_label="False"
132
+ )
133
+
134
+ then_empty = condition in then_exits
135
+ else_empty = condition in else_exits
136
+
137
+ non_terminals = [
138
+ node_id
139
+ for node_id in then_exits + else_exits
140
+ if node_id != condition and self._node_kind(node_id) != "terminal"
141
+ ]
142
+ if non_terminals or then_empty or else_empty:
143
+ merge = self._new_node("Merge", "merge", lineno=statement.lineno)
144
+
145
+ for source in non_terminals:
146
+ self._add_edge(source, merge)
147
+
148
+ if then_empty:
149
+ self._add_edge(condition, merge, label="True")
150
+ if else_empty:
151
+ self._add_edge(condition, merge, label="False")
152
+
153
+ return [merge]
154
+
155
+ return []
156
+
157
+ def _handle_loop(
158
+ self,
159
+ statement: ast.For | ast.AsyncFor | ast.While,
160
+ incoming: list[str],
161
+ ) -> list[str]:
162
+ loop_label = type(statement).__name__
163
+ loop = self._new_node(loop_label, "loop", lineno=statement.lineno)
164
+ for source in incoming:
165
+ self._add_edge(source, loop)
166
+
167
+ body_entry = self._new_node("Loop Body", "branch", lineno=statement.lineno)
168
+ self._add_edge(loop, body_entry, label="Iterate")
169
+
170
+ body_exits = (
171
+ self._walk_statements(statement.body, [body_entry])
172
+ if statement.body
173
+ else [body_entry]
174
+ )
175
+ for source in body_exits:
176
+ self._add_edge(source, loop, label="Next")
177
+
178
+ after_loop = self._new_node("After Loop", "merge", lineno=statement.lineno)
179
+ self._add_edge(loop, after_loop, label="Exit")
180
+
181
+ return [after_loop]
182
+
183
+ def _statement_label(self, statement: ast.stmt) -> str:
184
+ if isinstance(statement, ast.Assign):
185
+ if isinstance(statement.value, ast.Call): # simplify calls
186
+ return self._call_label(statement.value)
187
+ return "Assign"
188
+
189
+ if isinstance(statement, ast.AnnAssign):
190
+ if isinstance(statement.value, ast.Call):
191
+ return self._call_label(statement.value)
192
+ return "AnnAssign"
193
+
194
+ if isinstance(statement, ast.AugAssign):
195
+ return "AugAssign"
196
+
197
+ if isinstance(statement, ast.Expr):
198
+ if isinstance(statement.value, ast.Call):
199
+ return self._call_label(statement.value)
200
+ return self._expr_label(statement.value)
201
+
202
+ if isinstance(statement, ast.Pass):
203
+ return "Pass"
204
+
205
+ try:
206
+ return ast.unparse(statement)
207
+ except Exception:
208
+ return type(statement).__name__
209
+
210
+ def _expr_label(self, expr: ast.AST) -> str:
211
+ if isinstance(expr, ast.Call):
212
+ return self._call_label(expr)
213
+
214
+ try:
215
+ return ast.unparse(expr)
216
+ except Exception:
217
+ return type(expr).__name__
218
+
219
+ def _call_label(self, call: ast.Call) -> str:
220
+ callee = self._callable_name(call.func)
221
+ return f"{callee}(...)"
222
+
223
+ def _callable_name(self, node: ast.AST) -> str:
224
+ if isinstance(node, ast.Name):
225
+ return node.id
226
+
227
+ if isinstance(node, ast.Attribute):
228
+ parts: list[str] = []
229
+ current: ast.AST = node
230
+
231
+ while isinstance(current, ast.Attribute):
232
+ parts.append(current.attr)
233
+ current = current.value
234
+
235
+ if isinstance(current, ast.Name):
236
+ parts.append(current.id)
237
+
238
+ return ".".join(reversed(parts))
239
+
240
+ try:
241
+ return ast.unparse(node)
242
+ except Exception:
243
+ return type(node).__name__
244
+
245
+ def _new_node(self, label: str, kind: str, *, lineno: int | None = None) -> str:
246
+ self._counter += 1
247
+ node_id = f"n{self._counter}"
248
+ self.nodes.append(
249
+ ControlFlowNode(
250
+ id=node_id,
251
+ label=label,
252
+ kind=kind,
253
+ lineno=lineno,
254
+ )
255
+ )
256
+ return node_id
257
+
258
+ def _add_edge(self, source: str, target: str, *, label: str | None = None) -> None:
259
+ self.edges.append(
260
+ Edge(
261
+ source=source,
262
+ target=target,
263
+ kind="control_flow",
264
+ label=label,
265
+ )
266
+ )
267
+
268
+ def _node_kind(self, node_id: str) -> str:
269
+ for node in self.nodes:
270
+ if node.id == node_id:
271
+ return node.kind
272
+ raise KeyError(f"Unknown control-flow node id: {node_id}")
@@ -0,0 +1,162 @@
1
+ from codemarp.graph.models import Edge, FunctionNode
2
+ from codemarp.parser.python_parser import ParsedPythonModule
3
+
4
+
5
+ def build_mid_level_edges(
6
+ parsed_modules: list[ParsedPythonModule], functions: list[FunctionNode]
7
+ ) -> list[Edge]:
8
+ edges = []
9
+ by_name = {}
10
+ by_id = {fn.id: fn for fn in functions}
11
+ by_module_and_name = {}
12
+
13
+ for fn in functions:
14
+ by_name.setdefault(fn.name, []).append(fn)
15
+ by_name.setdefault(fn.name.split(".")[-1], []).append(fn)
16
+ by_module_and_name[(fn.module_id, fn.name)] = fn
17
+ by_module_and_name[(fn.module_id, fn.name.split(".")[-1])] = fn
18
+
19
+ parsed_by_module = {parsed.module_id: parsed for parsed in parsed_modules}
20
+
21
+ for module in parsed_modules:
22
+ for caller_id, callee_name in module.calls:
23
+ target = _resolve_callee(
24
+ caller_module_id=module.module_id,
25
+ callee_name=callee_name,
26
+ parsed_by_module=parsed_by_module,
27
+ by_module_and_name=by_module_and_name,
28
+ by_name=by_name,
29
+ by_id=by_id,
30
+ )
31
+ if target:
32
+ edges.append(
33
+ Edge(
34
+ source=caller_id, target=target.id, kind="calls", label="calls"
35
+ )
36
+ )
37
+ return _dedupe_edges(edges)
38
+
39
+
40
+ def _resolve_callee(
41
+ caller_module_id: str,
42
+ callee_name: str,
43
+ parsed_by_module: dict,
44
+ by_module_and_name: dict,
45
+ by_name: dict,
46
+ by_id: dict,
47
+ ) -> FunctionNode | None:
48
+ parsed_module = parsed_by_module[caller_module_id]
49
+
50
+ same_module = _resolve_same_module_call(
51
+ caller_module_id=caller_module_id,
52
+ callee_name=callee_name,
53
+ by_module_and_name=by_module_and_name,
54
+ )
55
+
56
+ if same_module:
57
+ return same_module
58
+
59
+ imported_symbol = _resolve_imported_symbol_call(
60
+ parsed_module=parsed_module,
61
+ callee_name=callee_name,
62
+ by_id=by_id,
63
+ by_module_and_name=by_module_and_name,
64
+ )
65
+
66
+ if imported_symbol:
67
+ return imported_symbol
68
+
69
+ imported_module = _resolve_imported_module_call(
70
+ parsed_module=parsed_module,
71
+ callee_name=callee_name,
72
+ by_module_and_name=by_module_and_name,
73
+ )
74
+
75
+ if imported_module:
76
+ return imported_module
77
+
78
+ return _resolve_unique_global_call(callee_name=callee_name, by_name=by_name)
79
+
80
+
81
+ def _resolve_same_module_call(
82
+ *,
83
+ caller_module_id: str,
84
+ callee_name: str,
85
+ by_module_and_name: dict[tuple[str, str], FunctionNode],
86
+ ) -> FunctionNode | None:
87
+ return by_module_and_name.get((caller_module_id, callee_name))
88
+
89
+
90
+ def _resolve_imported_symbol_call(
91
+ parsed_module: ParsedPythonModule,
92
+ callee_name: str,
93
+ by_id: dict,
94
+ by_module_and_name: dict,
95
+ ) -> FunctionNode | None:
96
+ if "." in callee_name:
97
+ return None
98
+
99
+ for imported in parsed_module.imported_symbols:
100
+ visible_name = imported.alias or imported.name
101
+ if visible_name != callee_name:
102
+ continue
103
+
104
+ direct_id = f"{imported.module}:{imported.name}"
105
+ if direct_id in by_id:
106
+ return by_id[direct_id]
107
+
108
+ candidate = by_module_and_name.get((imported.module, imported.name))
109
+ if candidate:
110
+ return candidate
111
+
112
+ return None
113
+
114
+
115
+ def _resolve_imported_module_call(
116
+ parsed_module: ParsedPythonModule,
117
+ callee_name: str,
118
+ by_module_and_name: dict,
119
+ ) -> FunctionNode | None:
120
+ if "." not in callee_name:
121
+ return None
122
+
123
+ prefix, member = callee_name.split(".", 1)
124
+
125
+ for imported in parsed_module.imported_modules:
126
+ visible_name = imported.alias or imported.module.split(".")[-1]
127
+ if visible_name != prefix:
128
+ continue
129
+
130
+ return by_module_and_name.get((imported.module, member))
131
+
132
+ return None
133
+
134
+
135
+ def _resolve_unique_global_call(
136
+ *,
137
+ callee_name: str,
138
+ by_name: dict[str, list[FunctionNode]],
139
+ ) -> FunctionNode | None:
140
+ if _is_dotted_call(callee_name):
141
+ return None
142
+
143
+ matches = by_name.get(callee_name, [])
144
+ unique = list({fn.id: fn for fn in matches}.values())
145
+ if len(unique) == 1:
146
+ return unique[0]
147
+ return None
148
+
149
+
150
+ def _is_dotted_call(callee_name: str) -> bool:
151
+ return "." in callee_name
152
+
153
+
154
+ def _dedupe_edges(edges: list[Edge]) -> list[Edge]:
155
+ seen: set[tuple[str, str, str]] = set()
156
+ out: list[Edge] = []
157
+ for edge in edges:
158
+ key = (edge.source, edge.target, edge.kind)
159
+ if key not in seen:
160
+ seen.add(key)
161
+ out.append(edge)
162
+ return out
@@ -0,0 +1 @@
1
+
codemarp/cli/main.py ADDED
@@ -0,0 +1,159 @@
1
+ import argparse
2
+
3
+ from codemarp.analyzers.low_level import build_low_level_view
4
+ from codemarp.errors import codemarpError
5
+ from codemarp.pipeline.apply_view import ViewType, apply_view
6
+ from codemarp.pipeline.build_bundle import build_bundle
7
+ from codemarp.pipeline.export_all import export_all, export_low_level
8
+
9
+
10
+ def analyze_command(
11
+ root: str,
12
+ out: str,
13
+ *,
14
+ view: ViewType,
15
+ focus: str | None = None,
16
+ module: str | None = None,
17
+ max_depth: int | None = None,
18
+ ) -> None:
19
+ build_result = build_bundle(root)
20
+
21
+ if view is ViewType.LOW:
22
+ assert focus is not None
23
+ low_view = build_low_level_view(root, focus)
24
+ export_low_level(build_result=build_result, low_view=low_view, out_dir=out)
25
+
26
+ print(f"Parsed {len(build_result.parsed_modules)} modules")
27
+ print(f"Discovered {len(build_result.bundle.functions)} functions")
28
+ print(f"View type: {view.value}")
29
+ print(f"Low-level view for {focus}")
30
+ print(f"Low-level view contains {len(low_view.nodes)} nodes")
31
+ print("Wrote graph.json")
32
+ print("Wrote high_level.mmd")
33
+ print("Wrote low_level.mmd")
34
+ print("Wrote low_level.json")
35
+ return
36
+
37
+ graph_view = apply_view(
38
+ build_result.bundle,
39
+ view=view,
40
+ focus=focus,
41
+ module=module,
42
+ max_depth=max_depth,
43
+ )
44
+ export_all(build_result=build_result, view=graph_view, out_dir=out)
45
+
46
+ print(f"Parsed {len(build_result.parsed_modules)} modules")
47
+ print(f"Discovered {len(build_result.bundle.functions)} functions")
48
+ print(f"View type: {view.value}")
49
+ if view is ViewType.TRACE:
50
+ print(f"Focused trace from {focus}")
51
+ print(f"Trace contains {len(graph_view.functions)} functions")
52
+ if view is ViewType.MODULE:
53
+ print(f"Module view for {module}")
54
+ print(f"Module view contains {len(graph_view.functions)} functions")
55
+ if view is ViewType.REVERSE:
56
+ print(f"Reverse trace from {focus}")
57
+ print(f"Reverse trace contains {len(graph_view.functions)} functions")
58
+ print("Wrote graph.json")
59
+ print("Wrote high_level.mmd")
60
+ print("Wrote mid_level.mmd")
61
+ print("Wrote mid_level.json")
62
+
63
+
64
+ def build_parser() -> argparse.ArgumentParser:
65
+ parser = argparse.ArgumentParser(prog="codemarp", description="3-level code mapper")
66
+ subparsers = parser.add_subparsers(dest="command", required=True)
67
+
68
+ analyze = subparsers.add_parser("analyze", help="Analyze a Python codebase")
69
+ analyze.add_argument("root", help="Path to the repository root")
70
+ analyze.add_argument("--out", default="./codemarp_out", help="Output directory")
71
+ analyze.add_argument(
72
+ "--view",
73
+ choices=[view.value for view in ViewType],
74
+ default=ViewType.FULL.value,
75
+ help="Graph view to export",
76
+ )
77
+ analyze.add_argument(
78
+ "--focus",
79
+ default=None,
80
+ help="Entrypoint for TRACE/REVERSE view (function id: module:function)",
81
+ )
82
+ analyze.add_argument(
83
+ "--module",
84
+ default=None,
85
+ help="Module id for MODULE view",
86
+ )
87
+ analyze.add_argument(
88
+ "--max-depth",
89
+ type=int,
90
+ default=None,
91
+ help="Maximum trace depth from the focused function",
92
+ )
93
+
94
+ return parser
95
+
96
+
97
+ def _validate_analyze_args(
98
+ args: argparse.Namespace, parser: argparse.ArgumentParser
99
+ ) -> None:
100
+ view = ViewType(args.view)
101
+
102
+ if view is ViewType.FULL:
103
+ if args.focus is not None:
104
+ parser.error("--focus cannot be used with --view full")
105
+ if args.module is not None:
106
+ parser.error("--module cannot be used with --view full")
107
+ if args.max_depth is not None:
108
+ parser.error("--max-depth cannot be used with --view full")
109
+
110
+ if view is ViewType.TRACE:
111
+ if not args.focus:
112
+ parser.error("--focus is required with --view trace")
113
+ if args.module is not None:
114
+ parser.error("--module cannot be used with --view trace")
115
+
116
+ if view is ViewType.MODULE:
117
+ if not args.module:
118
+ parser.error("--module is required with --view module")
119
+ if args.focus is not None:
120
+ parser.error("--focus cannot be used with --view module")
121
+ if args.max_depth is not None:
122
+ parser.error("--max-depth cannot be used with --view module")
123
+
124
+ if view is ViewType.REVERSE:
125
+ if not args.focus:
126
+ parser.error("--focus is required with --view reverse")
127
+ if args.module is not None:
128
+ parser.error("--module cannot be used with --view reverse")
129
+
130
+ if view is ViewType.LOW:
131
+ if not args.focus:
132
+ parser.error("--focus is required with --view low")
133
+ if args.module is not None:
134
+ parser.error("--module cannot be used with --view low")
135
+ if args.max_depth is not None:
136
+ parser.error("--max-depth cannot be used with --view low")
137
+
138
+
139
+ def main() -> None:
140
+ parser = build_parser()
141
+ args = parser.parse_args()
142
+
143
+ if args.command == "analyze":
144
+ _validate_analyze_args(args, parser)
145
+ try:
146
+ analyze_command(
147
+ args.root,
148
+ args.out,
149
+ view=ViewType(args.view),
150
+ focus=args.focus,
151
+ module=args.module,
152
+ max_depth=args.max_depth,
153
+ )
154
+ except codemarpError as exc:
155
+ raise SystemExit(str(exc)) from exc
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
codemarp/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ class codemarpError(Exception):
2
+ pass
3
+
4
+
5
+ class ParseError(codemarpError):
6
+ pass
7
+
8
+
9
+ class ResolutionError(codemarpError):
10
+ pass
11
+
12
+
13
+ class TraceError(ResolutionError):
14
+ pass
15
+
16
+
17
+ class ModuleViewError(ResolutionError):
18
+ pass
19
+
20
+
21
+ class FocusFormatError(codemarpError):
22
+ pass
@@ -0,0 +1 @@
1
+