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.
- resolvescript-0.1.2/LICENSE +21 -0
- resolvescript-0.1.2/PKG-INFO +146 -0
- resolvescript-0.1.2/README.md +123 -0
- resolvescript-0.1.2/pyproject.toml +57 -0
- resolvescript-0.1.2/setup.cfg +4 -0
- resolvescript-0.1.2/src/resolve_script/__init__.py +3 -0
- resolvescript-0.1.2/src/resolve_script/analyze.py +277 -0
- resolvescript-0.1.2/src/resolve_script/cli.py +748 -0
- resolvescript-0.1.2/src/resolve_script/config.py +62 -0
- resolvescript-0.1.2/src/resolve_script/consolidate.py +604 -0
- resolvescript-0.1.2/src/resolve_script/fetch.py +90 -0
- resolvescript-0.1.2/src/resolve_script/install/__init__.py +49 -0
- resolvescript-0.1.2/src/resolve_script/install/discovery.py +57 -0
- resolvescript-0.1.2/src/resolve_script/install/installer.py +397 -0
- resolvescript-0.1.2/src/resolve_script/install/registry.py +106 -0
- resolvescript-0.1.2/src/resolve_script/manifest/__init__.py +1 -0
- resolvescript-0.1.2/src/resolve_script/manifest/json_reader.py +40 -0
- resolvescript-0.1.2/src/resolve_script/manifest/model.py +316 -0
- resolvescript-0.1.2/src/resolve_script/manifest/validation.py +81 -0
- resolvescript-0.1.2/src/resolve_script/manifest/xml_reader.py +162 -0
- resolvescript-0.1.2/src/resolve_script/package.py +103 -0
- resolvescript-0.1.2/src/resolve_script/resolver.py +204 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/__init__.py +38 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/api.py +393 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/env.py +82 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/loader.py +72 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/repl.py +57 -0
- resolvescript-0.1.2/src/resolve_script/sandbox/smoke.py +104 -0
- resolvescript-0.1.2/src/resolve_script/scaffold.py +126 -0
- resolvescript-0.1.2/src/resolve_script/semver.py +236 -0
- resolvescript-0.1.2/src/resolve_script/sources/__init__.py +15 -0
- resolvescript-0.1.2/src/resolve_script/sources/archive.py +82 -0
- resolvescript-0.1.2/src/resolve_script/sources/git.py +107 -0
- resolvescript-0.1.2/src/resolve_script/sources/known.py +47 -0
- resolvescript-0.1.2/src/resolve_script/sources/release.py +55 -0
- resolvescript-0.1.2/src/resolve_script/spec.py +137 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@/__init__.py +7 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@/menu.py +12 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/@NAME@.py +13 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/README.md +20 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/conftest.py +13 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/manifest.json.j2 +23 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/manifest.xml.j2 +24 -0
- resolvescript-0.1.2/src/resolve_script/templates/extension/tests/test_smoke.py +26 -0
- resolvescript-0.1.2/src/resolve_script/templates/inapp/register.py +28 -0
- resolvescript-0.1.2/src/resolve_script/testing/__init__.py +6 -0
- resolvescript-0.1.2/src/resolve_script/testing/fixtures.py +47 -0
- resolvescript-0.1.2/src/resolve_script/workspace.py +66 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/PKG-INFO +146 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/SOURCES.txt +62 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/dependency_links.txt +1 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/entry_points.txt +2 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/requires.txt +4 -0
- resolvescript-0.1.2/src/resolvescript.egg-info/top_level.txt +1 -0
- resolvescript-0.1.2/tests/test_analyze.py +99 -0
- resolvescript-0.1.2/tests/test_cli.py +73 -0
- resolvescript-0.1.2/tests/test_consolidate.py +484 -0
- resolvescript-0.1.2/tests/test_install.py +351 -0
- resolvescript-0.1.2/tests/test_manage.py +60 -0
- resolvescript-0.1.2/tests/test_manifest.py +245 -0
- resolvescript-0.1.2/tests/test_package.py +66 -0
- resolvescript-0.1.2/tests/test_runtime.py +408 -0
- resolvescript-0.1.2/tests/test_sandbox.py +243 -0
- 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,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
|
+
)
|