DEVX-97: fix: wrap all user-facing strings with _() for i18n completeness
This commit is contained in:
@@ -18,6 +18,7 @@ import subprocess # nosec B404
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from devx.ci._shared import write_github_output
|
from devx.ci._shared import write_github_output
|
||||||
|
from devx.i18n import _
|
||||||
|
|
||||||
RELEASE_RE = re.compile(r"^release: v\d+\.\d+\.\d+")
|
RELEASE_RE = re.compile(r"^release: v\d+\.\d+\.\d+")
|
||||||
|
|
||||||
@@ -44,13 +45,13 @@ def is_release_commit(message: str) -> bool:
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Detect if the latest commit is a release commit and set GITHUB_OUTPUT."""
|
"""Detect if the latest commit is a release commit and set GITHUB_OUTPUT."""
|
||||||
msg = get_commit_message()
|
msg = get_commit_message()
|
||||||
click.echo(f"Commit message: {msg}")
|
click.echo(_("Commit message: {msg}", msg=msg))
|
||||||
is_release = is_release_commit(msg)
|
is_release = is_release_commit(msg)
|
||||||
write_github_output("is-release", "true" if is_release else "false")
|
write_github_output("is-release", "true" if is_release else "false")
|
||||||
if is_release:
|
if is_release:
|
||||||
click.echo("Release commit — skipping all post-merge jobs.")
|
click.echo(_("Release commit — skipping all post-merge jobs."))
|
||||||
else:
|
else:
|
||||||
click.echo("Regular merge commit — running all post-merge jobs.")
|
click.echo(_("Regular merge commit — running all post-merge jobs."))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import click
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
|
||||||
|
from devx.i18n import _
|
||||||
|
|
||||||
DEFAULT_MAX_RUNNERS = 3
|
DEFAULT_MAX_RUNNERS = 3
|
||||||
|
|
||||||
@@ -55,9 +56,9 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
total += data.get("total_count", 0)
|
total += data.get("total_count", 0)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Warning: repo-level runners query returned HTTP {r.status_code}", err=True)
|
click.echo(_("Warning: repo-level runners query returned HTTP {status}", status=r.status_code), err=True)
|
||||||
except (requests.RequestException, ValueError) as e:
|
except (requests.RequestException, ValueError) as e:
|
||||||
click.echo(f"Warning: repo-level runners query failed: {e}", err=True)
|
click.echo(_("Warning: repo-level runners query failed: {error}", error=e), err=True)
|
||||||
|
|
||||||
# 2. Organization-level runners
|
# 2. Organization-level runners
|
||||||
try:
|
try:
|
||||||
@@ -70,9 +71,9 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
total += data.get("total_count", 0)
|
total += data.get("total_count", 0)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Warning: org-level runners query returned HTTP {r.status_code}", err=True)
|
click.echo(_("Warning: org-level runners query returned HTTP {status}", status=r.status_code), err=True)
|
||||||
except (requests.RequestException, ValueError) as e:
|
except (requests.RequestException, ValueError) as e:
|
||||||
click.echo(f"Warning: org-level runners query failed: {e}", err=True)
|
click.echo(_("Warning: org-level runners query failed: {error}", error=e), err=True)
|
||||||
|
|
||||||
# 3. Instance-level runners (requires admin scope)
|
# 3. Instance-level runners (requires admin scope)
|
||||||
try:
|
try:
|
||||||
@@ -85,9 +86,12 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
|
|||||||
data = r.json()
|
data = r.json()
|
||||||
total += data.get("total_count", 0)
|
total += data.get("total_count", 0)
|
||||||
elif r.status_code != 403: # 403 is expected without admin scope
|
elif r.status_code != 403: # 403 is expected without admin scope
|
||||||
click.echo(f"Warning: instance-level runners query returned HTTP {r.status_code}", err=True)
|
click.echo(
|
||||||
|
_("Warning: instance-level runners query returned HTTP {status}", status=r.status_code),
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
except (requests.RequestException, ValueError) as e:
|
except (requests.RequestException, ValueError) as e:
|
||||||
click.echo(f"Warning: instance-level runners query failed: {e}", err=True)
|
click.echo(_("Warning: instance-level runners query failed: {error}", error=e), err=True)
|
||||||
|
|
||||||
return total
|
return total
|
||||||
|
|
||||||
@@ -165,8 +169,8 @@ def main(
|
|||||||
with open(gh_output, "a", encoding="utf-8") as f: # noqa: PTH123
|
with open(gh_output, "a", encoding="utf-8") as f: # noqa: PTH123
|
||||||
f.write(f"runner-count={count}\n")
|
f.write(f"runner-count={count}\n")
|
||||||
f.write(f"runner-indices={json.dumps(indices)}\n")
|
f.write(f"runner-indices={json.dumps(indices)}\n")
|
||||||
click.echo(f"Runner count: {count}")
|
click.echo(_("Runner count: {count}", count=count))
|
||||||
click.echo(f"Runner indices: {indices}")
|
click.echo(_("Runner indices: {indices}", indices=indices))
|
||||||
return
|
return
|
||||||
|
|
||||||
if output_count:
|
if output_count:
|
||||||
@@ -178,8 +182,8 @@ def main(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Default: output both as key=value pairs for CI consumption
|
# Default: output both as key=value pairs for CI consumption
|
||||||
click.echo(f"count={count}")
|
click.echo(_("count={count}", count=count))
|
||||||
click.echo(f"indices={json.dumps(indices)}")
|
click.echo(_("indices={indices}", indices=json.dumps(indices)))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -102,17 +102,25 @@ def main(pattern: str, runner_index: int | None, max_runners: int, github_env: b
|
|||||||
groups = distribute(files, max_runners)
|
groups = distribute(files, max_runners)
|
||||||
for i, group in enumerate(groups):
|
for i, group in enumerate(groups):
|
||||||
labels = " ".join(group) if group else "(none)"
|
labels = " ".join(group) if group else "(none)"
|
||||||
click.echo(f"Runner {i}: {labels}")
|
click.echo(_("Runner {i}: {labels}", i=i, labels=labels))
|
||||||
return
|
return
|
||||||
|
|
||||||
if skip_if_excess and github_env and runner_index > max_runners:
|
if skip_if_excess and github_env and runner_index > max_runners:
|
||||||
click.echo(f"Skipping — runner index {runner_index} > max runners {max_runners}")
|
click.echo(
|
||||||
|
_(
|
||||||
|
"Skipping — runner index {runner_index} > max runners {max_runners}",
|
||||||
|
runner_index=runner_index,
|
||||||
|
max_runners=max_runners,
|
||||||
|
)
|
||||||
|
)
|
||||||
write_github_env("ASSIGNED_FILES", "")
|
write_github_env("ASSIGNED_FILES", "")
|
||||||
write_github_env("SKIP", "true")
|
write_github_env("SKIP", "true")
|
||||||
return
|
return
|
||||||
|
|
||||||
if runner_index < 1:
|
if runner_index < 1:
|
||||||
raise click.ClickException(f"Runner index {runner_index} is out of range (must be >= 1)")
|
raise click.ClickException(
|
||||||
|
_("Runner index {runner_index} is out of range (must be >= 1)", runner_index=runner_index)
|
||||||
|
)
|
||||||
|
|
||||||
zero_based = runner_index - 1
|
zero_based = runner_index - 1
|
||||||
assigned = files_for_runner(files, zero_based, max_runners)
|
assigned = files_for_runner(files, zero_based, max_runners)
|
||||||
@@ -121,7 +129,7 @@ def main(pattern: str, runner_index: int | None, max_runners: int, github_env: b
|
|||||||
if github_env:
|
if github_env:
|
||||||
write_github_env("ASSIGNED_FILES", encoded)
|
write_github_env("ASSIGNED_FILES", encoded)
|
||||||
write_github_env("SKIP", "false")
|
write_github_env("SKIP", "false")
|
||||||
click.echo(f"Assigned {len(assigned)} files to runner {runner_index}")
|
click.echo(_("Assigned {count} files to runner {runner_index}", count=len(assigned), runner_index=runner_index))
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo(encoded)
|
click.echo(encoded)
|
||||||
|
|||||||
@@ -162,17 +162,25 @@ def main(
|
|||||||
groups = distribute(items, weights, max_runners)
|
groups = distribute(items, weights, max_runners)
|
||||||
for i, group in enumerate(groups):
|
for i, group in enumerate(groups):
|
||||||
labels = " ".join(group) if group else "(none)"
|
labels = " ".join(group) if group else "(none)"
|
||||||
click.echo(f"Runner {i}: {labels}")
|
click.echo(_("Runner {i}: {labels}", i=i, labels=labels))
|
||||||
return
|
return
|
||||||
|
|
||||||
if skip_if_excess and github_env and runner_index > max_runners:
|
if skip_if_excess and github_env and runner_index > max_runners:
|
||||||
click.echo(f"Skipping — runner index {runner_index} > max runners {max_runners}")
|
click.echo(
|
||||||
|
_(
|
||||||
|
"Skipping — runner index {runner_index} > max runners {max_runners}",
|
||||||
|
runner_index=runner_index,
|
||||||
|
max_runners=max_runners,
|
||||||
|
)
|
||||||
|
)
|
||||||
write_github_env("ASSIGNED_ITEMS", "")
|
write_github_env("ASSIGNED_ITEMS", "")
|
||||||
write_github_env("SKIP", "true")
|
write_github_env("SKIP", "true")
|
||||||
return
|
return
|
||||||
|
|
||||||
if runner_index < 1:
|
if runner_index < 1:
|
||||||
raise click.ClickException(f"Runner index {runner_index} is out of range (must be >= 1)")
|
raise click.ClickException(
|
||||||
|
_("Runner index {runner_index} is out of range (must be >= 1)", runner_index=runner_index)
|
||||||
|
)
|
||||||
|
|
||||||
zero_based = runner_index - 1
|
zero_based = runner_index - 1
|
||||||
assigned = items_for_runner(items, weights, zero_based, max_runners)
|
assigned = items_for_runner(items, weights, zero_based, max_runners)
|
||||||
@@ -181,7 +189,14 @@ def main(
|
|||||||
if github_env:
|
if github_env:
|
||||||
write_github_env("ASSIGNED_ITEMS", encoded)
|
write_github_env("ASSIGNED_ITEMS", encoded)
|
||||||
write_github_env("SKIP", "false")
|
write_github_env("SKIP", "false")
|
||||||
click.echo(f"Assigned {len(assigned)} items to runner {runner_index}: {encoded}")
|
click.echo(
|
||||||
|
_(
|
||||||
|
"Assigned {count} items to runner {runner_index}: {encoded}",
|
||||||
|
count=len(assigned),
|
||||||
|
runner_index=runner_index,
|
||||||
|
encoded=encoded,
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo(encoded)
|
click.echo(encoded)
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ def cli(pytest_args: tuple[str, ...]) -> None:
|
|||||||
cmd = [sys.executable, "-m", "pytest"]
|
cmd = [sys.executable, "-m", "pytest"]
|
||||||
cmd.extend(pytest_args)
|
cmd.extend(pytest_args)
|
||||||
|
|
||||||
click.echo(f"Running: {' '.join(cmd)}")
|
click.echo(_("Running: {cmd}", cmd=" ".join(cmd)))
|
||||||
|
|
||||||
process = subprocess.Popen( # nosec B603
|
process = subprocess.Popen( # nosec B603
|
||||||
cmd,
|
cmd,
|
||||||
|
|||||||
+22
-11
@@ -28,6 +28,8 @@ from typing import Any
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from devx.i18n import _
|
||||||
|
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
def _repo_root() -> Path:
|
||||||
"""Resolve repo root from GITHUB_WORKSPACE or cwd."""
|
"""Resolve repo root from GITHUB_WORKSPACE or cwd."""
|
||||||
@@ -68,7 +70,7 @@ def fetch_latest_master(branch: str = "master") -> None:
|
|||||||
"""
|
"""
|
||||||
_run(["git", "fetch", "origin", branch]) # nosec B607
|
_run(["git", "fetch", "origin", branch]) # nosec B607
|
||||||
_run(["git", "reset", "--hard", f"origin/{branch}"]) # nosec B607
|
_run(["git", "reset", "--hard", f"origin/{branch}"]) # nosec B607
|
||||||
click.echo(f"Synced to latest origin/{branch}")
|
click.echo(_("Synced to latest origin/{branch}", branch=branch))
|
||||||
|
|
||||||
|
|
||||||
def generate_badges(output_dir: str) -> None:
|
def generate_badges(output_dir: str) -> None:
|
||||||
@@ -76,8 +78,8 @@ def generate_badges(output_dir: str) -> None:
|
|||||||
_run([sys.executable, "-m", "devx.tools.generate_badges", "--output-dir", output_dir])
|
_run([sys.executable, "-m", "devx.tools.generate_badges", "--output-dir", output_dir])
|
||||||
badges = list(Path(output_dir).glob("*.svg"))
|
badges = list(Path(output_dir).glob("*.svg"))
|
||||||
if not badges:
|
if not badges:
|
||||||
raise click.ClickException("No badge SVG files generated")
|
raise click.ClickException(_("No badge SVG files generated"))
|
||||||
click.echo(f"Generated {len(badges)} badge files")
|
click.echo(_("Generated {count} badge files", count=len(badges)))
|
||||||
|
|
||||||
|
|
||||||
def push_to_badges_branch(badges_dir: str) -> str:
|
def push_to_badges_branch(badges_dir: str) -> str:
|
||||||
@@ -99,12 +101,12 @@ def push_to_badges_branch(badges_dir: str) -> str:
|
|||||||
_run(["git", "add", "./*.svg"]) # nosec B607
|
_run(["git", "add", "./*.svg"]) # nosec B607
|
||||||
_run(["git", "commit", "--no-verify", "-m", "Update badges [skip ci]"]) # nosec B607
|
_run(["git", "commit", "--no-verify", "-m", "Update badges [skip ci]"]) # nosec B607
|
||||||
_run(["git", "push", "origin", "badges", "--force"]) # nosec B607
|
_run(["git", "push", "origin", "badges", "--force"]) # nosec B607
|
||||||
click.echo("Badges pushed to badges branch")
|
click.echo(_("Badges pushed to badges branch"))
|
||||||
|
|
||||||
# Get the commit SHA of the badges branch
|
# Get the commit SHA of the badges branch
|
||||||
result = _run_capture(["git", "rev-parse", "HEAD"]) # nosec B607
|
result = _run_capture(["git", "rev-parse", "HEAD"]) # nosec B607
|
||||||
sha = result.stdout.strip()
|
sha = result.stdout.strip()
|
||||||
click.echo(f"Badges commit SHA: {sha}")
|
click.echo(_("Badges commit SHA: {sha}", sha=sha))
|
||||||
return sha
|
return sha
|
||||||
|
|
||||||
|
|
||||||
@@ -142,11 +144,11 @@ def update_readme_with_badge_sha(badges_sha: str, repo_root: Path | None = None)
|
|||||||
new_content = update_badge_urls(content, badges_sha)
|
new_content = update_badge_urls(content, badges_sha)
|
||||||
if new_content != content:
|
if new_content != content:
|
||||||
filepath.write_text(new_content)
|
filepath.write_text(new_content)
|
||||||
click.echo(f"Updated badge URLs in {filename}")
|
click.echo(_("Updated badge URLs in {filename}", filename=filename))
|
||||||
updated_any = True
|
updated_any = True
|
||||||
|
|
||||||
if not updated_any:
|
if not updated_any:
|
||||||
click.echo("No badge URLs found to update — README already up to date")
|
click.echo(_("No badge URLs found to update — README already up to date"))
|
||||||
return
|
return
|
||||||
|
|
||||||
_run(["git", "add", "README.md", "docs/index.md"]) # nosec B607
|
_run(["git", "add", "README.md", "docs/index.md"]) # nosec B607
|
||||||
@@ -160,7 +162,7 @@ def update_readme_with_badge_sha(badges_sha: str, repo_root: Path | None = None)
|
|||||||
]
|
]
|
||||||
) # nosec B607
|
) # nosec B607
|
||||||
_run(["git", "push", "origin", "master"]) # nosec B607
|
_run(["git", "push", "origin", "master"]) # nosec B607
|
||||||
click.echo(f"Pushed README update with badge SHA {badges_sha[:8]}")
|
click.echo(_("Pushed README update with badge SHA {sha}", sha=badges_sha[:8]))
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@@ -193,13 +195,22 @@ def main(output_dir: str, branch: str, no_readme_update: bool, retries: int) ->
|
|||||||
except (subprocess.CalledProcessError, RuntimeError) as exc:
|
except (subprocess.CalledProcessError, RuntimeError) as exc:
|
||||||
last_error = exc
|
last_error = exc
|
||||||
if attempt < retries:
|
if attempt < retries:
|
||||||
click.echo(f"Badge push attempt {attempt}/{retries} failed — retrying: {exc}")
|
click.echo(
|
||||||
|
_(
|
||||||
|
"Badge push attempt {attempt}/{retries} failed — retrying: {error}",
|
||||||
|
attempt=attempt,
|
||||||
|
retries=retries,
|
||||||
|
error=exc,
|
||||||
|
)
|
||||||
|
)
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
with contextlib.suppress(subprocess.CalledProcessError):
|
with contextlib.suppress(subprocess.CalledProcessError):
|
||||||
fetch_latest_master(branch)
|
fetch_latest_master(branch)
|
||||||
else:
|
else:
|
||||||
click.echo(f"Badge push failed after {retries} attempts: {exc}")
|
click.echo(_("Badge push failed after {retries} attempts: {error}", retries=retries, error=exc))
|
||||||
raise click.ClickException(f"Badge push failed after {retries} attempts: {last_error}")
|
raise click.ClickException(
|
||||||
|
_("Badge push failed after {retries} attempts: {error}", retries=retries, error=last_error)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ def main(
|
|||||||
total_kept = 0
|
total_kept = 0
|
||||||
total_failed = 0
|
total_failed = 0
|
||||||
for name in names:
|
for name in names:
|
||||||
click.echo(f"\n{'=' * 60}")
|
click.echo(_("\n{separator}", separator="=" * 60))
|
||||||
click.echo(f"Package: {owner}/{name}")
|
click.echo(_("Package: {owner}/{name}", owner=owner, name=name))
|
||||||
click.echo(f"{'=' * 60}")
|
click.echo(_("{separator}", separator="=" * 60))
|
||||||
try:
|
try:
|
||||||
versions = list_package_versions(base_url, owner, name, token)
|
versions = list_package_versions(base_url, owner, name, token)
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
@@ -217,17 +217,19 @@ def main(
|
|||||||
click.echo(_("No versions found."))
|
click.echo(_("No versions found."))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
click.echo(f"Found {len(versions)} version(s):")
|
click.echo(_("Found {count} version(s):", count=len(versions)))
|
||||||
for v in sort_versions_by_date(versions):
|
for v in sort_versions_by_date(versions):
|
||||||
click.echo(f" {v.get('version', '?')} (created: {v.get('created_at', '?')})")
|
click.echo(
|
||||||
|
_(" {version} (created: {created})", version=v.get("version", "?"), created=v.get("created_at", "?"))
|
||||||
|
)
|
||||||
|
|
||||||
to_delete = select_for_deletion(versions, keep)
|
to_delete = select_for_deletion(versions, keep)
|
||||||
kept_count = len(versions) - len(to_delete)
|
kept_count = len(versions) - len(to_delete)
|
||||||
click.echo(f"\nKeeping {kept_count}, would delete {len(to_delete)}")
|
click.echo(_("\nKeeping {kept}, would delete {count}", kept=kept_count, count=len(to_delete)))
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
for v in to_delete:
|
for v in to_delete:
|
||||||
click.echo(f" [dry-run] Would delete: {v.get('version', '?')}")
|
click.echo(_(" [dry-run] Would delete: {version}", version=v.get("version", "?")))
|
||||||
total_kept += kept_count
|
total_kept += kept_count
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -236,17 +238,24 @@ def main(
|
|||||||
for v in to_delete:
|
for v in to_delete:
|
||||||
version = str(v.get("version", ""))
|
version = str(v.get("version", ""))
|
||||||
if delete_package_version(base_url, owner, name, version, token):
|
if delete_package_version(base_url, owner, name, version, token):
|
||||||
click.echo(f" Deleted: {version}")
|
click.echo(_(" Deleted: {version}", version=version))
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
else:
|
else:
|
||||||
click.echo(f" FAILED to delete: {version}", err=True)
|
click.echo(_(" FAILED to delete: {version}", version=version), err=True)
|
||||||
failed_count += 1
|
failed_count += 1
|
||||||
|
|
||||||
total_deleted += deleted_count
|
total_deleted += deleted_count
|
||||||
total_kept += kept_count
|
total_kept += kept_count
|
||||||
total_failed += failed_count
|
total_failed += failed_count
|
||||||
|
|
||||||
click.echo(f"\nDone. Deleted {total_deleted}, kept {total_kept}, failed {total_failed}.")
|
click.echo(
|
||||||
|
_(
|
||||||
|
"\nDone. Deleted {deleted}, kept {kept}, failed {failed}.",
|
||||||
|
deleted=total_deleted,
|
||||||
|
kept=total_kept,
|
||||||
|
failed=total_failed,
|
||||||
|
)
|
||||||
|
)
|
||||||
if total_failed > 0:
|
if total_failed > 0:
|
||||||
raise click.ClickException(_("Failed to delete {count} image version(s)", count=total_failed))
|
raise click.ClickException(_("Failed to delete {count} image version(s)", count=total_failed))
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from devx.i18n import _
|
||||||
|
|
||||||
# Coverage regex matches "TOTAL ... NN%" or "TOTAL ... NN.NN%"
|
# Coverage regex matches "TOTAL ... NN%" or "TOTAL ... NN.NN%"
|
||||||
_COVERAGE_RE = re.compile(r"TOTAL.*?(\d+(?:\.\d+)?)%")
|
_COVERAGE_RE = re.compile(r"TOTAL.*?(\d+(?:\.\d+)?)%")
|
||||||
_PASSED_RE = re.compile(r"(\d+) passed")
|
_PASSED_RE = re.compile(r"(\d+) passed")
|
||||||
@@ -197,17 +199,19 @@ def read_version(repo_root: Path) -> str:
|
|||||||
"""
|
"""
|
||||||
pkg = detect_package_name(repo_root)
|
pkg = detect_package_name(repo_root)
|
||||||
if pkg is None:
|
if pkg is None:
|
||||||
click.echo(" WARNING: No Python package found under src/ — version badge will show 'unknown'")
|
click.echo(_(" WARNING: No Python package found under src/ — version badge will show 'unknown'"))
|
||||||
return "unknown"
|
return "unknown"
|
||||||
init_file = repo_root / "src" / pkg / "__init__.py"
|
init_file = repo_root / "src" / pkg / "__init__.py"
|
||||||
if not init_file.exists():
|
if not init_file.exists():
|
||||||
click.echo(f" WARNING: {init_file} not found — version badge will show 'unknown'")
|
click.echo(_(" WARNING: {init_file} not found — version badge will show 'unknown'", init_file=init_file))
|
||||||
return "unknown"
|
return "unknown"
|
||||||
content = init_file.read_text()
|
content = init_file.read_text()
|
||||||
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
|
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
click.echo(f" WARNING: No __version__ found in {init_file} — version badge will show 'unknown'")
|
click.echo(
|
||||||
|
_(" WARNING: No __version__ found in {init_file} — version badge will show 'unknown'", init_file=init_file)
|
||||||
|
)
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
@@ -278,11 +282,11 @@ def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], d
|
|||||||
"""
|
"""
|
||||||
cov_target = detect_coverage_target(repo_root)
|
cov_target = detect_coverage_target(repo_root)
|
||||||
if cov_target is None:
|
if cov_target is None:
|
||||||
click.echo(" WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)")
|
click.echo(_(" WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)"))
|
||||||
return make_badge("coverage", "unknown", "lightgrey"), make_badge("tests", "unknown", "lightgrey")
|
return make_badge("coverage", "unknown", "lightgrey"), make_badge("tests", "unknown", "lightgrey")
|
||||||
|
|
||||||
testpaths = detect_testpaths(repo_root)
|
testpaths = detect_testpaths(repo_root)
|
||||||
click.echo(f" Test paths: {testpaths or '(pytest defaults)'}")
|
click.echo(_(" Test paths: {testpaths}", testpaths=testpaths or "(pytest defaults)"))
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
@@ -302,18 +306,18 @@ def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], d
|
|||||||
if coverage is not None:
|
if coverage is not None:
|
||||||
cov_badge = make_badge("coverage", f"{coverage:.0f}%", coverage_color(coverage))
|
cov_badge = make_badge("coverage", f"{coverage:.0f}%", coverage_color(coverage))
|
||||||
else:
|
else:
|
||||||
click.echo(f" WARNING: Could not extract coverage from pytest output (rc={rc})")
|
click.echo(_(" WARNING: Could not extract coverage from pytest output (rc={rc})", rc=rc))
|
||||||
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
|
click.echo(_(" pytest stdout (last 300 chars): {stdout}", stdout=stdout.strip()[-300:]))
|
||||||
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
|
click.echo(_(" pytest stderr (last 300 chars): {stderr}", stderr=stderr.strip()[-300:]))
|
||||||
cov_badge = make_badge("coverage", "unknown", "red")
|
cov_badge = make_badge("coverage", "unknown", "red")
|
||||||
|
|
||||||
test_count = extract_test_count(combined)
|
test_count = extract_test_count(combined)
|
||||||
if test_count is not None:
|
if test_count is not None:
|
||||||
tests_badge = make_badge("tests", f"{test_count} passing", "brightgreen" if rc == 0 else "red")
|
tests_badge = make_badge("tests", f"{test_count} passing", "brightgreen" if rc == 0 else "red")
|
||||||
else:
|
else:
|
||||||
click.echo(f" WARNING: Could not extract test count from pytest output (rc={rc})")
|
click.echo(_(" WARNING: Could not extract test count from pytest output (rc={rc})", rc=rc))
|
||||||
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
|
click.echo(_(" pytest stdout (last 300 chars): {stdout}", stdout=stdout.strip()[-300:]))
|
||||||
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
|
click.echo(_(" pytest stderr (last 300 chars): {stderr}", stderr=stderr.strip()[-300:]))
|
||||||
tests_badge = make_badge("tests", "unknown", "red")
|
tests_badge = make_badge("tests", "unknown", "red")
|
||||||
|
|
||||||
return cov_badge, tests_badge
|
return cov_badge, tests_badge
|
||||||
@@ -328,8 +332,8 @@ def collect_doc_coverage(repo_root: Path) -> dict[str, str | int]:
|
|||||||
doc_pct = extract_doc_coverage(stdout)
|
doc_pct = extract_doc_coverage(stdout)
|
||||||
if doc_pct is not None:
|
if doc_pct is not None:
|
||||||
return make_badge("docs", f"{doc_pct}%", doc_coverage_color(doc_pct))
|
return make_badge("docs", f"{doc_pct}%", doc_coverage_color(doc_pct))
|
||||||
click.echo(f" WARNING: Could not extract doc coverage (rc={rc})")
|
click.echo(_(" WARNING: Could not extract doc coverage (rc={rc})", rc=rc))
|
||||||
click.echo(f" stderr: {stderr.strip()[:200]}")
|
click.echo(_(" stderr: {stderr}", stderr=stderr.strip()[:200]))
|
||||||
return make_badge("docs", "unknown", "red")
|
return make_badge("docs", "unknown", "red")
|
||||||
|
|
||||||
|
|
||||||
@@ -356,16 +360,16 @@ def collect_quality(repo_root: Path) -> dict[str, str | int]:
|
|||||||
results.append(False)
|
results.append(False)
|
||||||
# Distinguish "tool not installed" from "tool found issues"
|
# Distinguish "tool not installed" from "tool found issues"
|
||||||
if "No module named" in stderr or "not found" in stderr.lower():
|
if "No module named" in stderr or "not found" in stderr.lower():
|
||||||
click.echo(f" WARNING: {name} not installed — skipping (counted as pass)")
|
click.echo(_(" WARNING: {name} not installed — skipping (counted as pass)", name=name))
|
||||||
results[-1] = True
|
results[-1] = True
|
||||||
tool_names.append(f"{name}: not installed (skipped)")
|
tool_names.append(f"{name}: not installed (skipped)")
|
||||||
else:
|
else:
|
||||||
tool_names.append(f"{name}: FAIL")
|
tool_names.append(f"{name}: FAIL")
|
||||||
click.echo(f" WARNING: {name} failed (rc={rc})")
|
click.echo(_(" WARNING: {name} failed (rc={rc})", name=name, rc=rc))
|
||||||
click.echo(f" stderr: {stderr.strip()[:200]}")
|
click.echo(_(" stderr: {stderr}", stderr=stderr.strip()[:200]))
|
||||||
|
|
||||||
all_pass = all(results)
|
all_pass = all(results)
|
||||||
click.echo(f" Quality checks: {', '.join(tool_names)}")
|
click.echo(_(" Quality checks: {checks}", checks=", ".join(tool_names)))
|
||||||
return make_badge("code quality", "A" if all_pass else "F", "brightgreen" if all_pass else "red")
|
return make_badge("code quality", "A" if all_pass else "F", "brightgreen" if all_pass else "red")
|
||||||
|
|
||||||
|
|
||||||
@@ -377,28 +381,28 @@ def generate_badges(output_dir: Path, repo_root: Path | None = None) -> dict[str
|
|||||||
repo_root: Repository root (auto-detected if None).
|
repo_root: Repository root (auto-detected if None).
|
||||||
"""
|
"""
|
||||||
root = repo_root or resolve_repo_root()
|
root = repo_root or resolve_repo_root()
|
||||||
click.echo(f" Repo root: {root}")
|
click.echo(_(" Repo root: {root}", root=root))
|
||||||
pkg = detect_package_name(root)
|
pkg = detect_package_name(root)
|
||||||
click.echo(f" Package: {pkg or 'none'}")
|
click.echo(_(" Package: {pkg}", pkg=pkg or "none"))
|
||||||
|
|
||||||
badges: dict[str, dict[str, str | int]] = {}
|
badges: dict[str, dict[str, str | int]] = {}
|
||||||
|
|
||||||
# 1. Code coverage + test count (single pytest-cov run)
|
# 1. Code coverage + test count (single pytest-cov run)
|
||||||
click.echo(" Collecting coverage and tests...")
|
click.echo(_(" Collecting coverage and tests..."))
|
||||||
cov_badge, tests_badge = collect_coverage_and_tests(root)
|
cov_badge, tests_badge = collect_coverage_and_tests(root)
|
||||||
badges["coverage"] = cov_badge
|
badges["coverage"] = cov_badge
|
||||||
badges["tests"] = tests_badge
|
badges["tests"] = tests_badge
|
||||||
|
|
||||||
# 2. Documentation coverage
|
# 2. Documentation coverage
|
||||||
click.echo(" Collecting doc coverage...")
|
click.echo(_(" Collecting doc coverage..."))
|
||||||
badges["docs"] = collect_doc_coverage(root)
|
badges["docs"] = collect_doc_coverage(root)
|
||||||
|
|
||||||
# 3. Code quality (ruff + pyright + bandit)
|
# 3. Code quality (ruff + pyright + bandit)
|
||||||
click.echo(" Collecting code quality...")
|
click.echo(_(" Collecting code quality..."))
|
||||||
badges["quality"] = collect_quality(root)
|
badges["quality"] = collect_quality(root)
|
||||||
|
|
||||||
# 4. Version
|
# 4. Version
|
||||||
click.echo(" Collecting version...")
|
click.echo(_(" Collecting version..."))
|
||||||
version = read_version(root)
|
version = read_version(root)
|
||||||
badges["version"] = make_badge("version", f"v{version}", "blue")
|
badges["version"] = make_badge("version", f"v{version}", "blue")
|
||||||
|
|
||||||
@@ -411,7 +415,7 @@ def generate_badges(output_dir: Path, repo_root: Path | None = None) -> dict[str
|
|||||||
svg = render_svg(str(badge["label"]), str(badge["message"]), str(badge["color"]))
|
svg = render_svg(str(badge["label"]), str(badge["message"]), str(badge["color"]))
|
||||||
path = output_dir / f"{name}.svg"
|
path = output_dir / f"{name}.svg"
|
||||||
path.write_text(svg)
|
path.write_text(svg)
|
||||||
click.echo(f" Generated: {path}")
|
click.echo(_(" Generated: {path}", path=path))
|
||||||
|
|
||||||
return badges
|
return badges
|
||||||
|
|
||||||
@@ -431,11 +435,19 @@ def cli(output_dir: str, repo_root: str | None) -> None:
|
|||||||
"""Generate self-contained SVG badge files from project metrics."""
|
"""Generate self-contained SVG badge files from project metrics."""
|
||||||
out = Path(output_dir)
|
out = Path(output_dir)
|
||||||
root = Path(repo_root) if repo_root else None
|
root = Path(repo_root) if repo_root else None
|
||||||
click.echo(f"Generating badges in {out}...")
|
click.echo(_("Generating badges in {out}...", out=out))
|
||||||
badges = generate_badges(out, repo_root=root)
|
badges = generate_badges(out, repo_root=root)
|
||||||
click.echo(f"\nGenerated {len(badges)} badges:")
|
click.echo(_("\nGenerated {count} badges:", count=len(badges)))
|
||||||
for name, badge in badges.items():
|
for name, badge in badges.items():
|
||||||
click.echo(f" {name}: {badge['label']}={badge['message']} ({badge['color']})")
|
click.echo(
|
||||||
|
_(
|
||||||
|
" {name}: {label}={message} ({color})",
|
||||||
|
name=name,
|
||||||
|
label=badge["label"],
|
||||||
|
message=badge["message"],
|
||||||
|
color=badge["color"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
+823
-319
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user