specitems 1.6.2__tar.gz → 1.6.4__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 (27) hide show
  1. {specitems-1.6.2 → specitems-1.6.4}/PKG-INFO +1 -1
  2. {specitems-1.6.2 → specitems-1.6.4}/pyproject.toml +2 -1
  3. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/__init__.py +9 -4
  4. specitems-1.6.4/src/specitems/cliverify.py +117 -0
  5. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cliyamlquery.py +3 -3
  6. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentmarkdown.py +7 -0
  7. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/itemmapper.py +35 -4
  8. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/items.py +67 -1
  9. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/spec.pickle +0 -0
  10. specitems-1.6.4/src/specitems/specformatter.py +171 -0
  11. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/specverify.py +52 -16
  12. {specitems-1.6.2 → specitems-1.6.4}/README.md +0 -0
  13. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cite.py +0 -0
  14. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clihash.py +0 -0
  15. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clipickle.py +0 -0
  16. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clispecdoc.py +0 -0
  17. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cliutil.py +0 -0
  18. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/content.py +0 -0
  19. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentcommonmark.py +0 -0
  20. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentsphinx.py +0 -0
  21. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contenttext.py +0 -0
  22. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/getvaluesubprocess.py +0 -0
  23. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/glossary.py +0 -0
  24. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/hashutil.py +0 -0
  25. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/py.typed +0 -0
  26. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/specdoc.py +0 -0
  27. {specitems-1.6.2 → specitems-1.6.4}/src/specitems/subprocessaction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specitems
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Summary: Provides interfaces to work with specification items.
5
5
  Keywords: certification,documentation,markdown,qualification,requirements-management,traceability
6
6
  Author: The specitems Authors
@@ -4,7 +4,7 @@
4
4
 
5
5
  [project]
6
6
  name = "specitems"
7
- version = "1.6.2"
7
+ version = "1.6.4"
8
8
  description = "Provides interfaces to work with specification items."
9
9
  authors = [
10
10
  {name = "The specitems Authors", email = "specthings@embedded-brains.de"}
@@ -54,6 +54,7 @@ specpickle = "specitems.clipickle:clipickle"
54
54
  specdocitems = "specitems.clispecdoc:clispecdocitems"
55
55
  spechash = "specitems.clihash:clihash"
56
56
  specyamlquery = "specitems.cliyamlquery:cliyamlquery"
57
+ specverify = "specitems.cliverify:cliverify"
57
58
 
58
59
  [build-system]
59
60
  requires = ["uv_build>=0.10.0"]
@@ -62,10 +62,8 @@ from .contentcommonmark import (
62
62
  CommonMarkContent,
63
63
  CommonMarkMapper,
64
64
  )
65
- from .contentmarkdown import (
66
- MarkdownContent,
67
- MarkdownMapper,
68
- )
65
+ from .contentmarkdown import (MarkdownContent, MarkdownMapper,
66
+ format_markdown_text)
69
67
  from .contentsphinx import (
70
68
  SphinxContent,
71
69
  SphinxMapper,
@@ -128,11 +126,14 @@ from .itemmapper import (
128
126
  ItemMapper,
129
127
  ItemValueProvider,
130
128
  get_value_default,
129
+ to_at_variables,
130
+ to_dollar_variables,
131
131
  unpack_arg,
132
132
  unpack_args,
133
133
  )
134
134
  from .specdoc import (SpecDocumentConfig, add_specification_documentation,
135
135
  generate_specification_documentation)
136
+ from .specformatter import SpecYAMLFormatter
136
137
  from .specverify import (
137
138
  SpecVerifier,
138
139
  verify_specification_format,
@@ -185,6 +186,7 @@ __all__ = [
185
186
  "SpecDocumentConfig",
186
187
  "SpecTypeProvider",
187
188
  "SpecVerifier",
189
+ "SpecYAMLFormatter",
188
190
  "SphinxContent",
189
191
  "SphinxMapper",
190
192
  "TextContent",
@@ -197,6 +199,7 @@ __all__ = [
197
199
  "create_unique_link",
198
200
  "data_digest",
199
201
  "escape_code_line",
202
+ "format_markdown_text",
200
203
  "generate_glossary",
201
204
  "generate_specification_documentation",
202
205
  "get_arguments",
@@ -231,8 +234,10 @@ __all__ = [
231
234
  "run_subprocess_action",
232
235
  "save_data",
233
236
  "split_copyright_statement",
237
+ "to_at_variables",
234
238
  "to_camel_case",
235
239
  "to_collection",
240
+ "to_dollar_variables",
236
241
  "to_iterable",
237
242
  "unpack_arg",
238
243
  "unpack_args",
@@ -0,0 +1,117 @@
1
+ # SPDX-License-Identifier: BSD-2-Clause
2
+ """
3
+ Provides a command line interface to verify the specification item format.
4
+ """
5
+
6
+ # Copyright (C) 2026 embedded brains GmbH & Co. KG
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions
10
+ # are met:
11
+ # 1. Redistributions of source code must retain the above copyright
12
+ # notice, this list of conditions and the following disclaimer.
13
+ # 2. Redistributions in binary form must reproduce the above copyright
14
+ # notice, this list of conditions and the following disclaimer in the
15
+ # documentation and/or other materials provided with the distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ import importlib.metadata
30
+ import logging
31
+ from pathlib import Path
32
+ import sys
33
+
34
+ from .cliutil import get_arguments
35
+ from .items import (ItemCache, ItemCacheConfig, ItemDataByUID,
36
+ ItemTypeProvider, SpecTypeProvider)
37
+ from .specformatter import SpecYAMLFormatter
38
+ from .specverify import verify_specification_format
39
+
40
+
41
+ # Packages using specitems can register their types through the following
42
+ # plugin mechanism. They have to add something like this to their
43
+ # `pyproject.toml` file:
44
+ #
45
+ # [project.entry-points."specitems_type_provider.plugins"]
46
+ # mypackage = "mypackage.mymodule:load_mypackage_types"
47
+ def _create_type_provider() -> ItemTypeProvider:
48
+ data_by_uid: ItemDataByUID = {}
49
+ for ep in importlib.metadata.entry_points(
50
+ group="specitems_type_provider.plugins"):
51
+ load_types = ep.load()
52
+ data_by_uid.update(load_types())
53
+ return SpecTypeProvider(data_by_uid)
54
+
55
+
56
+ def cliverify(argv: list[str] = sys.argv) -> int:
57
+ """
58
+ Verify the specification item format and optionally format the items.
59
+ """
60
+
61
+ def _add_arguments(parser):
62
+ parser.add_argument("--format-items",
63
+ action="store_true",
64
+ help="format the items")
65
+ parser.add_argument("--clang-format-path",
66
+ default="clang-format",
67
+ help="the path to the clang-format executable")
68
+ parser.add_argument(
69
+ "--clang-format-style",
70
+ action="append",
71
+ default=[],
72
+ metavar="CLANG_FORMAT_STYLE",
73
+ help="a clang-format style name used by the "
74
+ "specification types and, separated by a colon, "
75
+ "the associated clang-format style "
76
+ "file (example: file-scope:.clang-format); "
77
+ "the style name is not related to the built-in styles "
78
+ "of clang-format; this option can be given multiple times")
79
+ parser.add_argument("spec_items_or_directories",
80
+ nargs="+",
81
+ metavar="SPEC_ITEM_OR_DIRECTORY",
82
+ help="specification item file or directory")
83
+
84
+ args = get_arguments(argv[1:],
85
+ default_log_level="WARNING",
86
+ description=cliverify.__doc__,
87
+ add_arguments=(_add_arguments, ))
88
+ uid_log_level = logging.ERROR
89
+ item_files: list[str] = []
90
+ spec_dirs: list[str] = []
91
+ for path in args.spec_items_or_directories:
92
+ if Path(path).is_file():
93
+ uid_log_level = logging.INFO
94
+ item_files.append(path)
95
+ else:
96
+ spec_dirs.append(path)
97
+ config = ItemCacheConfig(paths=spec_dirs)
98
+ item_cache = ItemCache(config, type_provider=_create_type_provider())
99
+ for path in item_files:
100
+ item_cache.add_item_from_file(path, path, initialize_links=False)
101
+ clang_format_style: dict[str, str] = {}
102
+ for style_file in args.clang_format_style:
103
+ style, sep, file = style_file.partition(":")
104
+ if not sep or not style or not file:
105
+ logging.error(
106
+ "the --clang-format-style option value '%s' "
107
+ "is not in <name>:<path> format", style_file)
108
+ return 1
109
+ clang_format_style[style] = file
110
+ if args.format_items:
111
+ formatter = SpecYAMLFormatter(clang_format_path=args.clang_format_path,
112
+ clang_format_style=clang_format_style)
113
+ else:
114
+ formatter = None
115
+ return verify_specification_format(item_cache,
116
+ uid_log_level=uid_log_level,
117
+ formatter=formatter).exit_code()
@@ -3,7 +3,7 @@
3
3
  Provides a command line interface to Print the specified value of a YAML file.
4
4
  """
5
5
 
6
- # Copyright (C) 2024 embedded brains GmbH & Co. KG
6
+ # Copyright (C) 2024, 2026 embedded brains GmbH & Co. KG
7
7
  #
8
8
  # Redistribution and use in source and binary forms, with or without
9
9
  # modification, are permitted provided that the following conditions
@@ -29,7 +29,7 @@ Provides a command line interface to Print the specified value of a YAML file.
29
29
  import argparse
30
30
  import sys
31
31
 
32
- from specitems import EmptyItemCache, Item, ItemMapper, load_data
32
+ from specitems import EmptyItemCache, Item, load_data
33
33
 
34
34
 
35
35
  def cliyamlquery(argv: list[str] = sys.argv) -> None:
@@ -43,4 +43,4 @@ def cliyamlquery(argv: list[str] = sys.argv) -> None:
43
43
  parser.add_argument("file", metavar="FILE", nargs=1, help="the YAML file")
44
44
  args = parser.parse_args(argv[1:])
45
45
  item = Item(EmptyItemCache(), "/x", load_data(args.file[0]))
46
- print(ItemMapper(item).substitute(f"${{.:{args.path[0]}}}"))
46
+ print(item.get_value(args.path[0]))
@@ -53,6 +53,13 @@ def _simple_row(row: Iterable[str], maxi: Iterable[int]) -> str:
53
53
  return f" | {line} |"
54
54
 
55
55
 
56
+ def format_markdown_text(text: str) -> str:
57
+ """ Format the Markdown text. """
58
+ return mdformat.text(text,
59
+ options=_MDFORMAT_OPTIONS,
60
+ extensions=_MDFORMAT_EXTENSIONS)
61
+
62
+
56
63
  class MarkdownContent(TextContent):
57
64
  """ This class builds MyST Markdown content. """
58
65
 
@@ -37,10 +37,39 @@ from .items import Item, ItemType
37
37
  ItemGetValue = Callable[["ItemGetValueContext"], Any]
38
38
  ItemGetValueMap = dict[str, tuple[ItemGetValue, Any]]
39
39
 
40
- _VAR_NAME = "(?:[a-zA-Z0-9._/-]+|\\*):[\\[\\]a-zA-Z0-9._/-]+(?::[^${}]*)?"
41
- _VAR_SINGLE = re.compile(f"^[$@]\\{{{_VAR_NAME}\\}}$")
42
- _VAR_TOKENS = re.compile(f"(?:(\\$+|@+)\\{{{_VAR_NAME}\\}})|"
43
- r"(?:(\$+|@+)\{)")
40
+ _VAR_NAME = "(?:[a-zA-Z0-9._/-]+|\\*):[\\[\\]a-zA-Z0-9._/-]+(?::[^$`{}]*)?"
41
+ _VAR_SINGLE = re.compile(f"^[$@][{{`]{_VAR_NAME}[}}`]$")
42
+ _VAR_TOKENS = re.compile(f"(?:(\\$+|@+)[{{`]{_VAR_NAME}[}}`])|"
43
+ r"(?:(\$+|@+)[{{`])")
44
+
45
+
46
+ def _to_something(mobj: re.Match, prefix: str, postfix: str) -> str:
47
+ designator = mobj.group(1)
48
+ token = mobj.group(0)
49
+ if designator:
50
+ brace = len(designator)
51
+ if brace & 1 == 0:
52
+ return token
53
+ return f"{token[:brace - 1]}{prefix}{token[brace + 1:-1]}{postfix}"
54
+ return token
55
+
56
+
57
+ def _to_at(mobj: re.Match) -> str:
58
+ return _to_something(mobj, "@`", "`")
59
+
60
+
61
+ def to_at_variables(text: str) -> str:
62
+ """ Convert the text to use ``@`...``` for all substitution variables. """
63
+ return _VAR_TOKENS.sub(_to_at, text)
64
+
65
+
66
+ def _to_dollar(mobj: re.Match) -> str:
67
+ return _to_something(mobj, "${", "}")
68
+
69
+
70
+ def to_dollar_variables(text: str) -> str:
71
+ """ Convert the text to use ``${...}`` for all substitution variables. """
72
+ return _VAR_TOKENS.sub(_to_dollar, text)
44
73
 
45
74
 
46
75
  class _ItemMapperError(Exception):
@@ -111,6 +140,8 @@ def unpack_arg(arg: str) -> str:
111
140
  arg_2.append(",")
112
141
  case "f":
113
142
  arg_2.append("\f")
143
+ case "g":
144
+ arg_2.append("`")
114
145
  case "n":
115
146
  arg_2.append("\n")
116
147
  case "r":
@@ -33,6 +33,7 @@ import base64
33
33
  import dataclasses
34
34
  import hashlib
35
35
  import itertools
36
+ import logging
36
37
  import os
37
38
  import pickle
38
39
  import re
@@ -407,6 +408,56 @@ class Item:
407
408
  """
408
409
  return self.data.get(key, default)
409
410
 
411
+ def get_value(self, path: str) -> Any:
412
+ """ Get the attribute value associated with the attribute path. """
413
+ value = self.data
414
+ path = path.strip("/")
415
+ while path:
416
+ key_end = path.find("/")
417
+ if key_end >= 0:
418
+ key_index = path[:key_end]
419
+ path = path[key_end + 1:]
420
+ else:
421
+ key_index = path
422
+ path = ""
423
+ parts = key_index.split("[")
424
+ try:
425
+ index = int(parts[1].split("]")[0])
426
+ except IndexError:
427
+ index = -1
428
+ value = value[parts[0]]
429
+ if index >= 0:
430
+ value = value[index]
431
+ return value
432
+
433
+ def set_value(self, path: str, value: Any) -> None:
434
+ """ Set the attribute value associated with the attribute path. """
435
+ parent = self.data
436
+ path = path.strip("/")
437
+ while path:
438
+ key_end = path.find("/")
439
+ if key_end >= 0:
440
+ key_index = path[:key_end]
441
+ path = path[key_end + 1:]
442
+ else:
443
+ key_index = path
444
+ path = ""
445
+ parts = key_index.split("[")
446
+ try:
447
+ index = int(parts[1].split("]")[0])
448
+ except IndexError:
449
+ index = -1
450
+ if path:
451
+ parent = parent[parts[0]]
452
+ if index >= 0:
453
+ parent = parent[index]
454
+ else:
455
+ if index >= 0:
456
+ parent = parent[parts[0]]
457
+ parent[index] = value
458
+ else:
459
+ parent[parts[0]] = value
460
+
410
461
  def to_abs_uid(self, abs_or_rel_uid: str) -> str:
411
462
  """
412
463
  Return the absolute UID of an absolute UID or an UID relative to this
@@ -1003,7 +1054,7 @@ class ItemCacheConfig:
1003
1054
  initialize_links: bool = True
1004
1055
  resolve_proxies: bool = True
1005
1056
  permissive_type_errors: bool = False
1006
- cache_directory: str = "cache"
1057
+ cache_directory: str = ".specitems/cache"
1007
1058
 
1008
1059
 
1009
1060
  class ItemCache(dict):
@@ -1054,6 +1105,21 @@ class ItemCache(dict):
1054
1105
  return item
1055
1106
  for item in proxy.children("proxy-member-default"):
1056
1107
  return item
1108
+ logging.warning("%s: cannot resolve proxy for enabled set %s", uid,
1109
+ sorted(self.enabled_set))
1110
+ for item in proxy.children("proxy-member",
1111
+ is_link_enabled=link_is_enabled):
1112
+ logging.warning(
1113
+ "%s: has disabled proxy member %s enabled by %s", uid,
1114
+ item.uid, item["enabled-by"])
1115
+ for item in proxy.children("proxy-member-default",
1116
+ is_link_enabled=link_is_enabled):
1117
+ logging.warning(
1118
+ "%s: has disabled default proxy member %s enabled by %s",
1119
+ uid, item.uid, item["enabled-by"])
1120
+ logging.info(
1121
+ "%s: note that items may get enabled recursively; "
1122
+ "maybe a parent is disabled", uid)
1057
1123
  return proxy
1058
1124
 
1059
1125
  @property
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: BSD-2-Clause
2
+ """ Provides a specification item formatter. """
3
+
4
+ # Copyright (C) 2026 embedded brains GmbH & Co. KG
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions
8
+ # are met:
9
+ # 1. Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # 2. Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ import abc
28
+ import subprocess
29
+ from typing import Any
30
+
31
+ import yaml
32
+
33
+ from .contentmarkdown import format_markdown_text
34
+ from .items import Item
35
+ from .itemmapper import to_at_variables, to_dollar_variables
36
+
37
+
38
+ class SpecFormatter(abc.ABC):
39
+ """ Provides a method to format values. """
40
+
41
+ def __init__(self, clang_format_path: str,
42
+ clang_format_style: dict[str, str]) -> None:
43
+ super().__init__()
44
+ self.clang_format_path = clang_format_path
45
+ self.clang_format_style = clang_format_style
46
+
47
+ @abc.abstractmethod
48
+ def format_value(self, item: Item, path: str, value: Any,
49
+ fmt: dict) -> None:
50
+ """ Format the value and set the item value according to the path. """
51
+
52
+ @abc.abstractmethod
53
+ def save(self, item: Item) -> None:
54
+ """ Save the item if it has a matching format. """
55
+
56
+
57
+ def _format_clang(formatter: SpecFormatter, _item: Item, value: str,
58
+ fmt: dict) -> str:
59
+ name = fmt["style"]
60
+ try:
61
+ style = formatter.clang_format_style[name]
62
+ except KeyError as err:
63
+ raise ValueError(f"unknown clang-format style '{name}', "
64
+ "configure it via --clang-format-style") from err
65
+ cmd = [
66
+ formatter.clang_format_path, f"--style={style}",
67
+ "--assume-filename=specitems.c"
68
+ ]
69
+ result = subprocess.run(cmd,
70
+ input=value,
71
+ capture_output=True,
72
+ encoding="utf-8",
73
+ check=True)
74
+ return result.stdout
75
+
76
+
77
+ def _format_myst(_formatter: SpecFormatter, _item: Item, value: str,
78
+ _fmt: dict) -> str:
79
+ value = to_at_variables(value)
80
+ value = format_markdown_text(value)
81
+ return to_dollar_variables(value)
82
+
83
+
84
+ class _ListOrder(dict):
85
+ pass
86
+
87
+
88
+ def _format_list_order(_formatter: SpecFormatter, item: Item, value: Any,
89
+ fmt: dict) -> Any:
90
+ # pylint: disable=attribute-defined-outside-init
91
+ value_2 = _ListOrder(value)
92
+ value_2.the_list = item.get_value( # type: ignore[attr-defined]
93
+ fmt["path"])
94
+ value_2.key = fmt["key"] # type: ignore[attr-defined]
95
+ return value_2
96
+
97
+
98
+ def _format_list_order_representer(
99
+ representer: yaml.representer.BaseRepresenter, data: Any):
100
+ assert isinstance(representer, yaml.representer.SafeRepresenter)
101
+ order: dict[str, int] = {}
102
+ for index, element in enumerate(data.the_list):
103
+ order[element[data.key]] = index
104
+ if data.keys() != order.keys():
105
+ raise ValueError(
106
+ "the dictionary and order list key sets are not equal: "
107
+ f"{sorted(set(data.keys()).symmetric_difference(order.keys()))}")
108
+ return representer.represent_dict(
109
+ sorted(data.items(), key=lambda x: order[x[0]]))
110
+
111
+
112
+ yaml.add_representer(_ListOrder, _format_list_order_representer)
113
+
114
+
115
+ class _FormatInt(int):
116
+ pass
117
+
118
+
119
+ def _format_int_presenter(representer: yaml.representer.BaseRepresenter,
120
+ data: Any):
121
+ return representer.represent_scalar('tag:yaml.org,2002:int',
122
+ data.fmt.format(data))
123
+
124
+
125
+ yaml.add_representer(_FormatInt, _format_int_presenter)
126
+
127
+
128
+ def _format_int_fmt(value: Any, format_string: str) -> Any:
129
+ # pylint: disable=attribute-defined-outside-init
130
+ value = _FormatInt(value)
131
+ value.fmt = format_string
132
+ return value
133
+
134
+
135
+ def _format_int_string(_formatter: SpecFormatter, _item: Item, value: Any,
136
+ fmt: dict) -> Any:
137
+ return _format_int_fmt(value, fmt["format"])
138
+
139
+
140
+ def _format_int_attribute(_formatter: SpecFormatter, item: Item, value: Any,
141
+ fmt: dict) -> Any:
142
+ try:
143
+ format_string = item.get_value(fmt["path"])
144
+ except KeyError:
145
+ format_string = fmt.get("default")
146
+ if format_string is None:
147
+ raise
148
+ return _format_int_fmt(value, format_string)
149
+
150
+
151
+ _YAML_FORMATTER = {
152
+ "myst": _format_myst,
153
+ "clang": _format_clang,
154
+ "int-format-string": _format_int_string,
155
+ "int-format-attribute": _format_int_attribute,
156
+ "list-order": _format_list_order,
157
+ }
158
+
159
+
160
+ class SpecYAMLFormatter(SpecFormatter):
161
+ """ Provides a method to format values for YAML files. """
162
+
163
+ # pylint: disable=too-few-public-methods
164
+ def format_value(self, item: Item, path: str, value: Any,
165
+ fmt: dict) -> None:
166
+ value = _YAML_FORMATTER[fmt["type"]](self, item, value, fmt)
167
+ item.set_value(path, value)
168
+
169
+ def save(self, item: Item) -> None:
170
+ if item.file.endswith(".yml"):
171
+ item.save()
@@ -34,10 +34,11 @@ represented by a :py:class:`LoggingStatus` object.
34
34
 
35
35
  import logging
36
36
  import re
37
- from typing import Any, NamedTuple
37
+ from typing import Any, NamedTuple, Optional
38
38
 
39
39
  from .cliutil import LoggingStatus, monitor_logging
40
40
  from .items import Item, ItemCache
41
+ from .specformatter import SpecFormatter
41
42
 
42
43
  _VerifierMap = dict[str, "_Verifier"]
43
44
 
@@ -202,10 +203,14 @@ def _prefix(prefix: _Path) -> str:
202
203
 
203
204
  class _Verifier:
204
205
 
205
- def __init__(self, name: str, verifier_map: _VerifierMap) -> None:
206
+ def __init__(self,
207
+ name: str,
208
+ verifier_map: _VerifierMap,
209
+ formatter: Optional[SpecFormatter] = None) -> None:
206
210
  self._name = name
207
211
  self._verifier_map = verifier_map
208
212
  self.is_subtype = False
213
+ self.formatter = formatter
209
214
  verifier_map[name] = self
210
215
 
211
216
  def verify_info(self, path: _Path) -> None:
@@ -312,13 +317,23 @@ class _ItemVerifier(_Verifier):
312
317
  """
313
318
 
314
319
  def __init__(self, name: str, verifier_map: _VerifierMap,
315
- info_map: dict[str, Any], item: Item) -> None:
316
- super().__init__(name, verifier_map)
320
+ formatter: Optional[SpecFormatter], info_map: dict[str, Any],
321
+ item: Item) -> None:
322
+ super().__init__(name, verifier_map, formatter)
317
323
  self._info_map = info_map
318
324
  self._item = item
319
325
  self._subtype_key = ""
320
326
  self._subtype_verifiers: _VerifierMap = {}
321
327
 
328
+ def _format(self, formatter: SpecFormatter, path: _Path, value: Any,
329
+ type_info: Any) -> None:
330
+ fmt = type_info.get("format")
331
+ if fmt is not None:
332
+ logging.info("%s format using type '%s'", _prefix(path),
333
+ fmt["type"])
334
+ _, _, attribute_path = path.path.partition(":")
335
+ formatter.format_value(path.item, attribute_path, value, fmt)
336
+
322
337
  def verify_bool(self, path: _Path, value: Any, type_info: Any) -> set[str]:
323
338
  """Verify a boolean value according to an optional assertion.
324
339
 
@@ -495,6 +510,9 @@ class _ItemVerifier(_Verifier):
495
510
  logging.error(
496
511
  "%s has unverfied keys for type '%s' and its subtypes: %s",
497
512
  _prefix(path), self._name, sorted(unverified_keys))
513
+ formatter = self.formatter
514
+ if formatter is not None:
515
+ self._format(formatter, path, value, type_info)
498
516
  return verified_keys
499
517
 
500
518
  def verify_int_or_float(self, path: _Path, value: Any,
@@ -510,8 +528,12 @@ class _ItemVerifier(_Verifier):
510
528
  Returns:
511
529
  Empty set. Logs an error if assertions fail.
512
530
  """
513
- if not _assert_int_or_float(path, value, type_info):
531
+ ok = _assert_int_or_float(path, value, type_info)
532
+ if not ok:
514
533
  logging.error("%s invalid value: %s", _prefix(path), value)
534
+ formatter = self.formatter
535
+ if ok and formatter is not None:
536
+ self._format(formatter, path, value, type_info)
515
537
  return set()
516
538
 
517
539
  def verify_list(self, path: _Path, value: Any, type_info: Any) -> set[str]:
@@ -557,8 +579,12 @@ class _ItemVerifier(_Verifier):
557
579
  Returns:
558
580
  Empty set. Logs an error if assertions fail.
559
581
  """
560
- if not _assert_str(path, value, type_info):
582
+ ok = _assert_str(path, value, type_info)
583
+ if not ok:
561
584
  logging.error("%s invalid value: %s", _prefix(path), value)
585
+ formatter = self.formatter
586
+ if ok and formatter is not None:
587
+ self._format(formatter, path, value, type_info)
562
588
  return set()
563
589
 
564
590
  def verify(self, path: _Path, value: Any) -> set[str]:
@@ -619,7 +645,8 @@ _ASSERT_KEYS = {
619
645
  }
620
646
 
621
647
 
622
- def _create_verifier(item: Item, verifier_map: _VerifierMap) -> _Verifier:
648
+ def _create_verifier(item: Item, verifier_map: _VerifierMap,
649
+ formatter: Optional[SpecFormatter]) -> _Verifier:
623
650
  spec_type = item["spec-type"]
624
651
  if spec_type in verifier_map:
625
652
  verifier = verifier_map[spec_type]
@@ -627,13 +654,14 @@ def _create_verifier(item: Item, verifier_map: _VerifierMap) -> _Verifier:
627
654
  return verifier
628
655
  spec_info = item["spec-info"]
629
656
  assert isinstance(spec_info, dict)
630
- return _ItemVerifier(spec_type, verifier_map, spec_info, item)
657
+ return _ItemVerifier(spec_type, verifier_map, formatter, spec_info, item)
631
658
 
632
659
 
633
- def _gather_item_verifiers(item: Item, verifier_map: _VerifierMap) -> None:
660
+ def _gather_item_verifiers(item: Item, verifier_map: _VerifierMap,
661
+ formatter: Optional[SpecFormatter]) -> None:
634
662
  for link in item.links_to_children():
635
663
  if link.role == "spec-member":
636
- _create_verifier(link.item, verifier_map)
664
+ _create_verifier(link.item, verifier_map, formatter)
637
665
 
638
666
 
639
667
  class SpecVerifier:
@@ -647,11 +675,11 @@ class SpecVerifier:
647
675
  creation, it resolves subtype refinements and is ready to verify items.
648
676
  """
649
677
 
650
- # pylint: disable=too-few-public-methods
651
678
  def __init__(self,
652
679
  item_cache: ItemCache,
653
680
  root_uid: str,
654
- uid_log_level: int = logging.ERROR) -> None:
681
+ uid_log_level: int = logging.ERROR,
682
+ formatter: Optional[SpecFormatter] = None) -> None:
655
683
  """Initialize and build verifier map.
656
684
 
657
685
  Args:
@@ -664,6 +692,7 @@ class SpecVerifier:
664
692
  uid_log_level: The log level to indicate that an UID cannot be
665
693
  resolved.
666
694
  """
695
+ self._formatter = formatter
667
696
  verifier_map: _VerifierMap = {}
668
697
  _AnyVerifier("any", verifier_map)
669
698
  _NameVerifier("name", verifier_map)
@@ -678,8 +707,9 @@ class SpecVerifier:
678
707
  except KeyError:
679
708
  self._root_verifier = None
680
709
  else:
681
- self._root_verifier = _create_verifier(root_item, verifier_map)
682
- _gather_item_verifiers(root_item, verifier_map)
710
+ self._root_verifier = _create_verifier(root_item, verifier_map,
711
+ formatter)
712
+ _gather_item_verifiers(root_item, verifier_map, formatter)
683
713
  for name in sorted(verifier_map):
684
714
  logging.info("type: %s", name)
685
715
  verifier_map[name].resolve_type_refinements()
@@ -694,10 +724,13 @@ class SpecVerifier:
694
724
  logging.error("root type item does not exist in item cache")
695
725
  else:
696
726
  logging.info("start specification item verification")
727
+ formatter = self._formatter
697
728
  for key in sorted(item_cache):
698
729
  item = item_cache[key]
699
730
  self._root_verifier.verify(_Path(item, f"{item.uid}:"),
700
731
  item.data)
732
+ if formatter is not None:
733
+ formatter.save(item)
701
734
  logging.info("finished specification item verification")
702
735
 
703
736
  def verify(self, item: Item) -> LoggingStatus:
@@ -720,7 +753,8 @@ class SpecVerifier:
720
753
 
721
754
  def verify_specification_format(
722
755
  item_cache: ItemCache,
723
- uid_log_level: int = logging.ERROR) -> LoggingStatus:
756
+ uid_log_level: int = logging.ERROR,
757
+ formatter: Optional[SpecFormatter] = None) -> LoggingStatus:
724
758
  """Verify all items using the specification root type from the item cache.
725
759
 
726
760
  Emits an error if the item cache has no specification root type.
@@ -729,6 +763,7 @@ def verify_specification_format(
729
763
  item_cache: The item cache containing the items to verify.
730
764
  uid_log_level: The log level to indicate that an UID cannot be
731
765
  resolved.
766
+ formatter: The optional specification item value formatter.
732
767
 
733
768
  Returns:
734
769
  The status summarizing the verification run.
@@ -738,6 +773,7 @@ def verify_specification_format(
738
773
  if root_type_uid is None:
739
774
  logging.error("item cache has no root type")
740
775
  else:
741
- verifier = SpecVerifier(item_cache, root_type_uid, uid_log_level)
776
+ verifier = SpecVerifier(item_cache, root_type_uid, uid_log_level,
777
+ formatter)
742
778
  verifier.verify_all(item_cache)
743
779
  return monitor.get_status()
File without changes