Skip to content

feat(ext.generate): add optional features support for templates#768

Merged
derks merged 3 commits into
mainfrom
feat/generate-optional-features
May 9, 2026
Merged

feat(ext.generate): add optional features support for templates#768
derks merged 3 commits into
mainfrom
feat/generate-optional-features

Conversation

@derks
Copy link
Copy Markdown
Member

@derks derks commented Mar 9, 2026

Allow .generate.yml templates to declare optional features with conditional variables, exclude patterns, and ignore patterns based on enabled/disabled state. Features support dependency resolution via requires with automatic transitive cascading — disabled dependencies skip prompting for dependent features entirely.

Also fix setup_template_items to catch ImportError and correct the variables default from {} to [].

Related:

Summary by CodeRabbit

  • New Features

    • Feature-driven template generation with conditional file inclusion, inter-feature dependencies, per-feature variables, prompts, and optional generation controls.
  • Bug Fixes

    • Ensure missing transitive template imports are surfaced.
    • Fix default variable handling so explicit nulls coalesce to empty lists (avoids unexpected {} vs [] behavior).
  • Documentation

    • Updated changelog and development setup instructions; added demo and README for feature-driven generation.
  • Assets

    • Added webapp template assets and numerous template fixtures.
  • Tests

    • Added tests covering feature resolution, cascading disables, and import-error handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Adds feature-driven conditional template generation: a new _process_features method validates feature configs, resolves requires to a fixpoint, collects feature variables/excludes/ignores, integrates into _generate, tightens template-module import handling, and adds demo templates plus extensive tests.

Changes

Feature-driven template generation

Layer / File(s) Summary
Docs / Changelog
CHANGELOG.md, CLAUDE.md, demo/generate-features/README.md
CHANGELOG entries for ext.generate bugfixes and feature notes; CLAUDE dev instruction changed to make init; demo README documents usage, schema, and expected outputs.
Demo Templates
demo/generate-features/templates/generate/webapp/...
New webapp template demonstrating feature-driven generation (project_name variable, docker / docker_compose features) and template files (Dockerfile, docker-compose.yml, .dockerignore, app.py, README, .generate.yml).
Core Implementation
cement/ext/ext_generate.py
Adds _process_features(...) to validate features, compute enabled/disabled states (defaults or interactive), cascade-disable unmet requires to a fixpoint, and merge feature blocks into vars, exclude_list, and ignore_list; updates _generate to treat lists and call _process_features.
Template-module Import Handling
cement/ext/ext_generate.py
Narrowed import error handling to swallow ModuleNotFoundError only when it matches the attempted template-module import path; re-raises transitive import errors.
Tests / Fixtures
tests/ext/test_ext_generate.py, tests/data/...
Many new tests and fixtures that exercise defaults, interactive prompts, missing/unknown requires, transitive/out-of-order dependency resolution, null blocks, and template-module import error propagation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I nibble YAML, hop through code,

toggles bloom on this template road,
requires checked with gentle thumps,
files appear with soft drum-thumps,
the rabbit cheers as projects grow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ext.generate): add optional features support for templates' accurately and concisely describes the main feature addition—optional features support in the generate extension for templates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/generate-optional-features

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
demo/generate-features/templates/generate/webapp/Dockerfile (1)

4-4: Consider narrowing the build context.

Line 4 copies the entire project tree into the image. For a template, that makes it easy for generated apps to accidentally bake in local junk or secrets unless a .dockerignore is added. Copying only the required files, or shipping a .dockerignore, would keep the demo safer and leaner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/generate-features/templates/generate/webapp/Dockerfile` at line 4, The
Dockerfile currently uses the broad "COPY . ." instruction which risks baking
local junk/secrets into images; update the template by replacing the "COPY . ."
step in the Dockerfile with an explicit list of COPY instructions that only add
the application's build assets and manifest files (e.g., package.json,
package-lock.json/yarn.lock, src/ or build/ folder as appropriate) and also
include a template .dockerignore file in the template set that excludes
node_modules, .git, .env, and other local artifacts—ensure the Dockerfile refers
only to the files you explicitly copy and the .dockerignore is present in the
generated project to prevent accidental inclusion.
tests/data/templates/generate/test6/.generate.yml (1)

26-35: Make feature2's disabled path explicit.

tests/data/templates/generate/test6/feature2-file references feature2_var, but feature2_var is only added from the enabled block and this disabled block never ignores/excludes that file. In the default test6 path, the result depends on undefined-variable handling rather than the feature toggle itself. I'd either gate feature2-file here or assert the current disabled output explicitly in the tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/templates/generate/test6/.generate.yml` around lines 26 - 35, The
disabled branch for the feature toggle "feature2" is missing an explicit
exclusion for the template that references feature2_var, causing test output to
depend on undefined-variable handling; update the "disabled.ignore" list to
include the "feature2-file" pattern (e.g., add a glob matching feature2-file or
'.*feature2-file.*') so that when feature2 is false the template is excluded, or
alternatively add a disabled.variables entry providing a safe default for
feature2_var; reference the "feature2", "feature2_var", and "feature2-file"
symbols when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cement/ext/ext_generate.py`:
- Around line 37-57: The current loop in ext_generate.py treats missing
prerequisite states as disabled because it checks requires inline; change this
by resolving dependencies before assigning feature_states: build a dependency
graph from features (use the existing 'features', 'feature_names' and each
feature's 'requires'), perform a topological sort or implement a recursive
resolver function (e.g., resolve_feature_state(feature_name) that consults and
sets feature_states) to compute each feature's enabled/disabled value, and then
replace the inline requires-checking block (the unsatisfied logic that uses
'requires', 'feature_states', 'self.app.pargs.defaults' and logs via
'self.app.log.warning') with calls to the resolver so chains like feature_c ->
feature_b -> feature_a are correctly resolved regardless of YAML order.
- Around line 223-226: The except block currently swallows all ImportError
instances; change it to catch AttributeError and ModuleNotFoundError as e, and
if e is a ModuleNotFoundError ensure e.name matches the requested module
identifier (variable _from) before handling it—otherwise re-raise so dependency
import failures propagate; keep the existing debug message that uses mod_name
and '.'.join(mod_parts) and call app.log.debug(msg) only when the missing module
is the requested one.

In `@demo/generate-features/README.md`:
- Around line 70-96: In demo/generate-features/README.md update the three fenced
code blocks that show the project tree outputs so each opening fence is ```text
instead of ``` (the blocks containing the Docker-enabled tree, the "With docker
disabled" tree, and the "With docker enabled, docker_compose disabled" tree);
this will satisfy markdownlint MD040 by explicitly marking the code blocks as
plain text.

In `@demo/generate-features/templates/generate/webapp/Dockerfile`:
- Around line 3-6: Create and switch to an unprivileged user in the Dockerfile:
add steps to create a non-root user (e.g., "appuser"), chown -R /app to transfer
ownership after COPY, and add a USER appuser line before the existing CMD
["python","app.py"] so the container drops root privileges when starting the
application; keep WORKDIR /app and COPY . . as-is but ensure the chown happens
after the copy.

In `@tests/ext/test_ext_generate.py`:
- Around line 233-299: The feature dependency resolver in _process_features() is
order-dependent because it computes `unsatisfied` from the partially built
`feature_states`, causing dependents declared before their requirements to be
auto-disabled; make resolution order-independent by computing feature enablement
via a fixpoint/graph traversal: build a map of feature -> requires list (from
the features input), validate unknown requirements, then iterate (or DFS) to
compute each feature's final enabled state using default values and transitive
requires until no changes occur (or use topological order/detect cycles),
replacing the current `unsatisfied` logic that reads partial `feature_states`;
also add a new test case (similar to existing tests) where a feature appears
before its requirement in the YAML (out-of-order) and assert the dependent is
enabled when its requirement defaults to enabled to prevent regressions.
- Around line 225-230: In _process_features(), replace the two assert statements
that validate a feature's missing 'name' and unknown 'requires' with explicit
exceptions (e.g., raise ValueError with the same descriptive messages such as
"Required feature config key missing: name" and the existing unknown-requires
message), and update the test test_generate_features_missing_name (and related
tests that expect AssertionError) to expect ValueError (or your chosen exception
type) instead of AssertionError so validation works even under python -O.

---

Nitpick comments:
In `@demo/generate-features/templates/generate/webapp/Dockerfile`:
- Line 4: The Dockerfile currently uses the broad "COPY . ." instruction which
risks baking local junk/secrets into images; update the template by replacing
the "COPY . ." step in the Dockerfile with an explicit list of COPY instructions
that only add the application's build assets and manifest files (e.g.,
package.json, package-lock.json/yarn.lock, src/ or build/ folder as appropriate)
and also include a template .dockerignore file in the template set that excludes
node_modules, .git, .env, and other local artifacts—ensure the Dockerfile refers
only to the files you explicitly copy and the .dockerignore is present in the
generated project to prevent accidental inclusion.

In `@tests/data/templates/generate/test6/.generate.yml`:
- Around line 26-35: The disabled branch for the feature toggle "feature2" is
missing an explicit exclusion for the template that references feature2_var,
causing test output to depend on undefined-variable handling; update the
"disabled.ignore" list to include the "feature2-file" pattern (e.g., add a glob
matching feature2-file or '.*feature2-file.*') so that when feature2 is false
the template is excluded, or alternatively add a disabled.variables entry
providing a safe default for feature2_var; reference the "feature2",
"feature2_var", and "feature2-file" symbols when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 135d9fa6-13c6-4365-b0b9-22206454922a

📥 Commits

Reviewing files that changed from the base of the PR and between 9768476 and e11c7d8.

📒 Files selected for processing (45)
  • CHANGELOG.md
  • CLAUDE.md
  • cement/ext/ext_generate.py
  • demo/generate-features/README.md
  • demo/generate-features/templates/generate/webapp/.generate.yml
  • demo/generate-features/templates/generate/webapp/Dockerfile
  • demo/generate-features/templates/generate/webapp/README.md
  • demo/generate-features/templates/generate/webapp/app.py
  • demo/generate-features/templates/generate/webapp/docker-compose.yml
  • tests/data/templates/generate/test10/.generate.yml
  • tests/data/templates/generate/test10/take-me
  • tests/data/templates/generate/test11/.generate.yml
  • tests/data/templates/generate/test11/feature1-only
  • tests/data/templates/generate/test11/feature2-only
  • tests/data/templates/generate/test11/take-me
  • tests/data/templates/generate/test12/.generate.yml
  • tests/data/templates/generate/test12/take-me
  • tests/data/templates/generate/test13/.generate.yml
  • tests/data/templates/generate/test13/take-me
  • tests/data/templates/generate/test14/.generate.yml
  • tests/data/templates/generate/test14/feature1-only
  • tests/data/templates/generate/test14/feature2-only
  • tests/data/templates/generate/test14/feature3-only
  • tests/data/templates/generate/test14/take-me
  • tests/data/templates/generate/test6/.generate.yml
  • tests/data/templates/generate/test6/feature1-file
  • tests/data/templates/generate/test6/feature1-only
  • tests/data/templates/generate/test6/feature2-file
  • tests/data/templates/generate/test6/feature2-only
  • tests/data/templates/generate/test6/no-feature1
  • tests/data/templates/generate/test6/take-me
  • tests/data/templates/generate/test7/.generate.yml
  • tests/data/templates/generate/test7/feature1-file
  • tests/data/templates/generate/test7/feature1-only
  • tests/data/templates/generate/test7/feature2-file
  • tests/data/templates/generate/test7/feature2-only
  • tests/data/templates/generate/test7/no-feature1
  • tests/data/templates/generate/test7/take-me
  • tests/data/templates/generate/test8/.generate.yml
  • tests/data/templates/generate/test8/take-me
  • tests/data/templates/generate/test9/.generate.yml
  • tests/data/templates/generate/test9/feature1-only
  • tests/data/templates/generate/test9/feature2-only
  • tests/data/templates/generate/test9/take-me
  • tests/ext/test_ext_generate.py

Comment thread cement/ext/ext_generate.py Outdated
Comment thread cement/ext/ext_generate.py Outdated
Comment thread demo/generate-features/README.md Outdated
Comment on lines +70 to +96
```
/tmp/myproject/
├── README.md
├── app.py
├── Dockerfile
└── docker-compose.yml
```

**With docker disabled** (docker_compose is auto-disabled via `requires`):

```
/tmp/myproject/
├── README.md
└── app.py
```

No Dockerfile, no docker-compose.yml — disabling docker automatically
disables docker_compose because it requires docker.

**With docker enabled, docker_compose disabled**:

```
/tmp/myproject/
├── README.md
├── app.py
└── Dockerfile
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifiers to the tree code fences.

The three fenced blocks starting at Line 70, Line 80, and Line 91 are missing a language tag, so markdownlint will keep flagging MD040 here. Using text is enough.

Suggested fix
-```
+```text
 /tmp/myproject/
 ├── README.md
 ├── app.py
 ├── Dockerfile
 └── docker-compose.yml

@@
- +text
/tmp/myproject/
├── README.md
└── app.py

@@
-```
+```text
/tmp/myproject/
├── README.md
├── app.py
└── Dockerfile
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 70-70: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

[warning] 80-80: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

[warning] 91-91: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @demo/generate-features/README.md around lines 70 - 96, In
demo/generate-features/README.md update the three fenced code blocks that show
the project tree outputs so each opening fence is text instead of (the
blocks containing the Docker-enabled tree, the "With docker disabled" tree, and
the "With docker enabled, docker_compose disabled" tree); this will satisfy
markdownlint MD040 by explicitly marking the code blocks as plain text.


</details>

<!-- fingerprinting:phantom:poseidon:grasshopper -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +3 to +6
WORKDIR /app
COPY . .

CMD ["python", "app.py"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the Dockerfile exists and read its full content
find demo -name "Dockerfile" -type f | head -20

Repository: datafolklabs/cement

Length of output: 122


🏁 Script executed:

# Look for the specific file mentioned
cat demo/generate-features/templates/generate/webapp/Dockerfile

Repository: datafolklabs/cement

Length of output: 149


🏁 Script executed:

# Check for .dockerignore in the same directory
ls -la demo/generate-features/templates/generate/webapp/

Repository: datafolklabs/cement

Length of output: 473


Drop root before starting the app.

The template never switches away from the default root user, so every generated container runs with full privileges. Add an unprivileged user and hand off /app before CMD.

Suggested fix
 FROM python:{{ python_version }}-slim
 
 WORKDIR /app
 COPY . .
+RUN useradd --create-home --shell /usr/sbin/nologin appuser \
+    && chown -R appuser:appuser /app
+USER appuser
 
 CMD ["python", "app.py"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WORKDIR /app
COPY . .
CMD ["python", "app.py"]
FROM python:{{ python_version }}-slim
WORKDIR /app
COPY . .
RUN useradd --create-home --shell /usr/sbin/nologin appuser \
&& chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/generate-features/templates/generate/webapp/Dockerfile` around lines 3 -
6, Create and switch to an unprivileged user in the Dockerfile: add steps to
create a non-root user (e.g., "appuser"), chown -R /app to transfer ownership
after COPY, and add a USER appuser line before the existing CMD
["python","app.py"] so the container drops root privileges when starting the
application; keep WORKDIR /app and COPY . . as-is but ensure the chown happens
after the copy.

Comment thread tests/ext/test_ext_generate.py
Comment on lines +233 to +299
def test_generate_features_requires_satisfied(tmp):
# test9: feature1=true (default), feature2=true (default), feature2 requires feature1
argv = ['generate', 'test9', tmp.dir, '--defaults']

with GenerateApp(argv=argv) as app:
app.run()

# both features enabled, both files present
assert exists_join(tmp.dir, 'take-me')
assert exists_join(tmp.dir, 'feature1-only')
assert exists_join(tmp.dir, 'feature2-only')


def test_generate_features_requires_not_satisfied(tmp):
# test11: feature1=false (default), feature2=true (default) but requires feature1
# feature2 should be auto-disabled because feature1 is off
argv = ['generate', 'test11', tmp.dir, '--defaults']

with GenerateApp(argv=argv) as app:
app.run()

assert exists_join(tmp.dir, 'take-me')

# feature1 disabled: feature1-only ignored
assert not exists_join(tmp.dir, 'feature1-only')

# feature2 auto-disabled via requires: feature2-only ignored
assert not exists_join(tmp.dir, 'feature2-only')


def test_generate_features_requires_unknown(tmp):
# test10: feature requires a nonexistent feature
argv = ['generate', 'test10', tmp.dir, '--defaults']

with GenerateApp(argv=argv) as app:
with raises(AssertionError, match="requires unknown feature"):
app.run()


def test_generate_features_minimal(tmp):
# test13: feature with only name and default, no enabled/disabled blocks
argv = ['generate', 'test13', tmp.dir, '--defaults']

with GenerateApp(argv=argv) as app:
app.run()

assert exists_join(tmp.dir, 'take-me')
with open(os.path.join(tmp.dir, 'take-me'), 'r') as f:
res = f.read()
assert 'myapp' in res


def test_generate_features_transitive_requires(tmp):
# test14: feature1=false, feature2=true requires feature1,
# feature3=true requires feature2
# disabling feature1 should cascade: feature2 disabled, then feature3 disabled
argv = ['generate', 'test14', tmp.dir, '--defaults']

with GenerateApp(argv=argv) as app:
app.run()

assert exists_join(tmp.dir, 'take-me')

# all three features should be disabled via transitive requires
assert not exists_join(tmp.dir, 'feature1-only')
assert not exists_join(tmp.dir, 'feature2-only')
assert not exists_join(tmp.dir, 'feature3-only')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dependency resolution is still order-sensitive.

The new requires coverage only uses fixtures where prerequisites are declared earlier in YAML. _process_features() resolves unsatisfied from the partially built feature_states map, so a dependent that appears before its requirement will be auto-disabled incorrectly even if the requirement defaults to enabled. Please add an out-of-order regression case and make the resolver order-independent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/ext/test_ext_generate.py` around lines 233 - 299, The feature
dependency resolver in _process_features() is order-dependent because it
computes `unsatisfied` from the partially built `feature_states`, causing
dependents declared before their requirements to be auto-disabled; make
resolution order-independent by computing feature enablement via a
fixpoint/graph traversal: build a map of feature -> requires list (from the
features input), validate unknown requirements, then iterate (or DFS) to compute
each feature's final enabled state using default values and transitive requires
until no changes occur (or use topological order/detect cycles), replacing the
current `unsatisfied` logic that reads partial `feature_states`; also add a new
test case (similar to existing tests) where a feature appears before its
requirement in the YAML (out-of-order) and assert the dependent is enabled when
its requirement defaults to enabled to prevent regressions.

Allow .generate.yml templates to declare optional features with
conditional variables, exclude patterns, and ignore patterns based
on enabled/disabled state. Features support dependency resolution
via `requires` with automatic transitive cascading — disabled
dependencies skip prompting for dependent features entirely.
Resolution is order-independent: a feature may declare `requires`
against another feature defined later in the YAML.

Validation failures (missing `name`, unknown `requires` target)
raise ValueError so they survive `python -O` (asserts get stripped).

Also tighten setup_template_items: separate ModuleNotFoundError
from AttributeError and only swallow when the missing module is the
template module being imported — transitive ModuleNotFoundError
raised from inside the user's template module now propagates
instead of being silently logged. Correct the variables default
from {} to [].

Related:

- Resolves Issue #743
@derks derks force-pushed the feat/generate-optional-features branch from e11c7d8 to 853e878 Compare May 8, 2026 21:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
cement/ext/ext_generate.py (1)

29-34: ⚡ Quick win

Use the repo’s full-form typing style in the new helper signature.

The new annotations use built-in generics (list[...], dict[...]), but this repo’s Python style requires List[...] / Dict[...] forms. Please align the new helper signature while touching this code so it stays consistent with the strict typing rules. As per coding guidelines "Type annotations must use full form: Dict[str, Any], Optional[str], List[str], Tuple[int, str]".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cement/ext/ext_generate.py` around lines 29 - 34, Update the
_process_features helper signature to use the repo’s full-form typing (replace
built-in generics with typing ones): change parameters typed as list[dict[str,
Any]] and dict[str, Any] to List[Dict[str, Any]] and Dict[str, Any]; ensure you
import List, Dict, Any from typing at the top of the module if not already
present and adjust any sibling annotations in the same file to match this style
(refer to the _process_features definition to locate the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cement/ext/ext_generate.py`:
- Around line 50-90: The interactive flow currently prompts every feature
up-front (FeaturePrompt in the pass 1 loop) which leads to prompting dependents
even when their prerequisites later get declined; change to lazy prompting so
prerequisites are resolved before prompting dependents: stop prompting in the
initial pass (when self.app.pargs.defaults is False) — only initialise
feature_states with default booleans or an explicit "undecided" marker — then in
the pass 2 cascade loop (the while changed loop that inspects
feature.get('requires', [])) when you encounter a required feature whose state
is undecided and interactive, instantiate and run the FeaturePrompt for that
specific required feature (using the same prompt text/default logic currently in
the FeaturePrompt inner class), set its feature_states entry based on the
answer, and continue dependency resolution; finally ensure dependents are never
prompted if a prerequisite resolves to False, and add one interactive regression
test exercising a declined prerequisite that would previously still prompt its
dependent to assert no prompt is shown for the dependent.
- Around line 124-126: The code uses g_config.get('variables', []),
g_config.get('exclude', []), and g_config.get('ignore', []) which still yield
None if YAML explicitly sets those keys to null; change these to use the
coalescing pattern (e.g., g_config.get('variables') or []) for vars,
exclude_list, and ignore_list so the subsequent loop (for defined_var in vars)
and any consumers are safe when the YAML contains null values, matching the
existing handling used for feature blocks.
- Around line 29-34: Rename the parameter named vars in the _process_features
method to variables and also rename the local variable currently assigned to
vars (the one flagged by Ruff) to variables so you no longer shadow Python's
built-in vars(); update all references inside _process_features to use variables
instead of vars (including any iteration, lookups, or passes to other functions)
to fix the A002/A001 lint violations.

---

Nitpick comments:
In `@cement/ext/ext_generate.py`:
- Around line 29-34: Update the _process_features helper signature to use the
repo’s full-form typing (replace built-in generics with typing ones): change
parameters typed as list[dict[str, Any]] and dict[str, Any] to List[Dict[str,
Any]] and Dict[str, Any]; ensure you import List, Dict, Any from typing at the
top of the module if not already present and adjust any sibling annotations in
the same file to match this style (refer to the _process_features definition to
locate the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f823932c-88b2-4e0c-951e-4037917d2421

📥 Commits

Reviewing files that changed from the base of the PR and between e11c7d8 and 853e878.

📒 Files selected for processing (51)
  • CHANGELOG.md
  • CLAUDE.md
  • cement/ext/ext_generate.py
  • demo/generate-features/README.md
  • demo/generate-features/templates/generate/webapp/.dockerignore
  • demo/generate-features/templates/generate/webapp/.generate.yml
  • demo/generate-features/templates/generate/webapp/Dockerfile
  • demo/generate-features/templates/generate/webapp/README.md
  • demo/generate-features/templates/generate/webapp/app.py
  • demo/generate-features/templates/generate/webapp/docker-compose.yml
  • tests/data/bad_template_module/__init__.py
  • tests/data/templates/generate/test10/.generate.yml
  • tests/data/templates/generate/test10/take-me
  • tests/data/templates/generate/test11/.generate.yml
  • tests/data/templates/generate/test11/feature1-only
  • tests/data/templates/generate/test11/feature2-only
  • tests/data/templates/generate/test11/take-me
  • tests/data/templates/generate/test12/.generate.yml
  • tests/data/templates/generate/test12/take-me
  • tests/data/templates/generate/test13/.generate.yml
  • tests/data/templates/generate/test13/take-me
  • tests/data/templates/generate/test14/.generate.yml
  • tests/data/templates/generate/test14/feature1-only
  • tests/data/templates/generate/test14/feature2-only
  • tests/data/templates/generate/test14/feature3-only
  • tests/data/templates/generate/test14/take-me
  • tests/data/templates/generate/test15/.generate.yml
  • tests/data/templates/generate/test15/feature_a-only
  • tests/data/templates/generate/test15/feature_b-only
  • tests/data/templates/generate/test15/take-me
  • tests/data/templates/generate/test6/.generate.yml
  • tests/data/templates/generate/test6/feature1-file
  • tests/data/templates/generate/test6/feature1-only
  • tests/data/templates/generate/test6/feature2-file
  • tests/data/templates/generate/test6/feature2-only
  • tests/data/templates/generate/test6/no-feature1
  • tests/data/templates/generate/test6/take-me
  • tests/data/templates/generate/test7/.generate.yml
  • tests/data/templates/generate/test7/feature1-file
  • tests/data/templates/generate/test7/feature1-only
  • tests/data/templates/generate/test7/feature2-file
  • tests/data/templates/generate/test7/feature2-only
  • tests/data/templates/generate/test7/no-feature1
  • tests/data/templates/generate/test7/take-me
  • tests/data/templates/generate/test8/.generate.yml
  • tests/data/templates/generate/test8/take-me
  • tests/data/templates/generate/test9/.generate.yml
  • tests/data/templates/generate/test9/feature1-only
  • tests/data/templates/generate/test9/feature2-only
  • tests/data/templates/generate/test9/take-me
  • tests/ext/test_ext_generate.py
✅ Files skipped from review due to trivial changes (39)
  • tests/data/templates/generate/test9/feature2-only
  • tests/data/templates/generate/test6/feature2-only
  • tests/data/templates/generate/test8/take-me
  • tests/data/templates/generate/test7/take-me
  • tests/data/templates/generate/test12/take-me
  • tests/data/templates/generate/test13/.generate.yml
  • tests/data/templates/generate/test6/no-feature1
  • demo/generate-features/templates/generate/webapp/.dockerignore
  • demo/generate-features/templates/generate/webapp/docker-compose.yml
  • tests/data/templates/generate/test15/feature_a-only
  • tests/data/templates/generate/test15/feature_b-only
  • tests/data/templates/generate/test6/feature1-only
  • tests/data/templates/generate/test10/take-me
  • tests/data/templates/generate/test6/feature2-file
  • tests/data/templates/generate/test14/feature1-only
  • tests/data/templates/generate/test9/take-me
  • tests/data/templates/generate/test11/feature2-only
  • tests/data/templates/generate/test7/feature1-only
  • tests/data/templates/generate/test7/feature2-file
  • tests/data/templates/generate/test7/feature1-file
  • tests/data/templates/generate/test14/feature2-only
  • tests/data/templates/generate/test6/take-me
  • tests/data/templates/generate/test13/take-me
  • demo/generate-features/templates/generate/webapp/.generate.yml
  • tests/data/templates/generate/test11/feature1-only
  • tests/data/templates/generate/test7/feature2-only
  • tests/data/templates/generate/test11/take-me
  • tests/data/templates/generate/test7/no-feature1
  • tests/data/templates/generate/test6/feature1-file
  • tests/data/templates/generate/test14/take-me
  • tests/data/templates/generate/test6/.generate.yml
  • tests/data/templates/generate/test14/feature3-only
  • tests/data/templates/generate/test9/.generate.yml
  • CLAUDE.md
  • tests/data/templates/generate/test7/.generate.yml
  • tests/data/templates/generate/test8/.generate.yml
  • demo/generate-features/README.md
  • tests/data/templates/generate/test9/feature1-only
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/data/templates/generate/test12/.generate.yml
  • tests/data/templates/generate/test11/.generate.yml
  • tests/data/templates/generate/test10/.generate.yml
  • demo/generate-features/templates/generate/webapp/app.py
  • tests/data/templates/generate/test14/.generate.yml

Comment on lines +29 to +34
def _process_features(self,
features: list[dict[str, Any]],
vars: list[dict[str, Any]],
exclude_list: list[str],
ignore_list: list[str],
data: dict[str, Any]) -> None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "ext_generate.py"

Repository: datafolklabs/cement

Length of output: 1941


🏁 Script executed:

cat -n cement/ext/ext_generate.py | head -150

Repository: datafolklabs/cement

Length of output: 1941


🏁 Script executed:

rg -n "def _process_features|def _generate" cement/ext/ext_generate.py -A 10

Repository: datafolklabs/cement

Length of output: 1186


🏁 Script executed:

cat -n cement/ext/ext_generate.py | sed -n '120,130p'

Repository: datafolklabs/cement

Length of output: 601


🏁 Script executed:

rg -n "vars\b" cement/ext/ext_generate.py | head -20

Repository: datafolklabs/cement

Length of output: 446


Rename vars parameter and variable before merge.

The parameter vars on line 31 and the variable assignment on line 124 shadow Python's built-in vars() function. Ruff flags these as A002 and A001 violations respectively. Renaming both to variables (or similar) fixes the lint failures and improves readability since the data actually represents variables. This is required per the coding guideline: "All code must pass ruff linting (v0.3.2+)".

🧰 Tools
🪛 Ruff (0.15.12)

[error] 31-31: Function argument vars is shadowing a Python builtin

(A002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cement/ext/ext_generate.py` around lines 29 - 34, Rename the parameter named
vars in the _process_features method to variables and also rename the local
variable currently assigned to vars (the one flagged by Ruff) to variables so
you no longer shadow Python's built-in vars(); update all references inside
_process_features to use variables instead of vars (including any iteration,
lookups, or passes to other functions) to fix the A002/A001 lint violations.

Comment thread cement/ext/ext_generate.py Outdated
Comment on lines +50 to +90
# pass 1: compute tentative enabled-state per feature from default /
# prompt, ignoring requires. Order-independent.
feature_states: dict[str, bool] = {}
for feature in features:
name = feature['name']
default = bool(feature.get('default', False))
if self.app.pargs.defaults:
feature_states[name] = default
else:
default_hint = 'Y/n' if default else 'y/N' # pragma: nocover
default_val = 'y' if default else 'n' # pragma: nocover

class FeaturePrompt(shell.Prompt): # pragma: nocover
class Meta:
text = f"Enable Feature: {name} [{default_hint}]:"
default = default_val

p = FeaturePrompt(auto=False) # pragma: nocover
val: str = p.prompt() or default_val # pragma: nocover
feature_states[name] = val.lower() == 'y' # pragma: nocover

# pass 2: cascade-disable features whose requires are unsatisfied;
# iterate to a fixpoint so transitive chains resolve regardless of
# YAML ordering (a feature can declare a `requires` against another
# feature defined later in the list).
changed = True
while changed:
changed = False
for feature in features:
name = feature['name']
if not feature_states[name]:
continue
for req in feature.get('requires', []):
if not feature_states[req]:
self.app.log.warning(
f"Feature '{name}' disabled (requires: {req})"
)
feature_states[name] = False
changed = True
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Skip dependent feature prompts until prerequisites are resolved.

The new two-pass flow still prompts every feature in interactive mode before requires is applied, so declining a prerequisite can still ask the user about dependents and then discard that answer in pass 2. That misses the PR goal of skipping prompts for disabled dependency chains. Please resolve prerequisites first, or prompt lazily during dependency resolution, and add one interactive regression test for that path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cement/ext/ext_generate.py` around lines 50 - 90, The interactive flow
currently prompts every feature up-front (FeaturePrompt in the pass 1 loop)
which leads to prompting dependents even when their prerequisites later get
declined; change to lazy prompting so prerequisites are resolved before
prompting dependents: stop prompting in the initial pass (when
self.app.pargs.defaults is False) — only initialise feature_states with default
booleans or an explicit "undecided" marker — then in the pass 2 cascade loop
(the while changed loop that inspects feature.get('requires', [])) when you
encounter a required feature whose state is undecided and interactive,
instantiate and run the FeaturePrompt for that specific required feature (using
the same prompt text/default logic currently in the FeaturePrompt inner class),
set its feature_states entry based on the answer, and continue dependency
resolution; finally ensure dependents are never prompted if a prerequisite
resolves to False, and add one interactive regression test exercising a declined
prerequisite that would previously still prompt its dependent to assert no
prompt is shown for the dependent.

Comment thread cement/ext/ext_generate.py Outdated
derks added 2 commits May 8, 2026 17:31
Replace the two-pass (compute-then-cascade) feature resolver in
_process_features with a recursive _resolve(name) helper. Behaviorally
this changes only the interactive flow: a feature's prompt is now
reached lazily — only after every one of its `requires` has resolved
truthy. Previously every feature was prompted up-front in pass 1, so
declining feature_a still produced a prompt for feature_b (which
requires feature_a) before pass 2 cascaded the disable. Addresses
review feedback.

The --defaults code path and all existing test cases (in-order /
out-of-order / transitive / requires-not-satisfied / unknown-requires
/ missing-name) behave identically. No new test surface — the prompt
branch is unchanged structurally and remains pragma: nocover (cement
has no harness for driving interactive prompts).
`g_config.get('variables', [])` only catches the *missing* key — if
the YAML explicitly sets `variables: null` (or `exclude: null` /
`ignore: null`), `.get` returns the literal None and the next line
(ignore_list.append, for defined_var in vars) crashes. Switch to the
`.get(key) or []` coalescing pattern so an explicit null is treated
the same as omission. Addresses review feedback.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
cement/ext/ext_generate.py (1)

31-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename vars to avoid shadowing the Python builtin (Ruff A002/A001).

Both the _process_features parameter at Line 31 and the local at Line 125 still shadow the builtin vars() and continue to fail Ruff. Renaming both to variables (and updating the references at lines 100, 147 accordingly) clears the lint violations and is required by the project's PEP8/ruff gating.

♻️ Proposed rename
     def _process_features(self,
                           features: list[dict[str, Any]],
-                          vars: list[dict[str, Any]],
+                          variables: list[dict[str, Any]],
                           exclude_list: list[str],
                           ignore_list: list[str],
                           data: dict[str, Any]) -> None:
@@
-            vars.extend(block.get('variables', []))
+            variables.extend(block.get('variables', []))
             exclude_list.extend(block.get('exclude', []))
             ignore_list.extend(block.get('ignore', []))
@@
-        vars = g_config.get('variables') or []
+        variables = g_config.get('variables') or []
         exclude_list = g_config.get('exclude') or []
         ignore_list = g_config.get('ignore') or []
@@
-            self._process_features(features, vars, exclude_list,
-                                   ignore_list, data)
+            self._process_features(features, variables, exclude_list,
+                                   ignore_list, data)
@@
-        for defined_var in vars:
+        for defined_var in variables:

As per coding guidelines: "Run ruff linting on Python files to ensure PEP8 compliance".

Also applies to: 125-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cement/ext/ext_generate.py` at line 31, The parameter named vars in the
_process_features signature and the local variable named vars (used inside
_process_features) shadow the Python builtin vars() and trigger Ruff A002/A001;
rename the parameter to variables and the local to variables (or
variables_local) and update all references inside the function (calls/usages
currently around where vars is referenced, e.g., the usages previously at lines
~100 and ~147) to the new name so the function _process_features and its
internal logic compile and pass linting.
🧹 Nitpick comments (1)
cement/ext/ext_generate.py (1)

256-256: 💤 Low value

Nit: prefer unpacking over list concatenation (RUF005).

-            expected = '.'.join(mod_parts + [mod_name])
+            expected = '.'.join([*mod_parts, mod_name])

As per coding guidelines: "Run ruff linting on Python files to ensure PEP8 compliance".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cement/ext/ext_generate.py` at line 256, Replace the list concatenation used
to build the module path in the assignment expected = '.'.join(mod_parts +
[mod_name]) with sequence unpacking to satisfy the linter; e.g. construct the
joinable list as '.'.join([*mod_parts, mod_name]) (referencing the variables
mod_parts and mod_name where expected is computed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@cement/ext/ext_generate.py`:
- Line 31: The parameter named vars in the _process_features signature and the
local variable named vars (used inside _process_features) shadow the Python
builtin vars() and trigger Ruff A002/A001; rename the parameter to variables and
the local to variables (or variables_local) and update all references inside the
function (calls/usages currently around where vars is referenced, e.g., the
usages previously at lines ~100 and ~147) to the new name so the function
_process_features and its internal logic compile and pass linting.

---

Nitpick comments:
In `@cement/ext/ext_generate.py`:
- Line 256: Replace the list concatenation used to build the module path in the
assignment expected = '.'.join(mod_parts + [mod_name]) with sequence unpacking
to satisfy the linter; e.g. construct the joinable list as '.'.join([*mod_parts,
mod_name]) (referencing the variables mod_parts and mod_name where expected is
computed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f621ebb9-d932-4848-a5f0-72a0011cf0e7

📥 Commits

Reviewing files that changed from the base of the PR and between 853e878 and 5c6e81e.

📒 Files selected for processing (1)
  • cement/ext/ext_generate.py

@derks derks merged commit 6895aa5 into main May 9, 2026
14 checks passed
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.

1 participant