ycleptic 2.2.2__tar.gz → 2.3.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.3.0}/.gitignore +1 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/CHANGELOG.md +11 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/PKG-INFO +1 -1
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.rst +1 -0
- ycleptic-2.3.0/docs/source/api/ycleptic.speccheck.rst +7 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/the_base_config.rst +15 -1
- ycleptic-2.3.0/docs/source/usage/yclept_check-spec.rst +72 -0
- ycleptic-2.3.0/docs/source/usage.rst +17 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/pyproject.toml +1 -1
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set.py +149 -3
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/__init__.py +2 -2
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/cli.py +22 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/errors.py +13 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/makedoc.py +3 -0
- ycleptic-2.3.0/ycleptic/speccheck.py +161 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/stringthings.py +3 -1
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/walkers.py +15 -2
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/yclept.py +21 -1
- ycleptic-2.2.2/docs/source/usage.rst +0 -16
- {ycleptic-2.2.2 → ycleptic-2.3.0}/.envrc +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/.github/workflows/ci.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/.github/workflows/release.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/.readthedocs.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/LICENSE +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/README.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/Makefile +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/make.bat +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/requirements.txt +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/_static/css/custom.css +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/about.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/API.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.cli.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.dictthings.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.errors.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.makedoc.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.resources.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.stringthings.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.walkers.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/api/ycleptic.yclept.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/changelog.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/conf.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/index.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/installation.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/roadmap.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/class.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/interactive_help.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/quickstart.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/resource_file.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/the_user_config.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/yclept_config-help.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/docs/source/usage/yclept_makedoc.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/pytest.ini +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/scripts/release.sh +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/conftest.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/README.md +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/config.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/data/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/data/base.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/docs/source/intro.rst +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/otherstuff/stuff.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/setup.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/dictthings.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/resources/__init__.py +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/resources/example_base.yaml +0 -0
- {ycleptic-2.2.2 → ycleptic-2.3.0}/ycleptic/src/__init__.py +0 -0
|
@@ -5,6 +5,17 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [2.3.0] - 2026-08-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- 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
|
|
12
|
+
- `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
|
|
13
|
+
- `ycleptic.speccheck.check_base_spec`, which returns the findings as a list of strings
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- `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
|
|
17
|
+
- An attribute with no declared `type` now reports a clean error naming the attribute, instead of raising `KeyError: 'type'`
|
|
18
|
+
|
|
8
19
|
## [2.2.2] - 2026-08-13
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -127,10 +127,24 @@ There are several other keys an attribute may have:
|
|
|
127
127
|
|
|
128
128
|
1. ``default``: the default value (or values) assigned to the attribute when the user declares it but provides no value.
|
|
129
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.
|
|
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
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
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
133
|
|
|
134
|
+
.. warning::
|
|
135
|
+
|
|
136
|
+
A base config is data, and ycleptic reads only the keys listed above. Any
|
|
137
|
+
other key is discarded without comment, so a misspelling leaves you with a
|
|
138
|
+
declaration that does nothing --- writing ``options:`` instead of
|
|
139
|
+
``choices:`` yields an attribute that looks constrained but accepts any
|
|
140
|
+
value. The same is true of the type names: ``type: string`` is not
|
|
141
|
+
``type: str``, and an attribute with an unrecognized type is left entirely
|
|
142
|
+
unvalidated, its ``choices`` unenforced and its ``default`` never applied.
|
|
143
|
+
|
|
144
|
+
Constructing a :class:`~ycleptic.yclept.Yclept` reports these as a
|
|
145
|
+
:class:`~ycleptic.errors.YclepticSpecWarning`, and ``yclept check-spec``
|
|
146
|
+
reports them from the command line. See :ref:`usage_yclept_check_spec`.
|
|
147
|
+
|
|
134
148
|
.. _base_config_docs_key:
|
|
135
149
|
|
|
136
150
|
Enriching generated documentation with ``docs``
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.. _usage_yclept_check_spec:
|
|
2
|
+
|
|
3
|
+
``yclept check-spec``
|
|
4
|
+
=====================
|
|
5
|
+
|
|
6
|
+
A base config is data, and ycleptic reads only the keys it recognizes. A
|
|
7
|
+
misspelled key or type name is therefore discarded without comment, leaving you
|
|
8
|
+
with a declaration that quietly does nothing. The classic case is writing
|
|
9
|
+
``options:`` where ycleptic expects ``choices:``: the attribute looks
|
|
10
|
+
constrained, but every value is accepted.
|
|
11
|
+
|
|
12
|
+
``yclept check-spec`` reads a base config file and reports every such
|
|
13
|
+
declaration:
|
|
14
|
+
|
|
15
|
+
.. code-block:: console
|
|
16
|
+
|
|
17
|
+
$ yclept check-spec base.yaml
|
|
18
|
+
base config base.yaml has 3 declarations ycleptic does not act on:
|
|
19
|
+
- attribute 'fetch->source': unrecognized key 'options' (did you mean 'choices'?); it is ignored
|
|
20
|
+
- attribute 'fetch->format': unrecognized type 'string' (did you mean 'str'?); the attribute is left unvalidated
|
|
21
|
+
- attribute 'run->nsteps': 'choices' is only enforced on 'str' attributes, and this one is 'int'; the allowed values are not applied
|
|
22
|
+
|
|
23
|
+
It exits ``0`` when the spec is clean and ``1`` when anything is reported, so it
|
|
24
|
+
can gate a CI run:
|
|
25
|
+
|
|
26
|
+
.. code-block:: yaml
|
|
27
|
+
|
|
28
|
+
- name: Check the base config spec
|
|
29
|
+
run: yclept check-spec mypackage/data/base.yaml
|
|
30
|
+
|
|
31
|
+
What it looks for
|
|
32
|
+
-----------------
|
|
33
|
+
|
|
34
|
+
1. Keys in an attribute node outside the recognized set (``name``, ``type``,
|
|
35
|
+
``text``, ``default``, ``required``, ``choices``, ``case_sensitive``,
|
|
36
|
+
``attributes``, ``docs``), and keys in a ``docs`` block outside ``title``,
|
|
37
|
+
``text``, and ``example``.
|
|
38
|
+
2. ``type`` values outside ``str``, ``int``, ``float``, ``bool``, ``tuple``,
|
|
39
|
+
``list``, and ``dict``. An unrecognized type name matches no branch of the
|
|
40
|
+
validator, so the attribute gets no handling at all --- including its
|
|
41
|
+
``choices``, even when ``choices`` is spelled correctly, and including its
|
|
42
|
+
``default``, which is never applied.
|
|
43
|
+
3. Attributes with no ``type`` at all.
|
|
44
|
+
4. ``choices`` on an attribute whose type is not ``str``. Allowed values are
|
|
45
|
+
currently enforced only for strings, so elsewhere the list has no effect.
|
|
46
|
+
|
|
47
|
+
Where a suggestion is obvious, it is offered: ``options`` for ``choices``,
|
|
48
|
+
``string`` for ``str``, and so on.
|
|
49
|
+
|
|
50
|
+
Checking from Python
|
|
51
|
+
--------------------
|
|
52
|
+
|
|
53
|
+
The same checks run whenever a :class:`~ycleptic.yclept.Yclept` is constructed.
|
|
54
|
+
By default anything found is reported as a
|
|
55
|
+
:class:`~ycleptic.errors.YclepticSpecWarning` and the config still loads, so an
|
|
56
|
+
existing schema keeps working while you clean it up:
|
|
57
|
+
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
Y = Yclept('base.yaml', userfile='user.yaml')
|
|
61
|
+
# YclepticSpecWarning: base config base.yaml has 1 declaration ycleptic does not act on:
|
|
62
|
+
# - attribute 'fetch->source': unrecognized key 'options' (did you mean 'choices'?); it is ignored
|
|
63
|
+
|
|
64
|
+
Pass ``strict_spec=True`` to make it an error instead:
|
|
65
|
+
|
|
66
|
+
.. code-block:: python
|
|
67
|
+
|
|
68
|
+
Y = Yclept('base.yaml', userfile='user.yaml', strict_spec=True)
|
|
69
|
+
# raises YclepticError
|
|
70
|
+
|
|
71
|
+
:func:`ycleptic.speccheck.check_base_spec` returns the same findings as a list
|
|
72
|
+
of strings if you would rather handle them yourself.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Usage
|
|
2
|
+
=====
|
|
3
|
+
|
|
4
|
+
``Ycleptic`` primarily exposes the :class:`ycleptic.yclept.Yclept` class to inherit in your own applications. It also has a command-line interface with three subcommands: ``yclept config-help`` provides interactive help for the configuration file, ``yclept make-doc`` generates documentation from the base configuration file, and ``yclept check-spec`` reports declarations in a base configuration file that ycleptic ignores.
|
|
5
|
+
|
|
6
|
+
.. toctree::
|
|
7
|
+
:maxdepth: 1
|
|
8
|
+
|
|
9
|
+
usage/quickstart
|
|
10
|
+
usage/class
|
|
11
|
+
usage/the_base_config
|
|
12
|
+
usage/the_user_config
|
|
13
|
+
usage/interactive_help
|
|
14
|
+
usage/resource_file
|
|
15
|
+
usage/yclept_config-help
|
|
16
|
+
usage/yclept_makedoc
|
|
17
|
+
usage/yclept_check-spec
|
|
@@ -1,18 +1,56 @@
|
|
|
1
1
|
import shutil
|
|
2
2
|
import unittest
|
|
3
|
-
from contextlib import redirect_stdout
|
|
3
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
4
4
|
import os
|
|
5
5
|
import yaml
|
|
6
6
|
|
|
7
7
|
from ycleptic.yclept import Yclept
|
|
8
8
|
from ycleptic import resources
|
|
9
|
-
from ycleptic import YclepticError
|
|
10
|
-
from ycleptic.
|
|
9
|
+
from ycleptic import YclepticError, YclepticSpecWarning
|
|
10
|
+
from ycleptic.speccheck import check_base_spec
|
|
11
|
+
from ycleptic.cli import config_help, check_spec
|
|
11
12
|
from ycleptic.dictthings import special_update
|
|
12
13
|
from ycleptic.stringthings import oxford, generate_footer, dict_to_rst_yaml_block
|
|
13
14
|
|
|
14
15
|
BFILE = os.path.join(os.path.dirname(resources.__file__), 'example_base.yaml')
|
|
15
16
|
|
|
17
|
+
# A base spec exercising every silent-typo trap the spec checker looks for.
|
|
18
|
+
BAD_SPEC_YAML = """
|
|
19
|
+
attributes:
|
|
20
|
+
- name: a_opts
|
|
21
|
+
type: str
|
|
22
|
+
text: uses options instead of choices
|
|
23
|
+
options: [red, green]
|
|
24
|
+
default: red
|
|
25
|
+
- name: a_string
|
|
26
|
+
type: string
|
|
27
|
+
text: uses type string instead of str
|
|
28
|
+
choices: [red, green]
|
|
29
|
+
default: red
|
|
30
|
+
- name: an_int
|
|
31
|
+
type: int
|
|
32
|
+
text: choices on a non-str attribute
|
|
33
|
+
choices: [1, 2, 3]
|
|
34
|
+
default: 1
|
|
35
|
+
- name: nested
|
|
36
|
+
type: dict
|
|
37
|
+
text: a nested block
|
|
38
|
+
attributes:
|
|
39
|
+
- name: deep
|
|
40
|
+
type: str
|
|
41
|
+
text: a misspelled key one level down
|
|
42
|
+
chioces: [red, green]
|
|
43
|
+
docs:
|
|
44
|
+
titel: a misspelled docs key
|
|
45
|
+
- name: untyped
|
|
46
|
+
text: no type declared at all
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# The one trap above that stops a load outright, so tests can drop it.
|
|
50
|
+
UNTYPED_ATTRIBUTE = """ - name: untyped
|
|
51
|
+
text: no type declared at all
|
|
52
|
+
"""
|
|
53
|
+
|
|
16
54
|
EXAMPLE1_YAML = """
|
|
17
55
|
attribute_2:
|
|
18
56
|
- attribute_2b:
|
|
@@ -306,6 +344,112 @@ base|attribute_2->attribute_2a
|
|
|
306
344
|
with open('console-out.txt', 'r') as f:
|
|
307
345
|
self.assertIn('! quit', f.read())
|
|
308
346
|
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
# Base-spec validation
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
def test_shipped_example_base_spec_is_clean(self):
|
|
352
|
+
"""The shipped example must not trip its own spec checker."""
|
|
353
|
+
with open(BFILE, 'r') as f:
|
|
354
|
+
base = yaml.safe_load(f)
|
|
355
|
+
self.assertEqual(check_base_spec(base), [])
|
|
356
|
+
|
|
357
|
+
def test_spec_check_flags_options_instead_of_choices(self):
|
|
358
|
+
"""'options' is read by nothing, so a schema using it is unvalidated."""
|
|
359
|
+
base = yaml.safe_load(BAD_SPEC_YAML)
|
|
360
|
+
problems = check_base_spec(base)
|
|
361
|
+
opt = [p for p in problems if "unrecognized key 'options'" in p]
|
|
362
|
+
self.assertEqual(len(opt), 1)
|
|
363
|
+
self.assertIn("attribute 'a_opts'", opt[0])
|
|
364
|
+
self.assertIn("did you mean 'choices'?", opt[0])
|
|
365
|
+
|
|
366
|
+
def test_spec_check_flags_string_instead_of_str(self):
|
|
367
|
+
"""'string' matches no branch, so the attribute gets no string handling."""
|
|
368
|
+
base = yaml.safe_load(BAD_SPEC_YAML)
|
|
369
|
+
problems = check_base_spec(base)
|
|
370
|
+
typ = [p for p in problems if "unrecognized type 'string'" in p]
|
|
371
|
+
self.assertEqual(len(typ), 1)
|
|
372
|
+
self.assertIn("attribute 'a_string'", typ[0])
|
|
373
|
+
self.assertIn("did you mean 'str'?", typ[0])
|
|
374
|
+
|
|
375
|
+
def test_spec_check_flags_choices_on_non_str(self):
|
|
376
|
+
"""'choices' is enforced only on str attributes; elsewhere it is inert."""
|
|
377
|
+
base = yaml.safe_load(BAD_SPEC_YAML)
|
|
378
|
+
problems = check_base_spec(base)
|
|
379
|
+
ch = [p for p in problems if "'choices' is only enforced" in p]
|
|
380
|
+
self.assertEqual(len(ch), 1)
|
|
381
|
+
self.assertIn("attribute 'an_int'", ch[0])
|
|
382
|
+
|
|
383
|
+
def test_spec_check_reports_nested_path_and_docs_keys(self):
|
|
384
|
+
"""Problems name the full attribute path, including inside a docs block."""
|
|
385
|
+
base = yaml.safe_load(BAD_SPEC_YAML)
|
|
386
|
+
problems = check_base_spec(base)
|
|
387
|
+
self.assertTrue(
|
|
388
|
+
any("attribute 'nested->deep': unrecognized key 'chioces'" in p for p in problems)
|
|
389
|
+
)
|
|
390
|
+
self.assertTrue(any("'titel' in its 'docs' block" in p for p in problems))
|
|
391
|
+
|
|
392
|
+
def test_spec_check_flags_missing_type(self):
|
|
393
|
+
base = yaml.safe_load(BAD_SPEC_YAML)
|
|
394
|
+
problems = check_base_spec(base)
|
|
395
|
+
self.assertTrue(any("attribute 'untyped': no 'type' declared" in p for p in problems))
|
|
396
|
+
|
|
397
|
+
def test_bad_spec_warns_by_default(self):
|
|
398
|
+
"""A questionable spec still loads, but says so."""
|
|
399
|
+
specfile = 'spec-check-base.yaml'
|
|
400
|
+
with open(specfile, 'w') as f:
|
|
401
|
+
f.write(BAD_SPEC_YAML.replace(UNTYPED_ATTRIBUTE, ''))
|
|
402
|
+
with self.assertWarns(YclepticSpecWarning) as cm:
|
|
403
|
+
Y = Yclept(specfile, userdict={'a_opts': 'red'})
|
|
404
|
+
self.assertIn('does not act on', str(cm.warning))
|
|
405
|
+
self.assertEqual(Y['user']['a_opts'], 'red')
|
|
406
|
+
|
|
407
|
+
def test_attribute_with_no_type_reports_cleanly(self):
|
|
408
|
+
"""A spec attribute with no declared type is an error, not a KeyError."""
|
|
409
|
+
specfile = 'spec-check-base.yaml'
|
|
410
|
+
with open(specfile, 'w') as f:
|
|
411
|
+
f.write(BAD_SPEC_YAML)
|
|
412
|
+
with self.assertWarns(YclepticSpecWarning):
|
|
413
|
+
with self.assertRaises(YclepticError) as cm:
|
|
414
|
+
Yclept(specfile, userdict={})
|
|
415
|
+
self.assertIn('declares no type', str(cm.exception))
|
|
416
|
+
|
|
417
|
+
def test_bad_spec_raises_under_strict_spec(self):
|
|
418
|
+
specfile = 'spec-check-base.yaml'
|
|
419
|
+
with open(specfile, 'w') as f:
|
|
420
|
+
f.write(BAD_SPEC_YAML)
|
|
421
|
+
with self.assertRaises(YclepticError) as cm:
|
|
422
|
+
Yclept(specfile, userdict={}, strict_spec=True)
|
|
423
|
+
self.assertIn("unrecognized key 'options'", str(cm.exception))
|
|
424
|
+
|
|
425
|
+
def test_good_spec_neither_warns_nor_raises(self):
|
|
426
|
+
import warnings
|
|
427
|
+
|
|
428
|
+
with warnings.catch_warnings():
|
|
429
|
+
warnings.simplefilter('error', YclepticSpecWarning)
|
|
430
|
+
Yclept(BFILE, strict_spec=True)
|
|
431
|
+
|
|
432
|
+
def test_check_spec_cli_reports_and_exits_nonzero(self):
|
|
433
|
+
"""The check-spec subcommand gates CI on a clean base spec."""
|
|
434
|
+
from argparse import Namespace
|
|
435
|
+
|
|
436
|
+
specfile = 'spec-check-base.yaml'
|
|
437
|
+
with open(specfile, 'w') as f:
|
|
438
|
+
f.write(BAD_SPEC_YAML)
|
|
439
|
+
with open('console-out.txt', 'w') as f:
|
|
440
|
+
with redirect_stderr(f):
|
|
441
|
+
with self.assertRaises(SystemExit) as cm:
|
|
442
|
+
check_spec(Namespace(config=specfile))
|
|
443
|
+
self.assertEqual(cm.exception.code, 1)
|
|
444
|
+
with open('console-out.txt', 'r') as f:
|
|
445
|
+
self.assertIn("unrecognized key 'options'", f.read())
|
|
446
|
+
|
|
447
|
+
with open('console-out.txt', 'w') as f:
|
|
448
|
+
with redirect_stdout(f):
|
|
449
|
+
check_spec(Namespace(config=BFILE))
|
|
450
|
+
with open('console-out.txt', 'r') as f:
|
|
451
|
+
self.assertIn('no unrecognized keys or types', f.read())
|
|
452
|
+
|
|
309
453
|
def test_makedoc(self):
|
|
310
454
|
Y = Yclept(BFILE)
|
|
311
455
|
Y.make_doctree('ydoc')
|
|
@@ -323,6 +467,8 @@ Single-valued attributes:
|
|
|
323
467
|
|
|
324
468
|
* ``attribute_5``: This is a description of Attribute 5
|
|
325
469
|
|
|
470
|
+
Allowed values: ``a``, ``b``, ``c``
|
|
471
|
+
|
|
326
472
|
|
|
327
473
|
|
|
328
474
|
Subattributes:
|
|
@@ -12,6 +12,6 @@ except PackageNotFoundError:
|
|
|
12
12
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
13
13
|
|
|
14
14
|
from ycleptic.yclept import Yclept
|
|
15
|
-
from ycleptic.errors import YclepticError
|
|
15
|
+
from ycleptic.errors import YclepticError, YclepticSpecWarning
|
|
16
16
|
|
|
17
|
-
__all__ = ['Yclept', 'YclepticError']
|
|
17
|
+
__all__ = ['Yclept', 'YclepticError', 'YclepticSpecWarning']
|
|
@@ -6,9 +6,11 @@ Command-line interface for ycleptic
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
import sys
|
|
9
|
+
import yaml
|
|
9
10
|
from .yclept import Yclept
|
|
10
11
|
import argparse as ap
|
|
11
12
|
import textwrap
|
|
13
|
+
from .speccheck import check_base_spec, format_problems
|
|
12
14
|
from .stringthings import oxford, banner_message
|
|
13
15
|
from .errors import YclepticError
|
|
14
16
|
|
|
@@ -44,18 +46,35 @@ def config_help(args):
|
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
def check_spec(args):
|
|
50
|
+
"""
|
|
51
|
+
Reports any declarations in a base config file that ycleptic ignores.
|
|
52
|
+
"""
|
|
53
|
+
with open(args.config, 'r') as f:
|
|
54
|
+
base = yaml.safe_load(f)
|
|
55
|
+
problems = check_base_spec(base)
|
|
56
|
+
if not problems:
|
|
57
|
+
print(f'{args.config}: no unrecognized keys or types')
|
|
58
|
+
return
|
|
59
|
+
print(format_problems(problems, args.config), file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
|
|
47
63
|
def cli():
|
|
48
64
|
commands = {
|
|
49
65
|
'make-doc': makedoc,
|
|
50
66
|
'config-help': config_help,
|
|
67
|
+
'check-spec': check_spec,
|
|
51
68
|
}
|
|
52
69
|
helps = {
|
|
53
70
|
'make-doc': 'Makes a sphinx/rtd-style doctree from the base config file provided and, optionally, a root node',
|
|
54
71
|
'config-help': 'Help on a base config file',
|
|
72
|
+
'check-spec': 'Reports declarations in a base config file that ycleptic ignores',
|
|
55
73
|
}
|
|
56
74
|
descs = {
|
|
57
75
|
'make-doc': 'If you provide the name of a base configuration file for your app, and optionally, a root attribute, this command will generate a sphinx/rtd-style doctree',
|
|
58
76
|
'config-help': 'If you provide the name of a base configuration file for your app, you can use this command to explore it the way a user would in your app',
|
|
77
|
+
'check-spec': 'Reads a base configuration file and reports every key or type name ycleptic does not recognize, each of which is silently ignored when configs are validated. Exits nonzero if any are found, so it can gate a CI run',
|
|
59
78
|
}
|
|
60
79
|
parser = ap.ArgumentParser(
|
|
61
80
|
description=textwrap.dedent(banner_message), formatter_class=ap.RawDescriptionHelpFormatter
|
|
@@ -84,6 +103,9 @@ def cli():
|
|
|
84
103
|
choices=['paragraph', 'comment', 'rubric', 'note', 'raw-html'],
|
|
85
104
|
help='footer style for the generated documentation; one of "paragraph", "comment", "rubric", "note", or "raw-html"; default %(default)s',
|
|
86
105
|
)
|
|
106
|
+
command_parsers['check-spec'].add_argument(
|
|
107
|
+
'config', type=str, default=None, help='input base configuration file in YAML format'
|
|
108
|
+
)
|
|
87
109
|
command_parsers['config-help'].add_argument(
|
|
88
110
|
'config', type=str, default=None, help='input base configuration file in YAML format'
|
|
89
111
|
)
|
|
@@ -17,3 +17,16 @@ class YclepticError(Exception):
|
|
|
17
17
|
catches :class:`YclepticError` and reports it as a clean, traceback-free
|
|
18
18
|
error message.
|
|
19
19
|
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class YclepticSpecWarning(UserWarning):
|
|
23
|
+
"""
|
|
24
|
+
Issued when a base config specification contains a declaration ycleptic
|
|
25
|
+
ignores, such as a misspelled key or an unrecognized type name.
|
|
26
|
+
|
|
27
|
+
Such a declaration does nothing, so an attribute its author believes is
|
|
28
|
+
constrained may in fact accept any value. This is a warning rather than an
|
|
29
|
+
error so that existing specifications keep loading; construct
|
|
30
|
+
:class:`~ycleptic.yclept.Yclept` with ``strict_spec=True`` to raise
|
|
31
|
+
:class:`YclepticError` instead.
|
|
32
|
+
"""
|
|
@@ -97,6 +97,9 @@ def make_doc(
|
|
|
97
97
|
default = sv.get('default', None)
|
|
98
98
|
default_text = f' (default: {default})' if default is not None else ''
|
|
99
99
|
fp.write(f' * ``{sv["name"]}``: {sv["text"]}{default_text}\n\n')
|
|
100
|
+
if 'choices' in sv:
|
|
101
|
+
allowed = ', '.join(f'``{c}``' for c in sv['choices'])
|
|
102
|
+
fp.write(f' Allowed values: {allowed}\n\n')
|
|
100
103
|
sv_example = sv.get('docs', {}).get('example', {})
|
|
101
104
|
if sv_example:
|
|
102
105
|
fp.write(' Example:\n\n')
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Author: Cameron F. Abrams <cfa22@drexel.edu>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Validation of the base config specification itself
|
|
5
|
+
|
|
6
|
+
A base config is data, and ycleptic reads only the keys it recognizes. A
|
|
7
|
+
misspelled key or type name is therefore discarded without comment, leaving the
|
|
8
|
+
schema author with a declaration that silently does nothing --- a
|
|
9
|
+
``choices:`` list that never constrains anything, for instance. The checks
|
|
10
|
+
here look over a base spec as it is loaded and report those declarations.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import difflib
|
|
16
|
+
|
|
17
|
+
#: Keys ycleptic reads from an attribute node of a base config.
|
|
18
|
+
KNOWN_ATTRIBUTE_KEYS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
'name',
|
|
21
|
+
'type',
|
|
22
|
+
'text',
|
|
23
|
+
'default',
|
|
24
|
+
'required',
|
|
25
|
+
'choices',
|
|
26
|
+
'case_sensitive',
|
|
27
|
+
'attributes',
|
|
28
|
+
'docs',
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
#: Keys ycleptic reads from an attribute's ``docs`` block.
|
|
33
|
+
KNOWN_DOCS_KEYS = frozenset({'title', 'text', 'example'})
|
|
34
|
+
|
|
35
|
+
#: Keys ycleptic reads from the top level of a base config.
|
|
36
|
+
KNOWN_TOP_KEYS = frozenset({'attributes', 'docs'})
|
|
37
|
+
|
|
38
|
+
#: Type names ycleptic recognizes.
|
|
39
|
+
KNOWN_TYPES = frozenset({'str', 'int', 'float', 'bool', 'tuple', 'list', 'dict'})
|
|
40
|
+
|
|
41
|
+
# Misspellings common enough to name outright rather than leave to difflib.
|
|
42
|
+
_KEY_ALIASES = {
|
|
43
|
+
'option': 'choices',
|
|
44
|
+
'options': 'choices',
|
|
45
|
+
'allowed': 'choices',
|
|
46
|
+
'allowed_values': 'choices',
|
|
47
|
+
'attribute': 'attributes',
|
|
48
|
+
'description': 'text',
|
|
49
|
+
'help': 'text',
|
|
50
|
+
'subattributes': 'attributes',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_TYPE_ALIASES = {
|
|
54
|
+
'array': 'list',
|
|
55
|
+
'boolean': 'bool',
|
|
56
|
+
'double': 'float',
|
|
57
|
+
'integer': 'int',
|
|
58
|
+
'map': 'dict',
|
|
59
|
+
'mapping': 'dict',
|
|
60
|
+
'number': 'float',
|
|
61
|
+
'object': 'dict',
|
|
62
|
+
'real': 'float',
|
|
63
|
+
'sequence': 'list',
|
|
64
|
+
'string': 'str',
|
|
65
|
+
'text': 'str',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _suggest(word, known, aliases):
|
|
70
|
+
"""
|
|
71
|
+
Return a ``did you mean`` clause for ``word``, or an empty string.
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(word, str):
|
|
74
|
+
return ''
|
|
75
|
+
alias = aliases.get(word.lower())
|
|
76
|
+
if alias is None:
|
|
77
|
+
close = difflib.get_close_matches(word.lower(), sorted(known), n=1, cutoff=0.7)
|
|
78
|
+
alias = close[0] if close else None
|
|
79
|
+
return f" (did you mean '{alias}'?)" if alias else ''
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _path_str(path: list[str]) -> str:
|
|
83
|
+
return '->'.join(path) if path else 'the top level'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_node(node: dict, path: list[str], problems: list[str]):
|
|
87
|
+
where = f"attribute '{_path_str(path)}'"
|
|
88
|
+
|
|
89
|
+
for key in node:
|
|
90
|
+
if key not in KNOWN_ATTRIBUTE_KEYS:
|
|
91
|
+
problems.append(
|
|
92
|
+
f'{where}: unrecognized key '
|
|
93
|
+
f"'{key}'{_suggest(key, KNOWN_ATTRIBUTE_KEYS, _KEY_ALIASES)}; it is ignored"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
typ = node.get('type')
|
|
97
|
+
if typ is None:
|
|
98
|
+
problems.append(f"{where}: no 'type' declared")
|
|
99
|
+
elif typ not in KNOWN_TYPES:
|
|
100
|
+
problems.append(
|
|
101
|
+
f"{where}: unrecognized type '{typ}'"
|
|
102
|
+
f'{_suggest(typ, KNOWN_TYPES, _TYPE_ALIASES)}; the attribute is left unvalidated'
|
|
103
|
+
)
|
|
104
|
+
elif 'choices' in node and typ != 'str':
|
|
105
|
+
problems.append(
|
|
106
|
+
f"{where}: 'choices' is only enforced on 'str' attributes, "
|
|
107
|
+
f"and this one is '{typ}'; the allowed values are not applied"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
docs = node.get('docs')
|
|
111
|
+
if isinstance(docs, dict):
|
|
112
|
+
for key in docs:
|
|
113
|
+
if key not in KNOWN_DOCS_KEYS:
|
|
114
|
+
problems.append(
|
|
115
|
+
f"{where}: unrecognized key '{key}' in its 'docs' block"
|
|
116
|
+
f'{_suggest(key, KNOWN_DOCS_KEYS, {})}; it is ignored'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for sub in node.get('attributes', []) or []:
|
|
120
|
+
if isinstance(sub, dict):
|
|
121
|
+
_check_node(sub, path + [str(sub.get('name', '?'))], problems)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_base_spec(base: dict) -> list[str]:
|
|
125
|
+
"""
|
|
126
|
+
Look over a base config specification and report declarations ycleptic ignores.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
base : dict
|
|
131
|
+
A base config specification, as loaded from a base config file.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
list of str
|
|
136
|
+
One message per problem found, in the order encountered. Empty if the
|
|
137
|
+
specification uses only keys and type names ycleptic recognizes.
|
|
138
|
+
"""
|
|
139
|
+
problems: list[str] = []
|
|
140
|
+
if not isinstance(base, dict):
|
|
141
|
+
return problems
|
|
142
|
+
for key in base:
|
|
143
|
+
if key not in KNOWN_TOP_KEYS:
|
|
144
|
+
problems.append(
|
|
145
|
+
f'the top level: unrecognized key '
|
|
146
|
+
f"'{key}'{_suggest(key, KNOWN_TOP_KEYS, _KEY_ALIASES)}; it is ignored"
|
|
147
|
+
)
|
|
148
|
+
for node in base.get('attributes', []) or []:
|
|
149
|
+
if isinstance(node, dict):
|
|
150
|
+
_check_node(node, [str(node.get('name', '?'))], problems)
|
|
151
|
+
return problems
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def format_problems(problems: list[str], basefile: str = '') -> str:
|
|
155
|
+
"""
|
|
156
|
+
Render ``problems`` as a single multi-line report.
|
|
157
|
+
"""
|
|
158
|
+
src = f' {basefile}' if basefile else ''
|
|
159
|
+
ess = 's' if len(problems) > 1 else ''
|
|
160
|
+
head = f'base config{src} has {len(problems)} declaration{ess} ycleptic does not act on:'
|
|
161
|
+
return '\n'.join([head] + [f' - {p}' for p in problems])
|
|
@@ -5,6 +5,8 @@ Various string manipulation functions for ycleptic
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import NoReturn
|
|
8
10
|
import yaml
|
|
9
11
|
from datetime import date
|
|
10
12
|
from . import __version__
|
|
@@ -19,7 +21,7 @@ banner_message = """
|
|
|
19
21
|
""".format(__version__)
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def raise_clean(ErrorInstance):
|
|
24
|
+
def raise_clean(ErrorInstance) -> NoReturn:
|
|
23
25
|
"""
|
|
24
26
|
Raises a :class:`~ycleptic.errors.YclepticError` carrying the message of
|
|
25
27
|
the given exception instance.
|
|
@@ -106,6 +106,19 @@ def _scalar_type_ok(typ: str, value) -> bool:
|
|
|
106
106
|
return True
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _declared_type(dx: dict, dname: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Return the declared type of attribute spec ``dx``, or report its absence.
|
|
112
|
+
|
|
113
|
+
Every attribute must declare a type; without one there is nothing to
|
|
114
|
+
validate the user's value against.
|
|
115
|
+
"""
|
|
116
|
+
typ = dx.get('type')
|
|
117
|
+
if typ is None:
|
|
118
|
+
raise_clean(ValueError(f"Attribute '{dx.get('name', '?')}' of '{dname}' declares no type."))
|
|
119
|
+
return typ
|
|
120
|
+
|
|
121
|
+
|
|
109
122
|
def dwalk(D: dict, I: dict):
|
|
110
123
|
"""
|
|
111
124
|
Recursively process the user's config-dict I by walking recursively through it
|
|
@@ -143,7 +156,7 @@ def dwalk(D: dict, I: dict):
|
|
|
143
156
|
dx = D['attributes'][tidx]
|
|
144
157
|
# logger.debug(f' d {d}')
|
|
145
158
|
# get its type
|
|
146
|
-
typ = dx
|
|
159
|
+
typ = _declared_type(dx, dname)
|
|
147
160
|
if typ == 'dict' and (d in I and not isinstance(I[d], dict)):
|
|
148
161
|
raise_clean(
|
|
149
162
|
ValueError(f"Attribute '{d}' of '{dname}' must be a dict; found {type(I[d])}.")
|
|
@@ -261,7 +274,7 @@ def lwalk(D: dict, L: list[dict]):
|
|
|
261
274
|
)
|
|
262
275
|
tidx = tld.index(itemname)
|
|
263
276
|
dx = D['attributes'][tidx]
|
|
264
|
-
typ = dx['
|
|
277
|
+
typ = _declared_type(dx, D['name'])
|
|
265
278
|
if typ in ['str', 'int', 'float']:
|
|
266
279
|
# because a list attribute indicates an ordered sequence of tasks and we expect each
|
|
267
280
|
# task to be a dictionary specifying the task and not a single scalar value,
|
|
@@ -7,12 +7,16 @@ from __future__ import annotations
|
|
|
7
7
|
import logging
|
|
8
8
|
import sys
|
|
9
9
|
import textwrap
|
|
10
|
+
import warnings
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
import yaml
|
|
12
13
|
from collections import UserDict
|
|
13
14
|
from argparse import Namespace
|
|
14
15
|
from . import __version__
|
|
16
|
+
from .errors import YclepticSpecWarning
|
|
15
17
|
from .makedoc import make_doc
|
|
18
|
+
from .speccheck import check_base_spec, format_problems
|
|
19
|
+
from .stringthings import raise_clean
|
|
16
20
|
from .walkers import make_def, mwalk, dwalk
|
|
17
21
|
|
|
18
22
|
logger = logging.getLogger(__name__)
|
|
@@ -34,10 +38,20 @@ class Yclept(UserDict):
|
|
|
34
38
|
A dictionary of user-defined configurations. Optional; used instead of ``userfile`` when provided.
|
|
35
39
|
rcfile : str
|
|
36
40
|
The path to a resource config file that extends the base config. Optional.
|
|
41
|
+
strict_spec : bool
|
|
42
|
+
If True, a base config containing declarations ycleptic ignores --- a
|
|
43
|
+
misspelled key or an unrecognized type name --- raises
|
|
44
|
+
:class:`~ycleptic.errors.YclepticError` instead of issuing a
|
|
45
|
+
:class:`~ycleptic.errors.YclepticSpecWarning`. Optional; defaults to False.
|
|
37
46
|
"""
|
|
38
47
|
|
|
39
48
|
def __init__(
|
|
40
|
-
self,
|
|
49
|
+
self,
|
|
50
|
+
basefile: str,
|
|
51
|
+
userfile: str = '',
|
|
52
|
+
userdict: dict | None = None,
|
|
53
|
+
rcfile: str = '',
|
|
54
|
+
strict_spec: bool = False,
|
|
41
55
|
):
|
|
42
56
|
data = {}
|
|
43
57
|
with open(basefile, 'r') as f:
|
|
@@ -46,6 +60,12 @@ class Yclept(UserDict):
|
|
|
46
60
|
with open(rcfile, 'r') as f:
|
|
47
61
|
rc = yaml.safe_load(f)
|
|
48
62
|
mwalk(data['base'], rc)
|
|
63
|
+
problems = check_base_spec(data['base'])
|
|
64
|
+
if problems:
|
|
65
|
+
report = format_problems(problems, basefile)
|
|
66
|
+
if strict_spec:
|
|
67
|
+
raise_clean(ValueError(report))
|
|
68
|
+
warnings.warn(report, YclepticSpecWarning, stacklevel=2)
|
|
49
69
|
super().__init__(data)
|
|
50
70
|
if userdict is None:
|
|
51
71
|
userdict = {}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Usage
|
|
2
|
-
=====
|
|
3
|
-
|
|
4
|
-
``Ycleptic`` primarily exposes the :class:`ycleptic.yclept.Yclept` class to inherit in your own applications. It also has a command-line interface with two subcommands: ``yclept config-help`` and ``yclept make-doc``. The former provides interactive help for the configuration file, and the latter generates documentation from the base configuration file.
|
|
5
|
-
|
|
6
|
-
.. toctree::
|
|
7
|
-
:maxdepth: 1
|
|
8
|
-
|
|
9
|
-
usage/quickstart
|
|
10
|
-
usage/class
|
|
11
|
-
usage/the_base_config
|
|
12
|
-
usage/the_user_config
|
|
13
|
-
usage/interactive_help
|
|
14
|
-
usage/resource_file
|
|
15
|
-
usage/yclept_config-help
|
|
16
|
-
usage/yclept_makedoc
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/data/__init__.py
RENAMED
|
File without changes
|
{ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/data/base.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{ycleptic-2.2.2 → ycleptic-2.3.0}/tests/test_set/test_package/rootdir/mypackage/otherstuff/stuff.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|