Skip to content

feat: report missing component dependencies with an actionable install hint - #262

Open
hasansezertasan wants to merge 1 commit into
mainfrom
feat/actionable-component-dependency-guard
Open

feat: report missing component dependencies with an actionable install hint#262
hasansezertasan wants to merge 1 commit into
mainfrom
feat/actionable-component-dependency-guard

Conversation

@hasansezertasan

Copy link
Copy Markdown
Owner

Closes #172

What

The pkg CLI root lazy-imports each non-primary component (pkg web, pkg worker, ...). When one of those imports hits a missing dependency, the failure was a bare ModuleNotFoundError traceback with no hint about the fix.

Every launcher command now wraps its lazy import in a private _component_dependencies(component) context manager (both the Typer and argparse variants):

with _component_dependencies("web"):
    from example.web.app import main
main()

A missing dependency becomes:

Error: The web component requires the 'uvicorn' package, which is not installed. Component dependencies ship with 'example', so this usually means your environment is out of sync — run uv sync (or reinstall the package) and try again.

and exits 1 (typer.Exit / SystemExit).

Rescoped from the issue

#172 proposed a core/_imports.py with a require(module, *, feature, extra) helper. Two findings from the code changed the design:

  • There are no per-component extras to name. [project.optional-dependencies] carries only an empty all (kept so the dev group's pkg[all] resolves); every component runtime dep is a core dependency. So pip install pkg[<extra>] can never be the right hint — a missing module means the environment is stale (a copier update that enabled a component without a re-sync, or a stale venv), and uv sync is the honest fix.
  • The proposed placement wouldn't have caught the target case. The CLI's lazy imports are all first-party (from pkg.web.app import main), rendered under the same Jinja toggle that put the third-party dep in dependencies. After a copier update the first-party module is present; what fails is the third-party import one frame deeper, inside the component module. A context manager around the import site catches that; a require() call replacing the import would not.

So: no new core module (nothing to add to the import-linter contract, no new coverage surface), no extras, and the guard is emitted only when the root actually lazy-imports something — derived from primary_component per ADR-019, never re-spelled inline. A CLI-only project renders byte-identical to before. The minimal-launcher Typer root's default callback is covered too, which the issue didn't mention but has the same exposure.

Verification

Seven shapes rendered with --vcs-ref=HEAD — typer-multi, argparse-multi, minimal-launcher, cli-only, argparse-only, gui-primary, tui-gui:

  • tox run -e style green on all — ruff, mypy (3.10 + 3.14), basedpyright, ty, pyrefly, zuban, pylint, slotscheck, import-linter, taplo, typos, ec, sphinx-lint, dead-fixtures.
  • tox run full 3.10–3.14 matrix green.
  • coverage combine && coverage report — 100%, fail_under = 99 satisfied. The guard's error path is unit-tested (test_missing_component_dependency_is_actionable), so it carries no blanket pragma.
  • Repo suite mise run test — 118 passed; golden files unchanged (the library preset has no launcher components).
  • prek clean on all changed files.

Three defects the gates caught during development, all fixed: a {{component}} f-string placeholder eaten by Jinja (surfaced only under the real tox run, not a quick editable pytest), basedpyright reportImplicitStringConcatenation on the message built with + PROJECT_NAME +, and reportDeprecated on Iterator under @contextmanager (now Generator[None, None, None], valid on the 3.10 floor).

Unrelated finding

On an untagged fresh render, hatch-vcs writes a src/<pkg>/_version.py that trips ruff's unsorted-dunder-all and bad-quotes-inline-string in tox -e style. It reproduces identically on a baseline render from the released tag, without this change — pre-existing, worth its own issue if not already known.

…l hint

The CLI root lazy-imports each non-primary component. When a dependency is
absent the failure was a bare ModuleNotFoundError traceback with no hint about
the fix.

Wrap every launcher's lazy import in a private `_component_dependencies`
context manager that exits 1 with a message naming the component, the missing
module, and `uv sync`. Component runtime deps are core `dependencies` (there
are no per-component extras), so a missing module can only mean the environment
is out of sync -- after a `copier update` that enabled a component, or in a
stale venv -- which is why the hint is `uv sync` and not `pip install pkg[x]`.

Emitted only when the root actually lazy-imports something, derived from
`primary_component` per ADR-019, so a CLI-only project renders unchanged. The
minimal launcher's default callback is covered too.

Refs #172
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hasansezertasan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 57 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e24bbb6-b1d2-4303-890e-180da6b1aa1f

📥 Commits

Reviewing files that changed from the base of the PR and between effbf80 and 418f637.

📒 Files selected for processing (3)
  • docs/template-architecture.md
  • template/src/{{github_repo_name}}/{% if include_console_root %}cli{% endif %}/app.py.jinja
  • template/tests/{% if include_console_root %}cli{% endif %}/test_app.py.jinja

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 418f6374a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +539 to 542
with _component_dependencies("web"):
from {{github_repo_name}}.web.app import main # noqa: PLC0415

main()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep web startup inside the dependency guard

When uvicorn alone is missing, importing the web app succeeds because uvicorn is lazily imported inside web.app.main; this context has already exited before main() runs, so pkg web still emits the bare ModuleNotFoundError that this change is intended to replace. The same ordering exists in the argparse launcher and the minimal launcher's web-primary callback, and the new test misses it by raising directly inside the helper rather than invoking the actual launcher path.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: actionable optional-import guard with install hints

1 participant