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.
Files changed (70) hide show
  1. {ycleptic-2.2.2 → ycleptic-2.4.0}/.gitignore +15 -0
  2. {ycleptic-2.2.2 → ycleptic-2.4.0}/CHANGELOG.md +35 -0
  3. ycleptic-2.4.0/CLAUDE.md +148 -0
  4. {ycleptic-2.2.2 → ycleptic-2.4.0}/PKG-INFO +1 -1
  5. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.rst +1 -0
  6. ycleptic-2.4.0/docs/source/api/ycleptic.speccheck.rst +7 -0
  7. ycleptic-2.4.0/docs/source/usage/the_base_config.rst +377 -0
  8. ycleptic-2.4.0/docs/source/usage/yclept_check-spec.rst +72 -0
  9. ycleptic-2.4.0/docs/source/usage.rst +17 -0
  10. {ycleptic-2.2.2 → ycleptic-2.4.0}/pyproject.toml +1 -1
  11. ycleptic-2.4.0/tests/test_set.py +1229 -0
  12. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/__init__.py +2 -2
  13. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/cli.py +22 -0
  14. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/errors.py +13 -0
  15. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/makedoc.py +49 -5
  16. ycleptic-2.4.0/ycleptic/speccheck.py +253 -0
  17. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/stringthings.py +3 -1
  18. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/walkers.py +243 -11
  19. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/yclept.py +40 -5
  20. ycleptic-2.2.2/docs/source/usage/the_base_config.rst +0 -163
  21. ycleptic-2.2.2/docs/source/usage.rst +0 -16
  22. ycleptic-2.2.2/tests/test_set.py +0 -591
  23. {ycleptic-2.2.2 → ycleptic-2.4.0}/.envrc +0 -0
  24. {ycleptic-2.2.2 → ycleptic-2.4.0}/.github/workflows/ci.yaml +0 -0
  25. {ycleptic-2.2.2 → ycleptic-2.4.0}/.github/workflows/release.yaml +0 -0
  26. {ycleptic-2.2.2 → ycleptic-2.4.0}/.readthedocs.yaml +0 -0
  27. {ycleptic-2.2.2 → ycleptic-2.4.0}/LICENSE +0 -0
  28. {ycleptic-2.2.2 → ycleptic-2.4.0}/README.md +0 -0
  29. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/Makefile +0 -0
  30. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/make.bat +0 -0
  31. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/requirements.txt +0 -0
  32. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/_static/css/custom.css +0 -0
  33. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/about.rst +0 -0
  34. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/API.rst +0 -0
  35. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.cli.rst +0 -0
  36. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.dictthings.rst +0 -0
  37. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.errors.rst +0 -0
  38. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.makedoc.rst +0 -0
  39. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.resources.rst +0 -0
  40. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.stringthings.rst +0 -0
  41. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.walkers.rst +0 -0
  42. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/api/ycleptic.yclept.rst +0 -0
  43. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/changelog.rst +0 -0
  44. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/conf.py +0 -0
  45. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/index.rst +0 -0
  46. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/installation.rst +0 -0
  47. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/roadmap.md +0 -0
  48. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/class.rst +0 -0
  49. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/interactive_help.rst +0 -0
  50. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/quickstart.rst +0 -0
  51. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/resource_file.rst +0 -0
  52. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/the_user_config.rst +0 -0
  53. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/yclept_config-help.rst +0 -0
  54. {ycleptic-2.2.2 → ycleptic-2.4.0}/docs/source/usage/yclept_makedoc.rst +0 -0
  55. {ycleptic-2.2.2 → ycleptic-2.4.0}/pytest.ini +0 -0
  56. {ycleptic-2.2.2 → ycleptic-2.4.0}/scripts/release.sh +0 -0
  57. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/__init__.py +0 -0
  58. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/conftest.py +0 -0
  59. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/README.md +0 -0
  60. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/__init__.py +0 -0
  61. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/config.py +0 -0
  62. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/data/__init__.py +0 -0
  63. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/data/base.yaml +0 -0
  64. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/docs/source/intro.rst +0 -0
  65. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/mypackage/otherstuff/stuff.py +0 -0
  66. {ycleptic-2.2.2 → ycleptic-2.4.0}/tests/test_set/test_package/rootdir/setup.py +0 -0
  67. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/dictthings.py +0 -0
  68. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/resources/__init__.py +0 -0
  69. {ycleptic-2.2.2 → ycleptic-2.4.0}/ycleptic/resources/example_base.yaml +0 -0
  70. {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
@@ -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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ycleptic
3
- Version: 2.2.2
3
+ Version: 2.4.0
4
4
  Summary: A YAML-format configuration generator
5
5
  Project-URL: Source, https://github.com/cameronabrams/ycleptic
6
6
  Project-URL: Documentation, https://ycleptic.readthedocs.io/en/latest/
@@ -24,6 +24,7 @@ Submodules
24
24
  ycleptic.dictthings
25
25
  ycleptic.errors
26
26
  ycleptic.makedoc
27
+ ycleptic.speccheck
27
28
  ycleptic.stringthings
28
29
  ycleptic.walkers
29
30
  ycleptic.yclept
@@ -0,0 +1,7 @@
1
+ ycleptic.speccheck module
2
+ =========================
3
+
4
+ .. automodule:: ycleptic.speccheck
5
+ :members:
6
+ :show-inheritance:
7
+ :undoc-members:
@@ -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.