"""`@command` — the whole contract in one decorator (D-263). Cross-cutting concerns are decorators, never call-site discipline. The point of composing them here is that a command author **cannot apply half the contract**: there is no way to get logging without error handling, or to remember one and forget the other on the 40th command of a long porting session. That failure mode is the reason D-263 makes these decorators rather than conventions. Usage, and it goes UNDER the Typer registration so it wraps the function Typer will call: @app.command("client-version") @command def client_version() -> None: ... """ from __future__ import annotations import functools import os from collections.abc import Callable from typing import Any, TypeVar from tooling.core import jobs from tooling.core.errors import handle_errors from tooling.core.logging import logged F = TypeVar("F", bound=Callable[..., Any]) # Set by @command and asserted by the conformance test. A marker attribute is # used rather than inspecting the composition after the fact, because unwrapping # functools.wraps chains to prove "this was decorated" is brittle in exactly the # way a conformance test must not be. MARKER = "__reach_command__" def command(func: F) -> F: """Compose the invocation contract onto one command function. **Order is load-bearing, in both directions.** `handle_errors` wraps `logged`, not the reverse: the logger's `finally` then sees the ORIGINAL exception and records its type as the outcome. Invert them and the error handler converts everything to `SystemExit` first, so every failure is logged as "SystemExit" and the record says nothing about what actually went wrong — while still looking like it worked. The **job context is outermost**, wrapping both. It has to be: `logged` emits from its `finally` and `handle_errors` emits its verdict while unwinding, so a context established inside either would already be gone by the time the two most important events are written. Those would then be the only untagged lines in the log — and they are precisely the ones a detached run is read back for. """ inner = handle_errors(logged(func)) @functools.wraps(func) def wrapped(*args: Any, **kwargs: Any) -> Any: # A detached child adopts the id its parent already reported; a # foreground run mints a fresh one. # A detached child adopts the id its parent already reported; a # foreground run mints a fresh one. token = jobs.begin(os.environ.get(jobs.ENV_JOB_ID)) try: return inner(*args, **kwargs) finally: jobs.end(token) setattr(wrapped, MARKER, True) return wrapped # type: ignore[return-value]