specitems 1.4.2__tar.gz → 1.4.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 (25) hide show
  1. {specitems-1.4.2 → specitems-1.4.4}/PKG-INFO +1 -1
  2. {specitems-1.4.2 → specitems-1.4.4}/pyproject.toml +2 -2
  3. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/__init__.py +10 -2
  4. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/cliutil.py +67 -1
  5. specitems-1.4.4/src/specitems/contentcommonmark.py +119 -0
  6. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/specverify.py +26 -90
  7. {specitems-1.4.2 → specitems-1.4.4}/README.md +0 -0
  8. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/cite.py +0 -0
  9. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/clihash.py +0 -0
  10. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/clipickle.py +0 -0
  11. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/clispecdoc.py +0 -0
  12. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/cliyamlquery.py +0 -0
  13. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/content.py +0 -0
  14. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/contentmarkdown.py +0 -0
  15. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/contentsphinx.py +0 -0
  16. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/contenttext.py +0 -0
  17. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/getvaluesubprocess.py +0 -0
  18. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/glossary.py +0 -0
  19. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/hashutil.py +0 -0
  20. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/itemmapper.py +0 -0
  21. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/items.py +0 -0
  22. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/py.typed +0 -0
  23. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/spec.pickle +0 -0
  24. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/specdoc.py +0 -0
  25. {specitems-1.4.2 → specitems-1.4.4}/src/specitems/subprocessaction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specitems
3
- Version: 1.4.2
3
+ Version: 1.4.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.4.2"
7
+ version = "1.4.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"}
@@ -56,7 +56,7 @@ spechash = "specitems.clihash:clihash"
56
56
  specyamlquery = "specitems.cliyamlquery:cliyamlquery"
57
57
 
58
58
  [build-system]
59
- requires = ["uv_build>=0.10.0,<0.11.0"]
59
+ requires = ["uv_build>=0.10.0"]
60
60
  build-backend = "uv_build"
61
61
 
62
62
  [dependency-groups]
@@ -26,11 +26,13 @@
26
26
 
27
27
  from .cite import BibTeXCitationProvider
28
28
  from .cliutil import (
29
+ LoggingStatus,
29
30
  create_config,
30
31
  get_arguments,
31
32
  get_item_cache_arguments,
32
33
  init_logging,
33
34
  load_config,
35
+ monitor_logging,
34
36
  )
35
37
  from .content import (
36
38
  Content,
@@ -56,6 +58,10 @@ from .contenttext import (
56
58
  latex_escape,
57
59
  make_label,
58
60
  )
61
+ from .contentcommonmark import (
62
+ CommonMarkContent,
63
+ CommonMarkMapper,
64
+ )
59
65
  from .contentmarkdown import (
60
66
  MarkdownContent,
61
67
  MarkdownMapper,
@@ -129,7 +135,6 @@ from .specdoc import (SpecDocumentConfig, add_specification_documentation,
129
135
  generate_specification_documentation)
130
136
  from .specverify import (
131
137
  SpecVerifier,
132
- VerifyStatus,
133
138
  verify_specification_format,
134
139
  )
135
140
  from .subprocessaction import (
@@ -140,6 +145,8 @@ from .subprocessaction import (
140
145
  __all__ = [
141
146
  "BibTeXCitationProvider",
142
147
  "COL_SPAN",
148
+ "CommonMarkContent",
149
+ "CommonMarkMapper",
143
150
  "Content",
144
151
  "ContentAddContext",
145
152
  "Copyright",
@@ -170,6 +177,7 @@ __all__ = [
170
177
  "ItemViewGetMissing",
171
178
  "JSONItemCache",
172
179
  "Link",
180
+ "LoggingStatus",
173
181
  "MARKDOWN_ROLES",
174
182
  "MarkdownContent",
175
183
  "MarkdownMapper",
@@ -181,7 +189,6 @@ __all__ = [
181
189
  "SphinxMapper",
182
190
  "TextContent",
183
191
  "TextMapper",
184
- "VerifyStatus",
185
192
  "add_specification_documentation",
186
193
  "augment_glossary_terms",
187
194
  "base64_to_hex",
@@ -219,6 +226,7 @@ __all__ = [
219
226
  "make_lines",
220
227
  "make_subprocess_environment",
221
228
  "make_text",
229
+ "monitor_logging",
222
230
  "pickle_load_data_by_uid",
223
231
  "run_subprocess_action",
224
232
  "save_data",
@@ -25,10 +25,12 @@
25
25
  # POSSIBILITY OF SUCH DAMAGE.
26
26
 
27
27
  import argparse
28
+ import contextlib
28
29
  import itertools
29
30
  import logging
30
31
  import os
31
- from typing import Any, Callable, Iterable, Optional, Type, TypeVar
32
+ from typing import (Any, Callable, Iterable, Iterator, NamedTuple, Optional,
33
+ Type, TypeVar)
32
34
 
33
35
  import yaml
34
36
 
@@ -142,6 +144,70 @@ def init_logging(args: argparse.Namespace) -> None:
142
144
  handlers=handlers)
143
145
 
144
146
 
147
+ class LoggingStatus(NamedTuple):
148
+ """Counts of log messages grouped by severity.
149
+
150
+ Attributes:
151
+ critical: Number of CRITICAL messages.
152
+ error: Number of ERROR messages.
153
+ warning: Number of WARNING messages.
154
+ info: Number of INFO messages.
155
+ debug: Number of DEBUG messages.
156
+ """
157
+ critical: int
158
+ error: int
159
+ warning: int
160
+ info: int
161
+ debug: int
162
+
163
+
164
+ class LogMonitor(logging.Filter):
165
+ """ Monitors log messages grouped by severity. """
166
+
167
+ def __init__(self) -> None:
168
+ super().__init__()
169
+ self._counts: dict[int, int] = {}
170
+
171
+ def filter(self, record: logging.LogRecord) -> bool:
172
+ """Count the given logging record and allow propagation.
173
+
174
+ Args:
175
+ record: The logging record to count.
176
+
177
+ Returns:
178
+ True so the log record is processed by the logging system.
179
+ """
180
+ count = self._counts.get(record.levelno, 0)
181
+ self._counts[record.levelno] = count + 1
182
+ return True
183
+
184
+ def get_status(self) -> LoggingStatus:
185
+ """Return a LoggingStatus summarizing collected log counts.
186
+
187
+ Returns:
188
+ Aggregated counts for each standard logging level.
189
+ """
190
+ return LoggingStatus(self._counts.get(logging.CRITICAL, 0),
191
+ self._counts.get(logging.ERROR, 0),
192
+ self._counts.get(logging.WARNING, 0),
193
+ self._counts.get(logging.INFO, 0),
194
+ self._counts.get(logging.DEBUG, 0))
195
+
196
+
197
+ @contextlib.contextmanager
198
+ def monitor_logging() -> Iterator[LogMonitor]:
199
+ """
200
+ Monitors the logging and counts the log records grouped by severity.
201
+ """
202
+ logger = logging.getLogger()
203
+ monitor = LogMonitor()
204
+ logger.addFilter(monitor)
205
+ try:
206
+ yield monitor
207
+ finally:
208
+ logger.removeFilter(monitor)
209
+
210
+
145
211
  def load_config(config_filename: str) -> Any:
146
212
  """ Loads the configuration file with recursive includes. """
147
213
 
@@ -0,0 +1,119 @@
1
+ # SPDX-License-Identifier: BSD-2-Clause
2
+ """ Provides interfaces for CommonMark content generation. """
3
+
4
+ # Copyright (C) 2025, 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
+ from typing import Iterable, Optional, Sequence
28
+
29
+ from .content import GenericContent, make_lines
30
+ from .contentmarkdown import MarkdownContent
31
+ from .contenttext import COL_SPAN, ROW_SPAN, TextContent, TextMapper
32
+
33
+
34
+ def _make_simple(cell: str | int) -> str:
35
+ if isinstance(cell, str):
36
+ return cell
37
+ if cell == COL_SPAN | ROW_SPAN:
38
+ return "↖"
39
+ if cell == ROW_SPAN:
40
+ return "←"
41
+ return "↑"
42
+
43
+
44
+ class CommonMarkContent(MarkdownContent):
45
+ """ This class builds CommonMark content. """
46
+
47
+ def reference(self, label: str, name: Optional[str] = None) -> str:
48
+ if not name:
49
+ name = label
50
+ return self.link(name, f"#{label}")
51
+
52
+ def term(self, text: str, term: str | None = None) -> str:
53
+ return text
54
+
55
+ def cite(self, identifier: str) -> str:
56
+ return ""
57
+
58
+ def add_label(self, label: str) -> None:
59
+ self.add(f"<a id=\"{label.strip()}\"></a>")
60
+
61
+ def add_rubric(self, name: str) -> None:
62
+ self.add([self.strong(name), ""])
63
+
64
+ def add_image(self, base: str, width: Optional[str] = None) -> None:
65
+ self.add(f"![]({base})")
66
+
67
+ def add_index_entries(self, entries: list[str]) -> None:
68
+ pass
69
+
70
+ def open_directive(self,
71
+ name: str,
72
+ value: Optional[str] = None,
73
+ options: Optional[list[str]] = None) -> None:
74
+ self.add(f"```{name.strip()}")
75
+ self.gap = False
76
+
77
+ def add_simple_table(self,
78
+ rows: Sequence[Iterable[str]],
79
+ widths: Optional[list[int]] = None,
80
+ font_size: Optional[str | int] = None) -> None:
81
+ super().add_simple_table(rows)
82
+
83
+ def add_grid_table(self,
84
+ rows: Sequence[Iterable[str | int]],
85
+ widths: Optional[list[int]] = None,
86
+ header_rows: int = 1,
87
+ font_size: Optional[str | int] = None) -> None:
88
+ if not rows:
89
+ return
90
+ simple_rows = [
91
+ tuple(_make_simple(cell) for cell in row) for row in rows
92
+ ]
93
+ self.add_simple_table(simple_rows)
94
+
95
+ def add_definition_item(self, name: GenericContent,
96
+ definition: GenericContent) -> None:
97
+ self.add(f"{self.strong(', '.join(make_lines(name)))}:")
98
+ self.append(definition)
99
+
100
+ def add_glossary_term(self, term: str, definition: str) -> None:
101
+ self.add_definition_item(term, definition)
102
+
103
+ def add_code_block(self,
104
+ code: list[str],
105
+ language: str = "none",
106
+ font_size: str | int = "footnotesize",
107
+ line_number_start: int = 1) -> None:
108
+ with self.directive(language):
109
+ self.append(code)
110
+
111
+
112
+ class CommonMarkMapper(TextMapper):
113
+ """ Provides an item mapper for CommonMark formatted text production. """
114
+
115
+ def create_content(
116
+ self,
117
+ section_level: int = 0,
118
+ the_license: str | set[str] | None = None) -> TextContent:
119
+ return CommonMarkContent(section_level, the_license)
@@ -6,7 +6,7 @@ verify that a set of specification items is in line with a specification item
6
6
  format specification. The format specification is defined by specification
7
7
  items. Logging is used for informational messages and for reporting errors
8
8
  found during verification. The result of the format verification is
9
- represented by a :py:class:`VerifyStatus` object.
9
+ represented by a :py:class:`LoggingStatus` object.
10
10
  """
11
11
 
12
12
  # Copyright (C) 2020, 2026 embedded brains GmbH & Co. KG
@@ -32,65 +32,16 @@ represented by a :py:class:`VerifyStatus` object.
32
32
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
33
  # POSSIBILITY OF SUCH DAMAGE.
34
34
 
35
- from contextlib import contextmanager
36
35
  import logging
37
36
  import re
38
- from typing import Any, Iterator, NamedTuple
37
+ from typing import Any, NamedTuple
39
38
 
39
+ from .cliutil import LoggingStatus, monitor_logging
40
40
  from .items import Item, ItemCache
41
41
 
42
42
  _VerifierMap = dict[str, "_Verifier"]
43
43
 
44
44
 
45
- class VerifyStatus(NamedTuple):
46
- """Counts of log messages produced by a verification run.
47
-
48
- Attributes:
49
- critical: Number of CRITICAL messages.
50
- error: Number of ERROR messages.
51
- warning: Number of WARNING messages.
52
- info: Number of INFO messages.
53
- debug: Number of DEBUG messages.
54
- """
55
- critical: int
56
- error: int
57
- warning: int
58
- info: int
59
- debug: int
60
-
61
-
62
- class _Filter(logging.Filter):
63
-
64
- def __init__(self) -> None:
65
- super().__init__()
66
- self._counts: dict[int, int] = {}
67
-
68
- def filter(self, record: logging.LogRecord) -> bool:
69
- """Count the given logging record and allow propagation.
70
-
71
- Args:
72
- record: The logging record to count.
73
-
74
- Returns:
75
- True so the log record is processed by the logging system.
76
- """
77
- count = self._counts.get(record.levelno, 0)
78
- self._counts[record.levelno] = count + 1
79
- return True
80
-
81
- def get_verify_info(self) -> VerifyStatus:
82
- """Return a VerifyStatus summarizing collected log counts.
83
-
84
- Returns:
85
- Aggregated counts for each standard logging level.
86
- """
87
- return VerifyStatus(self._counts.get(logging.CRITICAL, 0),
88
- self._counts.get(logging.ERROR, 0),
89
- self._counts.get(logging.WARNING, 0),
90
- self._counts.get(logging.INFO, 0),
91
- self._counts.get(logging.DEBUG, 0))
92
-
93
-
94
45
  def _type_name(value: Any):
95
46
  type_name = type(value).__name__
96
47
  if type_name == "NoneType":
@@ -680,15 +631,6 @@ def _gather_item_verifiers(item: Item, verifier_map: _VerifierMap) -> None:
680
631
  _create_verifier(link.item, verifier_map)
681
632
 
682
633
 
683
- @contextmanager
684
- def _add_filter() -> Iterator[_Filter]:
685
- logger = logging.getLogger()
686
- log_filter = _Filter()
687
- logger.addFilter(log_filter)
688
- yield log_filter
689
- logger.removeFilter(log_filter)
690
-
691
-
692
634
  class SpecVerifier:
693
635
  """Orchestrates construction of verifiers from spec items and running
694
636
  verification.
@@ -732,31 +674,23 @@ class SpecVerifier:
732
674
  logging.info("type: %s", name)
733
675
  verifier_map[name].resolve_type_refinements()
734
676
 
735
- def verify_all(self, item_cache: ItemCache) -> VerifyStatus:
677
+ def verify_all(self, item_cache: ItemCache) -> None:
736
678
  """Verify all items in the provided item cache.
737
679
 
738
- The method installs a logging filter to collect counts, iterates all
739
- items in the cache and invokes the root verifier on each item's data.
740
-
741
680
  Args:
742
681
  item_cache: The cache containing items to verify.
743
-
744
- Returns:
745
- Status with counts of messages emitted during verification.
746
682
  """
747
- with _add_filter() as log_filter:
748
- if self._root_verifier is None:
749
- logging.error("root type item does not exist in item cache")
750
- else:
751
- logging.info("start specification item verification")
752
- for key in sorted(item_cache):
753
- item = item_cache[key]
754
- self._root_verifier.verify(_Path(item, f"{item.uid}:"),
755
- item.data)
756
- logging.info("finished specification item verification")
757
- return log_filter.get_verify_info()
758
-
759
- def verify(self, item: Item) -> VerifyStatus:
683
+ if self._root_verifier is None:
684
+ logging.error("root type item does not exist in item cache")
685
+ else:
686
+ logging.info("start specification item verification")
687
+ for key in sorted(item_cache):
688
+ item = item_cache[key]
689
+ self._root_verifier.verify(_Path(item, f"{item.uid}:"),
690
+ item.data)
691
+ logging.info("finished specification item verification")
692
+
693
+ def verify(self, item: Item) -> LoggingStatus:
760
694
  """Verify the item format.
761
695
 
762
696
  Args:
@@ -765,16 +699,16 @@ class SpecVerifier:
765
699
  Returns:
766
700
  Status with counts of messages emitted during verification.
767
701
  """
768
- with _add_filter() as log_filter:
702
+ with monitor_logging() as monitor:
769
703
  if self._root_verifier is None:
770
704
  logging.error("root type item does not exist in item cache")
771
705
  else:
772
706
  self._root_verifier.verify(_Path(item, f"{item.uid}:"),
773
707
  item.data)
774
- return log_filter.get_verify_info()
708
+ return monitor.get_status()
775
709
 
776
710
 
777
- def verify_specification_format(item_cache: ItemCache) -> VerifyStatus:
711
+ def verify_specification_format(item_cache: ItemCache) -> LoggingStatus:
778
712
  """Verify all items using the specification root type from the item cache.
779
713
 
780
714
  Emits an error if the item cache has no specification root type.
@@ -785,9 +719,11 @@ def verify_specification_format(item_cache: ItemCache) -> VerifyStatus:
785
719
  Returns:
786
720
  The status summarizing the verification run.
787
721
  """
788
- root_type_uid = item_cache.type_provider.root_type_uid
789
- if root_type_uid is None:
790
- logging.error("item cache has no root type")
791
- return VerifyStatus(0, 1, 0, 0, 0)
792
- verifier = SpecVerifier(item_cache, root_type_uid)
793
- return verifier.verify_all(item_cache)
722
+ with monitor_logging() as monitor:
723
+ root_type_uid = item_cache.type_provider.root_type_uid
724
+ if root_type_uid is None:
725
+ logging.error("item cache has no root type")
726
+ else:
727
+ verifier = SpecVerifier(item_cache, root_type_uid)
728
+ verifier.verify_all(item_cache)
729
+ return monitor.get_status()
File without changes