Skip to content

Add Cyclopts CLI scaffolding for lading - #5

Merged
leynos merged 6 commits into
mainfrom
codex/implement-lading-project-structure-and-cli
Oct 6, 2025
Merged

Add Cyclopts CLI scaffolding for lading#5
leynos merged 6 commits into
mainfrom
codex/implement-lading-project-structure-and-cli

Conversation

@leynos

@leynos leynos commented Oct 5, 2025

Copy link
Copy Markdown
Owner

Summary

  • add the initial lading package with a Cyclopts CLI that exposes bump and publish placeholders and honours --workspace-root
  • cover the CLI shell with pytest unit tests and pytest-bdd behaviour tests using cmd-mox
  • document the scaffolding and update project configuration, including the roadmap and usage guide

Testing

  • make check-fmt
  • make lint
  • make typecheck
  • make test

https://chatgpt.com/codex/tasks/task_e_68e1b348fb0883229d1ca0883fd0d0c8

Summary by Sourcery

Add initial scaffolding for the lading CLI toolkit with cyclopts, including placeholder bump and publish commands, comprehensive tests, and supporting documentation.

New Features:

  • Scaffold initial lading CLI with bump and publish placeholder subcommands and global --workspace-root flag

Enhancements:

  • Update project configuration (pyproject.toml, Makefile) to include the lading package and testing dependencies

Documentation:

  • Document CLI design, update roadmap, and add a usage guide for the new lading commands

Tests:

  • Add pytest unit tests and pytest-bdd behavior tests using cmd-mox to cover end-to-end CLI invocation

Summary by CodeRabbit

  • New Features

    • Added a CLI with bump and publish subcommands, global --workspace-root option, and a python -m entry point; package exposes CLI entrypoint and app.
  • Documentation

    • Added usage guide and implementation notes; updated roadmap to mark project initialization and CLI shell complete.
  • Utilities

    • Added workspace-root normalization helper used by the CLI.
  • Tests

    • Added BDD and unit tests covering CLI dispatch, option parsing, environment handling, and placeholder outputs.
  • Chores

    • Updated project config, typecheck paths, scripts, and development dependencies.

Introduce the lading package with a Cyclopts-powered CLI shell.
@sourcery-ai

sourcery-ai Bot commented Oct 5, 2025

Copy link
Copy Markdown

Reviewer's Guide

This PR adds initial scaffolding for the lading CLI using Cyclopts, including placeholder bump and publish commands, comprehensive tests and behaviour suites, updated documentation, and necessary project configuration changes to integrate the new package.

Sequence diagram for lading CLI invocation and workspace root handling

sequenceDiagram
    actor User
    participant CLI as lading.cli.main
    participant Env as Environment
    participant Cyclopts as cyclopts.App
    User->>CLI: Run `python -m lading.cli [--workspace-root <path>] <subcommand>`
    CLI->>CLI: _extract_workspace_override(argv)
    CLI->>CLI: _normalise_workspace_root(workspace_override)
    CLI->>Env: Set LADING_WORKSPACE_ROOT env var
    CLI->>Cyclopts: Dispatch subcommand (bump/publish)
    Cyclopts->>CLI: Return result
    CLI->>User: Print acknowledgement message
Loading

File-Level Changes

Change Details Files
Introduce Cyclopts-based CLI scaffolding with bump and publish commands
  • Add lading/cli.py defining App, parameters, main entrypoint, and subcommands
  • Implement --workspace-root flag extraction, normalization, and environment injection
  • Provide context manager for LADING_WORKSPACE_ROOT environment variable
  • Dispatch commands and print placeholder acknowledgements
lading/cli.py
lading/__init__.py
lading/commands/bump.py
lading/commands/publish.py
Add unit and BDD tests for the new CLI scaffolding
  • Unit tests for workspace override extraction and main dispatch logic
  • BDD tests using pytest-bdd and cmd-mox for end-to-end CLI invocation
  • Conftest fixture to manage repo_root and isolate environment variable
tests/unit/test_cli.py
tests/bdd/steps/test_cli_steps.py
tests/bdd/features/cli.feature
tests/conftest.py
Document the new CLI scaffolding and update usage guidance
  • Append implementation notes to design doc (lading-design.md)
  • Mark CLI shell tasks complete in roadmap (roadmap.md)
  • Add detailed usage guide for global options and subcommands (usage-guide.md)
docs/lading-design.md
docs/roadmap.md
docs/usage-guide.md
Update project configuration for the lading package
  • Add pytest-bdd and cmd-mox to dev dependency groups
  • Include lading in Pyright type checking and setuptools package discovery
  • Adjust Makefile typecheck command to include lading as search path
pyproject.toml
Makefile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 5, 2025

Copy link
Copy Markdown

Warning

Rate limit exceeded

@leynos has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6a21fdd and edd65e6.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • pyproject.toml (2 hunks)
  • tests/unit/test_cli.py (1 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a Cyclopts-based CLI scaffold for lading (app, main, bump, publish) with workspace-root parsing and temporary LADING_WORKSPACE_ROOT handling, package and typecheck config updates, filesystem helper and exports, documentation and BDD/unit tests, and a Makefile typecheck tweak to include lading in search paths.

Changes

Cohort / File(s) Summary of changes
Build config
Makefile
typecheck target now passes --extra-search-path lading in addition to --extra-search-path crate_tools.
Documentation
docs/lading-design.md, docs/roadmap.md, docs/usage-guide.md
Add CLI implementation notes and usage guide; mark initialise/CLI tasks done in roadmap.
Package init
lading/__init__.py
New package initializer exposing app and main from lading.cli via __all__.
CLI module
lading/cli.py
New Cyclopts CLI: exposes app, main, bump, publish; parses and normalises --workspace-root (last-wins), validates presence, temporarily sets LADING_WORKSPACE_ROOT, dispatches to commands, and maps results/exit codes. Adds constants and a WorkspaceRootOption alias.
Commands
lading/commands/__init__.py, lading/commands/bump.py, lading/commands/publish.py
New commands package and placeholder run(workspace_root: Path) -> str implementations returning messages containing the normalized workspace path.
Project config
pyproject.toml
Add project script lading = "lading.cli:main", dev deps (pytest-bdd, cmd-mox), include lading in pyright and setuptools package discovery.
Utils
lading/utils/__init__.py, lading/utils/path.py
Export normalise_workspace_root and implement it to expand/resolve a provided path or default to the current working directory.
BDD tests & fixtures
tests/bdd/features/cli.feature, tests/bdd/steps/test_cli_steps.py, tests/conftest.py
Add BDD feature and steps for bump/publish; fixtures include repo_root and autouse env isolation for LADING_WORKSPACE_ROOT; integrate cmd_mox plugin.
Unit tests
tests/unit/test_cli.py, tests/unit/test_commands_placeholder.py
Unit tests for workspace-root extraction, error cases, CLI dispatch behavior, and placeholder commands verifying normalized path output.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as lading.cli.main
  participant Env as LADING_WORKSPACE_ROOT
  participant App as Cyclopts App
  participant Cmd as command.run
  participant Out as Stdout

  User->>CLI: python -m lading.cli [--workspace-root <p>] <subcmd>
  CLI->>CLI: extract last --workspace-root, validate, normalise
  alt missing or malformed
    CLI-->>User: SystemExit (workspace-root required)
  else valid
    CLI->>Env: set LADING_WORKSPACE_ROOT (temporarily)
    CLI->>App: dispatch remaining argv
    App->>Cmd: run(workspace_root)
    Cmd-->>App: "placeholder invoked for <path>"
    App-->>CLI: result string
    CLI->>Out: print(result)
    CLI->>Env: restore previous LADING_WORKSPACE_ROOT
    CLI-->>User: exit 0
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit taps the CLI at dawn,
"Bump!" and "Publish!" hop across the lawn.
Paths polished, env tucked in tight,
Tests scamper by beneath the light.
I nibble bugs and nudge it right. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the main change by announcing the addition of a Cyclopts-based CLI scaffold for the lading package, matching the PR’s primary objective of introducing the new CLI commands and structure.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Consider adding a console_scripts entry point in pyproject.toml so users can invoke lading directly instead of python -m lading.cli.
  • The usage guide links to cmd-mox-usage-guide.md but that file isn’t in the diff—either add it or remove/update the broken link.
  • The workspace‐root normalization logic is duplicated in both cli.py and the command modules; extracting it into a shared helper could reduce redundancy.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider adding a console_scripts entry point in pyproject.toml so users can invoke `lading` directly instead of `python -m lading.cli`.
- The usage guide links to `cmd-mox-usage-guide.md` but that file isn’t in the diff—either add it or remove/update the broken link.
- The workspace‐root normalization logic is duplicated in both `cli.py` and the command modules; extracting it into a shared helper could reduce redundancy.

## Individual Comments

### Comment 1
<location> `lading/cli.py:96-103` </location>
<code_context>
+    return 0
+
+
+def main(argv: typ.Sequence[str] | None = None) -> int:
+    """Entry point for ``python -m lading.cli``."""
+    if argv is None:
</code_context>

<issue_to_address>
**suggestion:** main does not handle KeyboardInterrupt or unexpected exceptions.

Catching these exceptions will allow the CLI to exit gracefully and display a user-friendly error message.

```suggestion
def main(argv: typ.Sequence[str] | None = None) -> int:
    """Entry point for ``python -m lading.cli``."""
    try:
        if argv is None:
            argv = sys.argv[1:]
        workspace_override, remaining = _extract_workspace_override(list(argv))
        workspace_root = _normalise_workspace_root(workspace_override)
        with _workspace_env(workspace_root):
            return _dispatch_and_print(remaining)
    except KeyboardInterrupt:
        print("\nOperation cancelled by user.", file=sys.stderr)
        return 130  # Standard exit code for interrupted process
    except Exception as exc:
        print(f"Unexpected error: {exc}", file=sys.stderr)
        return 1
```
</issue_to_address>

### Comment 2
<location> `lading/commands/bump.py:10` </location>
<code_context>
+from plumbum import local
+
+
+def run(workspace_root: Path) -> str:
+    """Return a placeholder message for the bump command.
+
</code_context>

<issue_to_address>
**suggestion:** Redundant use of plumbum.local.path for workspace_root normalization.

Path normalization for workspace_root is duplicated in bump.py and publish.py, while _normalise_workspace_root in cli.py already handles this. Centralize this logic to prevent inconsistencies.

Suggested implementation:

```python

```

```python
def run(workspace_root: Path) -> str:
    """Return a placeholder message for the bump command.

```

Make sure that wherever `run()` is called (likely in your CLI entrypoint), you use `_normalise_workspace_root` from `cli.py` to normalize the path before passing it to `run()`. This will centralize the normalization logic and prevent duplication.
</issue_to_address>

### Comment 3
<location> `lading/commands/publish.py:10` </location>
<code_context>
+from plumbum import local
+
+
+def run(workspace_root: Path) -> str:
+    """Return a placeholder message for the bump command.
+
</code_context>

<issue_to_address>
**suggestion:** Redundant workspace_root normalization logic duplicated from bump.py.

Refactor the normalization logic into a shared utility to avoid duplication and enhance maintainability.

Suggested implementation:

```python
from plumbum import local
from lading.utils.path import normalize_workspace_root


def run(workspace_root: Path) -> str:
    """Return a placeholder message for the publish command."""
    candidate = normalize_workspace_root(workspace_root)

```

You will need to implement the `normalize_workspace_root` function in `lading/utils/path.py` and refactor any other usages (such as in `bump.py`) to use this shared utility.
</issue_to_address>

### Comment 4
<location> `tests/unit/test_cli.py:48-51` </location>
<code_context>
+    assert remaining == expected_remaining
+
+
+def test_extract_workspace_override_requires_value() -> None:
+    """Require a value whenever ``--workspace-root`` appears."""
+    with pytest.raises(SystemExit):
+        cli._extract_workspace_override(["--workspace-root"])
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing test for '--workspace-root=' with no value.

Add a test to ensure that '--workspace-root=' without a value triggers SystemExit, matching the implementation's behavior.

```suggestion
def test_extract_workspace_override_requires_value() -> None:
    """Require a value whenever ``--workspace-root`` appears."""
    with pytest.raises(SystemExit):
        cli._extract_workspace_override(["--workspace-root"])

def test_extract_workspace_override_requires_value_equals() -> None:
    """Require a value whenever ``--workspace-root=`` appears with no value."""
    with pytest.raises(SystemExit):
        cli._extract_workspace_override(["--workspace-root="])
```
</issue_to_address>

### Comment 5
<location> `tests/unit/test_cli.py:54-73` </location>
<code_context>
+        cli._extract_workspace_override(["--workspace-root"])
+
+
+def test_main_dispatches_bump(
+    monkeypatch: pytest.MonkeyPatch,
+    tmp_path: Path,
+    capsys: pytest.CaptureFixture[str],
+) -> None:
+    """Route the bump subcommand through the placeholder implementation."""
+    called: dict[str, Path] = {}
+
+    def fake_run(workspace_root: Path) -> str:
+        called["workspace_root"] = workspace_root
+        return "bump placeholder"
+
+    monkeypatch.setattr(bump_command, "run", fake_run)
+    exit_code = cli.main(["--workspace-root", str(tmp_path), "bump"])
+    assert exit_code == 0
+    assert called["workspace_root"] == tmp_path.resolve()
+    captured = capsys.readouterr()
+    assert "bump placeholder" in captured.out
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** No test for missing subcommand or invalid subcommand.

Please add tests to cover scenarios where the CLI is invoked without a subcommand or with an invalid subcommand, verifying that appropriate error handling and exit codes are produced.

```suggestion
def test_main_dispatches_bump(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Route the bump subcommand through the placeholder implementation."""
    called: dict[str, Path] = {}

    def fake_run(workspace_root: Path) -> str:
        called["workspace_root"] = workspace_root
        return "bump placeholder"

    monkeypatch.setattr(bump_command, "run", fake_run)
    exit_code = cli.main(["--workspace-root", str(tmp_path), "bump"])
    assert exit_code == 0
    assert called["workspace_root"] == tmp_path.resolve()
    captured = capsys.readouterr()
    assert "bump placeholder" in captured.out


def test_main_missing_subcommand(
    capsys: pytest.CaptureFixture[str],
    tmp_path: Path,
) -> None:
    """Test CLI invoked without a subcommand."""
    exit_code = cli.main(["--workspace-root", str(tmp_path)])
    assert exit_code != 0
    captured = capsys.readouterr()
    assert "No subcommand provided" in captured.out or "error" in captured.out.lower()


def test_main_invalid_subcommand(
    capsys: pytest.CaptureFixture[str],
    tmp_path: Path,
) -> None:
    """Test CLI invoked with an invalid subcommand."""
    exit_code = cli.main(["--workspace-root", str(tmp_path), "not_a_real_subcommand"])
    assert exit_code != 0
    captured = capsys.readouterr()
    assert "Unknown subcommand" in captured.out or "error" in captured.out.lower()
```
</issue_to_address>

### Comment 6
<location> `tests/bdd/steps/test_cli_steps.py:24-33` </location>
<code_context>
+    return tmp_path
+
+
+@when("I invoke lading bump with that workspace", target_fixture="cli_run")
+def when_invoke_lading(
+    cmd_mox: CmdMox,
+    workspace_directory: Path,
+    repo_root: Path,
+) -> dict[str, typ.Any]:
+    """Execute the CLI via ``python -m`` and capture the result."""
+    command = [
+        sys.executable,
+        "-m",
+        "lading.cli",
+        "--workspace-root",
+        str(workspace_directory),
+        "bump",
+    ]
+    cmd_mox.spy(sys.executable).passthrough()
+    cmd_mox.spy(Path(sys.executable).name).passthrough()
+    # ``cmd-mox`` intercepts the invocation and only executes the command we
+    # configure in this test module. We therefore silence Ruff's security check
+    # that normally warns about untrusted input.
+    completed = subprocess.run(  # noqa: S603
+        command,
+        check=False,
</code_context>

<issue_to_address>
**suggestion (testing):** No behaviour test for the 'publish' subcommand.

Please add BDD tests for the 'publish' subcommand, including scenarios and step definitions, to ensure both CLI entry points are tested.

Suggested implementation:

```python
@when("I invoke lading bump with that workspace", target_fixture="cli_run")
def when_invoke_lading(
    cmd_mox: CmdMox,
    workspace_directory: Path,
    repo_root: Path,
) -> dict[str, typ.Any]:
    """Execute the CLI via ``python -m`` and capture the result."""
    command = [
        sys.executable,
        "-m",
        "lading.cli",
        "--workspace-root",
        str(workspace_directory),
        "bump",
    ]
    cmd_mox.spy(sys.executable).passthrough()
    cmd_mox.spy(Path(sys.executable).name).passthrough()
    # ``cmd-mox`` intercepts the invocation and only executes the command we
    # configure in this test module. We therefore silence Ruff's security check
    # that normally warns about untrusted input.
    completed = subprocess.run(  # noqa: S603
        command,
        check=False,
        cwd=str(repo_root),
        capture_output=True,
        text=True,
    )
    return {
        "returncode": completed.returncode,
        "stdout": completed.stdout,
        "stderr": completed.stderr,
        "workspace": workspace_directory.resolve(),
    }


@when("I invoke lading publish with that workspace", target_fixture="cli_run")
def when_invoke_lading_publish(
    cmd_mox: CmdMox,
    workspace_directory: Path,
    repo_root: Path,
) -> dict[str, typ.Any]:
    """Execute the CLI via ``python -m`` and capture the result for the publish subcommand."""
    command = [
        sys.executable,
        "-m",
        "lading.cli",
        "--workspace-root",
        str(workspace_directory),
        "publish",
    ]
    cmd_mox.spy(sys.executable).passthrough()
    cmd_mox.spy(Path(sys.executable).name).passthrough()
    completed = subprocess.run(  # noqa: S603
        command,
        check=False,
        cwd=str(repo_root),
        capture_output=True,
        text=True,
    )
    return {
        "returncode": completed.returncode,
        "stdout": completed.stdout,
        "stderr": completed.stderr,
        "workspace": workspace_directory.resolve(),
    }

```

You will also need to add corresponding scenarios for the 'publish' subcommand in your `features/cli.feature` file, and ensure any necessary @then step definitions are present to assert expected outcomes for 'publish'.
</issue_to_address>

### Comment 7
<location> `lading/cli.py:96` </location>
<code_context>
+    return 0
+
+
+def main(argv: typ.Sequence[str] | None = None) -> int:
+    """Entry point for ``python -m lading.cli``."""
+    if argv is None:
</code_context>

<issue_to_address>
**issue (review_instructions):** Add unit tests for the main() entry point and CLI argument parsing logic.

The CLI scaffolding introduces new logic for argument parsing, environment variable management, and command dispatch. While behavioural tests are present, there are no unit tests covering the main() function or its helpers. Add unit tests to verify argument extraction, environment variable handling, and error cases.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 8
<location> `lading/commands/bump.py:10` </location>
<code_context>
+from plumbum import local
+
+
+def run(workspace_root: Path) -> str:
+    """Return a placeholder message for the bump command.
+
</code_context>

<issue_to_address>
**issue (review_instructions):** Add unit tests for the bump.run() function.

The bump command implementation is new and should be covered by unit tests to ensure correct path normalization and output formatting. Behavioural tests alone are insufficient for this function's logic.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 9
<location> `lading/commands/publish.py:10` </location>
<code_context>
+from plumbum import local
+
+
+def run(workspace_root: Path) -> str:
+    """Return a placeholder message for the bump command.
+
</code_context>

<issue_to_address>
**issue (review_instructions):** Add unit tests for the publish.run() function.

The publish command implementation is new and should be covered by unit tests to ensure correct path normalization and output formatting. Behavioural tests alone are insufficient for this function's logic.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 10
<location> `docs/usage-guide.md:9` </location>
<code_context>
+
+## Installation and invocation
+
+The CLI ships with the repository. You can execute it directly with Python or
+via `uv`:
+
</code_context>

<issue_to_address>
**issue (review_instructions):** This sentence uses the second person pronoun "you", which should be avoided per the instructions.

Consider rephrasing to avoid "you". For example: "The CLI ships with the repository and can be executed directly with Python or via `uv`."

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Avoid 2nd person or 1st person pronouns ("I", "you", "we")

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lading/cli.py Outdated
Comment thread lading/commands/bump.py
Comment thread lading/commands/publish.py
Comment thread tests/unit/test_cli.py
Comment thread tests/unit/test_cli.py Outdated
Comment thread tests/bdd/steps/test_cli_steps.py Outdated
Comment thread docs/usage-guide.md Outdated
@leynos

leynos commented Oct 5, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

lading/cli.py

Comment on lines +37 to +69

def _extract_workspace_override(
    tokens: typ.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__`.
    """
    workspace: str | None = None
    remainder: list[str] = []
    index = 0
    while index < len(tokens):
        current_argument = tokens[index]
        if current_argument == "--workspace-root":
            try:
                workspace = tokens[index + 1]
            except IndexError as err:
                raise SystemExit(WORKSPACE_ROOT_REQUIRED_MESSAGE) from err
            if workspace.startswith("-"):
                raise SystemExit(WORKSPACE_ROOT_REQUIRED_MESSAGE)
            index += 2
            continue
        if current_argument.startswith("--workspace-root="):
            workspace = current_argument.partition("=")[2]
            if not workspace:
                raise SystemExit(WORKSPACE_ROOT_REQUIRED_MESSAGE)
            index += 1
            continue
        remainder.append(current_argument)
        index += 1
    return workspace, remainder

❌ New issue: Bumpy Road Ahead
_extract_workspace_override has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
lading/commands/publish.py (1)

10-14: Enhance the docstring to match the bump command's clarity.

The docstring for publish.run is minimal compared to bump.run (shown in the code snippets), which explains the placeholder rationale and testing intent. For consistency and maintainability, consider providing the same level of detail here.

Apply this diff to enhance the docstring:

 def run(workspace_root: Path) -> str:
-    """Return a placeholder message for the publish command."""
+    """Return a placeholder message for the publish command.
+
+    Step 1.1 only wires the CLI, so we provide a friendly acknowledgement
+    instead of performing actual publication. The message makes it trivial
+    for tests to assert that dispatch occurred correctly without
+    constraining future behaviour.
+    """
     candidate = local.path(str(workspace_root))
     root_path = Path(str(candidate)).expanduser().resolve(strict=False)
     return f"publish placeholder invoked for {root_path}"
lading/commands/bump.py (1)

18-19: Remove redundant path normalization.

The workspace_root parameter is already normalized (expanded and resolved) by cli.py's _normalise_workspace_root() before being passed to this function. Re-normalizing it here is redundant. Additionally, the plumbum round-trip (Path -> str -> plumbum path -> str -> Path) serves no purpose.

Simplify to:

-    candidate = local.path(str(workspace_root))
-    root_path = Path(str(candidate)).expanduser().resolve(strict=False)
-    return f"bump placeholder invoked for {root_path}"
+    return f"bump placeholder invoked for {workspace_root}"

If you need to keep normalization for defensive coding, at minimum remove the plumbum round-trip:

-    candidate = local.path(str(workspace_root))
-    root_path = Path(str(candidate)).expanduser().resolve(strict=False)
+    root_path = workspace_root.expanduser().resolve(strict=False)
     return f"bump placeholder invoked for {root_path}"
lading/cli.py (2)

28-34: Simplify path normalization by removing unnecessary plumbum round-trip.

The plumbum conversion (Path/str -> plumbum path -> str -> Path) is redundant here. The code converts to plumbum's LocalPath, immediately converts back to string, then to pathlib.Path, and uses pathlib methods for expansion and resolution. This achieves nothing that pathlib.Path cannot do directly.

Simplify to use only pathlib.Path:

 def _normalise_workspace_root(value: Path | str | None) -> Path:
     """Return an absolute workspace path with ``~`` expanded."""
     if value is None:
         return Path.cwd().resolve()
-    candidate = local.path(str(value))
-    expanded = Path(str(candidate)).expanduser()
-    return expanded.resolve(strict=False)
+    path = Path(value)
+    return path.expanduser().resolve(strict=False)

This maintains identical behavior while being clearer and more efficient.


106-121: Avoid redundant normalization in command functions.

Both bump() and publish() call _normalise_workspace_root() on parameters that may already be normalized. Additionally, the downstream commands.bump.run() and commands.publish.run() perform yet another normalization (see earlier comment on bump.py lines 18-19).

The normalization happens up to three times for a single invocation:

  1. In main() (line 101) before setting the environment variable
  2. In bump()/publish() (lines 111, 120) before calling command runners
  3. In the command runners themselves

Consider one of these approaches:

Option 1 (recommended): Remove normalization from command runners (commands.bump.run, commands.publish.run) and trust that the CLI layer provides normalized paths.

Option 2: Add a note documenting why defensive re-normalization is needed, if there's a use case for calling these commands programmatically with non-normalized paths.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2b091ae and c22f518.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Makefile (1 hunks)
  • docs/lading-design.md (1 hunks)
  • docs/roadmap.md (1 hunks)
  • docs/usage-guide.md (1 hunks)
  • lading/__init__.py (1 hunks)
  • lading/cli.py (1 hunks)
  • lading/commands/__init__.py (1 hunks)
  • lading/commands/bump.py (1 hunks)
  • lading/commands/publish.py (1 hunks)
  • pyproject.toml (2 hunks)
  • tests/bdd/features/cli.feature (1 hunks)
  • tests/bdd/steps/test_cli_steps.py (1 hunks)
  • tests/conftest.py (1 hunks)
  • tests/unit/test_cli.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.py: Python files must use snake_case filenames (e.g., http_client.py, task_queue.py)
Classes must use PascalCase
Variables and functions must use snake_case
Module-level constants must use UPPER_SNAKE_CASE
Prefix non-exported helpers or internal APIs with a single leading underscore
Use typing everywhere and maintain full static type coverage
Use TypedDict or @DataClass for structured data; prefer @DataClass(slots=True) for internal-only usage
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast only when necessary with justification; use object for unknown opaque values
Be explicit with return types (e.g., -> None, -> str) for all public functions and methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document third-party frozendict if used
Use # pyright: ignore sparingly and include an explanation comment when used
Avoid side effects at import time (no global state mutation or actions on import)
Use docstrings (NumPy format) for public functions, classes, and modules
Explain tricky code with inline comments

**/*.py: Prefer context managers to encapsulate setup/teardown for resources (files, locks, connections) instead of manual try/finally blocks
Use contextlib.contextmanager (@contextmanager) to implement simple, linear setup/teardown context managers
Implement a class with enter and exit for context managers that require internal state or more complex lifecycle handling
For file I/O, prefer with open(...) as ... over open()/try/finally/close patterns
Choose @contextmanager when control flow is linear and no persistent state is needed
Choose a class-based context manager when there is internal state, lifecycle methods, re-entry, or advanced context needs

**/*.py: Exception classes must end with the suffix 'Error' (N818)
Prefer specific built-ins (e.g., TypeError, ValueError) or domain exceptions over raising Exception directly (TRY003/TRY004)
Preserve causal chains...

Files:

  • lading/__init__.py
  • lading/commands/__init__.py
  • lading/commands/bump.py
  • tests/bdd/steps/test_cli_steps.py
  • lading/cli.py
  • lading/commands/publish.py
  • tests/unit/test_cli.py
  • tests/conftest.py
{README.md,docs/**}

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate documentation: keep README.md or a docs/ directory near reusable packages and include usage examples

Files:

  • docs/roadmap.md
  • docs/lading-design.md
  • docs/usage-guide.md
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Use markdown files in docs/ as the knowledge base and source of truth for requirements, dependencies, and architectural decisions.
Proactively update relevant docs/ markdown when decisions, requirements, dependencies, or architecture change.

Files:

  • docs/roadmap.md
  • docs/lading-design.md
  • docs/usage-guide.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Markdown files must pass markdownlint.
Markdown files containing Mermaid diagrams must pass nixie validation.

Files:

  • docs/roadmap.md
  • docs/lading-design.md
  • docs/usage-guide.md
{**/unittests/test_*.py,tests/**/*.py}

📄 CodeRabbit inference engine (.rules/python-00.md)

{**/unittests/test_*.py,tests/**/*.py}: Use pytest idioms: prefer fixtures over setup/teardown, parametrize broadly, avoid unnecessary mocks
Group related tests using classes with method names prefixed by test_
Write tests from a user's perspective: test public behaviour, not internals

Files:

  • tests/bdd/steps/test_cli_steps.py
  • tests/unit/test_cli.py
  • tests/conftest.py
tests/**/*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

In tests, assert specific exception types (and optional message via regex) rather than using broad Exception (B017)

Files:

  • tests/bdd/steps/test_cli_steps.py
  • tests/unit/test_cli.py
  • tests/conftest.py
pyproject.toml

📄 CodeRabbit inference engine (.rules/python-00.md)

pyproject.toml: Enable Ruff for linting (replacing flake8, isort, pyflakes, etc.) and configure it
Use Ruff as the project formatter; let Ruff handle all formatting
Configure tools (Ruff, Pyright, Pytest) via pyproject.toml
Enforce strict mode in Pyright

Configure Ruff to enforce TRY, BLE, EM, LOG, N818, PERF203, and B017 in pyproject.toml

pyproject.toml: Use the PEP 621 [project] table with at least name and version defined
Include description and readme in [project]; set readme to the README file path (e.g., README.md)
Set requires-python in [project] to declare supported Python versions (e.g., >=3.10)
Specify license in [project] using license = { text = "" } or license = { file = "LICENSE" }
Provide authors with name and email in [project].authors
Use keywords and valid Trove classifiers in [project]
Declare runtime dependencies in [project].dependencies using PEP 508 specifiers
Group non-runtime deps under [project.optional-dependencies] (e.g., dev, docs)
Define CLI entry points under [project.scripts] (e.g., mycli = "pkg.cli:main")
Define GUI entry points under [project.gui-scripts] when needed
Register plugin entry points under [project.entry-points.'group.name']
Declare a build system: [build-system] requires = ["setuptools>=61.0", "wheel"], build-backend = "setuptools.build_meta"
Set [tool.uv].package = true to ensure your project is built/installed on uv sync/run
If omitting [build-system], set [tool.uv].package = true so uv still builds/installs your package
Use semantic versioning (e.g., 1.2.3) for the [project].version value
Keep build constraints minimal; omit [build-system] if you don’t need editable installs
Prefer exact or bounded dependency ranges (e.g., requests>=2.25,<3.0) to avoid unexpected major bumps
Use dynamic fields (e.g., dynamic = ["version"]) sparingly and only if the build backend supports them

Files:

  • pyproject.toml
🧬 Code graph analysis (7)
lading/__init__.py (1)
lading/cli.py (1)
  • main (96-103)
lading/commands/__init__.py (1)
lading/cli.py (2)
  • bump (107-112)
  • publish (116-121)
lading/commands/bump.py (2)
lading/commands/publish.py (1)
  • run (10-14)
crate_tools/unittests/test_publish_workspace_dependencies.py (1)
  • workspace_root (16-21)
tests/bdd/steps/test_cli_steps.py (2)
tests/conftest.py (1)
  • repo_root (15-17)
lading/commands/bump.py (1)
  • run (10-20)
lading/cli.py (2)
lading/commands/bump.py (1)
  • run (10-20)
lading/commands/publish.py (1)
  • run (10-14)
lading/commands/publish.py (2)
lading/commands/bump.py (1)
  • run (10-20)
crate_tools/unittests/test_publish_workspace_dependencies.py (1)
  • workspace_root (16-21)
tests/unit/test_cli.py (1)
lading/cli.py (4)
  • bump (107-112)
  • publish (116-121)
  • _extract_workspace_override (37-69)
  • main (96-103)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (13)
docs/roadmap.md (1)

15-15: LGTM!

The roadmap correctly reflects the completion of Step 1.1 tasks. The status updates align with the CLI scaffolding and project initialization delivered in this PR.

Also applies to: 20-20

docs/usage-guide.md (1)

1-64: LGTM!

The usage guide is clear, accurate, and well-structured. It correctly documents:

  • The CLI entry point and invocation methods
  • The --workspace-root flag behavior and environment variable handling
  • Placeholder command implementations and future roadmap expectations
  • Testing approach with cmd-mox

The documentation aligns well with the implementation in lading/cli.py and provides helpful context for both users and maintainers.

pyproject.toml (1)

28-28: LGTM!

The Pyright include and setuptools package configuration correctly expose the new lading package for type checking and distribution. The changes align with the new package structure introduced in this PR.

Also applies to: 108-115

lading/__init__.py (1)

1-11: LGTM!

The package initializer properly exposes the public API (app and main) with clear documentation. The implementation follows Python best practices with future annotations and explicit __all__ exports.

tests/bdd/features/cli.feature (1)

1-5: LGTM!

The BDD feature provides appropriate smoke-test coverage for the CLI scaffolding. The scenario structure is clear and matches the documented behavior in the usage guide.

lading/commands/publish.py (1)

12-13: LGTM!

The path normalization approach using plumbum.local followed by pathlib.Path resolution is consistent with the bump command implementation. This ensures cross-platform path handling and proper resolution of symbolic links and relative paths.

lading/commands/__init__.py (1)

1-7: LGTM!

The commands package initializer properly exposes the command submodules with a clear docstring. The implementation follows Python packaging best practices and maintains consistency with the parent package structure.

docs/lading-design.md (1)

76-90: LGTM!

The implementation notes accurately document the CLI scaffolding delivered in Step 1.1. The content:

  • Clearly identifies the entry point and key implementation details
  • Explains the workspace-root handling and environment variable pattern
  • Documents the placeholder approach and testing strategy
  • Provides valuable context for future roadmap steps

The documentation aligns well with the actual implementation in lading/cli.py and related test files.

tests/conftest.py (1)

1-32: LGTM!

The pytest configuration is well-structured:

  • The repo_root fixture correctly derives the repository root
  • The _restore_workspace_env autouse fixture properly isolates environment state using try/finally
  • Importing WORKSPACE_ROOT_ENV_VAR inside the fixture avoids import-time side effects
tests/unit/test_cli.py (1)

1-97: LGTM!

The test suite effectively validates the CLI scaffolding:

  • Comprehensive parametrized tests for workspace override extraction
  • Proper error handling validation using pytest.raises
  • Appropriate use of monkeypatching to isolate dispatch logic
  • Good coverage of both the main entry point and direct app invocation
tests/bdd/steps/test_cli_steps.py (1)

1-64: LGTM!

The BDD step definitions are well-implemented:

  • Clear separation of concerns across given/when/then steps
  • Appropriate use of cmd_mox for subprocess interception
  • Justified use of subprocess.run with security linter suppression
  • Proper fixture wiring to pass context between steps
lading/cli.py (2)

37-69: LGTM!

The manual token parsing for --workspace-root is robust:

  • Correctly handles both --workspace-root <value> and --workspace-root=<value> forms
  • Properly validates that values are not flag-like (don't start with -)
  • Raises SystemExit with a clear error message for missing values
  • Honors last-occurrence-wins semantics

72-103: LGTM!

The environment management and dispatch logic is well-designed:

  • _workspace_env context manager properly saves/restores environment state
  • _dispatch_and_print handles both int and string results appropriately
  • main() cleanly orchestrates extraction, normalization, environment setup, and dispatch

Comment thread pyproject.toml Outdated
@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lading/utils/path.py (1)

10-16: Consider simplifying the path conversion.

The function converts the input to a plumbum path and immediately back to pathlib.Path. Since pathlib.Path already provides expanduser() and resolve() natively, the plumbum conversion appears unnecessary.

Apply this diff to simplify:

 def normalise_workspace_root(value: Path | str | None) -> Path:
     """Return an absolute workspace path with ``~`` expanded."""
     if value is None:
         return Path.cwd().resolve()
-    candidate = local.path(str(value))
-    expanded = Path(str(candidate)).expanduser()
-    return expanded.resolve(strict=False)
+    return Path(value).expanduser().resolve(strict=False)

If there's a specific reason for the plumbum conversion (e.g., handling platform-specific path formats), please document it in a comment.

lading/cli.py (1)

103-105: Clarify the early-exit logic for empty remaining tokens.

When remaining is empty, line 104 calls _dispatch_and_print(remaining) before returning 2 on line 105. This invokes the Cyclopts app with an empty argument list to display help, but the flow is implicit.

Consider making the intent explicit:

     workspace_root = normalise_workspace_root(workspace_override)
     if not remaining:
-        _dispatch_and_print(remaining)
+        _dispatch_and_print(remaining)  # Show usage/help for empty command
         return 2
     with _workspace_env(workspace_root):

Or refactor to be more explicit about showing help:

     workspace_root = normalise_workspace_root(workspace_override)
     if not remaining:
-        _dispatch_and_print(remaining)
-        return 2
+        # No subcommand provided; display help and return error code
+        return _dispatch_and_print(remaining) or 2
     with _workspace_env(workspace_root):
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c22f518 and a431f85.

📒 Files selected for processing (12)
  • docs/lading-design.md (1 hunks)
  • docs/usage-guide.md (1 hunks)
  • lading/cli.py (1 hunks)
  • lading/commands/bump.py (1 hunks)
  • lading/commands/publish.py (1 hunks)
  • lading/utils/__init__.py (1 hunks)
  • lading/utils/path.py (1 hunks)
  • pyproject.toml (2 hunks)
  • tests/bdd/features/cli.feature (1 hunks)
  • tests/bdd/steps/test_cli_steps.py (1 hunks)
  • tests/unit/test_cli.py (1 hunks)
  • tests/unit/test_commands_placeholder.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • lading/commands/publish.py
  • docs/usage-guide.md
  • lading/commands/bump.py
  • docs/lading-design.md
  • pyproject.toml
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.py: Python files must use snake_case filenames (e.g., http_client.py, task_queue.py)
Classes must use PascalCase
Variables and functions must use snake_case
Module-level constants must use UPPER_SNAKE_CASE
Prefix non-exported helpers or internal APIs with a single leading underscore
Use typing everywhere and maintain full static type coverage
Use TypedDict or @DataClass for structured data; prefer @DataClass(slots=True) for internal-only usage
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast only when necessary with justification; use object for unknown opaque values
Be explicit with return types (e.g., -> None, -> str) for all public functions and methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document third-party frozendict if used
Use # pyright: ignore sparingly and include an explanation comment when used
Avoid side effects at import time (no global state mutation or actions on import)
Use docstrings (NumPy format) for public functions, classes, and modules
Explain tricky code with inline comments

**/*.py: Prefer context managers to encapsulate setup/teardown for resources (files, locks, connections) instead of manual try/finally blocks
Use contextlib.contextmanager (@contextmanager) to implement simple, linear setup/teardown context managers
Implement a class with enter and exit for context managers that require internal state or more complex lifecycle handling
For file I/O, prefer with open(...) as ... over open()/try/finally/close patterns
Choose @contextmanager when control flow is linear and no persistent state is needed
Choose a class-based context manager when there is internal state, lifecycle methods, re-entry, or advanced context needs

**/*.py: Exception classes must end with the suffix 'Error' (N818)
Prefer specific built-ins (e.g., TypeError, ValueError) or domain exceptions over raising Exception directly (TRY003/TRY004)
Preserve causal chains...

Files:

  • lading/utils/path.py
  • tests/unit/test_commands_placeholder.py
  • lading/cli.py
  • tests/unit/test_cli.py
  • tests/bdd/steps/test_cli_steps.py
  • lading/utils/__init__.py
{**/unittests/test_*.py,tests/**/*.py}

📄 CodeRabbit inference engine (.rules/python-00.md)

{**/unittests/test_*.py,tests/**/*.py}: Use pytest idioms: prefer fixtures over setup/teardown, parametrize broadly, avoid unnecessary mocks
Group related tests using classes with method names prefixed by test_
Write tests from a user's perspective: test public behaviour, not internals

Files:

  • tests/unit/test_commands_placeholder.py
  • tests/unit/test_cli.py
  • tests/bdd/steps/test_cli_steps.py
tests/**/*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

In tests, assert specific exception types (and optional message via regex) rather than using broad Exception (B017)

Files:

  • tests/unit/test_commands_placeholder.py
  • tests/unit/test_cli.py
  • tests/bdd/steps/test_cli_steps.py
🧬 Code graph analysis (5)
tests/unit/test_commands_placeholder.py (4)
lading/cli.py (2)
  • bump (117-122)
  • publish (126-131)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
lading/commands/bump.py (1)
  • run (13-22)
lading/commands/publish.py (1)
  • run (13-16)
lading/cli.py (3)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
lading/commands/bump.py (1)
  • run (13-22)
lading/commands/publish.py (1)
  • run (13-16)
tests/unit/test_cli.py (3)
lading/cli.py (5)
  • bump (117-122)
  • publish (126-131)
  • _extract_workspace_override (28-60)
  • main (96-113)
  • _workspace_env (64-74)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
crate_tools/unittests/test_publish_workspace_dependencies.py (1)
  • workspace_root (16-21)
tests/bdd/steps/test_cli_steps.py (3)
tests/conftest.py (1)
  • repo_root (15-17)
lading/commands/bump.py (1)
  • run (13-22)
lading/commands/publish.py (1)
  • run (13-16)
lading/utils/__init__.py (1)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review

Comment thread lading/cli.py
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Oct 5, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/unit/test_cli.py

Comment on lines +72 to +89

def test_main_dispatches_bump(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
) -> None:
    """Route the bump subcommand through the placeholder implementation."""
    called: dict[str, Path] = {}

    def fake_run(workspace_root: Path) -> str:
        called["workspace_root"] = workspace_root
        return "bump placeholder"

    monkeypatch.setattr(bump_command, "run", fake_run)
    exit_code = cli.main(["--workspace-root", str(tmp_path), "bump"])
    assert exit_code == 0
    assert called["workspace_root"] == tmp_path.resolve()
    captured = capsys.readouterr()
    assert "bump placeholder" in captured.out

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: test_main_dispatches_bump,test_main_dispatches_publish,test_main_handles_keyboard_interrupt,test_main_handles_unexpected_exception

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Oct 5, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/unit/test_cli.py

Comment on lines +90 to +113

def test_main_dispatches_command(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    capsys: pytest.CaptureFixture[str],
    command_module: ModuleType,
    command_name: str,
    placeholder_text: str,
    cli_args: list[str],
) -> None:
    """Route subcommands through their placeholder implementations."""
    called: dict[str, Path] = {}

    def fake_run(workspace_root: Path) -> str:
        called["workspace_root"] = workspace_root
        return placeholder_text

    monkeypatch.setattr(command_module, "run", fake_run)
    args = [arg.replace("{tmp_path}", str(tmp_path)) for arg in cli_args]
    assert command_name in args
    exit_code = cli.main(args)
    assert exit_code == 0
    assert called["workspace_root"] == tmp_path.resolve()
    captured = capsys.readouterr()
    assert placeholder_text in captured.out

❌ New issue: Excess Number of Function Arguments
test_main_dispatches_command has 7 arguments, max arguments = 4


tests/unit/test_cli.py

Comment on lines +143 to +160

def test_main_handles_exceptions(
    monkeypatch: pytest.MonkeyPatch,
    capsys: pytest.CaptureFixture[str],
    tmp_path: Path,
    exception: BaseException,
    expected_exit_code: int,
    expected_message: str,
) -> None:
    """Handle exceptions during command execution."""

    def boom(_: typ.Sequence[str]) -> int:
        raise exception

    monkeypatch.setattr(cli, "_dispatch_and_print", boom)
    exit_code = cli.main(["bump", "--workspace-root", str(tmp_path)])
    assert exit_code == expected_exit_code
    captured = capsys.readouterr()
    assert expected_message in captured.err

❌ New issue: Excess Number of Function Arguments
test_main_handles_exceptions has 6 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lading/cli.py (1)

118-120: Consider adding a clarifying comment for the usage-error path.

The code intentionally calls _dispatch_and_print(remaining) to trigger Cyclopts' usage message, then unconditionally returns exit code 2 (standard for CLI usage errors). The dispatch call's return value is ignored.

While the behavior is correct, a brief inline comment would clarify the intent:

 if not remaining:
+    # Dispatch with empty args to print usage, then return standard usage-error code
     _dispatch_and_print(remaining)
     return 2

Alternatively, make the intention more explicit:

 if not remaining:
-    _dispatch_and_print(remaining)
-    return 2
+    _dispatch_and_print(remaining)  # Print usage message
+    return 2  # Standard exit code for missing subcommand
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a431f85 and 6a21fdd.

📒 Files selected for processing (2)
  • lading/cli.py (1 hunks)
  • tests/unit/test_cli.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/*.py: Python files must use snake_case filenames (e.g., http_client.py, task_queue.py)
Classes must use PascalCase
Variables and functions must use snake_case
Module-level constants must use UPPER_SNAKE_CASE
Prefix non-exported helpers or internal APIs with a single leading underscore
Use typing everywhere and maintain full static type coverage
Use TypedDict or @DataClass for structured data; prefer @DataClass(slots=True) for internal-only usage
Avoid Any; prefer precise types (TypeVar, Protocol, Literal, Union); use typing.cast only when necessary with justification; use object for unknown opaque values
Be explicit with return types (e.g., -> None, -> str) for all public functions and methods
Favor immutability: prefer tuples to lists and MappingProxyType for read-only mappings; document third-party frozendict if used
Use # pyright: ignore sparingly and include an explanation comment when used
Avoid side effects at import time (no global state mutation or actions on import)
Use docstrings (NumPy format) for public functions, classes, and modules
Explain tricky code with inline comments

**/*.py: Prefer context managers to encapsulate setup/teardown for resources (files, locks, connections) instead of manual try/finally blocks
Use contextlib.contextmanager (@contextmanager) to implement simple, linear setup/teardown context managers
Implement a class with enter and exit for context managers that require internal state or more complex lifecycle handling
For file I/O, prefer with open(...) as ... over open()/try/finally/close patterns
Choose @contextmanager when control flow is linear and no persistent state is needed
Choose a class-based context manager when there is internal state, lifecycle methods, re-entry, or advanced context needs

**/*.py: Exception classes must end with the suffix 'Error' (N818)
Prefer specific built-ins (e.g., TypeError, ValueError) or domain exceptions over raising Exception directly (TRY003/TRY004)
Preserve causal chains...

Files:

  • lading/cli.py
  • tests/unit/test_cli.py
{**/unittests/test_*.py,tests/**/*.py}

📄 CodeRabbit inference engine (.rules/python-00.md)

{**/unittests/test_*.py,tests/**/*.py}: Use pytest idioms: prefer fixtures over setup/teardown, parametrize broadly, avoid unnecessary mocks
Group related tests using classes with method names prefixed by test_
Write tests from a user's perspective: test public behaviour, not internals

Files:

  • tests/unit/test_cli.py
tests/**/*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

In tests, assert specific exception types (and optional message via regex) rather than using broad Exception (B017)

Files:

  • tests/unit/test_cli.py
🧬 Code graph analysis (2)
lading/cli.py (3)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
lading/commands/publish.py (1)
  • run (13-16)
lading/commands/bump.py (1)
  • run (13-22)
tests/unit/test_cli.py (2)
lading/cli.py (5)
  • bump (132-137)
  • publish (141-146)
  • _extract_workspace_override (52-75)
  • main (111-128)
  • _workspace_env (79-89)
lading/utils/path.py (1)
  • normalise_workspace_root (10-16)
🪛 GitHub Actions: CI
tests/unit/test_cli.py

[error] 3-14: I001 Import block is un-sorted or un-formatted. Organize imports.


[error] 6-6: ICN003 Members of dataclasses should not be imported explicitly.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (5)
tests/unit/test_cli.py (3)

110-114: Dataclass bundling successfully reduces argument count to guideline limit.

The parametrized tests correctly use dataclasses (CommandDispatchCase, ExceptionHandlingCase) to bundle test parameters, keeping function argument counts at exactly 4 (the guideline maximum). CodeScene may be incorrectly counting the dataclass fields as separate function parameters.

The refactor suggested in the PR objectives has been properly applied. The test structure is clean and follows pytest best practices.


133-141: LGTM: Missing subcommand case is properly tested.

The test correctly validates that invoking the CLI without a subcommand returns exit code 2 and displays usage information.


143-151: LGTM: Invalid subcommand case is properly tested.

The test correctly validates that invoking the CLI with an unknown subcommand returns a non-zero exit code and displays an error message.

lading/cli.py (2)

28-75: Refactoring successfully addresses nested conditional complexity.

The extraction of _validate_workspace_value, _parse_workspace_flag, and _parse_workspace_equals helper functions has successfully flattened the logic in _extract_workspace_override, eliminating the "Bumpy Road Ahead" issue flagged by CodeScene in previous reviews.

The refactor maintains identical behavior and error messages while improving readability and reducing cyclomatic complexity, aligning with the coding guideline: "Prefer clear, linear data flows over deeply nested conditionals and loop bodies."

Based on past review comments indicating this refactor was completed in commit 8ba2313.


131-146: LGTM: Command definitions follow consistent patterns.

Both bump and publish subcommands correctly:

  • Use the WorkspaceRootOption type alias for parameter annotation
  • Normalize the workspace root via normalise_workspace_root
  • Delegate to their respective command module run functions
  • Return string results for display by the dispatcher

The implementation provides clean separation between CLI wiring and command logic, making it easy to replace placeholder implementations with real functionality.

Comment thread tests/unit/test_cli.py
@leynos
leynos merged commit 6d08ef4 into main Oct 6, 2025
4 checks passed
@leynos
leynos deleted the codex/implement-lading-project-structure-and-cli branch October 6, 2025 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant