Smoothness / developer-experience ideas gathered while scaffolding the
derico.todos addon end-to-end with plonecli 7.0.0b10 /
copier-templates @ 8f5fabb (Plone 6.2.0). These are enhancements —
the concrete bugs (failing generated tests, copier-on-PATH, gitignored
*.pot, ruff S602 in update.py, etc.) are tracked separately in
TODO.md. Ordered by how much manual work they would have saved.
- plonecli: https://github.com/plone/plonecli
- templates: https://github.com/plone/copier-templates
Today the subtemplate emits a schema body of pass plus commented examples and
asks nothing about fields. Every real content type then needs the whole schema
hand-written. We hand-wrote priority (Choice+vocab), done (Bool),
description (RichText) for Todo.
Suggestion: accept a repeatable field spec, ideally via --data-file so it
stays non-interactive, e.g.
fields:
- {name: priority, type: choice, vocabulary: "derico.todos.Priorities", default: normal, required: true}
- {name: done, type: bool, default: false}
- {name: description, type: richtext, required: false}Map type → the right import (schema.Choice, schema.Bool,
plone.app.textfield.RichText, NamedBlobImage, …) and generate the import
lines + field declarations. Even supporting the common scalar types
(text/textline/bool/int/choice/datetime/richtext) would remove the single
biggest manual step. (Cf. the plone-ct-plan field-definition format.)
add content_type ... -d global_allow=false -d parent_content_type="Todos"
correctly sets the child FTI to non-global, but it does not add the child to
the parent's allowed_content_types. We had to hand-edit Todos.xml to add
<element value="Todo"/>. The subtemplate already knows
parent_content_type_resolved — it should append the child to that parent FTI's
allowed_content_types (and set filter_content_types=true there), so the
containment relationship actually works after install.
service_for is a fixed 4-item choice list (IDexterityContainer,
IDexterityContent, IPloneSiteRoot, Interface) with no <enter manually>.
To register @todos on the Todos type we had to generate for
IDexterityContainer and then hand-edit configure.zcml to
for="derico.todos.content.todos.ITodos".
Suggestion: mirror the view subtemplate — scan the addon's content-type
interfaces (shared/utils/content_types_scanner) and append them + an
<enter manually> option to service_for's choices.
The package ships a locales/ skeleton and a registerTranslations directive,
but turning that into a working German translation took several manual steps
that the template could own:
- No
MessageFactory.src/<pkg>/__init__.pyis just a docstring. Every i18n-aware addon needs_ = MessageFactory("<domain>"); scaffold it, and havecontent_type/view/vocabularyimport and use it for titles/labels instead of bare strings. .po/.potaren't generated from code.update.sh/update.pyrely on a systemgettext/i18ndudethat may be absent (nomsgfmthere). Consider a pure-Python path (pythongettextis already in the dep tree) and/or aplonecli i18nwrapper task.registerTranslationswon't compile.mounlesszope_i18n_compile_mo_filesis set (default unset). The generatedzope-setupinstance doesn't set it, so a fresh instance silently shows no translations. Suggestion: add<environment> zope_i18n_compile_mo_files true </environment>to the dev instance'szope.conf(at least in debug mode), and set the same env var in the generatedtests/conftest.pyso translation tests pass on a fresh checkout (where*.mois gitignored).- Scaffold at least one non-
enlocale (or aplonecli add language <code>subtemplate) so adding German doesn't mean creatingde/LC_MESSAGES/<domain>.poby hand.
test_factory/adding/deleting always create the type in the portal root, so for
an item addable only inside a parent they fail with
InvalidParameterError: Disallowed subobject type. When global_allow=false,
the generated test should create a parent_content_type container in _setup
and add the item there. (Also tracked in TODO.md.) Bonus: assert the type
shows up in the parent's allowedContentTypes().
When plonecli is a uv tool, only its own entry point is on PATH; the hook's
subprocess.run(["copier", ...]) dies with FileNotFoundError. We had to
prepend the tool venv's bin to PATH. Suggestion: invoke via
sys.executable -m copier (same interpreter, guaranteed import), which also
fixes the copier_template_extensions import.
locales/update.py trips the project's bundled ruff config (3× S602
shell=True, 1× E501). Either rewrite the subprocess.call(..., shell=True)
helpers as list-arg calls, or add locales/update.py to
[tool.ruff.lint.per-file-ignores] so ruff check is green out of the box.
invoke test runs uv run pytest, but pytest lives only in the test
extra (the dev extra has just invoke/watchdog), so it fails with
Failed to spawn: invoke/pytest-not-found. Either add the test deps to the
extra the task uses, or make the test task run uv run --extra test pytest.
Until then the working invocation is uv run --extra test pytest.
backend_addon wants a minor (6.1) while zope-setup wants a full
version (6.1.2), so a single -d plone_version=… can't satisfy both in
create addon. Derive the full version from the minor (or vice-versa) inside
the composite so it stays consistent without the user guessing.
We passed -d github_organization=MrTango, but the generated pyproject.toml
URLs still point at collective/derico.todos. Either wire the answer into the
[project.urls] block or drop the unused question.
.gitignore ignores both *.pot and *.mo. The .pot is the canonical
message catalog source (the .po files derive from it) and is normally
committed; only *.mo (a build artifact) should be ignored.
The add steps print success lines like "Extended configure.zcml with
browser:page 'view'" unconditionally. When a hook actually skips a write
(dedupe, _skip_if_exists), say so — silent "success" on a no-op is misleading.
create addoncomposite runs cleanly non-interactively (.gitignoreoverwrite, pyproject/README skip) — the old b9git rm .gitignoredance is gone.- Generated content_type/view tests use the correct
integrationfixture andqueryUtility(IDexterityFTI, …)and pass. - The
viewhook now registers a secondbrowser:pagewith the samename="view"but a differentfor=(both Todos and Todo views land). - Per-subtemplate auto-commit keeps a clean, reviewable history.