gd-tools-cli 0.8.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/PKG-INFO +26 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/README.md +25 -1
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/pyproject.toml +2 -2
- gd_tools_cli-0.10.0/src/gd_tools/addon_check.py +157 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_native_coverage.gd +110 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/atomic_io.py +35 -0
- gd_tools_cli-0.10.0/src/gd_tools/cli.py +219 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/__init__.py +6 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/clean.py +171 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/completion.py +61 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/config.py +255 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/coverage.py +378 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/doctor.py +24 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/format.py +73 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/init.py +25 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/install_hooks.py +118 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/lint.py +64 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/migrate.py +137 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/test.py +367 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/test_validation.py +210 -0
- gd_tools_cli-0.10.0/src/gd_tools/commands/version.py +36 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/config.py +6 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/plan_generator.py +28 -3
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/artifacts.py +30 -30
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/command.py +78 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/orchestrator.py +25 -20
- gd_tools_cli-0.10.0/src/gd_tools/native_test/ordering.py +110 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/protocol.py +2 -7
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/output.py +14 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/session.py +16 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/PKG-INFO +26 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/SOURCES.txt +15 -0
- gd_tools_cli-0.8.0/src/gd_tools/addon_check.py +0 -75
- gd_tools_cli-0.8.0/src/gd_tools/cli.py +0 -1790
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/setup.cfg +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/__main__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_mock.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_parameter_naming.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_serializer.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_store.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_context.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_preflight.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_runner.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/changes.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/clean.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/diff_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/html_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/omissions.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/orchestrator.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/patch.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/playtest.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/templates/file.html +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/templates/index.html +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/doctor.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/errors.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/file_discovery.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/format_runner.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/gh_annotations.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/godot.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/init.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/lint_runner.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/apply.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/gutconfig.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/rewrite.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/scan.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/migration/translate.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/discovery.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/flaky.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/import_cache.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/preflight.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/pre_commit.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/schema.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/test_results.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/update_check.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/verbosity.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/version.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/coalescer.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/loop.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/mapping.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/observer.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools/watch/scope.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/requires.txt +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.10.0}/src/gd_tools_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gd-tools-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A modern development workflow CLI for GDScript projects in Godot 4.5+
|
|
5
5
|
Author-email: mansyar <m.ansyarafi@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -142,7 +142,7 @@ func test_health_starts_at_full() -> void:
|
|
|
142
142
|
|---------|-------------|
|
|
143
143
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
144
144
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
145
|
-
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
145
|
+
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting; `--random-order` (with `--no-random-order` to turn a configured shuffle off) shuffles suite and test-method execution order, and `--seed N` pins the shuffle so a run can be replayed. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
146
146
|
| `gd-tools migrate` | Guided GUT-to-native migration. Default: read-only report with unsupported-construct inventory and proposed base-class rewrites. `--apply` renames clean suites to `GdToolsTest` and translates `.gutconfig.json` into `gd-tools.toml` (merge, never clobber). `--config-only` translates config only. |
|
|
147
147
|
| `gd-tools lint` | Lint GDScript files using gdlint with text, JSON, or GitHub Actions annotation output. Accepts one or more file or directory paths. |
|
|
148
148
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
@@ -165,6 +165,9 @@ gd-tools test --changed # only suites mapped from git-changed
|
|
|
165
165
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
166
166
|
gd-tools test --exitfirst # stop dispatching after the first failing suite
|
|
167
167
|
gd-tools test --shard 2/4 # CI sharding: run suite shard 2 of 4
|
|
168
|
+
gd-tools test --random-order # shuffle suite and test method order
|
|
169
|
+
gd-tools test --seed 42 # --seed implies --random-order; replayable
|
|
170
|
+
gd-tools test --no-random-order # discovery order for one run, config regardless
|
|
168
171
|
```
|
|
169
172
|
|
|
170
173
|
`--tag` is repeatable, and `--test-timeout` (per test) is separate from
|
|
@@ -216,6 +219,25 @@ own coverage report; merge the shards with `gd-tools coverage merge`.
|
|
|
216
219
|
[sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
|
|
217
220
|
for a GitHub Actions matrix example.
|
|
218
221
|
|
|
222
|
+
`--random-order` shuffles the execution order of the selected suites, and of
|
|
223
|
+
the test methods within each suite, to surface tests that depend on a
|
|
224
|
+
neighbour's side effects. The order is a pure function of the seed and the
|
|
225
|
+
selected suite/test set: the run summary reports
|
|
226
|
+
`Random order: seed <N> (replay with --seed <N>)` — exempt from `--quiet`,
|
|
227
|
+
since a shuffled failure cannot be reproduced without its seed — and
|
|
228
|
+
re-running with that
|
|
229
|
+
seed reproduces the order exactly. `--seed N` pins the shuffle and implies
|
|
230
|
+
`--random-order`; without it a fresh 31-bit seed is generated per run. Persist
|
|
231
|
+
the choice with `random_order = true` and `seed = 42` under `[test]`, and undo
|
|
232
|
+
it for one run with the paired `--no-random-order`. Shuffling
|
|
233
|
+
never changes shard membership — `--shard` assigns suites from the
|
|
234
|
+
deterministic plan order, so CI matrix jobs still cover every suite exactly
|
|
235
|
+
once — and `--watch` resolves one seed for the whole session so successive
|
|
236
|
+
re-runs stay comparable. `--verbose` lists the resolved order suite-by-suite,
|
|
237
|
+
and the seed is recorded in `artifacts.json`. A seed reproduces an order for
|
|
238
|
+
the same Python version, not across versions: `random.Random` is what draws
|
|
239
|
+
the order and CPython may change its shuffle algorithm between releases.
|
|
240
|
+
|
|
219
241
|
Repeat runs are faster: the `godot --headless --import` step is cached under
|
|
220
242
|
`.gd-tools/native/import-cache/` and skipped when `project.godot`, any
|
|
221
243
|
project source/resource file (scripts, scenes, import sidecars, addons), or
|
|
@@ -406,6 +428,8 @@ runtime = "native" # native (default); "gut" is no longer a runnable runt
|
|
|
406
428
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
407
429
|
retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
|
|
408
430
|
# parallel = 4 # opt-in parallel suite execution (1-32; unset runs sequentially)
|
|
431
|
+
# random_order = true # opt-in shuffled suite and test-method order
|
|
432
|
+
# seed = 42 # pin the shuffle seed so a shuffled run is replayable
|
|
409
433
|
tags = [] # native suite tag filters
|
|
410
434
|
|
|
411
435
|
[lint]
|
|
@@ -100,7 +100,7 @@ func test_health_starts_at_full() -> void:
|
|
|
100
100
|
|---------|-------------|
|
|
101
101
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
102
102
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
103
|
-
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
103
|
+
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting; `--random-order` (with `--no-random-order` to turn a configured shuffle off) shuffles suite and test-method execution order, and `--seed N` pins the shuffle so a run can be replayed. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
104
104
|
| `gd-tools migrate` | Guided GUT-to-native migration. Default: read-only report with unsupported-construct inventory and proposed base-class rewrites. `--apply` renames clean suites to `GdToolsTest` and translates `.gutconfig.json` into `gd-tools.toml` (merge, never clobber). `--config-only` translates config only. |
|
|
105
105
|
| `gd-tools lint` | Lint GDScript files using gdlint with text, JSON, or GitHub Actions annotation output. Accepts one or more file or directory paths. |
|
|
106
106
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
@@ -123,6 +123,9 @@ gd-tools test --changed # only suites mapped from git-changed
|
|
|
123
123
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
124
124
|
gd-tools test --exitfirst # stop dispatching after the first failing suite
|
|
125
125
|
gd-tools test --shard 2/4 # CI sharding: run suite shard 2 of 4
|
|
126
|
+
gd-tools test --random-order # shuffle suite and test method order
|
|
127
|
+
gd-tools test --seed 42 # --seed implies --random-order; replayable
|
|
128
|
+
gd-tools test --no-random-order # discovery order for one run, config regardless
|
|
126
129
|
```
|
|
127
130
|
|
|
128
131
|
`--tag` is repeatable, and `--test-timeout` (per test) is separate from
|
|
@@ -174,6 +177,25 @@ own coverage report; merge the shards with `gd-tools coverage merge`.
|
|
|
174
177
|
[sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
|
|
175
178
|
for a GitHub Actions matrix example.
|
|
176
179
|
|
|
180
|
+
`--random-order` shuffles the execution order of the selected suites, and of
|
|
181
|
+
the test methods within each suite, to surface tests that depend on a
|
|
182
|
+
neighbour's side effects. The order is a pure function of the seed and the
|
|
183
|
+
selected suite/test set: the run summary reports
|
|
184
|
+
`Random order: seed <N> (replay with --seed <N>)` — exempt from `--quiet`,
|
|
185
|
+
since a shuffled failure cannot be reproduced without its seed — and
|
|
186
|
+
re-running with that
|
|
187
|
+
seed reproduces the order exactly. `--seed N` pins the shuffle and implies
|
|
188
|
+
`--random-order`; without it a fresh 31-bit seed is generated per run. Persist
|
|
189
|
+
the choice with `random_order = true` and `seed = 42` under `[test]`, and undo
|
|
190
|
+
it for one run with the paired `--no-random-order`. Shuffling
|
|
191
|
+
never changes shard membership — `--shard` assigns suites from the
|
|
192
|
+
deterministic plan order, so CI matrix jobs still cover every suite exactly
|
|
193
|
+
once — and `--watch` resolves one seed for the whole session so successive
|
|
194
|
+
re-runs stay comparable. `--verbose` lists the resolved order suite-by-suite,
|
|
195
|
+
and the seed is recorded in `artifacts.json`. A seed reproduces an order for
|
|
196
|
+
the same Python version, not across versions: `random.Random` is what draws
|
|
197
|
+
the order and CPython may change its shuffle algorithm between releases.
|
|
198
|
+
|
|
177
199
|
Repeat runs are faster: the `godot --headless --import` step is cached under
|
|
178
200
|
`.gd-tools/native/import-cache/` and skipped when `project.godot`, any
|
|
179
201
|
project source/resource file (scripts, scenes, import sidecars, addons), or
|
|
@@ -364,6 +386,8 @@ runtime = "native" # native (default); "gut" is no longer a runnable runt
|
|
|
364
386
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
365
387
|
retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
|
|
366
388
|
# parallel = 4 # opt-in parallel suite execution (1-32; unset runs sequentially)
|
|
389
|
+
# random_order = true # opt-in shuffled suite and test-method order
|
|
390
|
+
# seed = 42 # pin the shuffle seed so a shuffled run is replayable
|
|
367
391
|
tags = [] # native suite tag filters
|
|
368
392
|
|
|
369
393
|
[lint]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gd-tools-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "A modern development workflow CLI for GDScript projects in Godot 4.5+"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -110,7 +110,7 @@ target-version = ["py310"]
|
|
|
110
110
|
|
|
111
111
|
[tool.commitizen]
|
|
112
112
|
name = "cz_conventional_commits"
|
|
113
|
-
version = "0.
|
|
113
|
+
version = "0.10.0"
|
|
114
114
|
version_files = ["pyproject.toml:version"]
|
|
115
115
|
changelog_file = "CHANGELOG.md"
|
|
116
116
|
tag_format = "v$version"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Stale addon detection module.
|
|
2
|
+
|
|
3
|
+
Checks whether the deployed coverage addon version matches the installed
|
|
4
|
+
gd-tools-cli package version. Prints warnings to stderr when the addon
|
|
5
|
+
is missing or stale. Fails silently on any error and can be disabled via
|
|
6
|
+
the GD_TOOLS_NO_UPDATE_CHECK environment variable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from packaging.version import parse as parse_version
|
|
13
|
+
|
|
14
|
+
from gd_tools import __version__
|
|
15
|
+
from gd_tools.config import find_project_root
|
|
16
|
+
|
|
17
|
+
ADDON_VERSION_FILENAME = "_version.txt"
|
|
18
|
+
COVERAGE_ADDON_DIR = "gd-tools-coverage"
|
|
19
|
+
NATIVE_ADDON_DIR = "gd-tools-test"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _stale_refusal(label: str, deployed: str) -> str:
|
|
23
|
+
"""Build the refusal message for a stale/unparseable addon.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
label: Human-readable addon label ("Coverage"/"Native test").
|
|
27
|
+
deployed: Deployed version string (raw, may be unparseable).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Refusal message naming both versions, the fix, and the abort.
|
|
31
|
+
"""
|
|
32
|
+
return (
|
|
33
|
+
f"Error: {label} addon is outdated (v{deployed} deployed, "
|
|
34
|
+
f"v{__version__} installed). Run `gd-tools init` to "
|
|
35
|
+
"update. Aborted before running tests."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _missing_refusal(label: str) -> str:
|
|
40
|
+
"""Build the refusal message for a missing version file.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
label: Human-readable addon label ("Coverage"/"Native test").
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Refusal message naming the CLI version, the fix, and abort.
|
|
47
|
+
"""
|
|
48
|
+
return (
|
|
49
|
+
f"Error: {label} addon version file not found "
|
|
50
|
+
f"(deployed version unknown, v{__version__} installed). "
|
|
51
|
+
"Run `gd-tools init` to deploy the addon. "
|
|
52
|
+
"Aborted before running tests."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_coverage_addon_gate() -> str | None:
|
|
57
|
+
"""Check the coverage-run addon gate before suite dispatch.
|
|
58
|
+
|
|
59
|
+
Compares the deployed coverage and native test addon versions
|
|
60
|
+
against the CLI version. A missing, unparseable, or older
|
|
61
|
+
deployed version refuses the run. Newer deployed addons and
|
|
62
|
+
matching versions pass. Suppressed by
|
|
63
|
+
``GD_TOOLS_NO_UPDATE_CHECK=1``. Never raises; unexpected
|
|
64
|
+
errors defer to the caller's existing behavior.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Refusal message when the run must abort, else None.
|
|
68
|
+
"""
|
|
69
|
+
if os.environ.get("GD_TOOLS_NO_UPDATE_CHECK") == "1":
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
project_root = find_project_root()
|
|
73
|
+
except Exception:
|
|
74
|
+
return None
|
|
75
|
+
for addon_dir, label in (
|
|
76
|
+
(COVERAGE_ADDON_DIR, "Coverage"),
|
|
77
|
+
(NATIVE_ADDON_DIR, "Native test"),
|
|
78
|
+
):
|
|
79
|
+
version_file = (
|
|
80
|
+
project_root / "addons" / addon_dir / ADDON_VERSION_FILENAME
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
exists = version_file.exists()
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
if not exists:
|
|
87
|
+
return _missing_refusal(label)
|
|
88
|
+
try:
|
|
89
|
+
addon_version = version_file.read_text(encoding="utf-8").strip()
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
addon_parsed = parse_version(addon_version)
|
|
94
|
+
package_parsed = parse_version(__version__)
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return _stale_refusal(label, addon_version)
|
|
97
|
+
if addon_parsed < package_parsed:
|
|
98
|
+
return _stale_refusal(label, addon_version)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_addon_version() -> None:
|
|
103
|
+
"""Check if the deployed coverage addon version is stale.
|
|
104
|
+
|
|
105
|
+
Compares the version in _version.txt against the installed package
|
|
106
|
+
version. Prints a warning to stderr if the addon is missing or stale.
|
|
107
|
+
Fails silently on any unexpected error.
|
|
108
|
+
|
|
109
|
+
FR2: Stale Addon Check on CLI Invocation.
|
|
110
|
+
FR3: Suppressed by GD_TOOLS_NO_UPDATE_CHECK=1.
|
|
111
|
+
FR4: Uses packaging.version.parse() for comparison.
|
|
112
|
+
"""
|
|
113
|
+
# FR3.1: Environment variable disables check entirely.
|
|
114
|
+
if os.environ.get("GD_TOOLS_NO_UPDATE_CHECK") == "1":
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
project_root = find_project_root()
|
|
119
|
+
version_file = (
|
|
120
|
+
project_root
|
|
121
|
+
/ "addons"
|
|
122
|
+
/ "gd-tools-coverage"
|
|
123
|
+
/ ADDON_VERSION_FILENAME
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not version_file.exists():
|
|
127
|
+
click.echo(
|
|
128
|
+
"WARNING: Coverage addon version file not found. "
|
|
129
|
+
"Run `gd-tools init` to deploy the addon.",
|
|
130
|
+
err=True,
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
addon_version = version_file.read_text(encoding="utf-8").strip()
|
|
135
|
+
stale_msg = (
|
|
136
|
+
f"WARNING: Coverage addon is outdated (v{addon_version} "
|
|
137
|
+
f"deployed, v{__version__} available). "
|
|
138
|
+
f"Run `gd-tools init` to update."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# FR4.1: Compare versions using packaging.version.parse().
|
|
142
|
+
try:
|
|
143
|
+
addon_parsed = parse_version(addon_version)
|
|
144
|
+
package_parsed = parse_version(__version__)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
# FR4.2: Unparseable version = treated as stale.
|
|
147
|
+
click.echo(stale_msg, err=True)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# FR2.4: Stale (addon < package)
|
|
151
|
+
if addon_parsed < package_parsed:
|
|
152
|
+
click.echo(stale_msg, err=True)
|
|
153
|
+
# FR2.5: Match = no warning
|
|
154
|
+
# FR2.6: Newer addon (downgrade) = no warning
|
|
155
|
+
except Exception:
|
|
156
|
+
# FR2.8: Fail silently on any unexpected error.
|
|
157
|
+
pass
|
|
@@ -358,8 +358,14 @@ static func _inject_trackers(
|
|
|
358
358
|
var branch_type = str(entry.get("branch_type", ""))
|
|
359
359
|
var insert_index := target_index
|
|
360
360
|
if branch_type in ["match_case", "if_false", "elif_true"]:
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
var header_end := _find_header_end(source_lines, target_index)
|
|
362
|
+
if header_end >= 0:
|
|
363
|
+
insert_index = header_end + 1
|
|
364
|
+
else:
|
|
365
|
+
insert_index = target_index + 1
|
|
366
|
+
var indent := ""
|
|
367
|
+
if insert_index >= 0 and insert_index < source_lines.size():
|
|
368
|
+
indent = _extract_indent(source_lines[insert_index])
|
|
363
369
|
if indent.is_empty():
|
|
364
370
|
indent = _extract_indent(source_lines[target_index])
|
|
365
371
|
var tracker := "%sGdToolsNativeCoverage.hit(%d, %d)" % [
|
|
@@ -512,3 +518,105 @@ static func _extract_indent(line: String) -> String:
|
|
|
512
518
|
else:
|
|
513
519
|
break
|
|
514
520
|
return indent
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
static func _find_header_end(source_lines: PackedStringArray, start_index: int) -> int:
|
|
524
|
+
## Scan forward from a branch header start line to its terminating colon.
|
|
525
|
+
##
|
|
526
|
+
## Returns the 0-based line index holding the first `:` at bracket-depth 0
|
|
527
|
+
## outside strings and comments, or -1 when it cannot be resolved. The
|
|
528
|
+
## caller fails open on -1 by keeping the historical insertion position
|
|
529
|
+
## rather than corrupting the source.
|
|
530
|
+
var size := source_lines.size()
|
|
531
|
+
if start_index < 0 or start_index >= size:
|
|
532
|
+
return -1
|
|
533
|
+
var depth := 0
|
|
534
|
+
var in_single := ""
|
|
535
|
+
var in_triple := ""
|
|
536
|
+
var escaped := false
|
|
537
|
+
var i := start_index
|
|
538
|
+
while i < size:
|
|
539
|
+
var line := source_lines[i]
|
|
540
|
+
var last_code := ""
|
|
541
|
+
var j := 0
|
|
542
|
+
var length := line.length()
|
|
543
|
+
while j < length:
|
|
544
|
+
if not in_triple.is_empty():
|
|
545
|
+
if escaped:
|
|
546
|
+
escaped = false
|
|
547
|
+
j += 1
|
|
548
|
+
continue
|
|
549
|
+
if line.substr(j, 1) == "\\":
|
|
550
|
+
escaped = true
|
|
551
|
+
j += 1
|
|
552
|
+
continue
|
|
553
|
+
if line.substr(j, 3) == in_triple:
|
|
554
|
+
in_triple = ""
|
|
555
|
+
j += 3
|
|
556
|
+
continue
|
|
557
|
+
j += 1
|
|
558
|
+
continue
|
|
559
|
+
if not in_single.is_empty():
|
|
560
|
+
if escaped:
|
|
561
|
+
escaped = false
|
|
562
|
+
j += 1
|
|
563
|
+
continue
|
|
564
|
+
if line.substr(j, 1) == "\\":
|
|
565
|
+
escaped = true
|
|
566
|
+
j += 1
|
|
567
|
+
continue
|
|
568
|
+
if line.substr(j, 1) == in_single:
|
|
569
|
+
in_single = ""
|
|
570
|
+
j += 1
|
|
571
|
+
continue
|
|
572
|
+
j += 1
|
|
573
|
+
continue
|
|
574
|
+
var ch := line.substr(j, 1)
|
|
575
|
+
if ch == "#":
|
|
576
|
+
break
|
|
577
|
+
if ch == "\"" or ch == "'":
|
|
578
|
+
var triple := ch + ch + ch
|
|
579
|
+
if line.substr(j, 3) == triple:
|
|
580
|
+
in_triple = triple
|
|
581
|
+
escaped = false
|
|
582
|
+
j += 3
|
|
583
|
+
continue
|
|
584
|
+
in_single = ch
|
|
585
|
+
escaped = false
|
|
586
|
+
j += 1
|
|
587
|
+
continue
|
|
588
|
+
if ch == "(" or ch == "[" or ch == "{":
|
|
589
|
+
depth += 1
|
|
590
|
+
last_code = ch
|
|
591
|
+
j += 1
|
|
592
|
+
continue
|
|
593
|
+
if ch == ")" or ch == "]" or ch == "}":
|
|
594
|
+
depth = maxi(0, depth - 1)
|
|
595
|
+
last_code = ch
|
|
596
|
+
j += 1
|
|
597
|
+
continue
|
|
598
|
+
if ch == ":":
|
|
599
|
+
if depth == 0:
|
|
600
|
+
return i
|
|
601
|
+
last_code = ch
|
|
602
|
+
j += 1
|
|
603
|
+
continue
|
|
604
|
+
if ch == " " or ch == "\t" or ch == "\r":
|
|
605
|
+
j += 1
|
|
606
|
+
continue
|
|
607
|
+
if ch == "\\":
|
|
608
|
+
last_code = ch
|
|
609
|
+
j += 1
|
|
610
|
+
continue
|
|
611
|
+
last_code = ch
|
|
612
|
+
j += 1
|
|
613
|
+
if not in_single.is_empty():
|
|
614
|
+
in_single = ""
|
|
615
|
+
escaped = false
|
|
616
|
+
else:
|
|
617
|
+
escaped = false
|
|
618
|
+
if depth > 0 or last_code == "\\":
|
|
619
|
+
i += 1
|
|
620
|
+
continue
|
|
621
|
+
return -1
|
|
622
|
+
return -1
|
|
@@ -8,9 +8,11 @@ this module: the content is staged in a temporary file beside the destination, f
|
|
|
8
8
|
and fsynced, and moved into place with a single ``os.replace``.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
import os
|
|
12
13
|
import tempfile
|
|
13
14
|
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
def atomic_write_bytes(path: Path | str, payload: bytes) -> Path:
|
|
@@ -76,3 +78,36 @@ def atomic_write_text(path: Path | str, text: str) -> Path:
|
|
|
76
78
|
written.
|
|
77
79
|
"""
|
|
78
80
|
return atomic_write_bytes(path, text.encode("utf-8"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def atomic_write_json(
|
|
84
|
+
path: Path | str,
|
|
85
|
+
payload: Any,
|
|
86
|
+
*,
|
|
87
|
+
sort_keys: bool = True,
|
|
88
|
+
indent: int | None = 2,
|
|
89
|
+
) -> Path:
|
|
90
|
+
"""Serialize ``payload`` as JSON and write it atomically as UTF-8.
|
|
91
|
+
|
|
92
|
+
Convenience wrapper around :func:`atomic_write_text` for JSON
|
|
93
|
+
documents. A trailing newline is appended so the file ends cleanly
|
|
94
|
+
for diffing and line-oriented tooling.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path: Destination file path (a :class:`~pathlib.Path` or a
|
|
98
|
+
string).
|
|
99
|
+
payload: JSON-serializable value (dicts, lists, scalars).
|
|
100
|
+
sort_keys: Sort dictionary keys; ``True`` matches the writers
|
|
101
|
+
this helper consolidates.
|
|
102
|
+
indent: JSON indentation depth, or ``None`` for compact output.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The destination path.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TypeError, ValueError: If ``payload`` is not JSON-serializable.
|
|
109
|
+
OSError: If the temporary file or final replacement cannot be
|
|
110
|
+
written.
|
|
111
|
+
"""
|
|
112
|
+
text = json.dumps(payload, indent=indent, sort_keys=sort_keys) + "\n"
|
|
113
|
+
return atomic_write_text(path, text)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""CLI entry point for gd-tools."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from click.shell_completion import (
|
|
9
|
+
BashComplete,
|
|
10
|
+
add_completion_class,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .addon_check import check_addon_version
|
|
15
|
+
from .commands.clean import clean
|
|
16
|
+
from .commands.completion import create_completion_command
|
|
17
|
+
from .commands.config import config
|
|
18
|
+
from .commands.coverage import coverage
|
|
19
|
+
from .commands.doctor import doctor
|
|
20
|
+
from .commands.format import format
|
|
21
|
+
from .commands.init import init
|
|
22
|
+
from .commands.install_hooks import install_hooks
|
|
23
|
+
from .commands.lint import lint
|
|
24
|
+
from .commands.migrate import migrate
|
|
25
|
+
from .commands.test import test
|
|
26
|
+
from .commands.version import version
|
|
27
|
+
from .config import set_explicit_project_root
|
|
28
|
+
from .update_check import check_for_update
|
|
29
|
+
from .verbosity import Verbosity, set_verbosity
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _configure_windows_utf8() -> None:
|
|
33
|
+
"""Reconfigure stdout/stderr to UTF-8 on Windows.
|
|
34
|
+
|
|
35
|
+
Windows defaults to the system codepage (e.g. cp1252) for console
|
|
36
|
+
output, which cannot encode Unicode characters used by Rich (✓, ✗,
|
|
37
|
+
⚠). This reconfigures the standard streams to UTF-8, matching the
|
|
38
|
+
effect of setting PYTHONUTF8=1.
|
|
39
|
+
"""
|
|
40
|
+
if sys.platform != "win32":
|
|
41
|
+
return
|
|
42
|
+
for stream in (sys.stdout, sys.stderr):
|
|
43
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
44
|
+
if reconfigure is not None:
|
|
45
|
+
try:
|
|
46
|
+
reconfigure(encoding="utf-8")
|
|
47
|
+
except (ValueError, OSError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_configure_windows_utf8()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_SOURCE_POWERSHELL = """Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock {
|
|
55
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
56
|
+
|
|
57
|
+
$words = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
|
|
58
|
+
$env:COMP_WORDS = $words -join ' '
|
|
59
|
+
$commandElements = $commandAst.CommandElements
|
|
60
|
+
$env:COMP_CWORD = $commandElements.Count
|
|
61
|
+
for ($i = 0; $i -lt $commandElements.Count; $i++) {
|
|
62
|
+
if ($cursorPosition -le $commandElements[$i].Extent.StartOffset) {
|
|
63
|
+
$env:COMP_CWORD = $i
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$env:%(complete_var)s = "powershell_complete"
|
|
69
|
+
%(prog_name)s | ForEach-Object {
|
|
70
|
+
$values = $_.Split(",")
|
|
71
|
+
if ($values[0] -eq "dir") {
|
|
72
|
+
[System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $values[1])
|
|
73
|
+
} elseif ($values[0] -eq "file") {
|
|
74
|
+
[System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $values[1])
|
|
75
|
+
} else {
|
|
76
|
+
$help = if ($values[2] -eq "_") { "" } else { $values[2] }
|
|
77
|
+
[System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $help)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$env:COMP_WORDS = $null
|
|
82
|
+
$env:COMP_CWORD = 0
|
|
83
|
+
$env:%(complete_var)s = $null
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PowerShellComplete(BashComplete):
|
|
89
|
+
"""Shell completion for PowerShell.
|
|
90
|
+
|
|
91
|
+
Click 8.2.x does not include a PowerShell completion class.
|
|
92
|
+
This subclass reuses BashComplete's completion args and
|
|
93
|
+
format_completion, providing a PowerShell-compatible source
|
|
94
|
+
template using Register-ArgumentCompleter.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
name = "powershell"
|
|
98
|
+
source_template = _SOURCE_POWERSHELL
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
add_completion_class(PowerShellComplete)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GdToolsGroup(click.Group):
|
|
105
|
+
"""Custom Click group that converts NotImplementedError to exit code 2.
|
|
106
|
+
|
|
107
|
+
Stub commands raise ``NotImplementedError`` to indicate they are not
|
|
108
|
+
yet implemented. This class catches that exception and exits with
|
|
109
|
+
code 2 (configuration/usage error), consistent with the gd-tools
|
|
110
|
+
error convention.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def invoke(self, ctx) -> Any:
|
|
114
|
+
"""Invoke the group, catching NotImplementedError as exit code 2.
|
|
115
|
+
|
|
116
|
+
Performs an update check before dispatching to the subcommand.
|
|
117
|
+
If a newer version is available, a notification is printed to
|
|
118
|
+
stderr. The check fails silently and never blocks execution.
|
|
119
|
+
|
|
120
|
+
In quiet mode (``--quiet``/``-q``), both the update check and
|
|
121
|
+
the addon version check are skipped entirely.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
ctx: The Click context for this invocation.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The result of the underlying command invocation.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
SystemExit: With code 2 if a command raises
|
|
131
|
+
NotImplementedError.
|
|
132
|
+
"""
|
|
133
|
+
# Use raw ctx.params here — set_verbosity() runs inside the group
|
|
134
|
+
# callback (invoked by super().invoke below), so get_verbosity()
|
|
135
|
+
# is not yet set at this point.
|
|
136
|
+
quiet = ctx.params.get("quiet", False)
|
|
137
|
+
if not quiet:
|
|
138
|
+
latest = check_for_update()
|
|
139
|
+
if latest is not None:
|
|
140
|
+
click.echo(
|
|
141
|
+
f"A new version of gd-tools is available: {latest} "
|
|
142
|
+
f"(you have {__version__}).\n"
|
|
143
|
+
f"Run `pip install --upgrade gd-tools-cli` to update.",
|
|
144
|
+
err=True,
|
|
145
|
+
)
|
|
146
|
+
check_addon_version()
|
|
147
|
+
try:
|
|
148
|
+
return super().invoke(ctx)
|
|
149
|
+
except NotImplementedError:
|
|
150
|
+
click.echo(
|
|
151
|
+
"Error: This command is not yet implemented.",
|
|
152
|
+
err=True,
|
|
153
|
+
)
|
|
154
|
+
ctx.exit(2)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@click.group(cls=GdToolsGroup)
|
|
158
|
+
@click.version_option(
|
|
159
|
+
version=__version__,
|
|
160
|
+
prog_name="gd-tools",
|
|
161
|
+
message="%(prog)s %(version)s",
|
|
162
|
+
)
|
|
163
|
+
@click.option(
|
|
164
|
+
"--verbose",
|
|
165
|
+
"-v",
|
|
166
|
+
is_flag=True,
|
|
167
|
+
default=False,
|
|
168
|
+
help="Show verbose output including underlying commands and timing.",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--quiet",
|
|
172
|
+
"-q",
|
|
173
|
+
is_flag=True,
|
|
174
|
+
default=False,
|
|
175
|
+
help="Suppress non-essential output (update checks, progress info).",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--project",
|
|
179
|
+
"-p",
|
|
180
|
+
"project_path",
|
|
181
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
182
|
+
default=None,
|
|
183
|
+
help="Path to the Godot project root. Defaults to discovery from "
|
|
184
|
+
"the current working directory.",
|
|
185
|
+
)
|
|
186
|
+
def cli(verbose: bool, quiet: bool, project_path: Path | None):
|
|
187
|
+
"""gd-tools: A modern development workflow CLI for GDScript."""
|
|
188
|
+
set_explicit_project_root(project_path)
|
|
189
|
+
if verbose and quiet:
|
|
190
|
+
click.echo(
|
|
191
|
+
"Error: --verbose and --quiet are mutually exclusive.", err=True
|
|
192
|
+
)
|
|
193
|
+
ctx = click.get_current_context()
|
|
194
|
+
ctx.exit(2)
|
|
195
|
+
if verbose:
|
|
196
|
+
set_verbosity(Verbosity.VERBOSE)
|
|
197
|
+
elif quiet:
|
|
198
|
+
set_verbosity(Verbosity.QUIET)
|
|
199
|
+
else:
|
|
200
|
+
set_verbosity(Verbosity.DEFAULT)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Register the extracted commands, preserving the original help order:
|
|
204
|
+
# init, doctor, version, clean, install-hooks, test, migrate, lint,
|
|
205
|
+
# format, coverage, config, completion.
|
|
206
|
+
cli.add_command(init)
|
|
207
|
+
cli.add_command(doctor)
|
|
208
|
+
cli.add_command(version)
|
|
209
|
+
cli.add_command(clean)
|
|
210
|
+
cli.add_command(install_hooks)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
cli.add_command(test)
|
|
214
|
+
cli.add_command(migrate)
|
|
215
|
+
cli.add_command(lint)
|
|
216
|
+
cli.add_command(format)
|
|
217
|
+
cli.add_command(coverage)
|
|
218
|
+
cli.add_command(config)
|
|
219
|
+
cli.add_command(create_completion_command(cli))
|