resolvescript 0.1.2__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.
Files changed (49) hide show
  1. resolve_script/__init__.py +3 -0
  2. resolve_script/analyze.py +277 -0
  3. resolve_script/cli.py +748 -0
  4. resolve_script/config.py +62 -0
  5. resolve_script/consolidate.py +604 -0
  6. resolve_script/fetch.py +90 -0
  7. resolve_script/install/__init__.py +49 -0
  8. resolve_script/install/discovery.py +57 -0
  9. resolve_script/install/installer.py +397 -0
  10. resolve_script/install/registry.py +106 -0
  11. resolve_script/manifest/__init__.py +1 -0
  12. resolve_script/manifest/json_reader.py +40 -0
  13. resolve_script/manifest/model.py +316 -0
  14. resolve_script/manifest/validation.py +81 -0
  15. resolve_script/manifest/xml_reader.py +162 -0
  16. resolve_script/package.py +103 -0
  17. resolve_script/resolver.py +204 -0
  18. resolve_script/sandbox/__init__.py +38 -0
  19. resolve_script/sandbox/api.py +393 -0
  20. resolve_script/sandbox/env.py +82 -0
  21. resolve_script/sandbox/loader.py +72 -0
  22. resolve_script/sandbox/repl.py +57 -0
  23. resolve_script/sandbox/smoke.py +104 -0
  24. resolve_script/scaffold.py +126 -0
  25. resolve_script/semver.py +236 -0
  26. resolve_script/sources/__init__.py +15 -0
  27. resolve_script/sources/archive.py +82 -0
  28. resolve_script/sources/git.py +107 -0
  29. resolve_script/sources/known.py +47 -0
  30. resolve_script/sources/release.py +55 -0
  31. resolve_script/spec.py +137 -0
  32. resolve_script/templates/extension/@NAME@/__init__.py +7 -0
  33. resolve_script/templates/extension/@NAME@/menu.py +12 -0
  34. resolve_script/templates/extension/@NAME@.py +13 -0
  35. resolve_script/templates/extension/README.md +20 -0
  36. resolve_script/templates/extension/conftest.py +13 -0
  37. resolve_script/templates/extension/manifest.json.j2 +23 -0
  38. resolve_script/templates/extension/manifest.xml.j2 +24 -0
  39. resolve_script/templates/extension/tests/test_smoke.py +26 -0
  40. resolve_script/templates/inapp/register.py +28 -0
  41. resolve_script/testing/__init__.py +6 -0
  42. resolve_script/testing/fixtures.py +47 -0
  43. resolve_script/workspace.py +66 -0
  44. resolvescript-0.1.2.dist-info/METADATA +146 -0
  45. resolvescript-0.1.2.dist-info/RECORD +49 -0
  46. resolvescript-0.1.2.dist-info/WHEEL +5 -0
  47. resolvescript-0.1.2.dist-info/entry_points.txt +2 -0
  48. resolvescript-0.1.2.dist-info/licenses/LICENSE +21 -0
  49. resolvescript-0.1.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """ResolveScript — CLI framework for DaVinci Resolve scripts."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,277 @@
1
+ """Static analyzer (M6): syntax, unused imports, manifest + API coverage.
2
+
3
+ ``analyze_project()`` scans an extension directory and reports:
4
+
5
+ - MANIFEST validation errors (reuses ``manifest/validation.py``)
6
+ - SYNTAX compile failures per .py file
7
+ - UNUSED_IMPORT imports bound but never referenced afterwards
8
+ - API_UNMOCKED calls to methods missing from the mock sandbox
9
+ - ATTR_KEY ``GetAttrs()``-style keys that the mock does not define
10
+ - TARGET target-name problems (already covered by MANIFEST)
11
+
12
+ The API surfaces are introspected from the mock itself (``sandbox/api.py``), so
13
+ the checker stays in sync with what ``dev``/``test`` actually provide.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ import json
20
+ import re
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+
24
+ from .manifest.validation import validate_manifest
25
+ from .sandbox.api import (
26
+ FakeClip,
27
+ FakeComp,
28
+ FakeFolder,
29
+ FakeFusion,
30
+ FakeKey,
31
+ FakeMediaPool,
32
+ FakeMediaPoolItem,
33
+ FakeProject,
34
+ FakeProjectManager,
35
+ FakeResolve,
36
+ FakeSpline,
37
+ FakeStroke,
38
+ FakeTimeline,
39
+ FakeTool,
40
+ )
41
+
42
+ # Conventional names practitioners use for Resolve API root objects.
43
+ KNOWN_ROOTS = {
44
+ "resolve",
45
+ "project",
46
+ "projectManager",
47
+ "timeline",
48
+ "clip",
49
+ "mediaPool",
50
+ "mediaPoolItem",
51
+ "mediapoolitem",
52
+ "folder",
53
+ "fusion",
54
+ "comp",
55
+ "tool",
56
+ "stroke",
57
+ "spline",
58
+ "key",
59
+ "currentComp",
60
+ "currentTool",
61
+ }
62
+
63
+ _SKIP_PARTS = {"__pycache__", ".venv", "venv", "build", "dist", ".git", "node_modules"}
64
+ _ATTR_KEY_RE = re.compile(r"^(TOOLS|COMPS|SPLINES|STROKES|INPOINT|OUTPOINT|EDIT|FRAME)_[A-Z0-9_]+$")
65
+
66
+
67
+ @dataclass
68
+ class Issue:
69
+ code: str
70
+ severity: str # "error" | "warning" | "info"
71
+ file: str
72
+ line: int
73
+ message: str
74
+
75
+
76
+ @dataclass
77
+ class Analysis:
78
+ issues: list[Issue] = field(default_factory=list)
79
+ api_used: set[str] = field(default_factory=set)
80
+ api_mocked: set[str] = field(default_factory=set)
81
+ attr_keys_used: set[str] = field(default_factory=set)
82
+ attr_keys_mocked: set[str] = field(default_factory=set)
83
+
84
+ def api_report(self) -> list[str]:
85
+ lines = [
86
+ f"API coverage: {len(self.api_used & self.api_mocked)} of "
87
+ f"{len(self.api_mocked)} mocked methods used",
88
+ ]
89
+ missing = sorted(self.api_used - self.api_mocked)
90
+ unused = sorted(self.api_mocked - self.api_used)
91
+ if missing:
92
+ lines.append(f" called but not mocked: {', '.join(missing)}")
93
+ if unused:
94
+ lines.append(f" mocked but unused: {', '.join(unused)}")
95
+ attr_missing = sorted(self.attr_keys_used - self.attr_keys_mocked)
96
+ if attr_missing:
97
+ lines.append(f" attr keys not in mock: {', '.join(attr_missing)}")
98
+ return lines
99
+
100
+
101
+ def _mocked_api_surface() -> set[str]:
102
+ classes = (
103
+ FakeResolve,
104
+ FakeProjectManager,
105
+ FakeProject,
106
+ FakeTimeline,
107
+ FakeClip,
108
+ FakeMediaPoolItem,
109
+ FakeFolder,
110
+ FakeMediaPool,
111
+ FakeKey,
112
+ FakeSpline,
113
+ FakeStroke,
114
+ FakeTool,
115
+ FakeComp,
116
+ FakeFusion,
117
+ )
118
+ return {
119
+ name
120
+ for cls in classes
121
+ for name in dir(cls)
122
+ if not name.startswith("_") and callable(getattr(cls, name, None))
123
+ }
124
+
125
+
126
+ def _mock_attr_keys() -> set[str]:
127
+ """Extract keys from ``self._attrs = {...}`` in sandbox/api.py (stays in sync)."""
128
+ source = Path(__file__).resolve().parent / "sandbox" / "api.py"
129
+ tree = ast.parse(source.read_text("utf-8"))
130
+ keys: set[str] = set()
131
+ for node in ast.walk(tree):
132
+ if not isinstance(node, ast.Assign):
133
+ continue
134
+ for target in node.targets:
135
+ if not (isinstance(target, ast.Attribute) and target.attr == "_attrs"):
136
+ continue
137
+ if not (isinstance(target.value, ast.Name) and target.value.id == "self"):
138
+ continue
139
+ if isinstance(node.value, ast.Dict):
140
+ keys.update(
141
+ key.value
142
+ for key in node.value.keys
143
+ if isinstance(key, ast.Constant) and isinstance(key.value, str)
144
+ )
145
+ return keys
146
+
147
+
148
+ def _is_skip(rel: Path) -> bool:
149
+ return any(part in _SKIP_PARTS for part in rel.parts)
150
+
151
+
152
+ def _unused_imports(tree: ast.AST, rel: Path, analysis: Analysis) -> None:
153
+ names_used = {
154
+ node.id
155
+ for node in ast.walk(tree)
156
+ if isinstance(node, ast.Name)
157
+ }
158
+ names_used.update(
159
+ node.attr
160
+ for node in ast.walk(tree)
161
+ if isinstance(node, ast.Attribute)
162
+ if isinstance(node.value, ast.Name)
163
+ )
164
+ for node in ast.walk(tree):
165
+ aliases: list[ast.alias] = []
166
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
167
+ aliases = node.names
168
+ for alias in aliases:
169
+ name = alias.asname or (alias.name.split(".")[0])
170
+ if name == "*":
171
+ continue
172
+ if name in {"__version__"}:
173
+ continue
174
+ count = sum(1 for token in names_used if token == name)
175
+ if count <= 1: # only the binding itself
176
+ analysis.issues.append(
177
+ Issue(
178
+ "UNUSED_IMPORT",
179
+ "warning",
180
+ rel.as_posix(),
181
+ node.lineno,
182
+ f"imported name '{name}' is never used",
183
+ )
184
+ )
185
+
186
+
187
+ def _collect_api(tree: ast.AST, analysis: Analysis) -> None:
188
+ for node in ast.walk(tree):
189
+ if not isinstance(node, ast.Call):
190
+ continue
191
+ attr = node.func
192
+ if not isinstance(attr, ast.Attribute):
193
+ continue
194
+ base = attr.value
195
+ if isinstance(base, ast.Attribute):
196
+ while isinstance(base, ast.Attribute):
197
+ base = base.value
198
+ if isinstance(base, ast.Name) and base.id in KNOWN_ROOTS:
199
+ analysis.api_used.add(attr.attr)
200
+
201
+
202
+ def _collect_attr_keys(tree: ast.AST, analysis: Analysis) -> None:
203
+ seen_constants = {
204
+ node.value
205
+ for node in ast.walk(tree)
206
+ if isinstance(node, ast.Constant) and isinstance(node.value, str)
207
+ }
208
+ for text in seen_constants:
209
+ if _ATTR_KEY_RE.match(text) or text in {"TOOLS_RegID", "COMPS_Name"}:
210
+ analysis.attr_keys_used.add(text)
211
+
212
+
213
+ def analyze_project(root: Path, manifest=None) -> Analysis:
214
+ root = Path(root).resolve()
215
+ analysis = Analysis()
216
+ analysis.api_mocked = _mocked_api_surface()
217
+ analysis.attr_keys_mocked = _mock_attr_keys()
218
+
219
+ if manifest is not None:
220
+ for message in validate_manifest(manifest):
221
+ analysis.issues.append(
222
+ Issue("MANIFEST", "error", "manifest.json", 0, message)
223
+ )
224
+
225
+ for py in sorted(root.rglob("*.py")):
226
+ try:
227
+ rel = py.relative_to(root)
228
+ except ValueError:
229
+ rel = Path(py.name)
230
+ if _is_skip(rel):
231
+ continue
232
+ try:
233
+ tree = ast.parse(py.read_text("utf-8"), filename=str(py))
234
+ except SyntaxError as exc:
235
+ analysis.issues.append(
236
+ Issue(
237
+ "SYNTAX",
238
+ "error",
239
+ rel.as_posix(),
240
+ exc.lineno or 0,
241
+ f"syntax error: {exc.msg}",
242
+ )
243
+ )
244
+ continue
245
+ except UnicodeDecodeError:
246
+ continue
247
+ _unused_imports(tree, rel, analysis)
248
+ _collect_api(tree, analysis)
249
+ _collect_attr_keys(tree, analysis)
250
+
251
+ for name in sorted(analysis.api_used - analysis.api_mocked):
252
+ analysis.issues.append(
253
+ Issue(
254
+ "API_UNMOCKED",
255
+ "info",
256
+ "",
257
+ 0,
258
+ f"'{name}()' is called but not provided by the mock sandbox",
259
+ )
260
+ )
261
+ for key in sorted(analysis.attr_keys_used - analysis.attr_keys_mocked):
262
+ analysis.issues.append(
263
+ Issue(
264
+ "ATTR_KEY",
265
+ "info",
266
+ "",
267
+ 0,
268
+ f"attr key '{key}' is read but not defined by the mock sandbox",
269
+ )
270
+ )
271
+ return analysis
272
+
273
+
274
+ def issues_to_json(analysis: Analysis) -> str:
275
+ return json.dumps(
276
+ [issue.__dict__ for issue in analysis.issues], indent=2, default=str
277
+ )