From f45cc72ef05e86e4742d28fe1bc895c6bf385e9e Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:10:41 +0530 Subject: [PATCH] Fix pre-parse JSON flag detection --- CHANGELOG.md | 5 +++++ lib/python/base_cli/app.py | 24 ++++++++++++++------- tests/test_json_contracts.py | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f216c..149baa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ and versions are tracked in the repo-root `VERSION` file. values, and header-style `key: value` arguments before they reach logs or persisted history. +### Fixed + +- Honor combined positive/negative JSON option declarations and explicit + `--no-json` values when deciding whether pre-parse errors use JSON output. + ### Added - Add a framework choice guide, five-minute evaluation path, and clearer diff --git a/lib/python/base_cli/app.py b/lib/python/base_cli/app.py index 52ff866..1f914af 100644 --- a/lib/python/base_cli/app.py +++ b/lib/python/base_cli/app.py @@ -2901,18 +2901,26 @@ def _json_requested(args: list[str], lifecycle_options: LifecycleOptions) -> boo option = lifecycle_options.json if option is None: return False - if option.default is True: - return True + + positive_declarations, negative_declarations = _lifecycle_flag_declarations(option) + explicit_value: bool | None = None + for argument in args: + if any( + argument == declaration or argument.startswith(f"{declaration}=") for declaration in positive_declarations + ): + explicit_value = True + elif any( + argument == declaration or argument.startswith(f"{declaration}=") for declaration in negative_declarations + ): + explicit_value = False + if explicit_value is not None: + return explicit_value + if option.envvar is not None: envvars = (option.envvar,) if isinstance(option.envvar, str) else option.envvar if any(os.environ.get(name, "").lower() in {"1", "true", "yes", "on"} for name in envvars): return True - declarations = tuple(declaration for declaration in option.param_decls if declaration.startswith(("-", "/"))) - return any( - argument == declaration or argument.startswith(f"{declaration}=") - for argument in args - for declaration in declarations - ) + return option.default is True def _captured_stdout(output_capture: io.StringIO | None) -> str: diff --git a/tests/test_json_contracts.py b/tests/test_json_contracts.py index 729c31d..06e4241 100644 --- a/tests/test_json_contracts.py +++ b/tests/test_json_contracts.py @@ -215,6 +215,48 @@ def main(ctx: base_cli.Context, x: bool) -> None: "hello from combined flags\n", ) + def test_json_preparse_errors_honor_combined_positive_declaration(self) -> None: + app = base_cli.App( + name="json-combined-declaration", + log_to_file=False, + lifecycle_options=base_cli.LifecycleOptions( + json=base_cli.LifecycleOption("--json/--no-json"), + ), + ) + + @app.command() + def main(ctx: base_cli.Context) -> None: + del ctx + + with tempfile.TemporaryDirectory() as home: + result = base_cli.testing.invoke(app, ["--json", "--unknown"], home=Path(home)) + + self.assertEqual(result.exit_code, 2) + envelope = json.loads(result.stdout) + self.assertEqual(envelope["schema"], "base-cli.error") + self.assertEqual(result.stderr, "") + self.assertIn("No such option", envelope["message"]) + + def test_json_preparse_errors_honor_explicit_negative_declaration_over_true_default(self) -> None: + app = base_cli.App( + name="json-negative-declaration", + log_to_file=False, + lifecycle_options=base_cli.LifecycleOptions( + json=base_cli.LifecycleOption("--json/--no-json", default=True), + ), + ) + + @app.command() + def main(ctx: base_cli.Context) -> None: + del ctx + + with tempfile.TemporaryDirectory() as home: + result = base_cli.testing.invoke(app, ["--no-json", "--unknown"], home=Path(home)) + + self.assertEqual(result.exit_code, 2) + self.assertEqual(result.stdout, "") + self.assertIn("No such option", result.stderr or result.output) + def test_json_mode_captures_default_map_values(self) -> None: app = base_cli.App( name="json-default-map",