A module-level logging.basicConfig(level=INFO) ran whenever any reach verb
imported the parser. That configured root logging process-wide and wrote plain
text to stderr, breaking reach's contract that stderr carries only JSONL events
and one verdict. A dry run of bake-biome emitted 205 KB this way, most of it a
line per body of every parsed system.
Its eight log calls now go through console.event. Per-body and per-system lines
become debug, visible only with verbose output. The four warnings stay warn, as
structured events: 66 of them across the bake scope, each an authored orbit
that contradicts the body's planet class ("too hot for temperate").
Those are real data findings and stay visible. A dry run is now two lines
plus those warnings.
Also: tooling/core/command.py carried the same two-line comment twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
"""`@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.
|
|
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]
|