gd-tools-cli 0.1.3__tar.gz → 0.2.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.1.3 → gd_tools_cli-0.2.0}/PKG-INFO +5 -4
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/README.md +3 -3
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/pyproject.toml +3 -2
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/gd-tools-coverage/pre_run_hook.gd +8 -2
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/cli.py +23 -7
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/orchestrator.py +2 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/plan_generator.py +59 -0
- gd_tools_cli-0.2.0/src/gd_tools/file_discovery.py +69 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/format_runner.py +11 -4
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/lint_runner.py +12 -4
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/test_runner.py +9 -2
- gd_tools_cli-0.2.0/src/gd_tools/update_check.py +117 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools_cli.egg-info/PKG-INFO +5 -4
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools_cli.egg-info/SOURCES.txt +1 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools_cli.egg-info/requires.txt +1 -0
- gd_tools_cli-0.1.3/src/gd_tools/file_discovery.py +0 -41
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/setup.cfg +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/__init__.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/__main__.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/__init__.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/gd-tools-coverage/post_run_hook.gd +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/config.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/__init__.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/html_reporter.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/reporter.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/templates/file.html +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/templates/index.html +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/doctor.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/errors.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/godot.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/init.py +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
- {gd_tools_cli-0.1.3 → gd_tools_cli-0.2.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.2.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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: tomli; python_version < "3.11"
|
|
|
31
31
|
Requires-Dist: tomli_w
|
|
32
32
|
Requires-Dist: pydantic>=2.0
|
|
33
33
|
Requires-Dist: requests
|
|
34
|
+
Requires-Dist: packaging
|
|
34
35
|
Provides-Extra: dev
|
|
35
36
|
Requires-Dist: pytest; extra == "dev"
|
|
36
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -108,9 +109,9 @@ gd-tools test --coverage --min 80
|
|
|
108
109
|
|---------|-------------|
|
|
109
110
|
| `gd-tools init` | Bootstrap a Godot project -- install GUT, deploy coverage addon, generate configs. |
|
|
110
111
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, GUT, coverage addon, tooling. |
|
|
111
|
-
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. |
|
|
112
|
-
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. |
|
|
113
|
-
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. |
|
|
112
|
+
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. Accepts optional path arguments to override configured test directories. |
|
|
113
|
+
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. Accepts one or more file or directory paths. |
|
|
114
|
+
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
114
115
|
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`. |
|
|
115
116
|
|
|
116
117
|
See the [User Guide](./docs/USER_GUIDE.md) for full command reference,
|
|
@@ -68,9 +68,9 @@ gd-tools test --coverage --min 80
|
|
|
68
68
|
|---------|-------------|
|
|
69
69
|
| `gd-tools init` | Bootstrap a Godot project -- install GUT, deploy coverage addon, generate configs. |
|
|
70
70
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, GUT, coverage addon, tooling. |
|
|
71
|
-
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. |
|
|
72
|
-
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. |
|
|
73
|
-
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. |
|
|
71
|
+
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. Accepts optional path arguments to override configured test directories. |
|
|
72
|
+
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. Accepts one or more file or directory paths. |
|
|
73
|
+
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
74
74
|
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`. |
|
|
75
75
|
|
|
76
76
|
See the [User Guide](./docs/USER_GUIDE.md) for full command reference,
|
|
@@ -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.2.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"}
|
|
@@ -36,6 +36,7 @@ dependencies = [
|
|
|
36
36
|
"tomli_w",
|
|
37
37
|
"pydantic >= 2.0",
|
|
38
38
|
"requests",
|
|
39
|
+
"packaging",
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
[project.urls]
|
|
@@ -98,7 +99,7 @@ target-version = ["py310"]
|
|
|
98
99
|
|
|
99
100
|
[tool.commitizen]
|
|
100
101
|
name = "cz_conventional_commits"
|
|
101
|
-
version = "0.
|
|
102
|
+
version = "0.2.0"
|
|
102
103
|
version_files = ["pyproject.toml:version"]
|
|
103
104
|
changelog_file = "CHANGELOG.md"
|
|
104
105
|
tag_format = "v$version"
|
{gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/gd-tools-coverage/pre_run_hook.gd
RENAMED
|
@@ -59,16 +59,22 @@ func _instrument_file(file_entry: Dictionary) -> bool:
|
|
|
59
59
|
)
|
|
60
60
|
return false
|
|
61
61
|
|
|
62
|
-
var
|
|
63
|
-
var instrumented: String = _inject_trackers(
|
|
62
|
+
var original_source: String = script.source_code
|
|
63
|
+
var instrumented: String = _inject_trackers(original_source, file_id, lines)
|
|
64
64
|
script.source_code = instrumented
|
|
65
65
|
var err: int = script.reload()
|
|
66
|
+
if err == ERR_ALREADY_IN_USE:
|
|
67
|
+
print("[gd-tools] [Warning] Skipping instrumented script with active instances: " + path)
|
|
68
|
+
script.source_code = original_source
|
|
69
|
+
return false
|
|
66
70
|
if err != OK:
|
|
67
71
|
_log_error(
|
|
68
72
|
"Failed to reload instrumented script.",
|
|
69
73
|
"reload() failed for: " + path,
|
|
70
74
|
"Check tracker injection logic for syntax errors."
|
|
71
75
|
)
|
|
76
|
+
script.source_code = original_source
|
|
77
|
+
script.reload()
|
|
72
78
|
return false
|
|
73
79
|
|
|
74
80
|
return true
|
|
@@ -27,6 +27,7 @@ from .format_runner import run_format
|
|
|
27
27
|
from .init import run_init
|
|
28
28
|
from .lint_runner import format_lint_json, format_lint_text, run_lint
|
|
29
29
|
from .test_runner import run_tests
|
|
30
|
+
from .update_check import check_for_update
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _configure_windows_utf8() -> None:
|
|
@@ -63,6 +64,10 @@ class GdToolsGroup(click.Group):
|
|
|
63
64
|
def invoke(self, ctx) -> Any:
|
|
64
65
|
"""Invoke the group, catching NotImplementedError as exit code 2.
|
|
65
66
|
|
|
67
|
+
Performs an update check before dispatching to the subcommand.
|
|
68
|
+
If a newer version is available, a notification is printed to
|
|
69
|
+
stderr. The check fails silently and never blocks execution.
|
|
70
|
+
|
|
66
71
|
Args:
|
|
67
72
|
ctx: The Click context for this invocation.
|
|
68
73
|
|
|
@@ -73,6 +78,14 @@ class GdToolsGroup(click.Group):
|
|
|
73
78
|
SystemExit: With code 2 if a command raises
|
|
74
79
|
NotImplementedError.
|
|
75
80
|
"""
|
|
81
|
+
latest = check_for_update()
|
|
82
|
+
if latest is not None:
|
|
83
|
+
click.echo(
|
|
84
|
+
f"A new version of gd-tools is available: {latest} "
|
|
85
|
+
f"(you have {__version__}).\n"
|
|
86
|
+
f"Run `pip install --upgrade gd-tools-cli` to update.",
|
|
87
|
+
err=True,
|
|
88
|
+
)
|
|
76
89
|
try:
|
|
77
90
|
return super().invoke(ctx)
|
|
78
91
|
except NotImplementedError:
|
|
@@ -123,6 +136,7 @@ def doctor():
|
|
|
123
136
|
|
|
124
137
|
|
|
125
138
|
@cli.command()
|
|
139
|
+
@click.argument("paths", nargs=-1)
|
|
126
140
|
@click.option("--coverage", is_flag=True, help="Generate coverage report.")
|
|
127
141
|
@click.option("--min", type=int, help="Minimum coverage threshold.")
|
|
128
142
|
@click.option("--suite", help="Specify which test suite to run.")
|
|
@@ -138,7 +152,7 @@ def doctor():
|
|
|
138
152
|
type=int,
|
|
139
153
|
help="Timeout in seconds for the test run.",
|
|
140
154
|
)
|
|
141
|
-
def test(coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
155
|
+
def test(paths, coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
142
156
|
"""Run GDScript tests using GUT."""
|
|
143
157
|
try:
|
|
144
158
|
config = load_config()
|
|
@@ -164,6 +178,7 @@ def test(coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
|
164
178
|
no_exit_code=no_exit_code,
|
|
165
179
|
min_percent=min,
|
|
166
180
|
timeout=timeout,
|
|
181
|
+
paths=list(paths) if paths else None,
|
|
167
182
|
)
|
|
168
183
|
else:
|
|
169
184
|
run_tests(
|
|
@@ -175,6 +190,7 @@ def test(coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
|
175
190
|
junit_xml=junit_xml,
|
|
176
191
|
no_exit_code=no_exit_code,
|
|
177
192
|
timeout=timeout,
|
|
193
|
+
paths=list(paths) if paths else None,
|
|
178
194
|
)
|
|
179
195
|
except TestFailureError as e:
|
|
180
196
|
click.echo(f"Error: {e}", err=True)
|
|
@@ -190,7 +206,7 @@ def test(coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
|
190
206
|
|
|
191
207
|
|
|
192
208
|
@cli.command()
|
|
193
|
-
@click.argument("
|
|
209
|
+
@click.argument("paths", nargs=-1)
|
|
194
210
|
@click.option(
|
|
195
211
|
"--report-format",
|
|
196
212
|
type=click.Choice(["text", "json"]),
|
|
@@ -202,7 +218,7 @@ def test(coverage, min, suite, test, junit_xml, no_exit_code, timeout):
|
|
|
202
218
|
is_flag=True,
|
|
203
219
|
help="Attempt to fix lint issues (no-op for gdlint).",
|
|
204
220
|
)
|
|
205
|
-
def lint(
|
|
221
|
+
def lint(paths, report_format, fix):
|
|
206
222
|
"""Lint GDScript files."""
|
|
207
223
|
if fix:
|
|
208
224
|
click.echo(
|
|
@@ -216,7 +232,7 @@ def lint(path, report_format, fix):
|
|
|
216
232
|
ctx = click.get_current_context()
|
|
217
233
|
ctx.exit(2)
|
|
218
234
|
|
|
219
|
-
result = run_lint(config,
|
|
235
|
+
result = run_lint(config, list(paths), report_format)
|
|
220
236
|
|
|
221
237
|
if report_format == "json":
|
|
222
238
|
output = format_lint_json(result)
|
|
@@ -232,10 +248,10 @@ def lint(path, report_format, fix):
|
|
|
232
248
|
|
|
233
249
|
|
|
234
250
|
@cli.command()
|
|
235
|
-
@click.argument("
|
|
251
|
+
@click.argument("paths", nargs=-1)
|
|
236
252
|
@click.option("--check", is_flag=True, help="Check only, don't modify files.")
|
|
237
253
|
@click.option("--diff", is_flag=True, help="Show diff of changes.")
|
|
238
|
-
def format(
|
|
254
|
+
def format(paths, check, diff):
|
|
239
255
|
"""Format GDScript files."""
|
|
240
256
|
if check and diff:
|
|
241
257
|
click.echo("Error: --check and --diff are mutually exclusive", err=True)
|
|
@@ -249,7 +265,7 @@ def format(path, check, diff):
|
|
|
249
265
|
ctx = click.get_current_context()
|
|
250
266
|
ctx.exit(2)
|
|
251
267
|
|
|
252
|
-
result = run_format(config,
|
|
268
|
+
result = run_format(config, list(paths), check=check, diff=diff)
|
|
253
269
|
|
|
254
270
|
ctx = click.get_current_context()
|
|
255
271
|
if result.files_checked == 0:
|
|
@@ -36,6 +36,7 @@ def run_coverage_test(
|
|
|
36
36
|
no_exit_code: bool = False,
|
|
37
37
|
min_percent: int | None = None,
|
|
38
38
|
timeout: int | None = None,
|
|
39
|
+
paths: list[str] | None = None,
|
|
39
40
|
) -> TestResult:
|
|
40
41
|
"""Run tests with coverage instrumentation and generate reports.
|
|
41
42
|
|
|
@@ -94,6 +95,7 @@ def run_coverage_test(
|
|
|
94
95
|
junit_xml=junit_xml,
|
|
95
96
|
no_exit_code=no_exit_code,
|
|
96
97
|
timeout=timeout,
|
|
98
|
+
paths=paths,
|
|
97
99
|
)
|
|
98
100
|
except TestFailureError as exc:
|
|
99
101
|
test_error = exc
|
|
@@ -306,6 +306,53 @@ class CoverageVisitor(Visitor):
|
|
|
306
306
|
# --- Plan Generation (FR-4, FR-6) ---
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
def resolve_autoload_paths(project_root: str) -> list[str]:
|
|
310
|
+
"""Read ``project.godot`` and resolve autoload script paths.
|
|
311
|
+
|
|
312
|
+
Parses the ``[autoload]`` section of ``project.godot`` and returns
|
|
313
|
+
a list of script paths relative to ``project_root`` (using forward
|
|
314
|
+
slashes, matching Godot's ``res://`` convention).
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
project_root: Root directory of the Godot project.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of relative paths for autoload scripts. Returns an empty
|
|
321
|
+
list if ``project.godot`` doesn't exist or has no ``[autoload]``
|
|
322
|
+
section.
|
|
323
|
+
"""
|
|
324
|
+
project_file = Path(project_root) / "project.godot"
|
|
325
|
+
if not project_file.exists():
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
lines = project_file.read_text(encoding="utf-8").splitlines()
|
|
329
|
+
|
|
330
|
+
in_autoload = False
|
|
331
|
+
paths: list[str] = []
|
|
332
|
+
for line in lines:
|
|
333
|
+
stripped = line.strip()
|
|
334
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
335
|
+
in_autoload = stripped == "[autoload]"
|
|
336
|
+
continue
|
|
337
|
+
if not in_autoload:
|
|
338
|
+
continue
|
|
339
|
+
if not stripped or stripped.startswith(";") or stripped.startswith("#"):
|
|
340
|
+
continue
|
|
341
|
+
if "=" not in stripped:
|
|
342
|
+
continue
|
|
343
|
+
# Parse key="value"
|
|
344
|
+
_, _, value = stripped.partition("=")
|
|
345
|
+
value = value.strip().strip('"').strip("'")
|
|
346
|
+
if value.startswith("*"):
|
|
347
|
+
value = value[1:]
|
|
348
|
+
if value.startswith("res://"):
|
|
349
|
+
value = value[len("res://") :]
|
|
350
|
+
if value:
|
|
351
|
+
paths.append(value.replace("\\", "/"))
|
|
352
|
+
|
|
353
|
+
return paths
|
|
354
|
+
|
|
355
|
+
|
|
309
356
|
def generate_plan(
|
|
310
357
|
project_root: str,
|
|
311
358
|
exclude_dirs: list[str] | None = None,
|
|
@@ -345,6 +392,18 @@ def generate_plan(
|
|
|
345
392
|
if not any(td in PurePath(f).parts for td in test_dirs)
|
|
346
393
|
]
|
|
347
394
|
|
|
395
|
+
# Filter out autoload scripts — they have active instances and
|
|
396
|
+
# cannot be safely instrumented (FR-2)
|
|
397
|
+
autoload_paths = resolve_autoload_paths(project_root)
|
|
398
|
+
if autoload_paths:
|
|
399
|
+
autoload_set = {ap.replace("\\", "/") for ap in autoload_paths}
|
|
400
|
+
gd_files = [
|
|
401
|
+
f
|
|
402
|
+
for f in gd_files
|
|
403
|
+
if str(Path(f).relative_to(project_root)).replace("\\", "/")
|
|
404
|
+
not in autoload_set
|
|
405
|
+
]
|
|
406
|
+
|
|
348
407
|
file_plans: list[FilePlan] = []
|
|
349
408
|
console = Console()
|
|
350
409
|
for file_id, gd_file in enumerate(gd_files):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared file discovery module for gd-tools.
|
|
2
|
+
|
|
3
|
+
Discovers ``.gd`` files under a given path, applying exclude
|
|
4
|
+
patterns from the configuration. Used by both the lint runner
|
|
5
|
+
and the format runner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from gd_tools.config import DEFAULT_EXCLUDES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def discover_gd_files(
|
|
14
|
+
path: str, excludes: list[str] | None = None
|
|
15
|
+
) -> list[str]:
|
|
16
|
+
"""Discover ``.gd`` files in ``path``, skipping excluded directories.
|
|
17
|
+
|
|
18
|
+
Exclude entries are interpreted with hybrid matching:
|
|
19
|
+
|
|
20
|
+
- **Bare names** (no path separator, e.g. ``addons``, ``.godot``)
|
|
21
|
+
match any directory *basename* at any depth (backward compatible
|
|
22
|
+
with :data:`DEFAULT_EXCLUDES`).
|
|
23
|
+
- **Path prefixes** (containing ``/`` or ``\\``, e.g.
|
|
24
|
+
``scripts/autoload``) match as a path prefix relative to the
|
|
25
|
+
search root — any file whose relative path starts with the
|
|
26
|
+
prefix is excluded.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
path: Root directory to search.
|
|
30
|
+
excludes: Exclude entries (bare names or path prefixes).
|
|
31
|
+
Defaults to :data:`DEFAULT_EXCLUDES` from ``config.py``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of file paths (as strings) to ``.gd`` files.
|
|
35
|
+
"""
|
|
36
|
+
if excludes is None:
|
|
37
|
+
excludes = DEFAULT_EXCLUDES
|
|
38
|
+
|
|
39
|
+
bare_excludes: set[str] = set()
|
|
40
|
+
path_excludes: list[str] = []
|
|
41
|
+
for entry in excludes:
|
|
42
|
+
if "/" in entry or "\\" in entry:
|
|
43
|
+
# Normalize separators to the OS-native form
|
|
44
|
+
path_excludes.append(
|
|
45
|
+
entry.replace("\\", os.sep).replace("/", os.sep)
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
bare_excludes.add(entry)
|
|
49
|
+
|
|
50
|
+
gd_files: list[str] = []
|
|
51
|
+
for root, dirs, files in os.walk(path):
|
|
52
|
+
# Prune basename-excluded dirs in-place so os.walk doesn't descend
|
|
53
|
+
dirs[:] = [d for d in dirs if d not in bare_excludes]
|
|
54
|
+
for file in files:
|
|
55
|
+
if file.lower().endswith(".gd"):
|
|
56
|
+
file_path = os.path.join(root, file)
|
|
57
|
+
if path_excludes:
|
|
58
|
+
rel_path = os.path.relpath(file_path, path)
|
|
59
|
+
normalized = rel_path.replace("\\", os.sep).replace(
|
|
60
|
+
"/", os.sep
|
|
61
|
+
)
|
|
62
|
+
if any(
|
|
63
|
+
normalized == prefix
|
|
64
|
+
or normalized.startswith(prefix + os.sep)
|
|
65
|
+
for prefix in path_excludes
|
|
66
|
+
):
|
|
67
|
+
continue
|
|
68
|
+
gd_files.append(file_path)
|
|
69
|
+
return gd_files
|
|
@@ -46,15 +46,16 @@ class FormatResult:
|
|
|
46
46
|
|
|
47
47
|
def run_format(
|
|
48
48
|
config: GdToolsConfig,
|
|
49
|
-
|
|
49
|
+
paths: list[str] | None = None,
|
|
50
50
|
check: bool = False,
|
|
51
51
|
diff: bool = False,
|
|
52
52
|
) -> FormatResult:
|
|
53
|
-
"""Format ``.gd`` files in ``
|
|
53
|
+
"""Format ``.gd`` files in ``paths`` using the gdtoolkit formatter.
|
|
54
54
|
|
|
55
55
|
Args:
|
|
56
56
|
config: Project configuration with format excludes.
|
|
57
|
-
|
|
57
|
+
paths: Root directories to search for .gd files. Defaults to
|
|
58
|
+
``["."]``.
|
|
58
59
|
check: If True, report files needing format without modifying.
|
|
59
60
|
diff: If True, show unified diffs without modifying.
|
|
60
61
|
|
|
@@ -64,6 +65,9 @@ def run_format(
|
|
|
64
65
|
Raises:
|
|
65
66
|
FormatError: If both check and diff are True (mutually exclusive).
|
|
66
67
|
"""
|
|
68
|
+
if not paths:
|
|
69
|
+
paths = ["."]
|
|
70
|
+
|
|
67
71
|
if check and diff:
|
|
68
72
|
raise FormatError(
|
|
69
73
|
"--check and --diff are mutually exclusive",
|
|
@@ -71,7 +75,10 @@ def run_format(
|
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
excludes = config.format.exclude
|
|
74
|
-
gd_files =
|
|
78
|
+
gd_files: list[str] = []
|
|
79
|
+
for p in paths:
|
|
80
|
+
gd_files.extend(discover_gd_files(p, excludes))
|
|
81
|
+
gd_files = list(dict.fromkeys(gd_files))
|
|
75
82
|
|
|
76
83
|
if not gd_files:
|
|
77
84
|
return FormatResult()
|
|
@@ -57,9 +57,11 @@ class LintResult:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def run_lint(
|
|
60
|
-
config: GdToolsConfig,
|
|
60
|
+
config: GdToolsConfig,
|
|
61
|
+
paths: list[str] | None = None,
|
|
62
|
+
report_format: str = "text",
|
|
61
63
|
) -> LintResult:
|
|
62
|
-
"""Run gdlint on ``
|
|
64
|
+
"""Run gdlint on ``paths``, respecting config excludes.
|
|
63
65
|
|
|
64
66
|
Discovers ``.gd`` files via :func:`discover_gd_files`, reads each
|
|
65
67
|
file, and invokes ``gdtoolkit.linter.lint_code`` to check for
|
|
@@ -68,7 +70,7 @@ def run_lint(
|
|
|
68
70
|
|
|
69
71
|
Args:
|
|
70
72
|
config: Project configuration with lint excludes.
|
|
71
|
-
|
|
73
|
+
paths: Root directories to lint. Defaults to ``["."]``.
|
|
72
74
|
report_format: Output format hint (unused here; formatting
|
|
73
75
|
is handled by :func:`format_lint_text` /
|
|
74
76
|
:func:`format_lint_json`).
|
|
@@ -76,8 +78,14 @@ def run_lint(
|
|
|
76
78
|
Returns:
|
|
77
79
|
:class:`LintResult` with file count and issue lists.
|
|
78
80
|
"""
|
|
81
|
+
if not paths:
|
|
82
|
+
paths = ["."]
|
|
83
|
+
|
|
79
84
|
excludes = config.lint.exclude
|
|
80
|
-
gd_files =
|
|
85
|
+
gd_files: list[str] = []
|
|
86
|
+
for p in paths:
|
|
87
|
+
gd_files.extend(discover_gd_files(p, excludes))
|
|
88
|
+
gd_files = list(dict.fromkeys(gd_files))
|
|
81
89
|
|
|
82
90
|
errors: list[LintIssue] = []
|
|
83
91
|
warnings: list[LintIssue] = []
|
|
@@ -84,6 +84,7 @@ def build_gut_args(
|
|
|
84
84
|
test_name: str | None = None,
|
|
85
85
|
junit_xml: str | None = None,
|
|
86
86
|
coverage: bool = False,
|
|
87
|
+
paths: list[str] | None = None,
|
|
87
88
|
) -> list[str]:
|
|
88
89
|
"""Build GUT CLI arguments from test configuration.
|
|
89
90
|
|
|
@@ -101,6 +102,8 @@ def build_gut_args(
|
|
|
101
102
|
``<project_root>/.gd-tools/results.xml``.
|
|
102
103
|
coverage: Whether coverage hooks should be included. When True,
|
|
103
104
|
adds ``-gpre_run_script`` and ``-gpost_run_script`` args.
|
|
105
|
+
paths: Optional test directories that override
|
|
106
|
+
``config.test_dirs`` for this invocation.
|
|
104
107
|
|
|
105
108
|
Returns:
|
|
106
109
|
List of CLI arguments for Godot/GUT.
|
|
@@ -113,8 +116,10 @@ def build_gut_args(
|
|
|
113
116
|
]
|
|
114
117
|
|
|
115
118
|
# Test directories (comma-separated per GUT 9.x CLI spec).
|
|
116
|
-
if config.
|
|
117
|
-
|
|
119
|
+
# Use paths override if provided, otherwise fall back to config.
|
|
120
|
+
test_dirs = paths if paths else config.test_dirs
|
|
121
|
+
if test_dirs:
|
|
122
|
+
dirs = ",".join(f"res://{d}/" for d in test_dirs)
|
|
118
123
|
args.append(f"-gdir={dirs}")
|
|
119
124
|
|
|
120
125
|
# Prefix/suffix for test file discovery.
|
|
@@ -320,6 +325,7 @@ def run_tests(
|
|
|
320
325
|
junit_xml: str | None = None,
|
|
321
326
|
no_exit_code: bool = False,
|
|
322
327
|
timeout: int | None = 300,
|
|
328
|
+
paths: list[str] | None = None,
|
|
323
329
|
) -> TestResult:
|
|
324
330
|
"""Run GUT tests via the Godot CLI.
|
|
325
331
|
|
|
@@ -393,6 +399,7 @@ def run_tests(
|
|
|
393
399
|
test_name=test_name,
|
|
394
400
|
junit_xml=junit_xml,
|
|
395
401
|
coverage=coverage,
|
|
402
|
+
paths=paths,
|
|
396
403
|
)
|
|
397
404
|
|
|
398
405
|
# Set up environment variables for coverage.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""PyPI update notification module.
|
|
2
|
+
|
|
3
|
+
Checks PyPI for a newer version of ``gd-tools-cli`` and caches the
|
|
4
|
+
result locally (24h TTL) to avoid network calls on every invocation.
|
|
5
|
+
Fails silently on any error and can be disabled via the
|
|
6
|
+
``GD_TOOLS_NO_UPDATE_CHECK`` environment variable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from packaging.version import parse as parse_version
|
|
17
|
+
|
|
18
|
+
from gd_tools import __version__
|
|
19
|
+
|
|
20
|
+
PYPI_URL = "https://pypi.org/pypi/gd-tools-cli/json"
|
|
21
|
+
REQUEST_TIMEOUT = 3
|
|
22
|
+
CACHE_TTL_HOURS = 24
|
|
23
|
+
CACHE_DIR = Path.home() / ".gd-tools"
|
|
24
|
+
CACHE_FILENAME = "update-check.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_for_update() -> Optional[str]:
|
|
28
|
+
"""Check PyPI for a newer version of gd-tools-cli.
|
|
29
|
+
|
|
30
|
+
Returns the latest version string if an update is available,
|
|
31
|
+
or ``None`` otherwise. The check is cached for 24 hours and
|
|
32
|
+
fails silently on any error.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The latest version string from PyPI if an update is
|
|
36
|
+
available, or ``None``.
|
|
37
|
+
"""
|
|
38
|
+
# FR4.1: Environment variable disables check entirely.
|
|
39
|
+
if os.environ.get("GD_TOOLS_NO_UPDATE_CHECK") == "1":
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# FR1.5: Skip dev install (editable install without metadata).
|
|
43
|
+
if __version__ == "0.0.0":
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
# Check cache first; fall back to a fresh PyPI request.
|
|
47
|
+
latest_version = _read_cached_version()
|
|
48
|
+
if latest_version is None:
|
|
49
|
+
latest_version = _fetch_latest_version()
|
|
50
|
+
if latest_version is not None:
|
|
51
|
+
_write_cache(latest_version)
|
|
52
|
+
|
|
53
|
+
if latest_version is None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# FR1.4: Compare versions using packaging.version.parse().
|
|
57
|
+
try:
|
|
58
|
+
is_newer = parse_version(latest_version) > parse_version(__version__)
|
|
59
|
+
except (TypeError, ValueError):
|
|
60
|
+
return None
|
|
61
|
+
if is_newer:
|
|
62
|
+
return latest_version
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_cached_version() -> Optional[str]:
|
|
67
|
+
"""Read the cached latest version if the cache is fresh (< 24h).
|
|
68
|
+
|
|
69
|
+
Returns ``None`` on cache miss, stale cache, or corrupt cache
|
|
70
|
+
file (FR3.3 — corrupt cache is treated as a cache miss).
|
|
71
|
+
"""
|
|
72
|
+
cache_file = CACHE_DIR / CACHE_FILENAME
|
|
73
|
+
if not cache_file.exists():
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
data = json.loads(cache_file.read_text(encoding="utf-8"))
|
|
77
|
+
last_check = datetime.fromisoformat(data["last_check"])
|
|
78
|
+
if last_check.tzinfo is None:
|
|
79
|
+
last_check = last_check.replace(tzinfo=timezone.utc)
|
|
80
|
+
age = datetime.now(timezone.utc) - last_check
|
|
81
|
+
if age < timedelta(hours=CACHE_TTL_HOURS):
|
|
82
|
+
return data["latest_version"]
|
|
83
|
+
return None
|
|
84
|
+
except (ValueError, KeyError, OSError, TypeError):
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fetch_latest_version() -> Optional[str]:
|
|
89
|
+
"""Fetch the latest version from the PyPI JSON API.
|
|
90
|
+
|
|
91
|
+
Returns ``None`` on any network or parsing error (FR3.1, FR3.2).
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
response = requests.get(PYPI_URL, timeout=REQUEST_TIMEOUT)
|
|
95
|
+
data = response.json()
|
|
96
|
+
return data["info"]["version"]
|
|
97
|
+
except (requests.RequestException, ValueError, KeyError):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _write_cache(version: str) -> None:
|
|
102
|
+
"""Write the cache file with the current timestamp and version.
|
|
103
|
+
|
|
104
|
+
Creates the cache directory if it does not exist (FR2.5).
|
|
105
|
+
Silently ignores write errors — the main functionality is
|
|
106
|
+
unaffected if the cache cannot be persisted.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
cache_file = CACHE_DIR / CACHE_FILENAME
|
|
111
|
+
entry = {
|
|
112
|
+
"last_check": datetime.now(timezone.utc).isoformat(),
|
|
113
|
+
"latest_version": version,
|
|
114
|
+
}
|
|
115
|
+
cache_file.write_text(json.dumps(entry), encoding="utf-8")
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gd-tools-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: tomli; python_version < "3.11"
|
|
|
31
31
|
Requires-Dist: tomli_w
|
|
32
32
|
Requires-Dist: pydantic>=2.0
|
|
33
33
|
Requires-Dist: requests
|
|
34
|
+
Requires-Dist: packaging
|
|
34
35
|
Provides-Extra: dev
|
|
35
36
|
Requires-Dist: pytest; extra == "dev"
|
|
36
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -108,9 +109,9 @@ gd-tools test --coverage --min 80
|
|
|
108
109
|
|---------|-------------|
|
|
109
110
|
| `gd-tools init` | Bootstrap a Godot project -- install GUT, deploy coverage addon, generate configs. |
|
|
110
111
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, GUT, coverage addon, tooling. |
|
|
111
|
-
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. |
|
|
112
|
-
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. |
|
|
113
|
-
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. |
|
|
112
|
+
| `gd-tools test` | Run GUT tests with optional coverage, thresholds, JUnit XML output. Accepts optional path arguments to override configured test directories. |
|
|
113
|
+
| `gd-tools lint` | Lint GDScript files using gdlint with text or JSON output. Accepts one or more file or directory paths. |
|
|
114
|
+
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
114
115
|
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`. |
|
|
115
116
|
|
|
116
117
|
See the [User Guide](./docs/USER_GUIDE.md) for full command reference,
|
|
@@ -12,6 +12,7 @@ src/gd_tools/godot.py
|
|
|
12
12
|
src/gd_tools/init.py
|
|
13
13
|
src/gd_tools/lint_runner.py
|
|
14
14
|
src/gd_tools/test_runner.py
|
|
15
|
+
src/gd_tools/update_check.py
|
|
15
16
|
src/gd_tools/addons/__init__.py
|
|
16
17
|
src/gd_tools/addons/gd-tools-coverage/coverage.gd
|
|
17
18
|
src/gd_tools/addons/gd-tools-coverage/post_run_hook.gd
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""Shared file discovery module for gd-tools.
|
|
2
|
-
|
|
3
|
-
Discovers ``.gd`` files under a given path, applying exclude
|
|
4
|
-
patterns from the configuration. Used by both the lint runner
|
|
5
|
-
and the format runner.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
|
|
10
|
-
from gd_tools.config import DEFAULT_EXCLUDES
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def discover_gd_files(
|
|
14
|
-
path: str, excludes: list[str] | None = None
|
|
15
|
-
) -> list[str]:
|
|
16
|
-
"""Discover ``.gd`` files in ``path``, skipping excluded directories.
|
|
17
|
-
|
|
18
|
-
Recursively walks ``path`` and collects all files with a ``.gd``
|
|
19
|
-
extension (case-insensitive). Directories whose names appear in
|
|
20
|
-
``excludes`` are pruned from the walk so their contents are never
|
|
21
|
-
visited.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
path: Root directory to search.
|
|
25
|
-
excludes: Directory names to skip. Defaults to
|
|
26
|
-
:data:`DEFAULT_EXCLUDES` from ``config.py``.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
List of file paths (as strings) to ``.gd`` files.
|
|
30
|
-
"""
|
|
31
|
-
if excludes is None:
|
|
32
|
-
excludes = DEFAULT_EXCLUDES
|
|
33
|
-
|
|
34
|
-
gd_files: list[str] = []
|
|
35
|
-
for root, dirs, files in os.walk(path):
|
|
36
|
-
# Prune excluded dirs in-place so os.walk doesn't descend into them
|
|
37
|
-
dirs[:] = [d for d in dirs if d not in excludes]
|
|
38
|
-
for file in files:
|
|
39
|
-
if file.lower().endswith(".gd"):
|
|
40
|
-
gd_files.append(os.path.join(root, file))
|
|
41
|
-
return gd_files
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gd_tools_cli-0.1.3 → gd_tools_cli-0.2.0}/src/gd_tools/addons/gd-tools-coverage/post_run_hook.gd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|