resolvescript 0.1.2__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 (64) hide show
  1. resolvescript-0.1.2/LICENSE +21 -0
  2. resolvescript-0.1.2/PKG-INFO +146 -0
  3. resolvescript-0.1.2/README.md +123 -0
  4. resolvescript-0.1.2/pyproject.toml +57 -0
  5. resolvescript-0.1.2/setup.cfg +4 -0
  6. resolvescript-0.1.2/src/resolve_script/__init__.py +3 -0
  7. resolvescript-0.1.2/src/resolve_script/analyze.py +277 -0
  8. resolvescript-0.1.2/src/resolve_script/cli.py +748 -0
  9. resolvescript-0.1.2/src/resolve_script/config.py +62 -0
  10. resolvescript-0.1.2/src/resolve_script/consolidate.py +604 -0
  11. resolvescript-0.1.2/src/resolve_script/fetch.py +90 -0
  12. resolvescript-0.1.2/src/resolve_script/install/__init__.py +49 -0
  13. resolvescript-0.1.2/src/resolve_script/install/discovery.py +57 -0
  14. resolvescript-0.1.2/src/resolve_script/install/installer.py +397 -0
  15. resolvescript-0.1.2/src/resolve_script/install/registry.py +106 -0
  16. resolvescript-0.1.2/src/resolve_script/manifest/__init__.py +1 -0
  17. resolvescript-0.1.2/src/resolve_script/manifest/json_reader.py +40 -0
  18. resolvescript-0.1.2/src/resolve_script/manifest/model.py +316 -0
  19. resolvescript-0.1.2/src/resolve_script/manifest/validation.py +81 -0
  20. resolvescript-0.1.2/src/resolve_script/manifest/xml_reader.py +162 -0
  21. resolvescript-0.1.2/src/resolve_script/package.py +103 -0
  22. resolvescript-0.1.2/src/resolve_script/resolver.py +204 -0
  23. resolvescript-0.1.2/src/resolve_script/sandbox/__init__.py +38 -0
  24. resolvescript-0.1.2/src/resolve_script/sandbox/api.py +393 -0
  25. resolvescript-0.1.2/src/resolve_script/sandbox/env.py +82 -0
  26. resolvescript-0.1.2/src/resolve_script/sandbox/loader.py +72 -0
  27. resolvescript-0.1.2/src/resolve_script/sandbox/repl.py +57 -0
  28. resolvescript-0.1.2/src/resolve_script/sandbox/smoke.py +104 -0
  29. resolvescript-0.1.2/src/resolve_script/scaffold.py +126 -0
  30. resolvescript-0.1.2/src/resolve_script/semver.py +236 -0
  31. resolvescript-0.1.2/src/resolve_script/sources/__init__.py +15 -0
  32. resolvescript-0.1.2/src/resolve_script/sources/archive.py +82 -0
  33. resolvescript-0.1.2/src/resolve_script/sources/git.py +107 -0
  34. resolvescript-0.1.2/src/resolve_script/sources/known.py +47 -0
  35. resolvescript-0.1.2/src/resolve_script/sources/release.py +55 -0
  36. resolvescript-0.1.2/src/resolve_script/spec.py +137 -0
  37. resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@/__init__.py +7 -0
  38. resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@/menu.py +12 -0
  39. resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@.py +13 -0
  40. resolvescript-0.1.2/src/resolve_script/templates/extension/README.md +20 -0
  41. resolvescript-0.1.2/src/resolve_script/templates/extension/conftest.py +13 -0
  42. resolvescript-0.1.2/src/resolve_script/templates/extension/manifest.json.j2 +23 -0
  43. resolvescript-0.1.2/src/resolve_script/templates/extension/manifest.xml.j2 +24 -0
  44. resolvescript-0.1.2/src/resolve_script/templates/extension/tests/test_smoke.py +26 -0
  45. resolvescript-0.1.2/src/resolve_script/templates/inapp/register.py +28 -0
  46. resolvescript-0.1.2/src/resolve_script/testing/__init__.py +6 -0
  47. resolvescript-0.1.2/src/resolve_script/testing/fixtures.py +47 -0
  48. resolvescript-0.1.2/src/resolve_script/workspace.py +66 -0
  49. resolvescript-0.1.2/src/resolvescript.egg-info/PKG-INFO +146 -0
  50. resolvescript-0.1.2/src/resolvescript.egg-info/SOURCES.txt +62 -0
  51. resolvescript-0.1.2/src/resolvescript.egg-info/dependency_links.txt +1 -0
  52. resolvescript-0.1.2/src/resolvescript.egg-info/entry_points.txt +2 -0
  53. resolvescript-0.1.2/src/resolvescript.egg-info/requires.txt +4 -0
  54. resolvescript-0.1.2/src/resolvescript.egg-info/top_level.txt +1 -0
  55. resolvescript-0.1.2/tests/test_analyze.py +99 -0
  56. resolvescript-0.1.2/tests/test_cli.py +73 -0
  57. resolvescript-0.1.2/tests/test_consolidate.py +484 -0
  58. resolvescript-0.1.2/tests/test_install.py +351 -0
  59. resolvescript-0.1.2/tests/test_manage.py +60 -0
  60. resolvescript-0.1.2/tests/test_manifest.py +245 -0
  61. resolvescript-0.1.2/tests/test_package.py +66 -0
  62. resolvescript-0.1.2/tests/test_runtime.py +408 -0
  63. resolvescript-0.1.2/tests/test_sandbox.py +243 -0
  64. resolvescript-0.1.2/tests/test_scaffold.py +117 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: resolvescript
3
+ Version: 0.1.2
4
+ Summary: CLI framework for building, coding, testing, packaging and installing DaVinci Resolve scripts.
5
+ License: MIT
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Multimedia :: Video
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # resolvescript
25
+
26
+ Build, test, package and install [DaVinci Resolve](https://www.blackmagicdesign.com/products/davinciresolve) Python scripts and plugins: a small CLI with an npm-style workflow for the Resolve `Scripts/` tree.
27
+
28
+ - `create` scaffolds a project with a `manifest.json` and smoke tests
29
+ - `dev` / `test` run against a mock `DaVinciResolveScript` API - no Resolve needed
30
+ - `build` consolidates the multi-file package into a single `.py` for distribution
31
+ - `add` / `install` / `update` resolve dependencies from GitHub, URLs, archives or local folders and record them in `resolvescript.json`
32
+ - `package` emits `dist/<name>-<version>.tar.gz` plus `SHA256SUMS.txt`
33
+
34
+ Install with pip: `pip install resolvescript` (Python 3.12+).
35
+
36
+ ## Quickstart
37
+
38
+ ```console
39
+ $ resolvescript create my-cool-tool
40
+ $ cd my-cool-tool
41
+ $ resolvescript dev # sandboxed dev loop against the mock API
42
+ $ resolvescript test # run the smoke tests
43
+ $ resolvescript build # single-file build -> dist/my_cool_tool.py
44
+ $ resolvescript analyze # static checks (imports, manifest, API usage)
45
+ $ resolvescript package # dist/my-cool-tool-0.1.0.tar.gz + SHA256SUMS.txt
46
+
47
+ # elsewhere, consume it:
48
+ $ resolvescript add github:example/my-cool-tool
49
+ $ resolvescript install # materialize all recorded deps into Resolve
50
+ ```
51
+
52
+ ## Authoring
53
+
54
+ ### Directory
55
+
56
+ ```
57
+ my-cool-tool/
58
+ manifest.json # project manifest (or manifest.xml)
59
+ my_cool_tool/ # your Python package (module name from manifest.name/python)
60
+ __init__.py
61
+ export.py
62
+ tests/
63
+ test_smoke.py
64
+ ```
65
+
66
+ ### Commands
67
+
68
+ | Command | Purpose |
69
+ | --- | --- |
70
+ | `create <name>` | Scaffold a new project (`--json`/`--xml`, `--template minimal\|toolkit`, `--dir`) |
71
+ | `dev` | Sandboxed dev loop / REPL against the mock Resolve API (`--built` for the consolidated file, `--repl`, `--editor`) |
72
+ | `test` | Run the project's pytest suite against the mock API (`--built`, `-k pattern`, `--api-coverage`) |
73
+ | `analyze` | Static checks: missing entrypoint/module, syntax, unused imports, unmocked API methods, attribute typing (`--json`) |
74
+ | `build` | Consolidate into a single file (`--output`, or the manifest `consolidate.output`) |
75
+ | `package` | Assemble `dist/<name>-<version>.tar.gz` + `SHA256SUMS.txt` (`--dist DIR`) |
76
+ | `add <spec>` | Resolve, install a dependency and record it (`--no-save`, `--target`, `--scripts-root`) |
77
+ | `install [<spec>]` | One-off install, local project, **or** materialize recorded deps (`--locked`, `--dry-run`) |
78
+ | `update [<name>]` | Re-resolve recorded deps within their ranges (`--precise X.Y.Z`, `--fix`) |
79
+ | `remove <name>` | Uninstall and unrecord (`--no-save`) |
80
+ | `search <query>` | Search the known sources table |
81
+ | `manage list\|remove` | Low-level registry operations (`--json`, `--all`) |
82
+
83
+ Exit codes: `0` ok, `1` error, `2` usage.
84
+
85
+ ### Specifiers
86
+
87
+ `add` / `install <spec>` accept:
88
+
89
+ - `name` - a name from the built-in known-sources table (`resolvescript search`)
90
+ - `owner/repo` or `github:owner/repo[#ref]` - GitHub repo (ref = branch/tag/commit, or `#semver:^1.2` for a range)
91
+ - `https://...tar.gz` / `.zip` - direct archive URL
92
+ - `https://...` - a URL whose root carries a `manifest.json` (GitHub Pages-style hosting)
93
+ - `file:path`, `./dir`, an absolute path - local directory or archive
94
+ - `.tar.gz` / `.zip` files and local directories
95
+
96
+ Recorded specs live in `resolvescript.json`; use `resolvescript install --locked` for a CI/npm-ci style check that recorded artifacts still satisfy the recorded ranges.
97
+
98
+ ### Manifest reference
99
+
100
+ `manifest.json` (or `manifest.xml`, same shape):
101
+
102
+ ```jsonc
103
+ {
104
+ "name": "my-cool-tool", // required
105
+ "version": "0.1.0", // required, semver
106
+ "author": "You",
107
+ "description": "...",
108
+ "python": "my_cool_tool", // package/module name (defaults to "name")
109
+ "entrypoint": "export.py", // recommended CLI/bootstrap module
110
+ "kind": "script", // "script" (default) | "extension" (plugin)
111
+ "targets": ["Comp"], // Scripts subfolders: Comp, Utility, Tool, Render,
112
+ // Deliver, Edit, WorkflowIntegrations, Fusion, root
113
+ "compat": { "resolve": "18.6.4", "python": "3.12" },
114
+ "release": { "owner": "you", "repo": "my-cool-tool" }, // for `add` discovery
115
+ "scripts_root": "", // per-OS override for the Resolve Scripts root
116
+ "consolidate": { "enabled": true, "output": "dist/my_tool.py", "exclude": [], "no_comment": [] },
117
+ "install": { "as_directory": true, "include": [], "exclude": [], "to": "resolve" },
118
+ "dependencies": ["github:example/dep"] // resolved on install
119
+ }
120
+ ```
121
+
122
+ ### Install model
123
+
124
+ Installs drop into each OS's Resolve Scripts root under the manifest `targets`. Directories install as a single folder; `.py`-only packages can opt into `install.as_directory: false` multi-file installs. Every install is recorded in `<ScriptsRoot>/.resolvescript/install.json` (schema v1):
125
+
126
+ - keys are `<name>:<target>` (directory) or `<name>:<target>:<relpath>` (file-based)
127
+ - each entry carries `version`, `targets`, `as_directory`, `files`, `source`, `resolved` and `integrity`
128
+ - `install --locked` verifies registry entries against recorded specs before reusing them
129
+
130
+ `--scripts-root` (or `RESOLVESCRIPT_SCRIPTS_ROOT`) overrides OS detection.
131
+
132
+ ## Frameworks
133
+
134
+ `resolvescript` ships a mock `DaVinciResolveScript` module with a faithful-enough Resolve color/editing/page object model for testing, plus `resolvescript test --api-coverage` to report which mock methods your tests actually exercise.
135
+
136
+ ## Development
137
+
138
+ ```console
139
+ python -m venv .venv
140
+ .\.venv\Scripts\activate # Windows; source .venv/bin/activate elsewhere
141
+ pip install -e ".[dev]"
142
+ pytest
143
+ ruff check .
144
+ ```
145
+
146
+ Milestones and the full plan live in [`TODO.md`](TODO.md). CI runs the shared `OseMine/workflows` action; cutting a `vX.Y.Z` tag runs the release pipeline (sdist+wheel, checksums, GitHub release, optional PyPI publishing).
@@ -0,0 +1,123 @@
1
+ # resolvescript
2
+
3
+ Build, test, package and install [DaVinci Resolve](https://www.blackmagicdesign.com/products/davinciresolve) Python scripts and plugins: a small CLI with an npm-style workflow for the Resolve `Scripts/` tree.
4
+
5
+ - `create` scaffolds a project with a `manifest.json` and smoke tests
6
+ - `dev` / `test` run against a mock `DaVinciResolveScript` API - no Resolve needed
7
+ - `build` consolidates the multi-file package into a single `.py` for distribution
8
+ - `add` / `install` / `update` resolve dependencies from GitHub, URLs, archives or local folders and record them in `resolvescript.json`
9
+ - `package` emits `dist/<name>-<version>.tar.gz` plus `SHA256SUMS.txt`
10
+
11
+ Install with pip: `pip install resolvescript` (Python 3.12+).
12
+
13
+ ## Quickstart
14
+
15
+ ```console
16
+ $ resolvescript create my-cool-tool
17
+ $ cd my-cool-tool
18
+ $ resolvescript dev # sandboxed dev loop against the mock API
19
+ $ resolvescript test # run the smoke tests
20
+ $ resolvescript build # single-file build -> dist/my_cool_tool.py
21
+ $ resolvescript analyze # static checks (imports, manifest, API usage)
22
+ $ resolvescript package # dist/my-cool-tool-0.1.0.tar.gz + SHA256SUMS.txt
23
+
24
+ # elsewhere, consume it:
25
+ $ resolvescript add github:example/my-cool-tool
26
+ $ resolvescript install # materialize all recorded deps into Resolve
27
+ ```
28
+
29
+ ## Authoring
30
+
31
+ ### Directory
32
+
33
+ ```
34
+ my-cool-tool/
35
+ manifest.json # project manifest (or manifest.xml)
36
+ my_cool_tool/ # your Python package (module name from manifest.name/python)
37
+ __init__.py
38
+ export.py
39
+ tests/
40
+ test_smoke.py
41
+ ```
42
+
43
+ ### Commands
44
+
45
+ | Command | Purpose |
46
+ | --- | --- |
47
+ | `create <name>` | Scaffold a new project (`--json`/`--xml`, `--template minimal\|toolkit`, `--dir`) |
48
+ | `dev` | Sandboxed dev loop / REPL against the mock Resolve API (`--built` for the consolidated file, `--repl`, `--editor`) |
49
+ | `test` | Run the project's pytest suite against the mock API (`--built`, `-k pattern`, `--api-coverage`) |
50
+ | `analyze` | Static checks: missing entrypoint/module, syntax, unused imports, unmocked API methods, attribute typing (`--json`) |
51
+ | `build` | Consolidate into a single file (`--output`, or the manifest `consolidate.output`) |
52
+ | `package` | Assemble `dist/<name>-<version>.tar.gz` + `SHA256SUMS.txt` (`--dist DIR`) |
53
+ | `add <spec>` | Resolve, install a dependency and record it (`--no-save`, `--target`, `--scripts-root`) |
54
+ | `install [<spec>]` | One-off install, local project, **or** materialize recorded deps (`--locked`, `--dry-run`) |
55
+ | `update [<name>]` | Re-resolve recorded deps within their ranges (`--precise X.Y.Z`, `--fix`) |
56
+ | `remove <name>` | Uninstall and unrecord (`--no-save`) |
57
+ | `search <query>` | Search the known sources table |
58
+ | `manage list\|remove` | Low-level registry operations (`--json`, `--all`) |
59
+
60
+ Exit codes: `0` ok, `1` error, `2` usage.
61
+
62
+ ### Specifiers
63
+
64
+ `add` / `install <spec>` accept:
65
+
66
+ - `name` - a name from the built-in known-sources table (`resolvescript search`)
67
+ - `owner/repo` or `github:owner/repo[#ref]` - GitHub repo (ref = branch/tag/commit, or `#semver:^1.2` for a range)
68
+ - `https://...tar.gz` / `.zip` - direct archive URL
69
+ - `https://...` - a URL whose root carries a `manifest.json` (GitHub Pages-style hosting)
70
+ - `file:path`, `./dir`, an absolute path - local directory or archive
71
+ - `.tar.gz` / `.zip` files and local directories
72
+
73
+ Recorded specs live in `resolvescript.json`; use `resolvescript install --locked` for a CI/npm-ci style check that recorded artifacts still satisfy the recorded ranges.
74
+
75
+ ### Manifest reference
76
+
77
+ `manifest.json` (or `manifest.xml`, same shape):
78
+
79
+ ```jsonc
80
+ {
81
+ "name": "my-cool-tool", // required
82
+ "version": "0.1.0", // required, semver
83
+ "author": "You",
84
+ "description": "...",
85
+ "python": "my_cool_tool", // package/module name (defaults to "name")
86
+ "entrypoint": "export.py", // recommended CLI/bootstrap module
87
+ "kind": "script", // "script" (default) | "extension" (plugin)
88
+ "targets": ["Comp"], // Scripts subfolders: Comp, Utility, Tool, Render,
89
+ // Deliver, Edit, WorkflowIntegrations, Fusion, root
90
+ "compat": { "resolve": "18.6.4", "python": "3.12" },
91
+ "release": { "owner": "you", "repo": "my-cool-tool" }, // for `add` discovery
92
+ "scripts_root": "", // per-OS override for the Resolve Scripts root
93
+ "consolidate": { "enabled": true, "output": "dist/my_tool.py", "exclude": [], "no_comment": [] },
94
+ "install": { "as_directory": true, "include": [], "exclude": [], "to": "resolve" },
95
+ "dependencies": ["github:example/dep"] // resolved on install
96
+ }
97
+ ```
98
+
99
+ ### Install model
100
+
101
+ Installs drop into each OS's Resolve Scripts root under the manifest `targets`. Directories install as a single folder; `.py`-only packages can opt into `install.as_directory: false` multi-file installs. Every install is recorded in `<ScriptsRoot>/.resolvescript/install.json` (schema v1):
102
+
103
+ - keys are `<name>:<target>` (directory) or `<name>:<target>:<relpath>` (file-based)
104
+ - each entry carries `version`, `targets`, `as_directory`, `files`, `source`, `resolved` and `integrity`
105
+ - `install --locked` verifies registry entries against recorded specs before reusing them
106
+
107
+ `--scripts-root` (or `RESOLVESCRIPT_SCRIPTS_ROOT`) overrides OS detection.
108
+
109
+ ## Frameworks
110
+
111
+ `resolvescript` ships a mock `DaVinciResolveScript` module with a faithful-enough Resolve color/editing/page object model for testing, plus `resolvescript test --api-coverage` to report which mock methods your tests actually exercise.
112
+
113
+ ## Development
114
+
115
+ ```console
116
+ python -m venv .venv
117
+ .\.venv\Scripts\activate # Windows; source .venv/bin/activate elsewhere
118
+ pip install -e ".[dev]"
119
+ pytest
120
+ ruff check .
121
+ ```
122
+
123
+ Milestones and the full plan live in [`TODO.md`](TODO.md). CI runs the shared `OseMine/workflows` action; cutting a `vX.Y.Z` tag runs the release pipeline (sdist+wheel, checksums, GitHub release, optional PyPI publishing).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "resolvescript"
7
+ version = "0.1.2"
8
+ description = "CLI framework for building, coding, testing, packaging and installing DaVinci Resolve scripts."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Multimedia :: Video",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=7.0",
28
+ "ruff>=0.1.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ resolvescript = "resolve_script.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+ include = ["resolve_script*"]
37
+
38
+ [tool.setuptools.package-data]
39
+ "resolve_script" = ["templates/**/*"]
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py39"
44
+ extend-exclude = [
45
+ "src/resolve_script/templates", # placeholders (@NAME@) are not valid Python
46
+ ]
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "UP", "B", "C4", "SIM", "W", "RET", "PTH"]
50
+ ignore = ["E501"]
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ "tests/*.py" = ["ARG001"]
54
+
55
+ [tool.pytest.ini_options]
56
+ pythonpath = ["src"]
57
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )