Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
10bb935
Truncate private helper docstrings (#162)
Jul 21, 2026
3f296f7
Normalize public bump docstrings (#162)
Jul 21, 2026
eee1a45
Normalize preflight fixture docstring (#162)
Jul 21, 2026
08b822c
Add runnable docstring examples (#162)
Jul 22, 2026
49eb3d4
Enforce DOC docstring rules across the codebase
Jul 26, 2026
b480b86
Scope DOC completeness rules to public interfaces
Jul 26, 2026
4705182
Apply docstring review feedback across the tree
Jul 26, 2026
c49e3a3
Apply second round of docstring review feedback
Jul 27, 2026
e412bf1
Trim publish.run docstring Examples for CodeScene
Jul 27, 2026
5bad6bb
Extract fixture-crate creation from create_nontrivial_workspace
Jul 27, 2026
0adc470
Document the LadingError contract in TOML-coercion Raises
Jul 27, 2026
0356674
Document _prepare_cmd_mox_context Returns and Raises
Jul 27, 2026
4169581
Complete make_plan_factory fixture docstring
Jul 27, 2026
41ffa88
Document _process_readme_transposition Returns
Jul 27, 2026
f34879d
Document E2E helper contracts and true exception type
Jul 27, 2026
57621b8
Document propagated exception contracts
Jul 27, 2026
dc47bd6
Document the true exception types in sequence/metadata Raises
Jul 28, 2026
f6cd77d
Align docstrings with the updated AGENTS.md guidance
leynos Jul 29, 2026
acf141d
Trim plan_publication docstring Examples for CodeScene
leynos Jul 30, 2026
0eee748
Correct and complete docstring contracts
leynos Jul 31, 2026
0c75b47
Document step failure modes and tidy fixture examples
leynos Jul 31, 2026
2c8e5da
Reconcile docstring work with the lockfile module split
leynos Aug 1, 2026
6278ef0
Restore WorkspaceGraph fields dropped by the entity merge
leynos Aug 1, 2026
5af1852
Complete exception contracts and correct docstring inaccuracies
leynos Aug 1, 2026
9fe366b
Align the subprocess env contract and apply the one-line helper conve…
leynos Aug 1, 2026
d66c933
Key the lockfile failure-mode docs on manifests attempted
leynos Aug 2, 2026
a6cb3b0
Widen the subprocess_runner env parameter to match the boundary
leynos Aug 2, 2026
7687469
Apply the one-line convention to two workspace coercion helpers
leynos Aug 2, 2026
82a1994
Share one typed contract for BDD CLI run results
leynos Aug 2, 2026
8fc709d
Type the end-to-end CLI result contract
leynos Aug 2, 2026
2d4ad7a
Inline the two single-use CargoMetadataParseError factories
leynos Aug 2, 2026
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
12 changes: 6 additions & 6 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ collections. It loops over every manifest, logging the start and calling
`Cargo.lock` in `lockfiles`, and on `LockfileRegenerationError` it logs the
exception and appends the manifest and error to `failures`. After the loop, if
there were no failures it logs overall success and returns the regenerated
lockfiles; otherwise it raises — when only the workspace-root lockfile was
regenerated it re-raises the original cargo error unchanged, and when several
lockfiles were regenerated it selects the first failure's cause, builds an
lockfiles; otherwise it raises — when one manifest was attempted it re-raises
the original cargo error unchanged, and when more than one manifest was
attempted it selects the first failure's cause, builds an
aggregated failure message, and raises `LockfileRegenerationError` chained from
that cause.

Expand Down Expand Up @@ -273,9 +273,9 @@ _Figure 1: Control flow of `regenerate_lockfiles` — every manifest is
attempted, and per-manifest failures are collected and reported together after
the loop._

The aggregate branch shown applies when several lockfiles are regenerated; when
only the workspace-root lockfile is processed, its lone failure is re-raised as
the original cargo error rather than wrapped in the aggregate message.
The aggregate branch shown applies when more than one manifest was attempted;
when one manifest was attempted, its lone failure is re-raised as the original
cargo error rather than wrapped in the aggregate message.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`BumpOptions` carries the dependency-injection points used by
`lading.commands.bump.run`. Lockfile operations are reached through the
Expand Down
4 changes: 2 additions & 2 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ Cargo lockfile regeneration failed for 2 manifest(s). Manifests already carry th
cargo update --workspace --manifest-path crates/b/Cargo.toml
```

When only the workspace-root lockfile is regenerated, its lone failure surfaces
the plain Cargo error instead. To recover, fix the underlying Cargo error and
When one manifest was attempted, its lone failure surfaces the plain Cargo
error instead. To recover, fix the underlying Cargo error and
rerun `lading bump`, run the printed repair command for each listed manifest,
or use `--no-rebuild-lockfiles` and regenerate the lockfiles manually before
committing the bump.
Expand Down
99 changes: 79 additions & 20 deletions lading/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,7 @@ def _resolve_allow_unpublished_workspace_deps(
live: bool,
allow_unpublished_workspace_deps: bool | None,
) -> bool:
"""Resolve the tri-state ``--allow-unpublished-workspace-deps`` flag.

An explicit flag value is honoured verbatim. When the flag is omitted the
default depends on the publish mode: ``False`` for live publishes and
``True`` for dry runs, so unpublished workspace members do not abort a
rehearsal.

Logging side effects: applying the dry-run default emits an INFO record so
operators can see the decision, and every call emits a DEBUG record with the
raw input, mode, resolved value, and the reason it was chosen.
"""
"""Resolve the tri-state ``--allow-unpublished-workspace-deps`` flag."""
if allow_unpublished_workspace_deps is not None:
resolved_value = allow_unpublished_workspace_deps
reason = "explicit flag"
Expand Down Expand Up @@ -151,13 +141,7 @@ def _resolve_allow_unpublished_workspace_deps(
def _extract_workspace_override(
tokens: cabc.Sequence[str],
) -> tuple[str | None, list[str]]:
"""Split ``--workspace-root`` from CLI tokens.

The flag can appear in either ``--workspace-root <path>`` or
``--workspace-root=<path>`` form. The last occurrence wins, matching
common CLI conventions. The returned token list can be passed directly
to :func:`cyclopts.App.__call__`.
"""
"""Split ``--workspace-root`` from CLI tokens."""
workspace: str | None = None
remainder: list[str] = []
index = 0
Expand Down Expand Up @@ -250,7 +234,25 @@ def _dispatch_and_print(tokens: cabc.Sequence[str]) -> int:


def main(argv: cabc.Sequence[str] | None = None) -> int:
"""Entry point for ``python -m lading.cli``."""
"""Entry point for ``python -m lading.cli``.

Parameters
----------
argv : cabc.Sequence[str] | None
Command-line arguments to parse; defaults to :data:`sys.argv`
without the program name when :data:`None`.

Returns
-------
int
The process exit code.

Examples
--------
>>> from lading.cli import main
>>> main(["bump", "1.2.3", "--dry-run"]) # doctest: +SKIP
0
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
try:
if argv is None:
argv = sys.argv[1:]
Expand Down Expand Up @@ -324,7 +326,33 @@ def bump(
dry_run: DryRunFlag = False,
rebuild_lockfiles: RebuildLockfilesFlag | None = None,
) -> str:
"""Update workspace manifests to ``version``."""
"""Update workspace manifests to ``version``.

Parameters
----------
version : VersionArgument
Target semantic version to write across workspace manifests.
workspace_root : WorkspaceRootOption | None
Optional path to the workspace root; resolved to the current
directory when :data:`None`.
dry_run : DryRunFlag
When ``True``, preview manifest changes without writing files.
rebuild_lockfiles : RebuildLockfilesFlag | None
Tri-state flag forwarded unresolved to the bump command, which owns
defaulting an unset value against the configuration.

Returns
-------
str
The rendered summary of the bump operation.

Examples
--------
>>> from lading.cli import bump
>>> summary = bump("1.2.3", dry_run=True) # doctest: +SKIP
>>> "Dry run; would update version to 1.2.3 in" in summary # doctest: +SKIP
True
"""
resolved = normalise_workspace_root(workspace_root)
return _run_with_context(
resolved,
Expand Down Expand Up @@ -360,6 +388,37 @@ def publish(
The command performs pre-flight validation, stages the workspace, runs
``cargo package`` for each publishable crate, and then executes ``cargo
publish`` (dry-run by default, live when ``--live`` is supplied).

Parameters
----------
workspace_root : WorkspaceRootOption | None
Optional path to the workspace root; resolved to the current
directory when :data:`None`.
forbid_dirty : ForbidDirtyFlag
When ``True``, require a clean working tree before pre-flight checks.
live : LiveFlag
When ``True``, run ``cargo publish`` without ``--dry-run``.
allow_unpublished_workspace_deps : AllowUnpublishedWorkspaceDepsFlag
Tri-state override for unpublished sibling workspace dependencies;
resolved against the publish mode when omitted.

Returns
-------
str
The rendered summary of the publish operation.

Examples
--------
The rendered summary is a formatted publication plan followed by a
blank line and staging-summary lines.

>>> from lading.cli import publish
>>> summary = publish(live=False) # doctest: +SKIP
>>> "Staged workspace at:" in summary # doctest: +SKIP
True
>>> readmes = "Workspace READMEs are handled by lading bump."
>>> readmes in summary # doctest: +SKIP
True
"""
resolved = normalise_workspace_root(workspace_root)
return _run_with_context(
Expand Down
21 changes: 20 additions & 1 deletion lading/commands/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,26 @@


def describe_crates(workspace: WorkspaceGraph) -> str:
"""Return a human-friendly crate count summary."""
"""Return a human-friendly crate count summary.

Parameters
----------
workspace : WorkspaceGraph
Workspace graph whose ``crates`` collection is counted.

Returns
-------
str
Crate count with a singular or plural ``crate`` label.

Examples
--------
>>> from types import SimpleNamespace
>>> describe_crates(SimpleNamespace(crates=("only",)))
'1 crate'
>>> describe_crates(SimpleNamespace(crates=("first", "second")))
'2 crates'
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
count = len(workspace.crates)
label = "crate" if count == 1 else "crates"
return f"{count} {label}"
41 changes: 33 additions & 8 deletions lading/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,37 @@ def run(
*,
options: BumpOptions | None = None,
) -> str:
"""Update workspace and crate manifest versions to ``target_version``."""
"""Update workspace and crate manifest versions to ``target_version``.

Parameters
----------
workspace_root : Path | str
Filesystem path to the workspace root containing the top-level
``Cargo.toml``.
target_version : str
Semantic version string to apply to every updated manifest.
options : BumpOptions | None, optional
Run configuration such as the dry-run flag, lockfile-rebuild override,
and pre-loaded configuration and workspace graph. ``None`` loads
configuration and the workspace from ``workspace_root``.

Returns
-------
str
Human-readable summary of the manifests, documents, README files, and
lockfiles that were changed, or reported as would be changed during
a dry run.

Examples
--------
>>> from lading.commands.bump import BumpOptions, run
>>> run( # doctest: +SKIP
... "path/to/workspace",
... "1.2.0",
... options=BumpOptions(dry_run=True),
... )
'Dry run; would update version to 1.2.0 in ...'
"""
context = _initialize_bump_context(workspace_root, options)
LOGGER.debug(
"Bump context initialised: %d excluded crate(s), %d to update",
Expand Down Expand Up @@ -161,7 +191,7 @@ def _initialize_bump_context(
workspace_root: Path | str,
options: BumpOptions | None,
) -> _BumpContext:
"""Return initialised bump context for ``workspace_root``."""
"""Return the initialised bump context for ``workspace_root``."""
resolved_options = BumpOptions() if options is None else options
root_path = normalise_workspace_root(workspace_root)
configuration = resolved_options.configuration
Expand Down Expand Up @@ -364,12 +394,7 @@ def _apply_crate_manifest_update(
target_version: str,
context: _BumpContext,
) -> _CrateManifestOutcome:
"""Apply updates for ``crate`` and return the closed manifest outcome.

The crate sets are read from ``context`` — they are derived exactly once
in :func:`_initialize_bump_context` (issue #97); helpers must not
recompute them per crate.
"""
"""Apply updates for ``crate`` and return the closed manifest outcome."""
selectors = _determine_package_selectors(crate.name, context.excluded)
dependency_sections = _dependency_sections_for_crate(
crate, context.updated_crate_names
Expand Down
2 changes: 1 addition & 1 deletion lading/commands/bump_lockfile_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def resolve_lockfile_paths(
# ["/workspace/Cargo.lock", "/workspace/fixtures/minimal/Cargo.lock"]
```

"""
""" # noqa: DOC502 -- propagated from resolve_manifest_paths, not raised here
manifests = resolve_manifest_paths(workspace_root, lockfile_manifests)
return tuple(manifest.parent / "Cargo.lock" for manifest in manifests)

Expand Down
20 changes: 11 additions & 9 deletions lading/commands/bump_lockfile_regeneration.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,23 @@ def regenerate_lockfiles(
LockfileRegenerationError
If any configured manifest path is invalid (outside the workspace or
not named ``Cargo.toml``), or — after every manifest has been
attempted — if ``cargo update --workspace`` failed. When only the
workspace-root lockfile is regenerated, the original cargo error is
re-raised unchanged. When several lockfiles are regenerated, one
aggregated error is raised whose message lists each failed manifest
with a repair command.
attempted — if ``cargo update --workspace`` failed. When only one
manifest was attempted, the original cargo error is re-raised
unchanged. When more than one manifest was attempted, one aggregated
error is raised whose message lists each failed manifest with a
repair command.

Notes
-----
**Partial-update semantics:** regeneration is not atomic and successful
updates are not rolled back when a later manifest fails. Every manifest
is attempted (issue #84), so a single cargo failure does not leave
unrelated lockfiles silently stale. When several lockfiles are
regenerated, the aggregated error tells the operator exactly which
lockfiles still need repair and how; a lone root-lockfile failure needs no
such disambiguation and surfaces the plain cargo error.
unrelated lockfiles silently stale. How the failure surfaces depends on
how many manifests were *attempted*, not on how many lockfiles were
regenerated: when one manifest was attempted, its original cargo error is
re-raised unchanged, needing no disambiguation; when multiple manifests
were attempted, the aggregated error tells the operator exactly which
lockfiles still need repair and how.

Examples
--------
Expand Down
53 changes: 3 additions & 50 deletions lading/commands/bump_manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,43 +61,15 @@ def _determine_package_selectors(
crate_name: str,
excluded: cabc.Collection[str],
) -> tuple[tuple[str, ...], ...]:
"""Return package selectors for the crate, respecting exclusion rules.

Parameters
----------
crate_name
Name of the crate to check.
excluded
Collection of excluded crate names.

Returns
-------
tuple[tuple[str, ...], ...]
Package selectors, or an empty tuple when the crate is excluded.

"""
"""Return package selectors for the crate, respecting exclusion rules."""
return () if crate_name in excluded else (("package",),)


def _should_skip_crate_update(
selectors: tuple[tuple[str, ...], ...],
dependency_sections: cabc.Mapping[str, cabc.Collection[str]],
) -> bool:
"""Return whether a crate update can be skipped for lack of work.

Parameters
----------
selectors
Package selectors identified for the crate.
dependency_sections
Dependency sections to update for the crate.

Returns
-------
bool
``True`` when both ``selectors`` and ``dependency_sections`` are empty.

"""
"""Return whether a crate update can be skipped for lack of work."""
return not selectors and not dependency_sections


Expand All @@ -117,26 +89,7 @@ def _update_manifest(
target_version: str,
options: BumpOptions,
) -> bool:
"""Apply ``target_version`` to each table described by ``selectors``.

Parameters
----------
manifest_path
Path to the Cargo.toml manifest file.
selectors
Key tuples identifying version tables to update.
target_version
The target version to apply.
options
Bump options controlling dry-run, dependency sections, and whether to
include workspace-level dependency sections.

Returns
-------
bool
``True`` when any changes were made.

"""
"""Apply ``target_version`` to each selected table and dependency section."""
document = bump_toml.parse_manifest(manifest_path)
changed = False
for selector in selectors:
Expand Down
3 changes: 3 additions & 0 deletions lading/commands/bump_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ def _format_manifest_path(manifest_path: Path, workspace_root: Path) -> str:
CrossHair contracts (issue #95); model-check with ``make crosshair``.

post: isinstance(__return__, str)

Falls back to returning ``manifest_path`` exactly as given, unmodified,
when it cannot be made relative to ``workspace_root``.
"""
try:
relative = manifest_path.relative_to(workspace_root)
Expand Down
Loading
Loading