Skip to content

Commit a487f1c

Browse files
authored
docs: schema validation and fixes (spack#51324)
Signed-off-by: Harmen Stoppels <[email protected]>
1 parent 381141a commit a487f1c

File tree

10 files changed

+150
-63
lines changed

10 files changed

+150
-63
lines changed

.github/workflows/bin/format-rst.py

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
whitespace. It exits with a non-zero status if any files were modified."""
88

99
import difflib
10+
import importlib
1011
import io
1112
import json
1213
import os
@@ -21,6 +22,25 @@
2122
from docutils.parsers.rst import Directive, directives
2223
from ruamel.yaml import YAML
2324

25+
from spack.vendor import jsonschema
26+
27+
import spack.schema
28+
29+
#: Map Spack config sections to their corresponding JSON schema
30+
SECTION_AND_SCHEMA = [
31+
# The first property's key is the config section name
32+
(next(iter(m.schema["properties"])), m.schema)
33+
# Dynamically load all modules in spack.schema to be future-proof
34+
for m in (
35+
importlib.import_module(f"spack.schema.{f[:-3]}")
36+
for f in os.listdir(os.path.dirname(spack.schema.__file__))
37+
if f.endswith(".py") and f != "__init__.py"
38+
)
39+
if hasattr(m, "schema") and len(m.schema.get("properties", {})) == 1
40+
]
41+
42+
assert SECTION_AND_SCHEMA, "no schemas found"
43+
2444
END_OF_SENTENCE = re.compile(
2545
r"""
2646
(
@@ -39,6 +59,32 @@
3959
DOCUTILS_SETTING = {"report_level": 5, "raw_enabled": False, "file_insertion_enabled": False}
4060

4161

62+
def _warning(msg: str) -> str:
63+
return f"\033[33mwarning:\033[0m {msg}"
64+
65+
66+
class Warning:
67+
def __init__(self, path: str, line: int, message: str) -> None:
68+
self.path = path
69+
self.line = line
70+
self.message = message
71+
72+
def __str__(self) -> str:
73+
return _warning(f"{self.path}:{self.line}: {self.message}")
74+
75+
76+
class CodeBlockWarning(Warning):
77+
def __init__(self, path: str, line: int, message: str, diff: str):
78+
super().__init__(path, line, f"{message}\n{diff}")
79+
80+
def __str__(self) -> str:
81+
return _warning(f"{self.path}:{self.line}: {self.message}")
82+
83+
84+
class ValidationWarning(Warning):
85+
pass
86+
87+
4288
class SphinxCodeBlock(Directive):
4389
"""Defines a code-block directive with the options Sphinx supports."""
4490

@@ -89,15 +135,25 @@ def _is_node_in_table(node: nodes.Node) -> bool:
89135
return False
90136

91137

92-
def _format_code_blocks(document: nodes.document, path: str) -> None:
138+
def _validate_schema(data: object) -> None:
139+
if not isinstance(data, dict):
140+
return
141+
for section, schema in SECTION_AND_SCHEMA:
142+
if section in data:
143+
jsonschema.validate(data, schema)
144+
145+
146+
def _format_code_blocks(document: nodes.document, path: str) -> List[Warning]:
93147
"""Try to parse and format Python, YAML, and JSON code blocks. This does *not* update the
94-
sources, but merely warns. That's because not all code examples are meant to be valid."""
148+
sources, but collects issues for later reporting. Returns a list of warnings."""
149+
issues: List[Warning] = []
95150
for code_block in document.findall(nodes.literal_block):
96151
language = code_block.attributes.get("language", "")
97152
if language not in ("python", "yaml", "json"):
98153
continue
99154
original = code_block.astext()
100155
line = code_block.line if code_block.line else 0
156+
possible_config_data = None
101157

102158
try:
103159
if language == "python":
@@ -107,17 +163,22 @@ def _format_code_blocks(document: nodes.document, path: str) -> None:
107163
yaml.width = 10000 # do not wrap lines
108164
yaml.preserve_quotes = True # do not force particular quotes
109165
buf = io.BytesIO()
110-
yaml.dump(yaml.load(original), buf)
166+
possible_config_data = yaml.load(original)
167+
yaml.dump(possible_config_data, buf)
111168
formatted = buf.getvalue().decode("utf-8")
112169
elif language == "json":
113170
formatted = json.dumps(json.loads(original), indent=2)
114171
else:
115172
assert False
116173
except Exception as e:
117-
print(
118-
f"{path}:{line}: formatting failed: {e}: {original!r}", flush=True, file=sys.stderr
119-
)
174+
issues.append(Warning(path, line, f"formatting failed: {e}: {original!r}"))
120175
continue
176+
177+
try:
178+
_validate_schema(possible_config_data)
179+
except jsonschema.ValidationError as e:
180+
issues.append(ValidationWarning(path, line, f"schema validation failed: {e.message}"))
181+
121182
if formatted == original:
122183
continue
123184
diff = "\n".join(
@@ -130,7 +191,8 @@ def _format_code_blocks(document: nodes.document, path: str) -> None:
130191
)
131192
)
132193
if diff:
133-
print(diff, flush=True, file=sys.stderr)
194+
issues.append(CodeBlockWarning(path, line, "formatting suggested:", diff))
195+
return issues
134196

135197

136198
def _format_paragraphs(document: nodes.document, path: str, src_lines: List[str]) -> bool:
@@ -171,15 +233,15 @@ def _format_paragraphs(document: nodes.document, path: str, src_lines: List[str]
171233
return modified
172234

173235

174-
def reformat_rst_file(path: str) -> bool:
236+
def reformat_rst_file(path: str, warnings: List[Warning]) -> bool:
175237
"""Reformat a reStructuredText file "in-place". Returns True if modified, False otherwise."""
176238
with open(path, "r", encoding="utf-8") as f:
177239
src = f.read()
178240

179241
src_lines = src.splitlines()
180242
document: nodes.document = publish_doctree(src, settings_overrides=DOCUTILS_SETTING)
181243

182-
_format_code_blocks(document, path)
244+
warnings.extend(_format_code_blocks(document, path))
183245

184246
if not _format_paragraphs(document, path, src_lines):
185247
return False
@@ -194,10 +256,22 @@ def reformat_rst_file(path: str) -> bool:
194256

195257
def main(*files: str) -> None:
196258
modified = False
259+
warnings: List[Warning] = []
197260
for f in files:
198-
modified |= reformat_rst_file(f)
261+
modified |= reformat_rst_file(f, warnings)
262+
199263
if modified:
200264
subprocess.run(["git", "--no-pager", "diff", "--color=always", "--", *files])
265+
266+
for warning in sorted(warnings, key=lambda w: isinstance(w, ValidationWarning)):
267+
print(warning, flush=True, file=sys.stderr)
268+
269+
if warnings:
270+
print(
271+
_warning(f"completed with {len(warnings)} potential issues"),
272+
flush=True,
273+
file=sys.stderr,
274+
)
201275
sys.exit(1 if modified else 0)
202276

203277

.github/workflows/prechecks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
bin/spack license verify
5858
pylint -j $(nproc) --disable=all --enable=unspecified-encoding --ignore-paths=lib/spack/spack/vendor lib
5959
- name: Run style tests (docs)
60-
run: .github/workflows/bin/format-rst.py $(git ls-files 'lib/spack/docs/*.rst')
60+
run: bin/spack python .github/workflows/bin/format-rst.py $(git ls-files 'lib/spack/docs/*.rst')
6161

6262

6363
# Check that spack can bootstrap the development environment on Python 3.6 - RHEL8

lib/spack/docs/configuration.rst

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Here is an example ``config.yaml`` file:
3636
.. code-block:: yaml
3737
3838
config:
39-
install_tree: $spack/opt/spack
39+
install_tree:
40+
root: $spack/opt/spack
4041
build_stage:
4142
- $tempdir/$user/spack-stage
4243
- ~/.spack/stage
@@ -295,7 +296,8 @@ If your configurations look like this:
295296
:name: code-example-defaults-config-yaml
296297
297298
config:
298-
install_tree: $spack/opt/spack
299+
install_tree:
300+
root: $spack/opt/spack
299301
build_stage:
300302
- $tempdir/$user/spack-stage
301303
- ~/.spack/stage
@@ -306,7 +308,8 @@ If your configurations look like this:
306308
:name: code-example-user-config-yaml
307309
308310
config:
309-
install_tree: /some/other/directory
311+
install_tree:
312+
root: /some/other/directory
310313
311314
312315
Spack will only override ``install_tree`` in the ``config`` section, and will take the site preferences for other settings.
@@ -317,7 +320,8 @@ You can see the final, combined configuration with the ``spack config get <confi
317320
318321
$ spack config get config
319322
config:
320-
install_tree: /some/other/directory
323+
install_tree:
324+
root: /some/other/directory
321325
build_stage:
322326
- $tempdir/$user/spack-stage
323327
- ~/.spack/stage
@@ -339,15 +343,17 @@ For example:
339343
:name: code-example-append-install-tree
340344
341345
config:
342-
install_tree-: /my/custom/suffix/
346+
install_tree:
347+
root-: /my/custom/suffix/
343348
344-
Spack will then append to the lower-precedence configuration under the ``install_tree-:`` section:
349+
Spack will then append to the lower-precedence configuration under the ``root`` key:
345350

346351
.. code-block:: console
347352
348353
$ spack config get config
349354
config:
350-
install_tree: /some/other/directory/my/custom/suffix
355+
install_tree:
356+
root: /some/other/directory/my/custom/suffix
351357
build_stage:
352358
- $tempdir/$user/spack-stage
353359
- ~/.spack/stage
@@ -361,7 +367,8 @@ Similarly, ``+:`` can be used to *prepend* to a path or name:
361367
:name: code-example-prepend-install-tree
362368
363369
config:
364-
install_tree+: /my/custom/suffix/
370+
install_tree:
371+
root+: /my/custom/suffix/
365372
366373
367374
.. _config-overrides:
@@ -380,15 +387,17 @@ For example:
380387
:name: code-example-override-config-section
381388
382389
config::
383-
install_tree: /some/other/directory
390+
install_tree:
391+
root: /some/other/directory
384392
385393
Spack will ignore all lower-precedence configuration under the ``config::`` section:
386394

387395
.. code-block:: console
388396
389397
$ spack config get config
390398
config:
391-
install_tree: /some/other/directory
399+
install_tree:
400+
root: /some/other/directory
392401
393402
394403
List-valued settings
@@ -428,7 +437,8 @@ The list in the higher-precedence scope is *prepended* to the defaults.
428437
429438
$ spack config get config
430439
config:
431-
install_tree: /some/other/directory
440+
install_tree:
441+
root: /some/other/directory
432442
build_stage:
433443
- /lustre-scratch/$user/spack
434444
- ~/mystage
@@ -457,7 +467,8 @@ The merged configuration would look like this:
457467
458468
$ spack config get config
459469
config:
460-
install_tree: /some/other/directory
470+
install_tree:
471+
root: /some/other/directory
461472
build_stage:
462473
- /lustre-scratch/$user/spack
463474
- ~/mystage
@@ -565,7 +576,8 @@ For example, to see the fully merged ``config.yaml``, you can type:
565576
verify_ssl: true
566577
dirty: false
567578
build_jobs: 8
568-
install_tree: $spack/opt/spack
579+
install_tree:
580+
root: $spack/opt/spack
569581
template_dirs:
570582
- $spack/templates
571583
directory_layout: {architecture}/{compiler.name}-{compiler.version}/{name}-{version}-{hash}

lib/spack/docs/containers.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ The ``Dockerfile`` that gets created uses multi-stage builds and other technique
121121
&& echo " concretizer:" \
122122
&& echo " unify: true" \
123123
&& echo " config:" \
124-
&& echo " install_tree: /opt/software" \
124+
&& echo " install_tree: \
125+
&& echo " root: /opt/software" \
125126
&& echo " view: /opt/view") > /opt/spack-environment/spack.yaml
126127
127128
# Install the software, remove unnecessary deps
@@ -311,7 +312,8 @@ uses ``spack/almalinux9:0.22.0`` and ``almalinux:9`` for the stages where the so
311312
&& echo ' concretizer:' \
312313
&& echo ' unify: true' \
313314
&& echo ' config:' \
314-
&& echo ' install_tree: /opt/software' \
315+
&& echo ' install_tree: ' \
316+
&& echo ' root: /opt/software' \
315317
&& echo ' view: /opt/views/view') > /opt/spack-environment/spack.yaml
316318
[ ... ]
317319
# Bare OS image to run the installed executables
@@ -420,7 +422,8 @@ produces, for instance, the following ``Dockerfile``:
420422
&& echo " concretizer:" \
421423
&& echo " unify: true" \
422424
&& echo " config:" \
423-
&& echo " install_tree: /opt/software" \
425+
&& echo " install_tree: " \
426+
&& echo " root: /opt/software" \
424427
&& echo " view: /opt/view") > /opt/spack-environment/spack.yaml
425428
426429
# Install the software, remove unnecessary deps
@@ -559,7 +562,8 @@ The recipe that gets generated contains the two extra instructions that we added
559562
&& echo " config:" \
560563
&& echo " template_dirs:" \
561564
&& echo " - /tmp/environment/templates" \
562-
&& echo " install_tree: /opt/software" \
565+
&& echo " install_tree: " \
566+
&& echo " root: /opt/software" \
563567
&& echo " view: /opt/view") > /opt/spack-environment/spack.yaml
564568
565569
# Install the software, remove unnecessary deps

lib/spack/docs/env_vars_yaml.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ For example:
2323
set:
2424
ENVAR_TO_SET_IN_ENV_LOAD: "FOO"
2525
unset:
26-
ENVAR_TO_UNSET_IN_ENV_LOAD:
26+
- ENVAR_TO_UNSET_IN_ENV_LOAD
2727
prepend_path:
2828
PATH_LIST: "path/to/prepend"
2929
append_path:

lib/spack/docs/environments.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ The environment can be configured to set, unset, prepend, or append using ``env_
862862
set:
863863
ENVAR_TO_SET_IN_ENV_LOAD: "FOO"
864864
unset:
865-
ENVAR_TO_UNSET_IN_ENV_LOAD:
865+
- ENVAR_TO_UNSET_IN_ENV_LOAD
866866
prepend_path:
867867
PATH_LIST: "path/to/prepend"
868868
append_path:

lib/spack/docs/frequently_asked_questions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ The following set of criteria (from lowest to highest precedence) explains commo
7070
foo:
7171
require:
7272
- "@1.2: +mpi"
73-
conflicts:
73+
conflict:
7474
- "@1.4"
7575
7676
Requirements and constraints restrict the set of possible solutions, while reuse behavior and preferences influence what an optimal solution looks like.

lib/spack/docs/module_file_support.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,8 @@ If the ``use_view`` value is set in the config, then the prefix inspections for
537537
- PATH
538538
view:
539539
my_view:
540+
root: /path/to/my/view
540541
projections:
541-
root: /path/to/my/view
542542
all: "{name}-{hash}"
543543
544544
The ``spack`` key is relevant to :ref:`environment <environments>` configuration, and the view key is discussed in detail in the section on :ref:`Configuring environment views <configuring_environment_views>`.

0 commit comments

Comments
 (0)