From e1b6aad51bcb5988143bca171dfbf7454e6642dc Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:30:58 +0200 Subject: [PATCH] fix: refuse abbreviated flags in every script argparse completed a prefix into the only flag starting with it: slots.py --emulator read profiles from a directory named after the emulator and reported nothing, and pipeline.py --all built every pack variant. Every parser now refuses an abbreviation. --- install.ps1 | 2 +- install.py | 1 + install.sh | 2 +- scripts/auto_fetch.py | 2 +- scripts/check_buildbot_system.py | 1 + scripts/check_freshness.py | 2 +- scripts/check_release_assets.py | 2 +- scripts/cross_reference.py | 2 +- scripts/dedup.py | 2 +- scripts/diff_truth.py | 2 +- scripts/download.py | 1 + scripts/export_native.py | 1 + scripts/fileless_audit.py | 2 +- scripts/find_pinned_revision.py | 2 +- scripts/generate_db.py | 2 +- scripts/generate_pack.py | 2 +- scripts/generate_readme.py | 2 +- scripts/generate_site.py | 1 + scripts/generate_truth.py | 1 + scripts/identity.py | 1 + scripts/list_platforms.py | 2 +- scripts/mame_ref_audit.py | 2 +- scripts/migrate.py | 1 + scripts/pipeline.py | 2 +- scripts/profile_sync.py | 1 + scripts/provenance_report.py | 2 +- scripts/refresh_data_dirs.py | 1 + scripts/refresh_stale.py | 2 +- scripts/region_audit.py | 1 + scripts/release_record.py | 2 +- scripts/restore_large_files.py | 2 +- scripts/romset_recipes.py | 2 +- scripts/scraper/base_scraper.py | 2 +- scripts/scraper/coreinfo_scraper.py | 1 + scripts/scraper/dat_pack_importer.py | 2 +- scripts/scraper/fbneo_hash_scraper.py | 1 + scripts/scraper/mame_hash_scraper.py | 1 + scripts/scraper/redump_dat_scraper.py | 2 +- scripts/scraper/romset_dat_importer.py | 2 +- .../targets/batocera_targets_scraper.py | 1 + .../targets/emudeck_targets_scraper.py | 2 +- .../targets/recalbox_targets_scraper.py | 2 +- .../targets/retroarch_targets_scraper.py | 1 + .../targets/retropie_targets_scraper.py | 1 + .../targets/rocknix_targets_scraper.py | 2 +- scripts/slots.py | 1 + scripts/split_pack.py | 1 + scripts/torrentzip.py | 2 +- scripts/validate_pr.py | 2 +- scripts/validate_schemas.py | 2 +- scripts/validate_site.py | 2 +- scripts/verify.py | 2 +- tests/test_audit_regressions.py | 27 +++++++++++++++++++ 53 files changed, 79 insertions(+), 33 deletions(-) diff --git a/install.ps1 b/install.ps1 index bf3da60b..141bc16c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "bfc3a2127689147e8a5f729a9ed668608f06dbd3b14ca6e768cdf2a7f90d5d32" +$defaultInstallSha256 = "a7c7a25573c6c96beb63eebbc079956dca4f173b9cc8aa6397935654ddc23f71" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index a4b2bfbe..0f60d924 100644 --- a/install.py +++ b/install.py @@ -2830,6 +2830,7 @@ def _requested_narrowing( def main() -> None: """Entry point.""" parser = argparse.ArgumentParser( + allow_abbrev=False, description="Download missing BIOS files for retrogaming emulators.", ) parser.add_argument( diff --git a/install.sh b/install.sh index d2f5f161..9ccaea9f 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="bfc3a2127689147e8a5f729a9ed668608f06dbd3b14ca6e768cdf2a7f90d5d32" +DEFAULT_INSTALL_SHA256="a7c7a25573c6c96beb63eebbc079956dca4f173b9cc8aa6397935654ddc23f71" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/scripts/auto_fetch.py b/scripts/auto_fetch.py index f338228a..092cff4f 100644 --- a/scripts/auto_fetch.py +++ b/scripts/auto_fetch.py @@ -415,7 +415,7 @@ def generate_issue_body(missing: list[dict], platform: str) -> str: def main() -> None: - parser = argparse.ArgumentParser(description="Auto-fetch missing BIOS files") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Auto-fetch missing BIOS files") parser.add_argument("--platform", "-p", help="Platform to check") parser.add_argument("--all", action="store_true", help="Check all platforms") parser.add_argument( diff --git a/scripts/check_buildbot_system.py b/scripts/check_buildbot_system.py index ba5cb2c9..61733317 100644 --- a/scripts/check_buildbot_system.py +++ b/scripts/check_buildbot_system.py @@ -177,6 +177,7 @@ def update_changed(report: dict) -> list[str]: def main() -> None: logging.basicConfig(level=logging.INFO, format="%(message)s") parser = argparse.ArgumentParser( + allow_abbrev=False, description="Check buildbot system directory for changes", ) parser.add_argument( diff --git a/scripts/check_freshness.py b/scripts/check_freshness.py index 2545b21e..fbbd15e5 100644 --- a/scripts/check_freshness.py +++ b/scripts/check_freshness.py @@ -1149,7 +1149,7 @@ def render(findings: list[Finding]) -> str: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0]) parser.add_argument("--only", help="comma-separated areas: " + ",".join(AREAS)) parser.add_argument("--json", action="store_true", help="machine-readable output") parser.add_argument("--profiles", action="store_true", help="also run profile_sync (slow)") diff --git a/scripts/check_release_assets.py b/scripts/check_release_assets.py index 517f9250..71896e43 100644 --- a/scripts/check_release_assets.py +++ b/scripts/check_release_assets.py @@ -318,7 +318,7 @@ def fetch_assets() -> dict[str, int]: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n")[0]) parser.add_argument("--db", default="database.json") parser.add_argument("--gitignore", default=".gitignore") parser.add_argument("--json", action="store_true") diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index 766fb4d8..8656a3d8 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -748,7 +748,7 @@ def print_report(report: dict, scope: str = "all emulators") -> None: def main() -> None: - parser = argparse.ArgumentParser(description="Emulator vs platform gap analysis") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Emulator vs platform gap analysis") parser.add_argument("--emulators-dir", default=DEFAULT_EMULATORS_DIR) parser.add_argument("--platforms-dir", default=DEFAULT_PLATFORMS_DIR) parser.add_argument("--db", default=DEFAULT_DB) diff --git a/scripts/dedup.py b/scripts/dedup.py index cfcfdc8a..44fb1b88 100644 --- a/scripts/dedup.py +++ b/scripts/dedup.py @@ -313,7 +313,7 @@ def deduplicate(bios_dir: str, dry_run: bool = False) -> dict: def main() -> None: - parser = argparse.ArgumentParser(description="Deduplicate bios/ directory") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Deduplicate bios/ directory") parser.add_argument( "--dry-run", action="store_true", help="Preview without deleting" ) diff --git a/scripts/diff_truth.py b/scripts/diff_truth.py index 7e1b9114..53b93af6 100644 --- a/scripts/diff_truth.py +++ b/scripts/diff_truth.py @@ -160,7 +160,7 @@ def _format_markdown(report: dict) -> str: def main() -> None: - parser = argparse.ArgumentParser(description="Compare scraped vs truth YAMLs") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Compare scraped vs truth YAMLs") group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--all", action="store_true", help="diff all registered platforms" diff --git a/scripts/download.py b/scripts/download.py index e0f3e10e..1678ae1e 100644 --- a/scripts/download.py +++ b/scripts/download.py @@ -311,6 +311,7 @@ def show_info(platform: str, release: dict) -> None: def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Download BIOS packs from GitHub Releases", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" diff --git a/scripts/export_native.py b/scripts/export_native.py index 5920694a..aba4155b 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -502,6 +502,7 @@ def run( def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Rewrite each platform's own BIOS file, corrected.", ) group = parser.add_mutually_exclusive_group(required=True) diff --git a/scripts/fileless_audit.py b/scripts/fileless_audit.py index c4620bdb..b6d050f6 100644 --- a/scripts/fileless_audit.py +++ b/scripts/fileless_audit.py @@ -85,7 +85,7 @@ def audit( def main() -> None: - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0]) parser.add_argument("emulators", nargs="+") parser.add_argument("--emulators-dir", default="emulators") parser.add_argument("--cache-dir", default=".cache") diff --git a/scripts/find_pinned_revision.py b/scripts/find_pinned_revision.py index d211fa9c..b643d480 100644 --- a/scripts/find_pinned_revision.py +++ b/scripts/find_pinned_revision.py @@ -69,7 +69,7 @@ def find_revision( def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0]) parser.add_argument("--repo", required=True, help="forge URL of the project") parser.add_argument("--path", required=True, help="path of the file in the project") parser.add_argument("--md5", required=True, help="md5 the platform pins") diff --git a/scripts/generate_db.py b/scripts/generate_db.py index 2557c445..fc3864c9 100644 --- a/scripts/generate_db.py +++ b/scripts/generate_db.py @@ -342,7 +342,7 @@ def _preserve_large_file_entries(files: dict, db_path: str) -> int: def main() -> int: - parser = argparse.ArgumentParser(description="Generate multi-indexed BIOS database") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate multi-indexed BIOS database") parser.add_argument("--force", action="store_true", help="Force rehash all files") parser.add_argument( "--bios-dir", default=DEFAULT_BIOS_DIR, help="BIOS directory path" diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 9e1fbf6d..1850eb72 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -2871,7 +2871,7 @@ def main() -> None: def _main(held: contextlib.ExitStack) -> None: - parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate platform BIOS ZIP packs") parser.add_argument("--platform", "-p", help="Platform name (e.g., retroarch)") parser.add_argument( "--all", action="store_true", help="Generate packs for all active platforms" diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 591f6333..1944866c 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -746,7 +746,7 @@ def generate_contributing() -> str: def main() -> None: - parser = argparse.ArgumentParser(description="Generate slim README.md") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate slim README.md") parser.add_argument("--db", default="database.json") parser.add_argument("--platforms-dir", default="platforms") args = parser.parse_args() diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 45cd079b..eaef8450 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3749,6 +3749,7 @@ def generate_mkdocs_nav( def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Generate MkDocs site from project data" ) parser.add_argument("--db", default="database.json") diff --git a/scripts/generate_truth.py b/scripts/generate_truth.py index 5d33bbfe..1df87dcb 100644 --- a/scripts/generate_truth.py +++ b/scripts/generate_truth.py @@ -36,6 +36,7 @@ DEFAULT_DB_FILE = "database.json" def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Generate ground-truth YAML from emulator profiles", ) group = parser.add_mutually_exclusive_group(required=True) diff --git a/scripts/identity.py b/scripts/identity.py index a2179cbe..20aec481 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -133,6 +133,7 @@ def main() -> int: import argparse parser = argparse.ArgumentParser( + allow_abbrev=False, description="Report one content stored under identities that disagree.", ) parser.add_argument("--bios-dir", default="bios") diff --git a/scripts/list_platforms.py b/scripts/list_platforms.py index 8112e37e..11bbe9f5 100644 --- a/scripts/list_platforms.py +++ b/scripts/list_platforms.py @@ -30,7 +30,7 @@ def list_platforms(include_archived: bool = False) -> list[str]: def main() -> None: - parser = argparse.ArgumentParser(description="List available platforms") + parser = argparse.ArgumentParser(allow_abbrev=False, description="List available platforms") parser.add_argument("--all", action="store_true", help="Include archived platforms") args = parser.parse_args() diff --git a/scripts/mame_ref_audit.py b/scripts/mame_ref_audit.py index af6b538f..d612b22b 100644 --- a/scripts/mame_ref_audit.py +++ b/scripts/mame_ref_audit.py @@ -114,7 +114,7 @@ def rewrite(name: str, emulators_dir: str, findings: list[Finding]) -> int: def main() -> None: - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0]) parser.add_argument("emulators", nargs="+") parser.add_argument("--emulators-dir", default="emulators") parser.add_argument("--cache-dir", default=upstream.CACHE_DIR) diff --git a/scripts/migrate.py b/scripts/migrate.py index c9cca370..b3a25780 100644 --- a/scripts/migrate.py +++ b/scripts/migrate.py @@ -493,6 +493,7 @@ def migrate_local(source: Path, target: Path, dry_run: bool) -> tuple: def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Migrate BIOS files to Manufacturer/Console structure" ) parser.add_argument( diff --git a/scripts/pipeline.py b/scripts/pipeline.py index 56d101a0..c67275f1 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -325,7 +325,7 @@ def _truth_steps(args: argparse.Namespace, results: dict) -> bool: def main() -> None: - parser = argparse.ArgumentParser(description="Run the full retrobios pipeline") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Run the full retrobios pipeline") parser.add_argument( "--include-archived", action="store_true", help="Include archived platforms" ) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index a7e2e998..47bf171e 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -3239,6 +3239,7 @@ def emulators_dir_is_dirty(emulators_dir: str) -> bool: def build_parser() -> argparse.ArgumentParser: """Command line: selection, output, detection, writes, network.""" parser = argparse.ArgumentParser( + allow_abbrev=False, description="Confront emulator profiles with their upstream source" ) group = parser.add_mutually_exclusive_group(required=True) diff --git a/scripts/provenance_report.py b/scripts/provenance_report.py index 8b128769..83cb33d5 100644 --- a/scripts/provenance_report.py +++ b/scripts/provenance_report.py @@ -130,7 +130,7 @@ def build_report(db: dict, snapshots: dict, members: dict | None = None) -> dict def main() -> int: - parser = argparse.ArgumentParser(description="Dump-catalog coverage report") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Dump-catalog coverage report") parser.add_argument("--db", default="database.json", help="Database path") parser.add_argument( "--provenance-dir", diff --git a/scripts/refresh_data_dirs.py b/scripts/refresh_data_dirs.py index 81905b74..dbbbb370 100644 --- a/scripts/refresh_data_dirs.py +++ b/scripts/refresh_data_dirs.py @@ -518,6 +518,7 @@ def refresh_all( def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Refresh cached data directories from upstream" ) parser.add_argument("--key", help="Refresh only this entry") diff --git a/scripts/refresh_stale.py b/scripts/refresh_stale.py index 003f7870..4aa643ab 100755 --- a/scripts/refresh_stale.py +++ b/scripts/refresh_stale.py @@ -377,7 +377,7 @@ def render( def main() -> int: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0]) parser.add_argument( "--only", help="comma-separated areas to check: " + ",".join(AUTO_AREAS), diff --git a/scripts/region_audit.py b/scripts/region_audit.py index 82697c16..a3ec6100 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -191,6 +191,7 @@ def print_report(report: dict) -> None: def main() -> int: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Cross-check profile region: values against No-Intro provenance" ) parser.add_argument("--db", default="database.json", help="Path to database.json") diff --git a/scripts/release_record.py b/scripts/release_record.py index ee1a6694..86e1844f 100644 --- a/scripts/release_record.py +++ b/scripts/release_record.py @@ -124,7 +124,7 @@ def _size(count: int) -> str: def main() -> int: - parser = argparse.ArgumentParser(description="Record what a release's packs hold") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Record what a release's packs hold") parser.add_argument("dist", type=Path, help="directory of the packs to publish") parser.add_argument("--tag", required=True, help="release tag, e.g. v2026.10.04") parser.add_argument("--install-dir", default="install", type=Path) diff --git a/scripts/restore_large_files.py b/scripts/restore_large_files.py index 0f95af08..eb878c9d 100644 --- a/scripts/restore_large_files.py +++ b/scripts/restore_large_files.py @@ -92,7 +92,7 @@ def restore( def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__) parser.add_argument("--cache", default=".cache/large") parser.add_argument("--db", default="database.json") parser.add_argument("--gitignore", default=".gitignore") diff --git a/scripts/romset_recipes.py b/scripts/romset_recipes.py index b8f7d33d..fe9a3bb1 100644 --- a/scripts/romset_recipes.py +++ b/scripts/romset_recipes.py @@ -451,7 +451,7 @@ def _resolve_modes(args: argparse.Namespace) -> None: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__) parser.add_argument("--identify", action="store_true", help="label local archives") parser.add_argument( "--missing", action="store_true", help="attempt pinned archives we lack" diff --git a/scripts/scraper/base_scraper.py b/scripts/scraper/base_scraper.py index ed98954a..1f86cc69 100644 --- a/scripts/scraper/base_scraper.py +++ b/scripts/scraper/base_scraper.py @@ -320,7 +320,7 @@ def scraper_cli( """Shared CLI entry point for all scrapers. Eliminates main() boilerplate.""" import argparse - parser = argparse.ArgumentParser(description=description) + parser = argparse.ArgumentParser(allow_abbrev=False, description=description) parser.add_argument("--dry-run", action="store_true", help="Show scraped data") parser.add_argument("--output", "-o", help="Output YAML file") parser.add_argument("--json", action="store_true", help="Output as JSON") diff --git a/scripts/scraper/coreinfo_scraper.py b/scripts/scraper/coreinfo_scraper.py index 1360d003..dce58bd8 100644 --- a/scripts/scraper/coreinfo_scraper.py +++ b/scripts/scraper/coreinfo_scraper.py @@ -308,6 +308,7 @@ def main() -> None: import argparse parser = argparse.ArgumentParser( + allow_abbrev=False, description="Scrape libretro-core-info firmware requirements" ) parser.add_argument("--dry-run", action="store_true") diff --git a/scripts/scraper/dat_pack_importer.py b/scripts/scraper/dat_pack_importer.py index 0430e59b..647ca781 100644 --- a/scripts/scraper/dat_pack_importer.py +++ b/scripts/scraper/dat_pack_importer.py @@ -91,7 +91,7 @@ def import_pack(source: str, pack: Path) -> tuple[dict, list[dict], int]: def main() -> int: - parser = argparse.ArgumentParser(description="Import BIOS provenance from a DAT pack") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Import BIOS provenance from a DAT pack") parser.add_argument("--source", required=True, choices=SOURCES) parser.add_argument("--pack", required=True, help="DAT pack (ZIP or directory)") parser.add_argument("--output", "-o", help="Snapshot output path") diff --git a/scripts/scraper/fbneo_hash_scraper.py b/scripts/scraper/fbneo_hash_scraper.py index e2a287a0..2d5b4ad4 100644 --- a/scripts/scraper/fbneo_hash_scraper.py +++ b/scripts/scraper/fbneo_hash_scraper.py @@ -303,6 +303,7 @@ def run( def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Scrape FBNeo BIOS set hashes from upstream source", ) parser.add_argument( diff --git a/scripts/scraper/mame_hash_scraper.py b/scripts/scraper/mame_hash_scraper.py index 0590a374..8bdcb190 100644 --- a/scripts/scraper/mame_hash_scraper.py +++ b/scripts/scraper/mame_hash_scraper.py @@ -421,6 +421,7 @@ def _write_release(args: argparse.Namespace) -> None: def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( + allow_abbrev=False, prog="mame_hash_scraper", description="Fetch MAME BIOS hashes from source and merge into profiles.", ) diff --git a/scripts/scraper/redump_dat_scraper.py b/scripts/scraper/redump_dat_scraper.py index 3d9f3719..d5c3edcc 100644 --- a/scripts/scraper/redump_dat_scraper.py +++ b/scripts/scraper/redump_dat_scraper.py @@ -83,7 +83,7 @@ def fetch_snapshot() -> tuple[dict, list[dict]]: def main() -> int: - parser = argparse.ArgumentParser(description="Fetch Redump BIOS DAT provenance") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Fetch Redump BIOS DAT provenance") parser.add_argument("--dry-run", action="store_true", help="Show fetched entries") parser.add_argument( "--output", "-o", default="provenance/redump.json", help="Snapshot output path" diff --git a/scripts/scraper/romset_dat_importer.py b/scripts/scraper/romset_dat_importer.py index ab2e149c..2a3f230f 100644 --- a/scripts/scraper/romset_dat_importer.py +++ b/scripts/scraper/romset_dat_importer.py @@ -443,7 +443,7 @@ def _listxml_opener(pack: Path) -> Callable[[], io.BufferedIOBase]: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__) parser.add_argument("--source", choices=SOURCES, required=True) parser.add_argument("--pack", help="DAT file, ZIP or directory") parser.add_argument( diff --git a/scripts/scraper/targets/batocera_targets_scraper.py b/scripts/scraper/targets/batocera_targets_scraper.py index bd4dc66f..de8df37a 100644 --- a/scripts/scraper/targets/batocera_targets_scraper.py +++ b/scripts/scraper/targets/batocera_targets_scraper.py @@ -430,6 +430,7 @@ class Scraper(BaseTargetScraper): def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Scrape Batocera per-board emulator targets" ) parser.add_argument("--dry-run", action="store_true", help="Show target summary") diff --git a/scripts/scraper/targets/emudeck_targets_scraper.py b/scripts/scraper/targets/emudeck_targets_scraper.py index daed345b..93108e1b 100644 --- a/scripts/scraper/targets/emudeck_targets_scraper.py +++ b/scripts/scraper/targets/emudeck_targets_scraper.py @@ -166,7 +166,7 @@ class Scraper(BaseTargetScraper): def main() -> None: - parser = argparse.ArgumentParser(description="Scrape EmuDeck emulator targets") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape EmuDeck emulator targets") parser.add_argument("--dry-run", action="store_true", help="Show target summary") parser.add_argument("--output", "-o", help="Output YAML file") args = parser.parse_args() diff --git a/scripts/scraper/targets/recalbox_targets_scraper.py b/scripts/scraper/targets/recalbox_targets_scraper.py index 8747a14e..e1e1b7fd 100644 --- a/scripts/scraper/targets/recalbox_targets_scraper.py +++ b/scripts/scraper/targets/recalbox_targets_scraper.py @@ -104,7 +104,7 @@ class Scraper(BaseTargetScraper): def main() -> None: - parser = argparse.ArgumentParser(description="Scrape Recalbox per-board cores") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape Recalbox per-board cores") parser.add_argument("--dry-run", action="store_true", help="Show target summary") parser.add_argument("--output", "-o", help="Output YAML file") args = parser.parse_args() diff --git a/scripts/scraper/targets/retroarch_targets_scraper.py b/scripts/scraper/targets/retroarch_targets_scraper.py index 4209d3fc..12312729 100644 --- a/scripts/scraper/targets/retroarch_targets_scraper.py +++ b/scripts/scraper/targets/retroarch_targets_scraper.py @@ -168,6 +168,7 @@ class Scraper(BaseTargetScraper): def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Scrape RetroArch buildbot nightly targets" ) parser.add_argument("--dry-run", action="store_true", help="Show target summary") diff --git a/scripts/scraper/targets/retropie_targets_scraper.py b/scripts/scraper/targets/retropie_targets_scraper.py index 49a2f3a3..a87d2838 100644 --- a/scripts/scraper/targets/retropie_targets_scraper.py +++ b/scripts/scraper/targets/retropie_targets_scraper.py @@ -177,6 +177,7 @@ class Scraper(BaseTargetScraper): def main() -> None: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Scrape RetroPie package targets from scriptmodules" ) parser.add_argument("--dry-run", action="store_true", help="Show target summary") diff --git a/scripts/scraper/targets/rocknix_targets_scraper.py b/scripts/scraper/targets/rocknix_targets_scraper.py index 37b6afa2..7f99ef0a 100644 --- a/scripts/scraper/targets/rocknix_targets_scraper.py +++ b/scripts/scraper/targets/rocknix_targets_scraper.py @@ -174,7 +174,7 @@ class Scraper(BaseTargetScraper): def main() -> None: - parser = argparse.ArgumentParser(description="Scrape ROCKNIX per-device cores") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape ROCKNIX per-device cores") parser.add_argument("--dry-run", action="store_true", help="Show target summary") parser.add_argument("--output", "-o", help="Output YAML file") args = parser.parse_args() diff --git a/scripts/slots.py b/scripts/slots.py index 6bd6e2de..a9097135 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -812,6 +812,7 @@ def main() -> int: ) parser = argparse.ArgumentParser( + allow_abbrev=False, description="Report destinations where an emulator profile contradicts " "the file a platform baseline would ship.", ) diff --git a/scripts/split_pack.py b/scripts/split_pack.py index a50b7c33..0a9ebc84 100644 --- a/scripts/split_pack.py +++ b/scripts/split_pack.py @@ -219,6 +219,7 @@ def _size(text: str) -> int: def main() -> int: parser = argparse.ArgumentParser( + allow_abbrev=False, description="Split packs over the release asset limit into ZIP parts" ) parser.add_argument("target", type=Path, help="a pack, or a directory of packs") diff --git a/scripts/torrentzip.py b/scripts/torrentzip.py index 580421c0..8cc1c4a8 100644 --- a/scripts/torrentzip.py +++ b/scripts/torrentzip.py @@ -116,7 +116,7 @@ def identify_romset( def main() -> int: """Entry point: 1 when a checked archive is not TorrentZip.""" - parser = argparse.ArgumentParser(description="TorrentZip builder and checker") + parser = argparse.ArgumentParser(allow_abbrev=False, description="TorrentZip builder and checker") parser.add_argument("--check", metavar="ZIP", help="report TorrentZip conformance") parser.add_argument("--rebuild", metavar="ZIP", help="normalize an archive") parser.add_argument("--output", "-o", help="output path for --rebuild") diff --git a/scripts/validate_pr.py b/scripts/validate_pr.py index 82aadc72..2159e985 100644 --- a/scripts/validate_pr.py +++ b/scripts/validate_pr.py @@ -336,7 +336,7 @@ def _load_database_if_any(path: str) -> dict | None: def main() -> None: - parser = argparse.ArgumentParser(description="Validate BIOS file contributions") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Validate BIOS file contributions") parser.add_argument("files", nargs="*", help="Files to validate") parser.add_argument( "--changed", action="store_true", help="Auto-detect changed BIOS files" diff --git a/scripts/validate_schemas.py b/scripts/validate_schemas.py index 87f9f397..85ecea5e 100644 --- a/scripts/validate_schemas.py +++ b/scripts/validate_schemas.py @@ -336,7 +336,7 @@ def _format_errors() -> list[str]: def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__) parser.add_argument( "--source-only", action="store_true", diff --git a/scripts/validate_site.py b/scripts/validate_site.py index 343bbcd6..d5bee228 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -264,7 +264,7 @@ def validate_site(site: Path, config_path: Path) -> list[str]: def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__) parser.add_argument("--site-dir", default="site") parser.add_argument("--config", default="mkdocs.yml") parser.add_argument("--max-errors", type=int, default=100) diff --git a/scripts/verify.py b/scripts/verify.py index cd406e7e..0a86fe1a 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -2080,7 +2080,7 @@ def main() -> None: def _main(held: contextlib.ExitStack) -> None: - parser = argparse.ArgumentParser(description="Platform-native BIOS verification") + parser = argparse.ArgumentParser(allow_abbrev=False, description="Platform-native BIOS verification") parser.add_argument("--platform", "-p", help="Platform name") parser.add_argument( "--all", action="store_true", help="Verify all active platforms" diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 602e4108..29914055 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -637,6 +637,33 @@ class AutoFetchReadsListsAndPlacesWithoutClobbering(unittest.TestCase): self.assertEqual(held.read_bytes(), b"other revision") +class NoParserAcceptsAnAbbreviation(unittest.TestCase): + """argparse completes a prefix: `slots.py --emulator dolphin` became + --emulators-dir dolphin and reported 0 contested destinations, exit 0; + `pipeline.py --all` became --all-variants and built six packs per + platform. The flags read like other scripts' and were never refused.""" + + def test_every_parser_refuses_prefixes(self): + import ast + + lax = [] + for path in sorted([*(ROOT / "scripts").rglob("*.py"), ROOT / "install.py"]): + for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))): + if not isinstance(node, ast.Call): + continue + func = node.func + name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "") + if name != "ArgumentParser": + continue + if not any( + kw.arg == "allow_abbrev" + and isinstance(kw.value, ast.Constant) and kw.value.value is False + for kw in node.keywords + ): + lax.append(f"{path.relative_to(ROOT)}:{node.lineno}") + self.assertEqual(lax, []) + + class FaqRegressions(unittest.TestCase): """The FAQ states facts the code owns; these tie it back to the source.