ycleptic 2.2.2__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ycleptic-2.2.2 → ycleptic-2.4.0}/.gitignore +15 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/CHANGELOG.md +35 -0
- ycleptic-2.4.0/CLAUDE.md +148 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/PKG-INFO +1 -1
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.rst +1 -0
- ycleptic-2.4.0/docs/source/api/ycleptic.speccheck.rst +7 -0
- ycleptic-2.4.0/docs/source/usage/the_base_config.rst +377 -0
- ycleptic-2.4.0/docs/source/usage/yclept_check-spec.rst +72 -0
- ycleptic-2.4.0/docs/source/usage.rst +17 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/pyproject.toml +1 -1
- ycleptic-2.4.0/tests/test_set.py +1229 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/__init__.py +2 -2
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/cli.py +22 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/errors.py +13 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/makedoc.py +49 -5
- ycleptic-2.4.0/ycleptic/speccheck.py +253 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/stringthings.py +3 -1
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/walkers.py +243 -11
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/yclept.py +40 -5
- ycleptic-2.2.2/docs/source/usage/the_base_config.rst +0 -163
- ycleptic-2.2.2/docs/source/usage.rst +0 -16
- ycleptic-2.2.2/tests/test_set.py +0 -591
- {ycleptic-2.2.2 → ycleptic-2.4.0}/.envrc +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/.github/workflows/ci.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/.github/workflows/release.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/.readthedocs.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/LICENSE +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/README.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/Makefile +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/make.bat +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/requirements.txt +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/_static/css/custom.css +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/about.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/API.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.cli.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.dictthings.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.errors.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.makedoc.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.resources.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.stringthings.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.walkers.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.yclept.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/changelog.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/conf.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/index.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/installation.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/roadmap.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/class.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/interactive_help.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/quickstart.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/resource_file.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/the_user_config.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/yclept_config-help.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/yclept_makedoc.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/pytest.ini +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/scripts/release.sh +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/conftest.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/README.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/config.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/data/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/data/base.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/docs/source/intro.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/otherstuff/stuff.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/setup.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/dictthings.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/resources/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/resources/example_base.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/src/__init__.py +0 -0
|
@@ -85,6 +85,16 @@ tests/test_set/console-out.txt
|
|
|
85
85
|
tests/test_set/ydoc.rst
|
|
86
86
|
tests/test_set/ydoc/
|
|
87
87
|
tests/test_set/rcfile.yaml
|
|
88
|
+
tests/test_set/spec-check-base.yaml
|
|
89
|
+
tests/test_set/freekey_base.yaml
|
|
90
|
+
tests/test_set/freekey_mut.yaml
|
|
91
|
+
tests/test_set/vt_base.yaml
|
|
92
|
+
tests/test_set/reclist_base.yaml
|
|
93
|
+
tests/test_set/reclist_mut.yaml
|
|
94
|
+
tests/test_set/alias_base.yaml
|
|
95
|
+
tests/test_set/listmode_base.yaml
|
|
96
|
+
tests/test_set/both_base.yaml
|
|
97
|
+
tests/test_set/type_base.yaml
|
|
88
98
|
|
|
89
99
|
# PyBuilder
|
|
90
100
|
target/
|
|
@@ -106,6 +116,11 @@ ipython_config.py
|
|
|
106
116
|
# install all needed dependencies.
|
|
107
117
|
#Pipfile.lock
|
|
108
118
|
|
|
119
|
+
# uv
|
|
120
|
+
# ycleptic is a library, not an application, so its resolved dependency set
|
|
121
|
+
# is not pinned in the repo; `uv run` regenerates uv.lock on demand.
|
|
122
|
+
uv.lock
|
|
123
|
+
|
|
109
124
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
110
125
|
__pypackages__/
|
|
111
126
|
|
|
@@ -5,6 +5,41 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.4.0] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- A `dict` attribute may declare `value_attributes` instead of `attributes`, describing the schema every *value* of a mapping must satisfy while leaving the keys for the user to invent. Sections keyed by names only the user knows — molecules in a formulation, named reactions — previously had to be declared as bare `type: dict` and passed through with no validation, no defaults and no generated documentation; they can now be specified as fully as any other attribute. Errors inside a value name the entry they came from, e.g. `under 'constituents[STY]'`
|
|
12
|
+
- `key_text`, a one-line description of what a free-form key means, rendered by `yclept make-doc` and interactive help. A free-key mapping has no list of legal keys for a user to read, so this is the only thing telling them what a key is
|
|
13
|
+
- `list_defaults: append | replace` on a `list` attribute, controlling what happens to a declared `default` when the user supplies a list of their own. `append` is the default and is the historical behavior. Under `replace` the user's list is used verbatim, the default applying only when the attribute is omitted entirely
|
|
14
|
+
- Generated documentation and interactive help now state, wherever a non-empty list default is shown, whether a user-supplied list is added to it or replaces it
|
|
15
|
+
- `value_type`, the scalar counterpart to `value_attributes`: a `dict` or `list` whose elements are plain scalars rather than attribute blocks can now declare `value_type: str | int | float | bool` and have every element type-checked. Note that `value_type: str` is genuinely enforced, unlike a declared `type: str` attribute, which is validated through `choices`
|
|
16
|
+
- `value_attributes` now also applies to a `list`, where it means every *item* satisfies the schema. This covers a list of flat, multi-key records that all look alike (`- ensemble: npt` with `temperature` and `ps` beside it), which the existing tagged-task idiom — where each item is a single-key mapping naming which attribute it is — cannot express. Per-item defaults are filled in and errors name the item, e.g. `under 'stages[1]'`. Tagged-task lists, declared with `attributes`, are unchanged
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- A base config's declared `default` is no longer handed to the user's config by reference, so loading a config can no longer rewrite the schema it was validated against. `special_update` writes into its first argument, which was the spec's own default object, so a user's values were merged into the base config; a `dict` or `list` default for an omitted attribute was likewise handed out as the schema's own object, and ordinary consumer code mutating its config grew the schema's default. `make_default_specs` was affected both ways: it reported the user's values as defaults after any load, and the dict it returned aliased the spec. Every default is now copied before it is handed out
|
|
20
|
+
|
|
21
|
+
**This changes observable behavior in one narrow case.** `update_user` does a top-level `dict.update`, so passing a `dict` attribute replaces that attribute's value entirely. Sub-keys set by an earlier `update_user` used to survive that replacement, but only because they had been merged into the schema's default; now they do not. Top-level attributes the new data does not mention are untouched either way. Generated documentation built from a `Yclept` that carries a user config also no longer renders that user's values as documented defaults
|
|
22
|
+
- A `choices` violation now names the enclosing block rather than repeating the attribute's own name, so the message reads `Attribute 'ensemble' of 'stages[0]'` instead of `Attribute 'ensemble' of 'ensemble'`
|
|
23
|
+
- `yclept check-spec` and the load-time spec check learned the new vocabulary, and report several further declarations that do nothing:
|
|
24
|
+
- more than one of `attributes`, `value_attributes` and `value_type` on a single node — a node describes what is under it exactly one way
|
|
25
|
+
- `value_attributes` or `value_type` on an attribute that is neither a `dict` nor a `list`
|
|
26
|
+
- an unrecognized `value_type`
|
|
27
|
+
- `key_text` on a node that declares neither `value_attributes` nor `value_type`, and `key_text` on a `list`, whose elements are positional and have no keys
|
|
28
|
+
- `list_defaults` on a non-`list` attribute, on a `list` that declares `attributes` (a tagged-task list never merges a default), and an unrecognized `list_defaults` value
|
|
29
|
+
|
|
30
|
+
Attribute specs nested under `value_attributes` are checked too, with `[*]` in the reported path marking the step through a free-form key
|
|
31
|
+
|
|
32
|
+
## [2.3.0] - 2026-08-13
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Base config specifications are now checked when they are loaded, and any declaration ycleptic ignores is reported: a key outside the recognized set (`options:` where `choices:` was meant), an unrecognized `type:` name (`string` instead of `str`), an attribute with no declared type, and `choices` on a non-`str` attribute, which is not enforced. Such a declaration silently does nothing, so an attribute its author believes is constrained may in fact accept any value. Findings are issued as a `YclepticSpecWarning` and the config still loads; `Yclept(..., strict_spec=True)` raises `YclepticError` instead
|
|
36
|
+
- `yclept check-spec <base.yaml>` reports the same findings from the command line and exits nonzero when any are found, so it can gate a CI run
|
|
37
|
+
- `ycleptic.speccheck.check_base_spec`, which returns the findings as a list of strings
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- `yclept make-doc` now renders an attribute's `choices` as "Allowed values" in the generated documentation. Interactive help has always shown them, so generated docs and interactive help disagreed about what the schema allowed
|
|
41
|
+
- An attribute with no declared `type` now reports a clean error naming the attribute, instead of raising `KeyError: 'type'`
|
|
42
|
+
|
|
8
43
|
## [2.2.2] - 2026-08-13
|
|
9
44
|
|
|
10
45
|
### Added
|
ycleptic-2.4.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# CLAUDE.md — working notes for ycleptic
|
|
2
|
+
|
|
3
|
+
Ycleptic turns one YAML file — the **base config**, a schema written as *data* —
|
|
4
|
+
into three things at once: validation of a user's config, a generated Sphinx
|
|
5
|
+
doctree, and an interactive help browser. The whole point is that those three
|
|
6
|
+
never drift apart, because they all read the same file. Keep that invariant in
|
|
7
|
+
mind before adding a feature: **anything that teaches the validator a new trick
|
|
8
|
+
must also teach `make-doc` and `console_help` the same trick**, or the schema
|
|
9
|
+
stops being a single source of truth. The v2.3.0 `choices` fix was exactly this
|
|
10
|
+
bug — interactive help showed allowed values, generated docs did not.
|
|
11
|
+
|
|
12
|
+
## Layout
|
|
13
|
+
|
|
14
|
+
ycleptic/
|
|
15
|
+
yclept.py Yclept(UserDict) — the public class; loads base + user,
|
|
16
|
+
make_doctree(), console_help(), dump_user(),
|
|
17
|
+
make_default_specs()
|
|
18
|
+
walkers.py the recursive core: dwalk (validate/fill a dict level),
|
|
19
|
+
lwalk (a list of task-items), mwalk (merge an rc/dotfile
|
|
20
|
+
base into the app base), make_def (defaults for a subtree)
|
|
21
|
+
speccheck.py validates the base config *itself* — the recognized-key
|
|
22
|
+
and recognized-type vocabulary lives here
|
|
23
|
+
makedoc.py base spec -> RST tree
|
|
24
|
+
cli.py `yclept` entry point: make-doc, config-help, check-spec
|
|
25
|
+
errors.py YclepticError, YclepticSpecWarning
|
|
26
|
+
dictthings.py special_update (list-append / dict-merge semantics)
|
|
27
|
+
stringthings.py raise_clean, oxford, footer + RST helpers
|
|
28
|
+
resources/example_base.yaml the shipped example schema
|
|
29
|
+
src/__init__.py deprecation shim for the pre-2.3 `ycleptic.src.*` paths
|
|
30
|
+
|
|
31
|
+
`docs/source/` mirrors this: `usage/` is hand-written prose, `api/` is autodoc
|
|
32
|
+
stubs — a new module needs a new `api/ycleptic.<mod>.rst`.
|
|
33
|
+
|
|
34
|
+
## Two kinds of validation — don't confuse them
|
|
35
|
+
|
|
36
|
+
1. **User config vs. base config** — `dwalk`/`lwalk`. Failures are the user's
|
|
37
|
+
fault. They raise `YclepticError` via `raise_clean()`.
|
|
38
|
+
2. **Base config vs. ycleptic's own vocabulary** — `speccheck.check_base_spec`.
|
|
39
|
+
Failures are the *schema author's* fault: a key or type name ycleptic does
|
|
40
|
+
not read, which is therefore silently discarded. These warn
|
|
41
|
+
(`YclepticSpecWarning`) so existing schemas keep loading;
|
|
42
|
+
`Yclept(..., strict_spec=True)` promotes them to errors, and
|
|
43
|
+
`yclept check-spec` exits nonzero so CI can gate on them.
|
|
44
|
+
|
|
45
|
+
Because a base config is data, **an unrecognized key is not an error, it is a
|
|
46
|
+
no-op** — that is the failure mode speccheck exists to surface. If you add a
|
|
47
|
+
key or type name to the schema vocabulary, add it to `KNOWN_ATTRIBUTE_KEYS`,
|
|
48
|
+
`KNOWN_DOCS_KEYS`, `KNOWN_TOP_KEYS`, or `KNOWN_TYPES` in `speccheck.py` in the
|
|
49
|
+
same change, or `check-spec` will start reporting your own new feature as a
|
|
50
|
+
mistake. Consider adding a `_KEY_ALIASES`/`_TYPE_ALIASES` entry for the
|
|
51
|
+
misspelling people will actually make; anything else falls back to difflib.
|
|
52
|
+
|
|
53
|
+
## Error convention
|
|
54
|
+
|
|
55
|
+
Library code never calls `sys.exit`. It calls
|
|
56
|
+
`raise_clean(ValueError(...))`, which re-raises the message as a
|
|
57
|
+
`YclepticError` with `from None` so embedding applications get a clean,
|
|
58
|
+
catchable exception and the CLI gets a traceback-free message. New validation
|
|
59
|
+
failures should follow this — a bare `raise ValueError` leaks a traceback and
|
|
60
|
+
is not part of the documented `try/except YclepticError` contract.
|
|
61
|
+
|
|
62
|
+
## Typing rules that look like bugs but are deliberate
|
|
63
|
+
|
|
64
|
+
In `walkers._scalar_type_ok`:
|
|
65
|
+
|
|
66
|
+
- `bool` is **not** an `int`. A YAML boolean must not satisfy a numeric
|
|
67
|
+
attribute, so both the `int` and `float` checks explicitly exclude `bool`.
|
|
68
|
+
- an `int` **is** accepted where `float` is declared (widening).
|
|
69
|
+
- a YAML sequence satisfies `tuple` — YAML has no tuple type — and is stored
|
|
70
|
+
as a real `tuple`.
|
|
71
|
+
- `str` is not type-checked here at all; string attributes are constrained
|
|
72
|
+
through `choices` (and `case_sensitive`, which casefolds in place).
|
|
73
|
+
- `choices` is only enforced on `str`. Declaring it elsewhere does nothing,
|
|
74
|
+
which is why speccheck reports it.
|
|
75
|
+
|
|
76
|
+
Defaults merge, they do not overwrite: a `dict` attribute with no
|
|
77
|
+
subattributes merges the user's value into the default via `special_update`
|
|
78
|
+
(lists append, dicts merge, scalars overwrite); a `list` attribute prepends its
|
|
79
|
+
default (`defaults + I[d]`). Changing this quietly changes behavior for every
|
|
80
|
+
downstream app.
|
|
81
|
+
|
|
82
|
+
## Conventions
|
|
83
|
+
|
|
84
|
+
- `I` is the input/user dict parameter throughout `walkers.py`; ruff's `E741`
|
|
85
|
+
is disabled repo-wide for exactly this reason. Don't rename it piecemeal.
|
|
86
|
+
- Single quotes — `ruff format` is configured with `quote-style = 'single'`,
|
|
87
|
+
and CI runs `ruff format --check`.
|
|
88
|
+
- Public re-exports in `ycleptic/__init__.py` must follow the `__version__`
|
|
89
|
+
assignment, so `E402` is ignored there.
|
|
90
|
+
- `__version__` comes from installed package metadata, not a literal; only
|
|
91
|
+
`pyproject.toml` carries the version, and only `release.sh` edits it.
|
|
92
|
+
|
|
93
|
+
**A consequence of that last point, which has bitten a downstream consumer.**
|
|
94
|
+
Between releases, `main` carries the *previous* release's version number —
|
|
95
|
+
unreleased work on `main` today still reports `2.3.0`. The code is current; only
|
|
96
|
+
the number lags. Anything that keys on the version rather than the tree cannot
|
|
97
|
+
tell the difference: `uv` and `pip` wheel caches will happily serve a cached
|
|
98
|
+
`2.3.0` artifact built from older source. Symptoms are new features appearing
|
|
99
|
+
absent while the checkout plainly contains them.
|
|
100
|
+
|
|
101
|
+
When testing a consumer against an unreleased checkout, verify what you actually
|
|
102
|
+
loaded rather than trusting the version:
|
|
103
|
+
|
|
104
|
+
python -c "import ycleptic, ycleptic.walkers as w; \
|
|
105
|
+
print(ycleptic.__file__, hasattr(w, 'subattributes'))"
|
|
106
|
+
|
|
107
|
+
An editable install, `PYTHONPATH`, or `uv run --project <checkout>` all resolve
|
|
108
|
+
to the working tree; a plain non-editable install of the checkout is the case
|
|
109
|
+
that can be served stale.
|
|
110
|
+
|
|
111
|
+
`uv run --with <checkout>` is the trap, and it is silent. If the consuming
|
|
112
|
+
project declares `ycleptic>=2.3.0` and the checkout also calls itself `2.3.0`,
|
|
113
|
+
the floor is already satisfied and uv may resolve to the PyPI build instead of
|
|
114
|
+
the path you named. Nothing errors, and the version string is identical either
|
|
115
|
+
way, so the only reliable gate is file content — check `ycleptic.__file__`, or
|
|
116
|
+
probe for a symbol the branch introduced. Observed 2026-09-08 while a downstream
|
|
117
|
+
repo tried to test against an unreleased branch.
|
|
118
|
+
|
|
119
|
+
## Working commands
|
|
120
|
+
|
|
121
|
+
uv run --extra test pytest tests -q # 52 tests, <1s. NOT tests/unit
|
|
122
|
+
uv run --extra lint ruff check ycleptic/
|
|
123
|
+
uv run --extra lint ruff format --check ycleptic/
|
|
124
|
+
uv run --extra lint mypy ycleptic/
|
|
125
|
+
|
|
126
|
+
Tests live in one file, `tests/test_set.py` (single `TestYclept` unittest
|
|
127
|
+
class). `tests/conftest.py` chdirs each test into `tests/test_set/`, so tests
|
|
128
|
+
read and write fixture files by bare filename; several test outputs there are
|
|
129
|
+
gitignored. `tests/test_set/test_package/` is a miniature installable package
|
|
130
|
+
used to exercise `make-doc` against a realistic app layout.
|
|
131
|
+
|
|
132
|
+
CI (`.github/workflows/ci.yaml`) runs pytest on Python 3.9–3.13 plus a lint job.
|
|
133
|
+
The floor is **3.9**, so no `match`, no PEP 604 unions at runtime — every module that
|
|
134
|
+
needs a modern annotation carries `from __future__ import annotations` and
|
|
135
|
+
relies on it.
|
|
136
|
+
|
|
137
|
+
## Releasing
|
|
138
|
+
|
|
139
|
+
scripts/release.sh <version>
|
|
140
|
+
|
|
141
|
+
It rotates `CHANGELOG.md`'s `[Unreleased]` section, bumps `pyproject.toml`,
|
|
142
|
+
commits, tags, and pushes. **Pushing the tag is what publishes to PyPI.** Never
|
|
143
|
+
hand-roll any of those steps. Write the changelog entries under `[Unreleased]`
|
|
144
|
+
as you go, not at release time.
|
|
145
|
+
|
|
146
|
+
`pestifer` pins `ycleptic>=2.3.0` and is the only known downstream consumer —
|
|
147
|
+
coordinate with it before a release that moves the floor, and keep the
|
|
148
|
+
`ycleptic.src` deprecation shim until it is confirmed unused.
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
.. _usage_the_base_config:
|
|
2
|
+
|
|
3
|
+
The Base Configuration
|
|
4
|
+
========================
|
|
5
|
+
|
|
6
|
+
The heart of ``ycleptic`` is the base configuration file, which the app developer must write. The base configuration is the developer's expression of what a *user* can configure when they run the developer's app. Below is an example:
|
|
7
|
+
|
|
8
|
+
.. code-block:: yaml
|
|
9
|
+
|
|
10
|
+
attributes:
|
|
11
|
+
- name: attribute_1
|
|
12
|
+
type: dict
|
|
13
|
+
text: This is a description of Attribute 1
|
|
14
|
+
attributes:
|
|
15
|
+
- name: attribute_1_1
|
|
16
|
+
type: list
|
|
17
|
+
text: This is a description of Attribute 1.1
|
|
18
|
+
default:
|
|
19
|
+
- 1
|
|
20
|
+
- 2
|
|
21
|
+
- 3
|
|
22
|
+
- name: attribute_1_2
|
|
23
|
+
type: str
|
|
24
|
+
text: This is a description of Attribute 1.2
|
|
25
|
+
choices: [valA, valB]
|
|
26
|
+
- name: attribute_2
|
|
27
|
+
type: list
|
|
28
|
+
text: Attribute 2 is interpretable as an ordered list of attributes
|
|
29
|
+
attributes:
|
|
30
|
+
- name: attribute_2a
|
|
31
|
+
type: dict
|
|
32
|
+
text: Attribute 2a is one possible attribute in a user's list
|
|
33
|
+
attributes:
|
|
34
|
+
- name: d2a_val1
|
|
35
|
+
type: float
|
|
36
|
+
text: A floating point value for Value 1 of Attribute 2a
|
|
37
|
+
default: 1.0
|
|
38
|
+
- name: d2a_val2
|
|
39
|
+
type: int
|
|
40
|
+
text: An int for Value 2 of Attribute 2a
|
|
41
|
+
default: 6
|
|
42
|
+
- name: d2_a_dict
|
|
43
|
+
type: dict
|
|
44
|
+
text: this is a dict
|
|
45
|
+
default:
|
|
46
|
+
a: 123
|
|
47
|
+
b: 567
|
|
48
|
+
c: 987
|
|
49
|
+
- name: attribute_2b
|
|
50
|
+
type: dict
|
|
51
|
+
text: Attribute 2b is another possible attribute
|
|
52
|
+
attributes:
|
|
53
|
+
- name: val1
|
|
54
|
+
type: str
|
|
55
|
+
text: Val 1 of D2b
|
|
56
|
+
default: a_nice_value
|
|
57
|
+
- name: val2
|
|
58
|
+
type: str
|
|
59
|
+
text: Val 2 of D2b
|
|
60
|
+
default: a_not_so_nice_value
|
|
61
|
+
- name: attribute_3
|
|
62
|
+
type: dict
|
|
63
|
+
text: Attribute 3 has a lot of nesting
|
|
64
|
+
attributes:
|
|
65
|
+
- name: attribute_3_1
|
|
66
|
+
type: dict
|
|
67
|
+
text: This is a description of Attribute 3.1
|
|
68
|
+
attributes:
|
|
69
|
+
- name: attribute_3_1_1
|
|
70
|
+
type: dict
|
|
71
|
+
text: This is a description of Attribute 3.1.1
|
|
72
|
+
attributes:
|
|
73
|
+
- name: attribute_3_1_1_1
|
|
74
|
+
type: dict
|
|
75
|
+
text: This is a description of Attribute 3.1.1.1
|
|
76
|
+
attributes:
|
|
77
|
+
- name: d3111v1
|
|
78
|
+
type: str
|
|
79
|
+
text: Value 1 of D 3.1.1.1
|
|
80
|
+
default: ABC
|
|
81
|
+
- name: d3111v2
|
|
82
|
+
type: float
|
|
83
|
+
text: Value 2 of D 3.1.1.1
|
|
84
|
+
required: False
|
|
85
|
+
- name: attribute_3_2
|
|
86
|
+
type: dict
|
|
87
|
+
text: This is a description of Attribute 3.2
|
|
88
|
+
attributes:
|
|
89
|
+
- name: d322
|
|
90
|
+
type: list
|
|
91
|
+
text: Attribute 3.2.2 has a list of possible subattributes
|
|
92
|
+
attributes:
|
|
93
|
+
- name: d322a
|
|
94
|
+
type: dict
|
|
95
|
+
text: D 3.2.2a executes a series of flips
|
|
96
|
+
attributes:
|
|
97
|
+
- name: nflips
|
|
98
|
+
type: int
|
|
99
|
+
text: Number of flips
|
|
100
|
+
default: 0
|
|
101
|
+
- name: flipaxis
|
|
102
|
+
type: str
|
|
103
|
+
text: Axis around which flip is performed
|
|
104
|
+
choices: ['x','y','z']
|
|
105
|
+
- name: d322b
|
|
106
|
+
type: dict
|
|
107
|
+
text: Subattribute D 3.2.2b saves the result
|
|
108
|
+
attributes:
|
|
109
|
+
- name: filename
|
|
110
|
+
type: str
|
|
111
|
+
text: name of file to save
|
|
112
|
+
default: flipfile.dat
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
The base config must open with the single identifier ``attributes``, under which is a list of one or more top-level attributes. Every attribute must have a declared type, and attributes can be nested.
|
|
116
|
+
|
|
117
|
+
``type`` can be one of ``int``, ``float``, ``str``, ``bool``, ``tuple``, ``list``, or ``dict``. The data content in an attribute is of type ``type`` unless two conditions are met:
|
|
118
|
+
|
|
119
|
+
1. ``type`` is either ``list`` or ``dict``; and
|
|
120
|
+
2. the keyword ``attributes`` is present.
|
|
121
|
+
|
|
122
|
+
In this case, there are subattributes. If the ``type`` was ``dict``, then the subattributes are children of the parent attribute and all operate at the same level. If the ``type`` was ``list``, then the subattributes defined are expected to be ordered as a list of tasks that the parent attribute executes in the order they appear in the user's config file. In the base file, both are entered as lists of attributes.
|
|
123
|
+
|
|
124
|
+
``text`` is just meant for helpful text describing the attribute, and it can be completely free-form as long as it is on one line or blocked multiline using ``|``.
|
|
125
|
+
|
|
126
|
+
There are several other keys an attribute may have:
|
|
127
|
+
|
|
128
|
+
1. ``default``: the default value (or values) assigned to the attribute when the user declares it but provides no value.
|
|
129
|
+
2. ``required``: a boolean. If ``True``, the attribute must be declared (and if it is nested, all its antecedent attributes must be declared too). If ``False``, no defaults are assigned: the user need not declare the attribute at all, but declaring it without providing a value is an error.
|
|
130
|
+
3. ``choices``: a list of allowed values; if the user gives a value that is not in the list, an error occurs. This is currently enforced for ``str`` attributes only, and the comparison honors ``case_sensitive`` (below); on an attribute of any other type the list has no effect. The allowed values appear in interactive help and in the output of ``yclept make-doc``.
|
|
131
|
+
4. ``case_sensitive``: for ``str`` attributes only; a boolean that defaults to ``True``. When ``False``, the user's value is matched against ``choices`` case-insensitively and stored in casefolded (lower-case) form.
|
|
132
|
+
5. ``docs``: a block that enriches the output of ``yclept make-doc``. It may contain ``title`` and ``text`` strings and a YAML-format ``example`` showing the attribute in use. See :ref:`base_config_docs_key` below.
|
|
133
|
+
6. ``value_attributes``: for ``dict`` and ``list`` attributes; declares the schema every *element* must satisfy --- every value of a mapping whose keys the user invents, or every item of a list. See :ref:`base_config_free_keys` and :ref:`base_config_record_lists` below.
|
|
134
|
+
7. ``value_type``: the same idea for elements that are plain scalars rather than attribute blocks; one of ``str``, ``int``, ``float`` or ``bool``.
|
|
135
|
+
8. ``key_text``: a one-line description of what a free-form key *means*, used alongside ``value_attributes`` or ``value_type`` on a ``dict``.
|
|
136
|
+
9. ``list_defaults``: for ``list`` attributes only; either ``append`` (the default) or ``replace``, controlling what happens to a declared ``default`` when the user supplies a list of their own. See :ref:`base_config_list_defaults` below.
|
|
137
|
+
|
|
138
|
+
.. warning::
|
|
139
|
+
|
|
140
|
+
A base config is data, and ycleptic reads only the keys listed above. Any
|
|
141
|
+
other key is discarded without comment, so a misspelling leaves you with a
|
|
142
|
+
declaration that does nothing --- writing ``options:`` instead of
|
|
143
|
+
``choices:`` yields an attribute that looks constrained but accepts any
|
|
144
|
+
value. The same is true of the type names: ``type: string`` is not
|
|
145
|
+
``type: str``, and an attribute with an unrecognized type is left entirely
|
|
146
|
+
unvalidated, its ``choices`` unenforced and its ``default`` never applied.
|
|
147
|
+
|
|
148
|
+
Constructing a :class:`~ycleptic.yclept.Yclept` reports these as a
|
|
149
|
+
:class:`~ycleptic.errors.YclepticSpecWarning`, and ``yclept check-spec``
|
|
150
|
+
reports them from the command line. See :ref:`usage_yclept_check_spec`.
|
|
151
|
+
|
|
152
|
+
.. _base_config_free_keys:
|
|
153
|
+
|
|
154
|
+
Mappings whose keys the user chooses
|
|
155
|
+
--------------------------------------
|
|
156
|
+
|
|
157
|
+
``attributes`` names the keys a user may write, and any other key is an error.
|
|
158
|
+
That is the right model for most configuration, but some sections are keyed by
|
|
159
|
+
names only the user knows --- molecules in a formulation, named reactions,
|
|
160
|
+
labelled species. Declaring such a section as a bare ``type: dict`` passes it
|
|
161
|
+
through unvalidated, which means no defaults, no type checking and no generated
|
|
162
|
+
documentation for exactly the content users are most likely to get wrong.
|
|
163
|
+
|
|
164
|
+
A ``dict`` attribute may instead declare ``value_attributes``: the schema that
|
|
165
|
+
*every value* under it must satisfy. The keys stay free.
|
|
166
|
+
|
|
167
|
+
.. code-block:: yaml
|
|
168
|
+
|
|
169
|
+
- name: constituents
|
|
170
|
+
type: dict
|
|
171
|
+
text: Molecular constituents of the system
|
|
172
|
+
key_text: a molecule name, e.g. STY, BPA, GMA
|
|
173
|
+
value_attributes:
|
|
174
|
+
- name: smiles
|
|
175
|
+
type: str
|
|
176
|
+
required: True
|
|
177
|
+
text: SMILES string for this molecule
|
|
178
|
+
- name: count
|
|
179
|
+
type: int
|
|
180
|
+
default: 100
|
|
181
|
+
text: how many molecules to place
|
|
182
|
+
|
|
183
|
+
A user then writes whatever keys they like, and each value is validated as an
|
|
184
|
+
ordinary attribute block:
|
|
185
|
+
|
|
186
|
+
.. code-block:: yaml
|
|
187
|
+
|
|
188
|
+
constituents:
|
|
189
|
+
STY:
|
|
190
|
+
smiles: "C=Cc1ccccc1"
|
|
191
|
+
GMA:
|
|
192
|
+
smiles: "CC(=C)C(=O)OCC1CO1"
|
|
193
|
+
count: 50
|
|
194
|
+
|
|
195
|
+
``STY`` gets ``count: 100`` from the default; ``GMA`` keeps its 50. A
|
|
196
|
+
misspelled attribute *inside* a value is still an error, and the message names
|
|
197
|
+
the entry it came from --- ``Attribute 'smiels' invalid; expecting one of
|
|
198
|
+
['smiles', 'count'] under 'constituents[STY]'``.
|
|
199
|
+
|
|
200
|
+
``value_attributes`` holds ordinary attribute specifications, so anything you
|
|
201
|
+
can write under ``attributes`` you can write here, nesting included.
|
|
202
|
+
|
|
203
|
+
.. note::
|
|
204
|
+
|
|
205
|
+
``attributes`` and ``value_attributes`` are mutually exclusive on one
|
|
206
|
+
attribute: a node either names its legal keys or accepts any key, never
|
|
207
|
+
both. Declaring both is an error.
|
|
208
|
+
|
|
209
|
+
``key_text`` is optional but worth writing. For an ordinary attribute, a user
|
|
210
|
+
learns what to type by reading the list of legal keys; for a free-key mapping
|
|
211
|
+
there is no such list, so ``key_text`` is the only thing telling them what a key
|
|
212
|
+
*is*. It is rendered by both ``yclept make-doc`` and interactive help.
|
|
213
|
+
|
|
214
|
+
Values that are plain scalars
|
|
215
|
+
++++++++++++++++++++++++++++++
|
|
216
|
+
|
|
217
|
+
``value_attributes`` describes a value that is an attribute *block*. When the
|
|
218
|
+
values are bare scalars --- an atom label mapped to an atom name, a slot number
|
|
219
|
+
mapped to a reactant --- declare ``value_type`` instead:
|
|
220
|
+
|
|
221
|
+
.. code-block:: yaml
|
|
222
|
+
|
|
223
|
+
- name: reactive_atoms
|
|
224
|
+
type: dict
|
|
225
|
+
text: Reactive atoms, keyed by label
|
|
226
|
+
key_text: an atom label such as C1
|
|
227
|
+
value_type: str
|
|
228
|
+
|
|
229
|
+
Every value is then checked against that type. Note that ``value_type: str``
|
|
230
|
+
*is* enforced, unlike a declared ``type: str`` attribute, which ycleptic
|
|
231
|
+
validates through ``choices`` rather than by type; an element schema has no
|
|
232
|
+
such fallback, so an unchecked ``str`` would accept anything.
|
|
233
|
+
|
|
234
|
+
.. _base_config_record_lists:
|
|
235
|
+
|
|
236
|
+
Lists of records that all look alike
|
|
237
|
+
--------------------------------------
|
|
238
|
+
|
|
239
|
+
A ``list`` attribute with ``attributes`` means something specific in ycleptic:
|
|
240
|
+
each item is a single-key mapping naming *which* of the declared attributes it
|
|
241
|
+
is, and the list is an ordered sequence of such tasks.
|
|
242
|
+
|
|
243
|
+
.. code-block:: yaml
|
|
244
|
+
|
|
245
|
+
tasks:
|
|
246
|
+
- md:
|
|
247
|
+
ps: 100
|
|
248
|
+
- minimize:
|
|
249
|
+
steps: 500
|
|
250
|
+
|
|
251
|
+
That idiom does not fit a list whose items are flat, multi-key records that all
|
|
252
|
+
have the same shape:
|
|
253
|
+
|
|
254
|
+
.. code-block:: yaml
|
|
255
|
+
|
|
256
|
+
stages:
|
|
257
|
+
- ensemble: npt
|
|
258
|
+
temperature: 300
|
|
259
|
+
ps: 100
|
|
260
|
+
- ensemble: nvt
|
|
261
|
+
|
|
262
|
+
For that, declare ``value_attributes`` on the list. It is the same key used for
|
|
263
|
+
free-key mappings and means the same thing --- *every element satisfies this
|
|
264
|
+
schema* --- with a list's elements being its items:
|
|
265
|
+
|
|
266
|
+
.. code-block:: yaml
|
|
267
|
+
|
|
268
|
+
- name: stages
|
|
269
|
+
type: list
|
|
270
|
+
text: The equilibration stages, in order
|
|
271
|
+
value_attributes:
|
|
272
|
+
- name: ensemble
|
|
273
|
+
type: str
|
|
274
|
+
required: True
|
|
275
|
+
choices: [min, nvt, npt]
|
|
276
|
+
text: which ensemble this stage runs in
|
|
277
|
+
- name: temperature
|
|
278
|
+
type: float
|
|
279
|
+
default: 300.0
|
|
280
|
+
text: temperature in K
|
|
281
|
+
- name: ps
|
|
282
|
+
type: int
|
|
283
|
+
default: 100
|
|
284
|
+
text: duration in ps
|
|
285
|
+
|
|
286
|
+
Each item is validated as an ordinary attribute block, so defaults are filled in
|
|
287
|
+
per item and errors name the item they came from --- ``Attribute 'ensemlbe'
|
|
288
|
+
invalid; expecting one of ['ensemble', 'temperature', 'ps'] under 'stages[1]'``.
|
|
289
|
+
|
|
290
|
+
``value_type`` works on a list too, for a list of plain scalars whose type you
|
|
291
|
+
want checked:
|
|
292
|
+
|
|
293
|
+
.. code-block:: yaml
|
|
294
|
+
|
|
295
|
+
- name: temperatures
|
|
296
|
+
type: list
|
|
297
|
+
value_type: float
|
|
298
|
+
text: the temperatures to sample
|
|
299
|
+
|
|
300
|
+
.. note::
|
|
301
|
+
|
|
302
|
+
``attributes``, ``value_attributes`` and ``value_type`` are mutually
|
|
303
|
+
exclusive: a node describes what is under it exactly one way. Choosing
|
|
304
|
+
between the first two on a list is a question of shape --- tagged tasks that
|
|
305
|
+
may differ from one another, or homogeneous records that do not.
|
|
306
|
+
|
|
307
|
+
.. _base_config_list_defaults:
|
|
308
|
+
|
|
309
|
+
What a ``default`` list does when the user supplies one
|
|
310
|
+
---------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
When a ``list`` attribute has a ``default`` and the user also supplies a list,
|
|
313
|
+
ycleptic's historical behavior is to **add** the two: the default comes first,
|
|
314
|
+
then the user's entries. That is what you want for a list the user extends ---
|
|
315
|
+
a set of standard force-field files, say, to which a user adds one of their own.
|
|
316
|
+
|
|
317
|
+
It is emphatically *not* what you want for a list the user is meant to
|
|
318
|
+
*supersede*. A default equilibration ladder of ``[min, nvt, npt]`` and a user
|
|
319
|
+
who writes ``[nvt]`` produces ``[min, nvt, npt, nvt]``: the whole default
|
|
320
|
+
protocol runs, and then the user's step runs again at the end. Nothing raises;
|
|
321
|
+
the run simply does the wrong thing.
|
|
322
|
+
|
|
323
|
+
Declare which you mean with ``list_defaults``:
|
|
324
|
+
|
|
325
|
+
.. code-block:: yaml
|
|
326
|
+
|
|
327
|
+
- name: equilibration
|
|
328
|
+
type: list
|
|
329
|
+
list_defaults: replace
|
|
330
|
+
text: the equilibration ladder
|
|
331
|
+
default: [min, nvt, npt]
|
|
332
|
+
|
|
333
|
+
``append`` is the default and preserves the historical behavior, so existing
|
|
334
|
+
base configs are unaffected. Under ``replace``, a list the user supplies is
|
|
335
|
+
used verbatim, and the declared default applies only when the user omits the
|
|
336
|
+
attribute altogether.
|
|
337
|
+
|
|
338
|
+
Both settings are stated in generated documentation and in interactive help
|
|
339
|
+
wherever a list default is shown, so a user need not guess which applies.
|
|
340
|
+
|
|
341
|
+
.. warning::
|
|
342
|
+
|
|
343
|
+
``list_defaults`` governs the ``list`` attribute it is declared on. It does
|
|
344
|
+
not reach inside a bare ``dict`` attribute, where list-valued entries are
|
|
345
|
+
merged by :func:`~ycleptic.dictthings.special_update` and are always
|
|
346
|
+
appended.
|
|
347
|
+
|
|
348
|
+
.. _base_config_docs_key:
|
|
349
|
+
|
|
350
|
+
Enriching generated documentation with ``docs``
|
|
351
|
+
-------------------------------------------------
|
|
352
|
+
|
|
353
|
+
The ``text`` key gives every attribute a one-line description that appears in
|
|
354
|
+
interactive help and generated documentation. When you want richer, prose
|
|
355
|
+
documentation for an attribute — the kind you would write into a reference
|
|
356
|
+
manual — add a ``docs`` block. It is used only by ``yclept make-doc`` and is
|
|
357
|
+
ignored during configuration validation:
|
|
358
|
+
|
|
359
|
+
.. code-block:: yaml
|
|
360
|
+
|
|
361
|
+
- name: integrator
|
|
362
|
+
type: str
|
|
363
|
+
text: Integration scheme to use
|
|
364
|
+
choices: [verlet, langevin]
|
|
365
|
+
default: verlet
|
|
366
|
+
docs:
|
|
367
|
+
title: Integration scheme
|
|
368
|
+
text: |
|
|
369
|
+
Selects the time-integration algorithm. ``verlet`` is symplectic and
|
|
370
|
+
conserves energy well; ``langevin`` couples the system to a heat bath
|
|
371
|
+
and is appropriate for constant-temperature runs.
|
|
372
|
+
example:
|
|
373
|
+
integrator: langevin
|
|
374
|
+
|
|
375
|
+
``title`` and ``text`` supply a heading and prose for the attribute's generated
|
|
376
|
+
page, and ``example`` is rendered as a YAML code block showing the attribute in
|
|
377
|
+
use. See :ref:`usage_yclept_makedoc` for how the tree is generated.
|