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
50 changes: 25 additions & 25 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1602,7 +1602,7 @@ base_cli.testing
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Installed base-cli distribution version exposed by the public facade.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1617,7 +1617,7 @@ value = base_cli.__version__
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Describe the wire type and nullability of one command-record field.
**Behavior:** Schema field descriptor for a required boolean command-record value.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1632,7 +1632,7 @@ value = base_cli.BOOLEAN
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Type variable.
**Behavior:** Consumer-owned application-state type parameter carried by Context.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1647,7 +1647,7 @@ value = base_cli.ApplicationStateT
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Public facade symbol; see the linked contract and source annotations for details.
**Behavior:** Callable type for canonicalizing command filter names.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1662,7 +1662,7 @@ value = base_cli.CommandFilterNormalizer
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Entry-point group name used to discover command extensions.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1677,7 +1677,7 @@ value = base_cli.COMMAND_ENTRY_POINT_GROUP
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Type variable.
**Behavior:** Consumer-owned configuration type parameter carried by Context and App.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1692,7 +1692,7 @@ value = base_cli.ConfigT
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Built-in immutable sequence.
**Behavior:** Supported base-cli extension entry-point groups in discovery order.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1707,7 +1707,7 @@ value = base_cli.ENTRY_POINT_GROUPS
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Extension API version negotiated through entry-point metadata.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1722,7 +1722,7 @@ value = base_cli.EXTENSION_API_VERSION
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Own an isolated set of command record schemas.
**Behavior:** Process-local registry containing built-in and consumer record schemas.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1737,7 +1737,7 @@ value = base_cli.DEFAULT_SCHEMA_REGISTRY
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Metadata key used to mark lifecycle-owned Click parameters.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1752,7 +1752,7 @@ value = base_cli.LIFECYCLE_META_KEY
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Describe the wire type and nullability of one command-record field.
**Behavior:** Schema field descriptor for a nullable string command-record value.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1767,7 +1767,7 @@ value = base_cli.NULLABLE_STRING
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Describe the wire type and nullability of one command-record field.
**Behavior:** Schema field descriptor for a required string command-record value.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1782,7 +1782,7 @@ value = base_cli.STRING
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** int([x]) -> integer int(x, base=10) -> integer
**Behavior:** Version number for the stable JSON output, error, and log contracts.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1797,7 +1797,7 @@ value = base_cli.JSON_CONTRACT_VERSION
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Schema identifier emitted by machine-readable error envelopes.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1812,7 +1812,7 @@ value = base_cli.JSON_ERROR_SCHEMA
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Schema identifier emitted by structured JSON log records.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1827,7 +1827,7 @@ value = base_cli.JSON_LOG_SCHEMA
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Schema identifier emitted by successful JSON output envelopes.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1842,7 +1842,7 @@ value = base_cli.JSON_OUTPUT_SCHEMA
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** int([x]) -> integer int(x, base=10) -> integer
**Behavior:** Maximum retained message length for one structured JSON log record.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1857,7 +1857,7 @@ value = base_cli.MAX_JSON_LOG_MESSAGE_LENGTH
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Schema identifier emitted by versioned newline-delimited record output.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1872,7 +1872,7 @@ value = base_cli.NDJSON_SCHEMA
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** int([x]) -> integer int(x, base=10) -> integer
**Behavior:** Version number for the newline-delimited record contract.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1887,7 +1887,7 @@ value = base_cli.NDJSON_SCHEMA_VERSION
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Built-in immutable sequence.
**Behavior:** Supported public output format names and their resolution order.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1902,7 +1902,7 @@ value = base_cli.PUBLIC_OUTPUT_FORMATS
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** A Mapping is a generic container for associating key/value pairs.
**Behavior:** Mapping shape accepted by the versioned command-record serializers.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1917,7 +1917,7 @@ value = base_cli.StructuredRecord
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
**Behavior:** Mutable registry mapping consumer record names to FieldSpec definitions.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1932,7 +1932,7 @@ value = base_cli.RECORD_SCHEMAS
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Entry-point group name used to discover optional plugin extensions.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1947,7 +1947,7 @@ value = base_cli.PLUGIN_ENTRY_POINT_GROUP
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
**Behavior:** Entry-point group name used to discover profile extensions.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand All @@ -1962,7 +1962,7 @@ value = base_cli.PROFILE_ENTRY_POINT_GROUP
**Kind:** constant/type
**Signature:** (value is not callable)

**Behavior:** Type variable.
**Behavior:** Consumer-owned service-container type parameter carried by Context.

**Errors and compatibility:** Follow the contract documentation linked in the description. Callers should handle the documented exception types and pin a compatible minor release.

Expand Down
8 changes: 6 additions & 2 deletions docs/typer-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,12 @@ command = adapter.attach(name="example")

`adapter.command` exposes the cached Click command returned by the most recent
`attach()` call. Reuse that object when the application needs to inspect or
pass the generated command to another integration boundary; calling
`attach()` again refreshes the cached command.
pass the generated command to another integration boundary. Repeating
`attach()` without new lifecycle arguments is idempotent and returns the same
Click object. A second call with lifecycle arguments after attachment is
rejected with
`TypeError`; construct a new adapter when the lifecycle configuration needs to
change rather than silently mutating an attached command tree.

Typer is an optional extra and is imported lazily. Importing `base_cli` and
using the Click integration never imports or requires Typer.
Expand Down
8 changes: 6 additions & 2 deletions lib/python/base_cli/_private_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,16 @@ def _sync_directory(parent_fd: int) -> None:
def _replace_with_retry(source: Path, destination: Path) -> None:
"""Replace a private file, tolerating transient Windows sharing races."""

attempts = 1 if os.name != "nt" else 10
# Antivirus/indexer handles and concurrent writers can hold the destination
# briefly on Windows. Use a bounded, linear backoff long enough for those
# transient sharing violations without making a persistent permission error
# unbounded.
attempts = 1 if os.name != "nt" else 50
for attempt in range(attempts):
try:
os.replace(source, destination)
return
except PermissionError:
if attempt == attempts - 1:
raise
time.sleep(0.001 * (attempt + 1))
time.sleep(0.005 * (attempt + 1))
43 changes: 41 additions & 2 deletions scripts/generate_api_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@
HEADING_PATTERN = re.compile(r"^### `([^`]+)`$", re.MULTILINE)


SYMBOL_DESCRIPTIONS = {
"__version__": "Installed base-cli distribution version exposed by the public facade.",
"BOOLEAN": "Schema field descriptor for a required boolean command-record value.",
"ApplicationStateT": "Consumer-owned application-state type parameter carried by Context.",
"CommandFilterNormalizer": "Callable type for canonicalizing command filter names.",
"COMMAND_ENTRY_POINT_GROUP": "Entry-point group name used to discover command extensions.",
"ConfigT": "Consumer-owned configuration type parameter carried by Context and App.",
"ENTRY_POINT_GROUPS": "Supported base-cli extension entry-point groups in discovery order.",
"EXTENSION_API_VERSION": "Extension API version negotiated through entry-point metadata.",
"DEFAULT_SCHEMA_REGISTRY": "Process-local registry containing built-in and consumer record schemas.",
"LIFECYCLE_META_KEY": "Metadata key used to mark lifecycle-owned Click parameters.",
"NULLABLE_STRING": "Schema field descriptor for a nullable string command-record value.",
"STRING": "Schema field descriptor for a required string command-record value.",
"JSON_CONTRACT_VERSION": "Version number for the stable JSON output, error, and log contracts.",
"JSON_ERROR_SCHEMA": "Schema identifier emitted by machine-readable error envelopes.",
"JSON_LOG_SCHEMA": "Schema identifier emitted by structured JSON log records.",
"JSON_OUTPUT_SCHEMA": "Schema identifier emitted by successful JSON output envelopes.",
"MAX_JSON_LOG_MESSAGE_LENGTH": "Maximum retained message length for one structured JSON log record.",
"NDJSON_SCHEMA": "Schema identifier emitted by versioned newline-delimited record output.",
"NDJSON_SCHEMA_VERSION": "Version number for the newline-delimited record contract.",
"PUBLIC_OUTPUT_FORMATS": "Supported public output format names and their resolution order.",
"StructuredRecord": "Mapping shape accepted by the versioned command-record serializers.",
"RECORD_SCHEMAS": "Mutable registry mapping consumer record names to FieldSpec definitions.",
"PLUGIN_ENTRY_POINT_GROUP": "Entry-point group name used to discover optional plugin extensions.",
"PROFILE_ENTRY_POINT_GROUP": "Entry-point group name used to discover profile extensions.",
"ServicesT": "Consumer-owned service-container type parameter carried by Context.",
}


def _load_package() -> Any:
sys.path.insert(0, str(PACKAGE_ROOT))
import base_cli
Expand Down Expand Up @@ -53,7 +82,9 @@ def _signature(name: str, value: Any) -> str:
return "(value is not callable)"


def _description(value: Any) -> str:
def _description(name: str, value: Any) -> str:
if name in SYMBOL_DESCRIPTIONS:
return SYMBOL_DESCRIPTIONS[name]
doc = inspect.getdoc(value)
if not doc:
return "Public facade symbol; see the linked contract and source annotations for details."
Expand Down Expand Up @@ -82,7 +113,7 @@ def render(base_cli: Any) -> str:
f"**Kind:** {kind} ",
f"**Signature:** {_signature(name, value)}",
"",
f"**Behavior:** {_description(value)}",
f"**Behavior:** {_description(name, value)}",
"",
"**Errors and compatibility:** Follow the contract documentation linked in the description. "
"Callers should handle the documented exception types and pin a compatible minor release.",
Expand Down Expand Up @@ -138,6 +169,14 @@ def main() -> None:
if documented != exported:
print("generated API headings do not match base_cli.__all__", file=sys.stderr)
raise SystemExit(1)
for name in exported:
value = getattr(base_cli, name)
if _kind(value) == "constant/type" and name not in SYMBOL_DESCRIPTIONS:
print(f"constant/type {name} has no symbol-owned description", file=sys.stderr)
raise SystemExit(1)
if "str(object='')" in current or "int([x])" in current or "dict(**kwargs)" in current:
print("generated API reference contains built-in constructor prose", file=sys.stderr)
raise SystemExit(1)
print(f"Validated {len(exported)} public API symbols.")
return
REFERENCE.write_text(rendered, encoding="utf-8")
Expand Down
17 changes: 17 additions & 0 deletions tests/test_generated_api_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def test_generated_reference_uses_framework_descriptions_for_runtime_constants() -> None:
reference = (ROOT / "docs" / "api-reference.md").read_text(encoding="utf-8")
assert "Installed base-cli distribution version exposed by the public facade." in reference
assert "Version number for the stable JSON output, error, and log contracts." in reference
assert "str(object='')" not in reference
assert "dict(**kwargs)" not in reference


def test_typer_guide_states_repeated_attachment_contract() -> None:
guide = (ROOT / "docs" / "typer-adapter.md").read_text(encoding="utf-8")
assert "without new lifecycle arguments is idempotent" in guide
assert "lifecycle arguments after attachment is" in guide
14 changes: 14 additions & 0 deletions tests/test_typer_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ def status() -> None:
self.assertIs(command, adapter.command)
self.assertEqual(command.name, "cached-cli")

def test_repeated_attachment_is_idempotent_and_changed_arguments_are_rejected(self) -> None:
cli = self.typer.Typer()

@cli.command()
def status() -> None:
self.typer.echo("ready")

adapter = base_cli.TyperAdapter(cli)
first = adapter.attach(name="cached-cli", log_to_file=False)
second = adapter.attach()
self.assertIs(first, second)
with self.assertRaisesRegex(TypeError, "cannot be changed"):
adapter.attach(name="other-cli", log_to_file=False)

def test_adapter_uses_owner_dialect_for_version_option(self) -> None:
cli = self.typer.Typer()

Expand Down
Loading