Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .planning/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-04-24)
Phase: 6
Plan: Not started
Status: Executing Phase 05
Last activity: 2026-05-08
Last activity: 2026-05-08 - Completed quick task 260507-to4: rewrite PR #760 (template_dirs config override)

Progress: [██████████] 100% (21/21 plans completed across Phases 1, 01.1, 2, 3 — Phases 4-6 plan counts TBD)

Expand Down Expand Up @@ -156,6 +156,7 @@ None yet.
|---|-------------|------|--------|-----------|
| 260430-3b0 | fix scripts/cli-smoke-test.sh — drop py3.9 default and modernize generated-project install path (phase-1 gap closure) | 2026-04-30 | 020ec7b7 | [260430-3b0-fix-scripts-cli-smoke-test-sh-drop-py3-9](./quick/260430-3b0-fix-scripts-cli-smoke-test-sh-drop-py3-9/) |
| 260430-i7q | add cli-smoke-test Makefile target wiring scripts/cli-smoke-test.sh | 2026-04-30 | 786a440e | [260430-i7q-add-cli-smoke-test-target-to-makefile-th](./quick/260430-i7q-add-cli-smoke-test-target-to-makefile-th/) |
| 260507-to4 | rewrite PR #760 — template_dirs config override via core_meta_override (resolves #746) | 2026-05-08 | c25562d5 | [260507-to4-rewrite-pr-760-template-dirs-config-over](./quick/260507-to4-rewrite-pr-760-template-dirs-config-over/) |

## Session Continuity

Expand Down
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.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,13 @@ Bugs:

Features:

- None
- `[core.foundation]` Support config override of `App.Meta.template_dirs`
via the `[<app>]` section. Accepts a list (under config handlers with
native list support, e.g. `ext_yaml` or `ext_json`) or a comma-separated
string (required for the default `ext_configparser` / INI handler).
String form is split + whitespace-trimmed, parallel to the existing
`extensions` config handling.
- [Issue #746](https://github.com/datafolklabs/cement/issues/746)

Refactoring:

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ documentation, or testing:
- Christian Hengl (rednar)
- sigma67
- Blake Jameson (blakejameson)
- Tom Freudenberg (TomFreudenberg)
34 changes: 34 additions & 0 deletions cement/core/foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ class Meta:
'plugin_dir',
'ignore_deprecation_warnings',
'template_dir',
'template_dirs',
'mail_handler',
'cache_handler',
'log_handler',
Expand Down Expand Up @@ -589,6 +590,15 @@ class Meta:

Templates are attempted to be loaded in order, and will stop loading
once a template is successfully loaded from a directory.

This setting can also be overridden by the ``myapp.template_dirs``
config setting parsed from any of the application configuration
files. The config value may be a list (when using a config
handler that supports native lists, e.g. ``ext_yaml`` or
``ext_json``) or a comma-separated string (required when using
the default ``ext_configparser`` / INI handler, since INI has no
native list syntax — the string form is split and
whitespace-trimmed).
"""

template_dir: str | None = None
Expand Down Expand Up @@ -1452,6 +1462,30 @@ def _setup_config_handler(self) -> None:
else:
setattr(self._meta, key, base_dict[key])

# convert template_dirs to a list if it is a comma-separated string
# (the core_meta_override loop above blindly setattr's the raw config
# value onto self._meta, so without this _setup_template_handler would
# iterate characters of a string rather than a list of paths).
if 'template_dirs' in self.config.keys(self._meta.config_section):
dirs = self.config.get(self._meta.config_section, 'template_dirs')

# convert a comma-separated string to a list
if type(dirs) is str:
# strip whitespace and drop empty tokens (trailing or
# repeated commas would otherwise leave '' in the list,
# which _setup_template_handler would silently resolve
# as the empty path).
dir_list = [x.strip() for x in dirs.split(',') if x.strip()]

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# set the new template_dirs value in the config
self.config.set(
self._meta.config_section, 'template_dirs', dir_list
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# also update _meta.template_dirs which the override loop
# above set to the raw string
self._meta.template_dirs = dir_list

# load extensions from configuraton file
if 'extensions' in self.config.keys(self._meta.config_section):
exts = self.config.get(label, 'extensions')
Expand Down
97 changes: 97 additions & 0 deletions tests/core/test_foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

Avoid mutable list defaults in nested Meta class attributes (RUF012).

core_system_template_dirs, core_user_template_dirs, and template_dirs are list literals on class attrs. Ruff flags this pattern and it can break compliance gates.

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 make comply-ruff and auto-fix issues with make comply-ruff-fix" and "Maintain 100% PEP8 compliance via ruff ... enforced as absolute quality gates."

🧰 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
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/core/test_foundation.py` around lines 844 - 847, The Meta class
currently uses mutable list literals for core_system_template_dirs,
core_user_template_dirs, and template_dirs which Ruff flags; replace those list
literals with immutable tuples (e.g., ('/sys/dir',)) or set them to None and
initialize immutable sequences at runtime so they are not mutable class
defaults; update the attributes core_system_template_dirs,
core_user_template_dirs, and template_dirs accordingly (leave template_dir
as-is) to satisfy RUF012 and re-run the ruff/comply checks.


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:
Expand Down
Loading