Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion lib/python/base_cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2876,7 +2876,16 @@ def run_app(
invocation_token = _INVOCATION_ARGV.set(invocation_argv)
try:
bypass_token = _INVOCATION_MAIN_BYPASS.set(command)
output_capture = io.StringIO() if state.json_output else None
# A configured JSON option may be enabled by any Click-supported
# source (for example ``default_map`` or a combined short flag),
# so raw argv cannot determine capture eligibility. Buffer the
# command whenever JSON mode exists and let the parsed lifecycle
# value decide whether to emit an envelope or replay human text.
output_capture = (
io.StringIO()
if app.lifecycle_options.json is not None
else None
)
try:
if output_capture is None:
result = command.main(
Expand Down
48 changes: 48 additions & 0 deletions tests/test_json_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,54 @@ def main(ctx: base_cli.Context) -> None:
self.assertEqual(envelope["type"], "success")
self.assertEqual(envelope["details"]["stdout"], "hello from env\n")

def test_json_mode_captures_combined_short_flags(self) -> None:
app = base_cli.App(
name="json-combined-short",
log_to_file=False,
lifecycle_options=base_cli.LifecycleOptions(
json=base_cli.LifecycleOption("--json", "-j"),
),
)

@app.command()
@base_cli.option("-x", is_flag=True)
def main(ctx: base_cli.Context, x: bool) -> None:
del ctx, x
print("hello from combined flags")

with tempfile.TemporaryDirectory() as home:
result = base_cli.testing.invoke(app, ["-xj"], home=Path(home))

self.assertEqual(result.exit_code, 0, result.output)
envelope = json.loads(result.stdout)
self.assertEqual(envelope["type"], "success")
self.assertEqual(
envelope["details"]["stdout"],
"hello from combined flags\n",
)

def test_json_mode_captures_default_map_values(self) -> None:
app = base_cli.App(
name="json-default-map",
log_to_file=False,
lifecycle_options=base_cli.LifecycleOptions(
json=base_cli.LifecycleOption("--json"),
),
)

@app.command(context_settings={"default_map": {"json": True}})
def main(ctx: base_cli.Context) -> None:
del ctx
print("hello from default map")

with tempfile.TemporaryDirectory() as home:
result = base_cli.testing.invoke(app, [], home=Path(home))

self.assertEqual(result.exit_code, 0, result.output)
envelope = json.loads(result.stdout)
self.assertEqual(envelope["type"], "success")
self.assertEqual(envelope["details"]["stdout"], "hello from default map\n")

def test_json_mode_is_opt_in_and_human_output_remains_unchanged(self) -> None:
app = base_cli.App(name="human-default", log_to_file=False)

Expand Down