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.
- {specitems-1.6.2 → specitems-1.6.4}/PKG-INFO +1 -1
- {specitems-1.6.2 → specitems-1.6.4}/pyproject.toml +2 -1
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/__init__.py +9 -4
- specitems-1.6.4/src/specitems/cliverify.py +117 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cliyamlquery.py +3 -3
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentmarkdown.py +7 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/itemmapper.py +35 -4
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/items.py +67 -1
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/spec.pickle +0 -0
- specitems-1.6.4/src/specitems/specformatter.py +171 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/specverify.py +52 -16
- {specitems-1.6.2 → specitems-1.6.4}/README.md +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cite.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clihash.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clipickle.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/clispecdoc.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/cliutil.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/content.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentcommonmark.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contentsphinx.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/contenttext.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/getvaluesubprocess.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/glossary.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/hashutil.py +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/py.typed +0 -0
- {specitems-1.6.2 → specitems-1.6.4}/src/specitems/specdoc.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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"^[$@]
|
|
42
|
-
_VAR_TOKENS = re.compile(f"(?:(\\$+|@+)
|
|
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
|
|
Binary file
|
|
@@ -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,
|
|
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],
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|