feat(ext.generate): unify features into typed variables (#782)#783
feat(ext.generate): unify features into typed variables (#782)#783derks wants to merge 19 commits into
Conversation
- Migrate test6/test7/test13 fixtures from features: to variables: with type: boolean + extend: (enabled:->when:true, disabled:->when:false) - Add test31 string-form bool-prompt fixture + patched-prompt unit tests covering y/yes->True, n/no->False, empty->default mapping - Add test32 D-17 fixture + test: type:boolean declaring case: -> ValueError - Tests fail until the resolver lands in Task 2 (expected RED)
- Add single recursive resolve_and_emit walking variables: in declaration order; every variable emits at top-level data[name] (delete the data['features'] namespace — #782 bug-4 fix) - Add type: key (string|boolean|choice, default string); type: string is byte-identical to the released schema (test1-5/22/27/29 oracle) - type: boolean emits a real Python bool so {% if feature_x %} works; string-form prompt '[(Y)es/(N)o] [default]:' with y/yes->True, n/no->False, empty->default mapping (covered via patched-prompt test) - D-17 guard: type: boolean/choice declaring case:/validate: -> ValueError - Unknown type: -> ValueError (T-05.1-01 input-validation mitigation) - extend: boolean when:true/false accumulates variables/exclude/ignore and recurses nested vars depth-first in place - Keep a thin legacy features: bridge folding enabled/disabled/options into the unified accumulation so un-migrated test8-30 stay green; bridge removed in Plan 03 (user-approved Rule 4 branch-by-abstraction) - Add test33 (silent bool) + test34 (invalid type) for 100% coverage
- migrate test16/19/20/21/26/28/30 from prompt_mode:select to type:
choice + options: + extend: (when-keyed effects)
- add test35 object-form bool prompt {text, accept, reject} fixture
- add test36 unquoted bool-like accept member (YAML 1.1 coercion)
- add test37 type:choice declaring case: (D-17 string-only) fixture
- rewrite select tests for choice schema; add picker/accept/reject/
junk-abort/coercion-guard/D-17-choice tests (RED before Task 2)
- type: choice numbered picker emits the chosen option str at
data[name]; fail-fast ValueError on empty options / option missing
value / default-not-in-values / duplicate labels (D-16, Q3)
- polymorphic boolean prompt object-form {text, accept, reject}: author
owns the text; accept/reject case-insensitive token lists map to bool;
junk input asserts + aborts 'Invalid Response' (D-12/D-14)
- YAML 1.1 bool-coercion load guard: a bool-coerced accept/reject member
raises ValueError telling the author to quote it (Pitfall 1, T-05.1-04)
- extend.when scalar str()-coerced equality for choice/string; booleans
still match by Python == (Q2) — test16/test19 fire their per-option
rules at this commit
- D-17 case:/validate: on type:choice -> ValueError (string-only)
- pragma the now-dead legacy select-validation raises (owned by the new
resolver under real coverage; bridge removed in Plan 03)
- migrate test8-12/14/15/23/24/25 to type:/extend:/requires: schema - remove obsolete test17/test18 (prompt_mode/enabled/disabled gone; type: whitelist already covered by test34) - add test38 (extend.when list membership), test39 (string regex), test40 (Q1 requires-gated-out renders default) - adapt assertions to new semantics: gated-out var renders its default and omits its extend effects (Q1); requires unknown -> ValueError - RED until the resolver lands list/regex/nesting/requires in Task 2
- generalize _when_matches to compose scalar/in-list/string-regex
forms; multiple matching extend rules compose and accumulate
- nested extend.variables resolve depth-first in place only when the
parent rule fires (D-08)
- add top-level requires: with full match vocab ([name] truthy,
{name: value} equality, {name: [v]} in-list), AND-ed and resolved
order-independently via lazy memoized recursion (D-09/D-11)
- a requires-gated-out var renders as its default (Q1) and its extend
rules do not fire; unknown requires target -> ValueError
- DELETE the legacy features:/_process_features bridge; the engine no
longer reads a top-level features: key (GEN-01 fully true)
- variable missing name -> ValueError (was assert)
- test38/41 cover boolean list-form when + dict-form requires equality
Resolves #782
- Rewrite webapp .generate.yml under variables: with type:/extend:/requires: (docker/docker_compose boolean, web_framework choice; drop features:) - docker_compose requires: [docker]; per-branch extend.when ignore + silent framework_version; docker extend.when true prompts python_version - Demonstrate #782 fix: app.py/README use {% if docker %} and {% if web_framework == ... %} against top-level bool/choice exposure - Rewrite demo README to type:/extend:/requires: vocabulary, document the {{ bool }} -> True/False interpolation gotcha
- Remove the four bare # pragma: nocover markers on the type: choice picker setup (default_label / prompt_text / SelectPrompt class) in _resolve_choice — they lacked a locked-vocabulary category label and excluded code that the patched-prompt test actually exercises. - test_generate_choice_picker_maps_label_to_value drives the picker end to end, so the bug-prone label->value mapping is now under real coverage (269 stmts, 0 miss) rather than pragma'd. - Only the raw SelectPrompt().prompt() input read stays excluded, with its existing # defensive: unreachable label. - Reword the dynamic-import FIXME so it no longer contains a literal 'pragma: nocover' token that tripped the inverse audit grep.
- Reference PR #780 (Tom Freudenberg's review feedback) alongside #782 on the unified type:/extend:/requires: Features entry, calling out the four addressed points: features after vars in declaration order, custom per-feature prompt text, vars-style input, top-level Jinja boolean exposure. - Rewrap the demo-migration [dev] entry to keep every line <=78 chars.
A type: boolean variable with no prompt: key rendered the literal
"None [(Y)es/(N)o] [<hint>]:" because var['prompt'] was None. Default
the label to f"Enable {name}" when prompt is falsy (D-12), mirroring
the choice path's `or f"Select: {name}"` fallback. An explicit
prompt: still overrides; the object-form is unaffected.
Covered by test42 (boolean, no prompt:) via a recorder patched onto
shell.Prompt.prompt that captures the bound Meta.text and asserts the
"Enable enable_thing [(Y)es/(N)o]" label plus the explicit-override
case.
Correct the demo README: requires: uses a bare-name list (truthy) or a
{name: value} map (equality/in-list/regex) — not the list-of-objects
form the engine rejects. Document the boolean default label
(Enable <name>) and the object-form prompt (text/accept/reject) with
the YAML bool-coercion quoting note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR replaces Cement's legacy ChangesTyped Variable System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)demo/generate-features/README.mdmarkdownlint-cli2 v0.22.1 (markdownlint v0.40.0) 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 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 308-328: In _when_matches, don't treat every string when as a
regex; change the regex branch to only run when when is an actual compiled regex
(e.g., isinstance(when, re.Pattern)) or when an explicit regex marker is used,
and otherwise fall back to exact string equality for vtype == 'string' (so
literal when values like "flask" match exactly and values with regex
metacharacters are not interpreted). Update the regex check in the _when_matches
function and ensure the final fallback uses str(when) == str(value) for string
scalars.
- Around line 340-375: Add cycle detection for recursive variable resolution by
introducing a "resolving" set and using it in resolve_and_emit to detect
re-entrance: when resolve_and_emit(var) starts, if var['name'] is already in
resolving raise a ValueError describing the cycle (include the var name and
ideally the current stack of resolving names); otherwise add the name to
resolving, proceed with resolution, memoize into data as before, and finally
remove the name from resolving in a finally block. Keep _requires_satisfied, and
other call sites (the code that calls resolve_and_emit in the other blocks
referenced) unchanged except they will now get a clear ValueError instead of
RecursionError; reference resolve_and_emit, _requires_satisfied, var_by_name,
data and ensure resolving is accessible to both resolve_and_emit and the
callers' scope.
- Around line 330-338: _gated_default currently stringifies None for gated-out
string/choice vars; change _gated_default(var, vtype) to first verify that var
contains a real default (e.g. 'default' in var and var['default'] is not None)
and if not, raise a validation error (or otherwise reject) instead of returning
"None"; keep the boolean branch casting to bool, and for choice/string types
ensure the default passes the same validation used by _resolve_choice (reuse or
call that validation) before returning str(default). Apply the same guard to the
duplicate implementation around the other occurrence (the block at the later
406-412 location).
In `@CHANGELOG.md`:
- Around line 225-230: The CHANGELOG entry for `[ext.generate]` spans multiple
lines; collapse it to a single-line entry prefixed with `[ext.generate]` that
summarizes the change succinctly (mention removal of the unreleased `features:`
schema and deletion of the legacy compatibility bridge, and reference issue `#782`
if needed) so the entire note fits on one line in CHANGELOG.md.
- Around line 190-196: The CHANGELOG entry for [ext.generate] describing the
`type: choice` template variables spans multiple lines; collapse it into a
single-line entry prefixed with `[ext.generate]` that summarizes the feature
(mentioning `type: choice`, `cement.utils.shell.Prompt`, and key behaviors like
`options:` format and fail-fast validation) so it follows the one-line-per-entry
guideline.
- Around line 160-165: The CHANGELOG entry for [ext.generate] spans multiple
lines; collapse it into a single-line entry prefixed with [ext.generate] that
briefly states the change (e.g. "type: boolean template variables now render as
Python bools so jinja2/mustache conditionals work; boolean prompt format and
single-pass prompting; resolves `#782`") and move the detailed explanation about
`data[name]`, `{% if feature_x %}`, the `[(Y)es/(N)o] [default]:` prompt format,
and the removal of the former `features:` namespace/pre-pass into the associated
PR/issue description.
- Around line 197-205: Consolidate the multi-line CHANGELOG entry for
[ext.generate] into a single line: rewrite the paragraph as one line starting
with [ext.generate] and briefly state that `type: boolean` `prompt:` is now
polymorphic — supports framework string, `prompt: false`, or an object form
`{text, accept, reject}` where case-insensitive `accept`/`reject` tokens map
input to a bool (unmatched input errors as `Invalid Response` mirroring
`validate:`) and YAML 1.1 bool-like `accept`/`reject` tokens raise `ValueError`
instructing authors to quote them; ensure no line breaks so the entire entry is
one line.
- Around line 232-236: Consolidate the multi-line CHANGELOG entry into a
single-line entry prefixed with [dev]; replace the 5-line block mentioning
`demo/generate-features/`, the migration from `features:` to
`type:`/`extend:`/`requires:`, and the `#782` rendering note into one concise line
(e.g. "[dev] Migrate demo/generate-features/ template to unified
type/extend/requires schema; demonstrate `#782` fix where `{% if docker %}` and
`{% if web_framework == ... %}` now render against top-level bool/choice
exposure").
- Around line 206-221: The changelog entry for the `[ext.generate]` feature is
currently multi-line; collapse it into a single-line entry starting with the
`[ext.generate]` prefix that summarizes the change (mention compose three match
forms, requires gating, and PR/Issue references) and keep the links `(Issue
`#782`, PR `#780`)` inline; ensure the line is one continuous line (no line breaks)
and follows the "Each CHANGELOG.md entry must be one line, prefixed with [area]"
rule so the existing multi-line block is replaced with that single consolidated
line.
In `@demo/generate-features/README.md`:
- Line 272: Remove the unused Markdown link reference "[779]" by deleting the
line that defines "[779]: https://github.com/datafolklabs/cement/issues/779"
(ensure no other references to [779] exist elsewhere in the document); this
cleans up the README and avoids an orphaned link definition now superseded by
issue `#782`.
- Around line 157-160: The blockquote containing the text starting with "Quote
bool-like tokens (`\"yes\"`, `\"no\"`, `\"on\"`, `\"off\"`) inside `accept:` /
`reject:` — under YAML 1.1 they otherwise decode to a Python `bool` and the
loader rejects them with a clear error." contains an extra blank line that
violates MD028; remove the blank line so the two quoted lines are merged into a
single blockquote paragraph, or alternatively split them into two separate
blockquote blocks so there is no empty line inside a single blockquote.
In `@tests/ext/test_ext_generate.py`:
- Around line 365-374: The test test_generate_boolean_silent is not asserting
the actual typed output and can pass if the boolean is wrongly stringified;
change the test to verify the raw rendered value or flip the default to false so
string 'True' fails — specifically update test_generate_boolean_silent (and/or
the fixture used by GenerateApp) to assert the file contents equal the expected
literal boolean representation (or use a false default so only a real Python
True/False yields the expected branch), e.g., open the generated 'take-me' file
after app.run() and assert the exact value/type rather than only that 'flag-on'
appears.
- Around line 294-305: The new tests (e.g., test_generate_boolean_prompt_yes)
and related helpers like record() lack Python type annotations; update each test
function signature to add parameter and return type annotations (e.g., annotate
the tmp parameter type and the function return as -> None) and annotate the
record() helper's parameters and return type so all new/changed tests comply
with the project's typing rule; ensure every test and helper introduced in this
diff uses explicit type hints for parameters and return values.
🪄 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: f13485d2-bbc2-4666-9aee-0416b870d66a
📒 Files selected for processing (64)
CHANGELOG.mdcement/ext/ext_generate.pydemo/generate-features/README.mddemo/generate-features/templates/generate/webapp/.generate.ymldemo/generate-features/templates/generate/webapp/README.mddemo/generate-features/templates/generate/webapp/app.pytests/data/templates/generate/test10/.generate.ymltests/data/templates/generate/test11/.generate.ymltests/data/templates/generate/test12/.generate.ymltests/data/templates/generate/test13/.generate.ymltests/data/templates/generate/test14/.generate.ymltests/data/templates/generate/test14/take-metests/data/templates/generate/test15/.generate.ymltests/data/templates/generate/test16/.generate.ymltests/data/templates/generate/test17/.generate.ymltests/data/templates/generate/test17/take-metests/data/templates/generate/test18/.generate.ymltests/data/templates/generate/test18/take-metests/data/templates/generate/test19/.generate.ymltests/data/templates/generate/test20/.generate.ymltests/data/templates/generate/test21/.generate.ymltests/data/templates/generate/test23/.generate.ymltests/data/templates/generate/test24/.generate.ymltests/data/templates/generate/test25/.generate.ymltests/data/templates/generate/test26/.generate.ymltests/data/templates/generate/test28/.generate.ymltests/data/templates/generate/test30/.generate.ymltests/data/templates/generate/test31/.generate.ymltests/data/templates/generate/test31/take-metests/data/templates/generate/test32/.generate.ymltests/data/templates/generate/test32/take-metests/data/templates/generate/test33/.generate.ymltests/data/templates/generate/test33/take-metests/data/templates/generate/test34/.generate.ymltests/data/templates/generate/test34/take-metests/data/templates/generate/test35/.generate.ymltests/data/templates/generate/test35/take-metests/data/templates/generate/test36/.generate.ymltests/data/templates/generate/test36/take-metests/data/templates/generate/test37/.generate.ymltests/data/templates/generate/test37/take-metests/data/templates/generate/test38/.generate.ymltests/data/templates/generate/test38/bool-list-ignoredtests/data/templates/generate/test38/fastapi-onlytests/data/templates/generate/test38/not-webtests/data/templates/generate/test38/take-metests/data/templates/generate/test39/.generate.ymltests/data/templates/generate/test39/not-v2tests/data/templates/generate/test39/take-metests/data/templates/generate/test39/v3-onlytests/data/templates/generate/test40/.generate.ymltests/data/templates/generate/test40/should-be-kepttests/data/templates/generate/test40/take-metests/data/templates/generate/test41/.generate.ymltests/data/templates/generate/test41/dep-no-kepttests/data/templates/generate/test41/dep-ok-ignoredtests/data/templates/generate/test41/take-metests/data/templates/generate/test42/.generate.ymltests/data/templates/generate/test42/take-metests/data/templates/generate/test6/.generate.ymltests/data/templates/generate/test7/.generate.ymltests/data/templates/generate/test8/.generate.ymltests/data/templates/generate/test9/.generate.ymltests/ext/test_ext_generate.py
💤 Files with no reviewable changes (4)
- tests/data/templates/generate/test18/take-me
- tests/data/templates/generate/test18/.generate.yml
- tests/data/templates/generate/test17/take-me
- tests/data/templates/generate/test17/.generate.yml
| def _when_matches(value: Any, when: Any, vtype: str) -> bool: | ||
| # Compose the three `extend.when` / `requires` match forms | ||
| # (D-07), dispatching on the carrying variable's `type` (Q2): | ||
| # - list -> in-list membership. Booleans compare by Python | ||
| # `==`; choice/string members str()-coerce both sides. | ||
| # - regex -> `re.match(when, value)` — STRING type only | ||
| # (mirrors `validate:`); a non-string `when` on a string | ||
| # var still falls through to scalar equality below. | ||
| # - scalar -> equality. Booleans by Python `==` (so | ||
| # `when: true` matches `True`, not the string "True"); | ||
| # choice/string by str()-coerced equality (so `when: 1` | ||
| # matches a "1" option value; per the L219 discipline). | ||
| if isinstance(when, list): | ||
| if vtype == 'boolean': | ||
| return value in when | ||
| return str(value) in [str(w) for w in when] | ||
| if vtype == 'string' and isinstance(when, str): | ||
| return re.match(when, value) is not None | ||
| if vtype == 'boolean': | ||
| return bool(value == when) | ||
| return str(when) == str(value) |
There was a problem hiding this comment.
Don't treat every string when as a regex.
Line 324 makes scalar string rules impossible: when: flask also matches flask2, and literals containing regex metacharacters are interpreted unexpectedly. That breaks the advertised equality semantics for string variables in both requires and extend; this needs an explicit regex form or a separate exact-match 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 308 - 328, In _when_matches, don't
treat every string when as a regex; change the regex branch to only run when
when is an actual compiled regex (e.g., isinstance(when, re.Pattern)) or when an
explicit regex marker is used, and otherwise fall back to exact string equality
for vtype == 'string' (so literal when values like "flask" match exactly and
values with regex metacharacters are not interpreted). Update the regex check in
the _when_matches function and ensure the final fallback uses str(when) ==
str(value) for string scalars.
| - `[ext.generate]` `type: boolean` template variables now emit a real | ||
| Python bool at the top level of the render context (`data[name]`), | ||
| so `{% if feature_x %}` works in jinja2/mustache; the boolean prompt | ||
| uses a vars-style `[(Y)es/(N)o] [default]:` format and all variables | ||
| prompt in a single declaration-order pass — resolves #782 (the | ||
| former `features:` namespace and pre-pass are removed) |
There was a problem hiding this comment.
Consolidate to a single-line CHANGELOG entry.
CHANGELOG entries must be one line per the coding guidelines. This entry spans 6 lines. Condense the description or move detail to issue/PR discussions.
As per coding guidelines: "Each CHANGELOG.md entry must be one line, prefixed with [area]".
Example consolidation
-- `[ext.generate]` `type: boolean` template variables now emit a real
- Python bool at the top level of the render context (`data[name]`),
- so `{% if feature_x %}` works in jinja2/mustache; the boolean prompt
- uses a vars-style `[(Y)es/(N)o] [default]:` format and all variables
- prompt in a single declaration-order pass — resolves `#782` (the
- former `features:` namespace and pre-pass are removed)
+- `[ext.generate]` `type: boolean` variables emit real Python bools at top level for direct Jinja2/mustache conditionals; unified prompt pass — resolves `#782`📝 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.
| - `[ext.generate]` `type: boolean` template variables now emit a real | |
| Python bool at the top level of the render context (`data[name]`), | |
| so `{% if feature_x %}` works in jinja2/mustache; the boolean prompt | |
| uses a vars-style `[(Y)es/(N)o] [default]:` format and all variables | |
| prompt in a single declaration-order pass — resolves #782 (the | |
| former `features:` namespace and pre-pass are removed) | |
| - `[ext.generate]` `type: boolean` variables emit real Python bools at top level for direct Jinja2/mustache conditionals; unified prompt pass — resolves `#782` |
🤖 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 `@CHANGELOG.md` around lines 160 - 165, The CHANGELOG entry for [ext.generate]
spans multiple lines; collapse it into a single-line entry prefixed with
[ext.generate] that briefly states the change (e.g. "type: boolean template
variables now render as Python bools so jinja2/mustache conditionals work;
boolean prompt format and single-pass prompting; resolves `#782`") and move the
detailed explanation about `data[name]`, `{% if feature_x %}`, the `[(Y)es/(N)o]
[default]:` prompt format, and the removal of the former `features:`
namespace/pre-pass into the associated PR/issue description.
| - `[ext.generate]` Add `type: choice` template variables — a numbered | ||
| `cement.utils.shell.Prompt` picker that emits the chosen option string | ||
| at the top level of the render context. `options:` accepts a scalar | ||
| list or per-option `{value, prompt}` objects; per-option effects live | ||
| in `extend:` rules keyed by `when: <value>`. Misconfig (empty options, | ||
| option missing `value`, default not in options, duplicate labels) is | ||
| fail-fast `ValueError`. |
There was a problem hiding this comment.
Consolidate to a single-line CHANGELOG entry.
This feature entry spans 7 lines. Per coding guidelines, each CHANGELOG entry must be one line.
As per coding guidelines: "Each CHANGELOG.md entry must be one line, prefixed with [area]".
Example consolidation
-- `[ext.generate]` Add `type: choice` template variables — a numbered
- `cement.utils.shell.Prompt` picker that emits the chosen option string
- at the top level of the render context. `options:` accepts a scalar
- list or per-option `{value, prompt}` objects; per-option effects live
- in `extend:` rules keyed by `when: <value>`. Misconfig (empty options,
- option missing `value`, default not in options, duplicate labels) is
- fail-fast `ValueError`.
+- `[ext.generate]` Add `type: choice` variables with numbered picker; emits string at top level; `extend:` rules per option; fail-fast validation📝 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.
| - `[ext.generate]` Add `type: choice` template variables — a numbered | |
| `cement.utils.shell.Prompt` picker that emits the chosen option string | |
| at the top level of the render context. `options:` accepts a scalar | |
| list or per-option `{value, prompt}` objects; per-option effects live | |
| in `extend:` rules keyed by `when: <value>`. Misconfig (empty options, | |
| option missing `value`, default not in options, duplicate labels) is | |
| fail-fast `ValueError`. | |
| - `[ext.generate]` Add `type: choice` variables with numbered picker; emits string at top level; `extend:` rules per option; fail-fast validation |
🤖 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 `@CHANGELOG.md` around lines 190 - 196, The CHANGELOG entry for [ext.generate]
describing the `type: choice` template variables spans multiple lines; collapse
it into a single-line entry prefixed with `[ext.generate]` that summarizes the
feature (mentioning `type: choice`, `cement.utils.shell.Prompt`, and key
behaviors like `options:` format and fail-fast validation) so it follows the
one-line-per-entry guideline.
| - `[dev]` Migrate the `demo/generate-features/` webapp template to the | ||
| unified `type:`/`extend:`/`requires:` schema (off the removed | ||
| `features:` schema) and demonstrate the #782 fix — `{% if docker %}` | ||
| and `{% if web_framework == ... %}` now render against the top-level | ||
| bool/choice exposure |
There was a problem hiding this comment.
Consolidate to a single-line CHANGELOG entry.
This refactoring entry spans 5 lines. Per coding guidelines, each CHANGELOG entry must be one line.
As per coding guidelines: "Each CHANGELOG.md entry must be one line, prefixed with [area]".
Example consolidation
-- `[dev]` Migrate the `demo/generate-features/` webapp template to the
- unified `type:`/`extend:`/`requires:` schema (off the removed
- `features:` schema) and demonstrate the `#782` fix — `{% if docker %}`
- and `{% if web_framework == ... %}` now render against the top-level
- bool/choice exposure
+- `[dev]` Migrate `demo/generate-features/` to unified typed variable schema; demonstrates `#782` top-level bool/choice exposure📝 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.
| - `[dev]` Migrate the `demo/generate-features/` webapp template to the | |
| unified `type:`/`extend:`/`requires:` schema (off the removed | |
| `features:` schema) and demonstrate the #782 fix — `{% if docker %}` | |
| and `{% if web_framework == ... %}` now render against the top-level | |
| bool/choice exposure | |
| - `[dev]` Migrate `demo/generate-features/` to unified typed variable schema; demonstrates `#782` top-level bool/choice exposure |
🤖 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 `@CHANGELOG.md` around lines 232 - 236, Consolidate the multi-line CHANGELOG
entry into a single-line entry prefixed with [dev]; replace the 5-line block
mentioning `demo/generate-features/`, the migration from `features:` to
`type:`/`extend:`/`requires:`, and the `#782` rendering note into one concise line
(e.g. "[dev] Migrate demo/generate-features/ template to unified
type/extend/requires schema; demonstrate `#782` fix where `{% if docker %}` and
`{% if web_framework == ... %}` now render against top-level bool/choice
exposure").
| def test_generate_boolean_prompt_yes(tmp): | ||
| # test31: type: boolean with a string-form prompt, run interactively | ||
| # (no --defaults). Patch shell.Prompt.prompt to return "yes" → the | ||
| # y/yes→True mapping runs and data[name] is True (jinja renders the | ||
| # enabled branch). | ||
| with patch.object(shell.Prompt, 'prompt', return_value='yes'): | ||
| argv = ['generate', 'test31', tmp.dir] | ||
| with GenerateApp(argv=argv) as app: | ||
| app.run() | ||
| with open(os.path.join(tmp.dir, 'take-me')) as f: | ||
| assert 'enabled' in f.read() | ||
|
|
There was a problem hiding this comment.
Add type annotations to the new tests and helper.
The new test cases start introducing unannotated tmp params and None returns, and the same issue repeats across the other added tests plus record(). That leaves the changed code out of compliance with the repo's Python typing rule.
Example pattern to apply
+from typing import Any
+
-def test_generate_boolean_prompt_yes(tmp):
+def test_generate_boolean_prompt_yes(tmp: Any) -> None:
...
- def record(self):
+ def record(self: shell.Prompt) -> str:
seen.append(self._meta.text)
return ''As per coding guidelines "Use type annotations for all function parameters and return types in Python code".
🤖 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 `@tests/ext/test_ext_generate.py` around lines 294 - 305, The new tests (e.g.,
test_generate_boolean_prompt_yes) and related helpers like record() lack Python
type annotations; update each test function signature to add parameter and
return type annotations (e.g., annotate the tmp parameter type and the function
return as -> None) and annotate the record() helper's parameters and return type
so all new/changed tests comply with the project's typing rule; ensure every
test and helper introduced in this diff uses explicit type hints for parameters
and return values.
|
Hi @derks that worked for me. I also tested with different "whens" and "requires". Looked complete by now. Let me please ask about your workflow, while many of your codes now generated by claude. Do you review all changes or is it mostly "YOLO" since 3.0.14? Have a nice sunday |
|
Hey @TomFreudenberg thanks for the feedback.
Right now I'm in a bit of an experimental phase, but have locked in at the moment to using GSD for many projects. Not sure if you've seen this issue/post yet; it outlines the current direction with AI (and the what/why):
Yes, I review all changes. Working with GSD through a phase looks like this: I won't say that I read everything - it's a lot. But I do review the plans before execution, otherwise you end up having to backout or redo a lot of the work. I review the code 100% ... that is where this one is at in the process. I'm treating the agent like I would working with a human ... set the direction, guide the work, let it make some of it's own informed decisions (research), and then submit the work for code review. Code Rabbit is here for a first pass "second set of eyes" - an alternative AI that is 100% outside the context of the planning/execution/etc. It reviews the outcome - same as if it were written by a human. My next pass will be to go through Code Rabbit's review with Claude, and discuss/implement changes based on the review. After that - I'll read though all of the code line by line and work through another session with Claude, maybe ask a lot of "why did we do X", and direct a lot of changes to clean it up. I might open my IDE and actually touch code - but to be honest it really disrupts the flow to modify files "out of band" ... changes might get reverted/stomped on by the agents if you don't remember to inform it to re-read files you touched. Once happy with the code - I'll squash/rebase down to a smaller set of commits and merge to |
|
Good morning @derks, do you have already defined a timeline when to release that an the last features? 3.0.16? I guess that I will publish my stuff within the next 10 days as beta. So if you are not ready then, I will use a branch and tag from my fork, otherwise the 3.0.16 stable. Cheers, |
|
Dear @derks could you be so kind and tell me about delivering date for 3.0.16 as next stable release? Thanks and hope you are fine |
|
@TomFreudenberg it is a work-in progress ... there is still more that I want to do before 3.0.16 - at the moment I am slightly delayed due to other responsibilities. I will be getting back to Cement progress in roughly a week from now. |
When requires: gates a variable out and it declares no default, _gated_default returned str(None) -> the literal string "None" landed in the template context (and the choice path skipped its required-default validation). Add a fail-fast ValueError guard (survives python -O, matching the D-17 guard style already in the file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A requires B + B requires A recursed to RecursionError because memoization only happened after a var fully resolved (data[name] set at the end). Add an in-flight resolving set: a name re-entered while still resolving raises a fail-fast ValueError. The set is cleared in a finally so every exit path (gated-out early return, normal completion, exceptions) leaves the happy path unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip test33 fixture default true -> false. With a false default a real Python False renders the else branch (flag-off); a regressed impl emitting the string 'False' is truthy and would render flag-on, failing the test. Update the assertion to flag-off and the comment to explain the typed-output contract being guarded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the bare blank line splitting two adjacent blockquotes with a quoted-empty line (>) so they form one continuous blockquote, clearing the markdownlint MD028 warning. Remove the unused [779] link reference definition (MD053; verified no [779] usage remains in the doc). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add test43 (a string variable gated out by requires: with no default) and test44 (alpha<->beta requires cycle) fixtures plus tests, exercising the two new fail-fast ValueError guards added in this branch. Restores 100% coverage on cement/ext/ext_generate.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeRabbit review triageThanks @coderabbitai. Triaged all 13 findings — 4 addressed, 9 declined (with reasons so it's on the record). Addressed
Plus Declined
|
|
Tip For best results, initiate chat on the files or code changes.
The declined items are well-reasoned and I'm happy to accept them:
372 passing, ruff + mypy clean — looks good to go. 🐇 |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
tests/ext/test_ext_generate.py (1)
281-281: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd type annotations to updated test function signatures.
These tests still have untyped
tmpparameters and implicit return types. Annotate params and set-> None.Suggested patch
+from typing import Any + -def test_generate_requires_gated_no_default(tmp): +def test_generate_requires_gated_no_default(tmp: Any) -> None: @@ -def test_generate_requires_cycle(tmp): +def test_generate_requires_cycle(tmp: Any) -> None: @@ -def test_generate_boolean_silent(tmp): +def test_generate_boolean_silent(tmp: Any) -> None:As per coding guidelines "Use type annotations for all function parameters and return types in Python code".
Also applies to: 293-293, 388-388
🤖 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 `@tests/ext/test_ext_generate.py` at line 281, The updated test functions still have untyped parameters and implicit return types; add type annotations to the affected test signatures and mark them with an explicit None return. Update the test functions identified by test_generate_requires_gated_no_default and the other matching tests in the same module so every parameter has a type annotation, including tmp, and each function uses -> None.Source: Coding guidelines
🤖 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 `@tests/ext/test_ext_generate.py`:
- Line 281: The updated test functions still have untyped parameters and
implicit return types; add type annotations to the affected test signatures and
mark them with an explicit None return. Update the test functions identified by
test_generate_requires_gated_no_default and the other matching tests in the same
module so every parameter has a type annotation, including tmp, and each
function uses -> None.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0c9a48da-ddc8-4de9-8fb5-8f90a5f7c077
📒 Files selected for processing (8)
cement/ext/ext_generate.pydemo/generate-features/README.mdtests/data/templates/generate/test33/.generate.ymltests/data/templates/generate/test43/.generate.ymltests/data/templates/generate/test43/take-metests/data/templates/generate/test44/.generate.ymltests/data/templates/generate/test44/take-metests/ext/test_ext_generate.py
✅ Files skipped from review due to trivial changes (5)
- tests/data/templates/generate/test44/take-me
- tests/data/templates/generate/test43/take-me
- tests/data/templates/generate/test43/.generate.yml
- tests/data/templates/generate/test44/.generate.yml
- demo/generate-features/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- cement/ext/ext_generate.py
Add the GSD quick-task PLAN/SUMMARY for the PR #783 CodeRabbit follow-ups and log the task in STATE.md. Planning/execution docs only — the code changes land separately in ee0a4ccb and 29e41bc9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Resolves #782 (Tom
Freudenberg's feedback on closed PR #780). The
generateextension's optionalfeatures mechanism is merged into the template variables mechanism so
that optional/conditional template inputs behave consistently with ordinary
variables — prompted in declaration order, exposed at the top level of the
template context, and with a sensible default-but-overridable prompt.
The entire
features:mechanism (boolean features and theprompt_mode: selectvariant) was unreleased — it first ships in 3.0.16 — so it is reshaped here
with no backward-compat shim. All changes to the released
variables:schema are strictly additive: a variable that declares none of the new keys
behaves byte-identically to today.
What an author writes now
A single
variables:list where each entry carries atype::Because every variable now lands at the top level of the render context
(
data[name]),{% if docker %}and{% if web_framework == "flask" %}workdirectly — the original Jinja bug in #782 (features were namespaced under
data['features']).Changes (by wave)
Wave 1 — unified typed resolver. Collapse
_process_featuresand the oldvariablesloop into one ordered, single-passresolve_and_emitwalker.Add
type: string|boolean|choice(defaultstring). Every variable emits attop-level
data[name]; thedata['features']namespace is removed. Booleanstring-form prompt renders
<prompt> [(Y)es/(N)o] [default]:, parsingy/yes→True,n/no→False, empty→default. D-17 guard:case:/validate:on a boolean/choice variable is a fail-fastValueError.Wave 2 — choice + polymorphic boolean prompt.
type: choicenumberedpicker (scalar list or
{value, prompt}option objects), emitting the chosenstring. Polymorphic boolean
prompt:— a plain string (framework owns thehint),
false(silent, usesdefault), or an object{text, accept, reject}where the author owns the text andaccept:/reject:are case-insensitive token lists (non-matching input aborts like a failed
validate:). A YAML 1.1 bool-coercion guard rejects unquotedyes/no/on/off/true/falsetokens inaccept:/reject:lists with aclear "quote it" error.
Wave 3 — conditional effects + dependencies; bridge removed.
extend.whencomposes three match forms — scalar equality, in-list membership, and (string
variables only) regex via
re.match— and multiple matching rules accumulatetheir
variables/exclude/ignore.extend.variablesnest depth-first,prompted in place only when their parent rule fires. Top-level
requires:gates a variable on others (bare-name list = truthy;
{name: value}map =equality/in-list/regex), AND-ed and resolved order-independently; a gated-out
variable takes its typed default so templates never error. The legacy
features:reader (~200 lines) is deleted —features:/prompt_mode:/enabled:/disabled:no longer exist in the engine.Wave 4 — demo migration.
demo/generate-features/rewritten to the unifiedschema and its README documents
type:/extend:/requires:, the polymorphicboolean prompt, and the
{{ bool }}capitalized-repr gotcha. All five CLIgeneratetemplates verified to still generate unchanged.Wave 5 — coverage audit + changelog. Removed redundant unlabeled
pragma: nocovermarkers that were masking already-tested mapping logic;every surviving pragma carries a locked-vocabulary label. CHANGELOG entries
for all four of #782's points.
Post-verification fix. Live interactive testing surfaced a defect the
automated suite missed: a boolean variable with no explicit
prompt:renderedits label as the literal
None. Fixed — the label now defaults toEnable <name>(e.g.Enable docker [(Y)es/(N)o] [Y]:); explicitprompt:still overrides. Covered by a new test.
Acceptance status
features:/prompt_mode:/enabled:/disabled:removed from the enginetype: string|boolean|choice; boolean→bool, choice→str, all top-level ({% if x %}works)[(Y)es/(N)o] [default]:boolean prompt; polymorphicprompt:(string/false/object)extend.whenvalue/list/regex (composing) + depth-first nesting + top-levelrequires:Files touched
cement/ext/ext_generate.py— unified typed resolver (+329 / −216).tests/ext/test_ext_generate.py+ ~30tests/data/templates/generate/test*/fixtures — migrated to the new schema; new tests for choice, accept/reject,
YAML guard,
extend.whenlist/regex, nesting,requires:, and thedefault-label fix.
demo/generate-features/— template, README, and webapp files migrated.CHANGELOG.md—[ext.generate]entries under the 3.0.16 developmentsection.
Test plan
make test— 370 passed, 100.00% coverage acrosscement/(requires Redis on 6379 + memcached on 11211 for the ext-cache tests).
make comply— ruff clean, mypy clean (51 source files).variables:oracle tests (test1–5/22/27/29) passunchanged.
generatetemplates produce output under--defaults.order with correct
Enable <name> [(Y)es/(N)o]labels, depth-first nesting,and
requires:gating.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
features:workflow with a unified typed variables system (type: string,type: boolean,type: choice) with top-level rendering for Jinja2.requires:gating andextend:rules withwhen:conditions (including list membership and regex matching) to control what gets extended vs defaulted.Documentation
Tests