-
-
Notifications
You must be signed in to change notification settings - Fork 119
feat: support template_dirs config settings #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| --- | ||
| id: 260507-to4 | ||
| status: in_progress | ||
| type: quick | ||
| title: Rewrite PR #760 — template_dirs config override via core_meta_override | ||
| date: 2026-05-08 | ||
| branch: feat/support-template-dirs-meta-override | ||
| --- | ||
|
|
||
| # Quick Task 260507-to4: Rewrite PR #760 | ||
|
|
||
| ## Why | ||
|
|
||
| PR #760 (resolves issue #746) adds support for setting `App.Meta.template_dirs` | ||
| from config. The original implementation hand-rolled custom logic inside | ||
| `_setup_template_handler`, which: | ||
|
|
||
| 1. **Reordered the precedence chain** — moved `core_user_template_dirs` ahead | ||
| of `template_dir`, breaking BC for downstream apps that ship a default | ||
| `template_dir` and rely on user-installed templates overriding it. | ||
| 2. **Skipped `.format(**template_dict)`** on config-supplied dirs, so | ||
| `{label}` / `{home_dir}` substitution silently failed. | ||
| 3. **Skipped the `not in template_dirs` dedup check** for config-supplied dirs. | ||
| 4. **Diverged precedence semantics** between Meta-supplied and config-supplied | ||
| `template_dirs` (config: first entry = highest; Meta: first entry = lowest). | ||
|
|
||
| The codebase already has the canonical mechanism for "config overrides Meta": | ||
| the generic `core_meta_override` loop at `cement/core/foundation.py:1443-1453`. | ||
| `plugin_dir`, `template_dir`, `debug`, etc. all use it. The proper fix is to | ||
| let `template_dirs` ride that same path — no custom code in | ||
| `_setup_template_handler` required. | ||
|
|
||
| ## What | ||
|
|
||
| 1. **`cement/core/foundation.py`** | ||
| - Keep `'template_dirs'` in `core_meta_override` list. | ||
| - Revert all custom logic inside `_setup_template_handler` — function | ||
| becomes byte-identical to current `main`. | ||
| - After the generic override loop (~line 1453), add a `str → list` | ||
| conversion block for `template_dirs`, mirroring the existing | ||
| `extensions` block at lines 1456-1471. Accept either YAML list or | ||
| comma-separated string from config. | ||
| - Update the `Meta.template_dirs` docstring (~line 572) to note | ||
| config-overridability via the `[<app>]` section. | ||
|
|
||
| 2. **`tests/core/test_foundation.py`** — replace the 3 stale PR tests with: | ||
| - List override: config `template_dirs` (as list) replaces | ||
| `Meta.template_dirs` and lands in `app._meta.template_dirs`. | ||
| - String conversion: comma-separated string parses into a list. | ||
| - `{label}` / `{home_dir}` substitution works for config-supplied dirs | ||
| (proves they go through the unchanged `_setup_template_handler`). | ||
| - Precedence: config-supplied dirs sit between | ||
| `core_system_template_dirs` and `template_dir` / | ||
| `core_user_template_dirs` (same slot as Meta-supplied). | ||
|
|
||
| 3. **`CHANGELOG.md`** — under `## 3.0.15 - DEVELOPMENT`, Features bucket: | ||
| `- [core.foundation] Support config override of App.Meta.template_dirs via [app] section.` | ||
| with `#746` sub-bullet (correct link). | ||
|
|
||
| 4. **`CONTRIBUTORS.md`** — keep TomFreudenberg entry (issue reporter). | ||
|
|
||
| ## Constraints | ||
|
|
||
| - **BC**: `Meta.template_dirs` (alone, no config override) must behave | ||
| identically to `main`. Precedence chain unchanged. | ||
| - **Coverage**: 100% test coverage required. `make test && make comply` green. | ||
| - **History**: Branch is rewritten from main with one clean `feat:` commit | ||
| (force-push to existing PR #760 branch). | ||
| - **Stop before push**: User reviews local state before any force-push. | ||
|
|
||
| ## Done When | ||
|
|
||
| - [ ] `make test` passes with 100% coverage | ||
| - [ ] `make comply` passes (ruff + mypy) | ||
| - [ ] `_setup_template_handler` is byte-identical to `main` | ||
| - [ ] Single atomic `feat:` commit on `feat/support-template-dirs-meta-override` | ||
| - [ ] STATE.md Quick Tasks table updated | ||
| - [ ] SUMMARY.md written | ||
| - [ ] User notified, no push performed |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| --- | ||
| id: 260507-to4 | ||
| status: complete | ||
| type: quick | ||
| title: Rewrite PR #760 — template_dirs config override via core_meta_override | ||
| date: 2026-05-08 | ||
| branch: feat/support-template-dirs-meta-override | ||
| commit: c25562d5 | ||
| resolves: "#746" | ||
| --- | ||
|
|
||
| # Quick Task 260507-to4: Summary | ||
|
|
||
| ## What Shipped | ||
|
|
||
| A 113-line, single-commit rewrite of PR #760 on the existing | ||
| `feat/support-template-dirs-meta-override` branch. The branch was reset to | ||
| `origin/main` (`4d62b88a`) and rebuilt from scratch; the original buggy | ||
| commit `0cf3873c` is no longer in branch history. | ||
|
|
||
| **Commit:** `c25562d5 feat(core.foundation): support config override of Meta.template_dirs` | ||
|
|
||
| ### Changes | ||
|
|
||
| | File | Change | | ||
| |---|---| | ||
| | `cement/core/foundation.py` | +25 lines — added `'template_dirs'` to `Meta.core_meta_override`; added a 14-line str→list conversion block immediately after the generic override loop (parallel to the existing `extensions` block); expanded the `Meta.template_dirs` docstring with config-override notes. **`_setup_template_handler` is byte-identical to `main`.** | | ||
| | `tests/core/test_foundation.py` | +82 lines — 4 new tests: list override, str→list conversion, `{label}`/`{home_dir}` substitution, full precedence chain. | | ||
| | `CHANGELOG.md` | +6 lines — Features bucket under `## 3.0.15 - DEVELOPMENT` with correct `#746` issue link. | | ||
| | `CONTRIBUTORS.md` | +1 line — Tom Freudenberg (issue reporter). | | ||
|
|
||
| ### Verification | ||
|
|
||
| - `make test`: **324 passed**, 100% coverage (3249/3249 statements covered). | ||
| - `make comply`: ruff clean, mypy clean (51 source files). | ||
| - `_setup_template_handler` byte-equality with `main`: confirmed via | ||
| `git diff origin/main -- cement/core/foundation.py` (only 3 hunks, all | ||
| outside the function body). | ||
|
|
||
| ## Why This Differs From the Original PR | ||
|
|
||
| The original PR hand-rolled custom logic inside `_setup_template_handler`, | ||
| which: | ||
|
|
||
| 1. **BC-broke the precedence chain** — moved `core_user_template_dirs` | ||
| ahead of `template_dir`, breaking downstream apps that ship a default | ||
| `template_dir`. | ||
| 2. **Skipped `.format(**template_dict)`** on config-supplied dirs — | ||
| `{label}` / `{home_dir}` substitution silently failed. | ||
| 3. **Skipped the `not in template_dirs` dedup check** for config-supplied | ||
| dirs. | ||
| 4. **Diverged precedence semantics** between Meta and config (config | ||
| first-listed = highest; Meta first-listed = lowest). | ||
|
|
||
| Codebase research surfaced the canonical pattern: `Meta.core_meta_override` | ||
| is the generic config→Meta override hook (used by `plugin_dir`, | ||
| `template_dir`, `debug`, etc.). Routing `template_dirs` through the same | ||
| mechanism and adding only the str→list parsing collapses the new code from | ||
| ~25 lines of branching logic in `_setup_template_handler` to a 14-line | ||
| parsing block alongside the existing `extensions` handling — and inherits | ||
| the existing precedence chain, substitution, and dedup for free. | ||
|
|
||
| ## Decisions Locked In | ||
|
|
||
| - **Replace, not extend.** Config `template_dirs` replaces | ||
| `Meta.template_dirs` entirely (matches `template_dir`, `plugin_dir`, | ||
| every other config-overridable Meta attr). | ||
| - **No precedence divergence.** Config-supplied dirs flow through the same | ||
| `_setup_template_handler` as Meta-supplied — they sit in the | ||
| `template_dirs` precedence slot, with first-listed = lowest precedence | ||
| (consistent with the existing `Meta.template_dirs` semantics). | ||
|
|
||
| ## Local State (Pre-Push) | ||
|
|
||
| - Branch: `feat/support-template-dirs-meta-override` (1 commit ahead of | ||
| `origin/main`, 263 commits ahead of `origin/feat/support-template-dirs-meta-override`). | ||
| - Force-push **not yet performed** — awaiting user review per task | ||
| constraints. | ||
| - After review: `git push --force-with-lease origin feat/support-template-dirs-meta-override` | ||
| will update PR #760 with the rewrite. | ||
|
|
||
| ## Next Action | ||
|
|
||
| User reviews the local diff (`git diff origin/main..HEAD -- '*.py' '*.md'`) | ||
| and the new commit, then triggers force-push. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -781,6 +781,103 @@ class Meta: | |
| assert tmp.dir in app._meta.template_dirs | ||
|
|
||
|
|
||
| def test_template_dirs_via_config(tmp, rando): | ||
| # config-supplied template_dirs (as a list) should land in | ||
| # app._meta.template_dirs after setup, mirroring the existing | ||
| # ``extensions`` config-override pattern. | ||
| label = f"app-{rando}" | ||
| defaults = init_defaults(label) | ||
| defaults[label]['template_dirs'] = [tmp.dir, '/another/path'] | ||
|
|
||
| with TestApp(label=label, config_defaults=defaults) as app: | ||
| app.run() | ||
| assert tmp.dir in app._meta.template_dirs | ||
| assert '/another/path' in app._meta.template_dirs | ||
|
|
||
|
|
||
| def test_template_dirs_via_config_string(tmp, rando): | ||
| # comma-separated string from config should be split + stripped | ||
| # into a list (parallel to the ``extensions`` string handling). | ||
| label = f"app-{rando}" | ||
| defaults = init_defaults(label) | ||
| defaults[label]['template_dirs'] = f'{tmp.dir}, /path/two , /path/three' | ||
|
|
||
| with TestApp(label=label, config_defaults=defaults) as app: | ||
| app.run() | ||
| assert tmp.dir in app._meta.template_dirs | ||
| assert '/path/two' in app._meta.template_dirs | ||
| assert '/path/three' in app._meta.template_dirs | ||
|
|
||
|
|
||
| def test_template_dirs_via_config_string_drops_empty_tokens(rando): | ||
| # malformed comma-separated input (trailing comma, repeated commas, | ||
| # whitespace-only tokens) must not leave '' in the resolved list, | ||
| # which would silently resolve to the empty path inside | ||
| # _setup_template_handler. | ||
| label = f"app-{rando}" | ||
| defaults = init_defaults(label) | ||
| defaults[label]['template_dirs'] = '/path/a,,/path/b, ,/path/c,' | ||
|
|
||
| with TestApp(label=label, config_defaults=defaults) as app: | ||
| app.run() | ||
| assert '/path/a' in app._meta.template_dirs | ||
| assert '/path/b' in app._meta.template_dirs | ||
| assert '/path/c' in app._meta.template_dirs | ||
| assert '' not in app._meta.template_dirs | ||
|
|
||
|
|
||
| def test_template_dirs_via_config_substitution(rando): | ||
| # config-supplied dirs go through the unchanged | ||
| # _setup_template_handler precedence loop, which means | ||
| # ``{label}`` and ``{home_dir}`` placeholders must expand | ||
| # the same way they do for ``Meta.template_dirs``. | ||
| label = f"app-{rando}" | ||
| defaults = init_defaults(label) | ||
| defaults[label]['template_dirs'] = [ | ||
| '/etc/{label}/templates', | ||
| '{home_dir}/.{label}/templates', | ||
| ] | ||
|
|
||
| with TestApp(label=label, config_defaults=defaults) as app: | ||
| app.run() | ||
| assert f'/etc/{label}/templates' in app._meta.template_dirs | ||
| assert ( | ||
| f'{fs.HOME_DIR}/.{label}/templates' | ||
| in app._meta.template_dirs | ||
| ) | ||
|
|
||
|
|
||
| def test_template_dirs_via_config_precedence(rando): | ||
| # config-supplied template_dirs replaces ``Meta.template_dirs`` | ||
| # and sits in that same precedence slot — between | ||
| # ``core_system_template_dirs`` and ``template_dir`` / | ||
| # ``core_user_template_dirs``. | ||
| label = f"app-{rando}" | ||
| defaults = init_defaults(label) | ||
| defaults[label]['template_dirs'] = ['/cfg/dir'] | ||
|
|
||
| class ThisTestApp(TestApp): | ||
| class Meta: | ||
| core_system_template_dirs = ['/sys/dir'] | ||
| core_user_template_dirs = ['/usr/dir'] | ||
| template_dir = '/single/dir' | ||
| template_dirs = ['/meta/dir/should/be/replaced'] | ||
|
Comment on lines
+861
to
+864
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid mutable list defaults in nested
Suggested fix- core_system_template_dirs = ['/sys/dir']
- core_user_template_dirs = ['/usr/dir']
+ core_system_template_dirs = ('/sys/dir',)
+ core_user_template_dirs = ('/usr/dir',)
template_dir = '/single/dir'
- template_dirs = ['/meta/dir/should/be/replaced']
+ template_dirs = ('/meta/dir/should/be/replaced',)As per coding guidelines, "**/*.py: Run ruff linting via 🧰 Tools🪛 Ruff (0.15.12)[warning] 844-844: Mutable default value for class attribute (RUF012) [warning] 845-845: Mutable default value for class attribute (RUF012) [warning] 847-847: Mutable default value for class attribute (RUF012) 🤖 Prompt for AI Agents |
||
|
|
||
| with ThisTestApp(label=label, config_defaults=defaults) as app: | ||
| app.run() | ||
| dirs = app._meta.template_dirs | ||
|
|
||
| # config replaced the Meta value entirely | ||
| assert '/meta/dir/should/be/replaced' not in dirs | ||
|
|
||
| # precedence chain (lower index = higher precedence after | ||
| # the final reverse in _setup_template_handler): | ||
| # core_user > template_dir > template_dirs (from config) > core_system | ||
| assert dirs.index('/usr/dir') < dirs.index('/single/dir') | ||
| assert dirs.index('/single/dir') < dirs.index('/cfg/dir') | ||
| assert dirs.index('/cfg/dir') < dirs.index('/sys/dir') | ||
|
|
||
|
|
||
| def test_core_system_plugin_dirs(tmp, rando): | ||
| class ThisTestApp(TestApp): | ||
| class Meta: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.