feat: add engine descriptor module (Meta/Arg/Flag/Opt/Rest, argparse)

This commit is contained in:
deytenit committed 2026-07-04 21:20:28 +03:00
1 parent e9c7e13848
commit f96c53add9
2 files changed
+116

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# engine/descriptor.py
"""Declarative action descriptors — successor to the bash action_metadata() heredoc.
Meta drives BOTH the help text and argparse parsing. Exit code 64 (EX_USAGE) on a
usage error, matching the bash engine.
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass, field
from typing import List
class _Parser(argparse.ArgumentParser):
"""ArgumentParser that exits 64 (EX_USAGE) instead of 2 on a usage error."""
def error(self, message: str):
self.print_usage()
self.exit(64, f"{self.prog}: error: {message}\n")
@dataclass
class Arg:
"""A positional argument. required=False -> optional positional (default None)."""
name: str
help: str = ""
required: bool = True
def add_to(self, p: argparse.ArgumentParser) -> None:
if self.required:
p.add_argument(self.name, help=self.help)
else:
p.add_argument(self.name, nargs="?", default=None, help=self.help)
@dataclass
class Opt:
"""An optional value flag, e.g. --tier PATH, with a default."""
name: str
help: str = ""
default: object = None
def add_to(self, p: argparse.ArgumentParser) -> None:
p.add_argument(self.name, default=self.default, help=self.help)
@dataclass
class Flag:
"""A boolean flag, e.g. --dry-run (store_true)."""
name: str
help: str = ""
def add_to(self, p: argparse.ArgumentParser) -> None:
p.add_argument(self.name, action="store_true", help=self.help)
@dataclass
class Rest:
"""Greedy passthrough of all remaining argv (argparse.REMAINDER)."""
name: str
help: str = ""
def add_to(self, p: argparse.ArgumentParser) -> None:
p.add_argument(self.name, nargs=argparse.REMAINDER, help=self.help)
@dataclass
class Meta:
summary: str
args: List[object] = field(default_factory=list)
def build_parser(meta: Meta, prog: str) -> argparse.ArgumentParser:
p = _Parser(prog=prog, description=meta.summary)
for a in meta.args:
a.add_to(p)
return p
+39
View File
@@ -0,0 +1,39 @@
# tests/test_descriptor.py
import unittest
from engine.descriptor import Meta, Arg, Flag, Opt, Rest, build_parser
class TestDescriptor(unittest.TestCase):
def test_required_positional(self):
m = Meta("s", args=[Arg("src", "source")])
ns = build_parser(m, "apex x").parse_args(["a"])
self.assertEqual(ns.src, "a")
def test_optional_positional_defaults_none(self):
m = Meta("s", args=[Arg("src", "s"), Arg("bot", "b", required=False)])
ns = build_parser(m, "apex x").parse_args(["a"])
self.assertEqual(ns.src, "a")
self.assertIsNone(ns.bot)
def test_flag(self):
m = Meta("s", args=[Flag("--dry-run", "d")])
self.assertFalse(build_parser(m, "x").parse_args([]).dry_run)
self.assertTrue(build_parser(m, "x").parse_args(["--dry-run"]).dry_run)
def test_opt_with_default(self):
m = Meta("s", args=[Opt("--tier", "t", default="/d")])
self.assertEqual(build_parser(m, "x").parse_args([]).tier, "/d")
def test_rest_collects_remainder(self):
m = Meta("s", args=[Arg("action", "a"), Rest("extra", "e")])
ns = build_parser(m, "x").parse_args(["up", "--force-recreate", "-x"])
self.assertEqual(ns.action, "up")
self.assertEqual(ns.extra, ["--force-recreate", "-x"])
def test_usage_error_exits_64(self):
m = Meta("s", args=[Arg("src", "s")])
with self.assertRaises(SystemExit) as cm:
build_parser(m, "x").parse_args([]) # missing required
self.assertEqual(cm.exception.code, 64)
if __name__ == "__main__":
unittest.main()