pyEDAA.OutputFilter 0.7.1__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/PKG-INFO +23 -25
  2. pyedaa_outputfilter-0.9.0/pyEDAA/OutputFilter/CLI/Configuration.py +617 -0
  3. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/Xilinx/Exception.py → pyedaa_outputfilter-0.9.0/pyEDAA/OutputFilter/CLI/Filter.py +43 -33
  4. pyedaa_outputfilter-0.9.0/pyEDAA/OutputFilter/CLI/Vivado.py +544 -0
  5. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/CLI/__init__.py +9 -6
  6. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/Xilinx/OptimizeDesign.py +3 -3
  7. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/Xilinx/PhysicalOptimizeDesign.py +11 -3
  8. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/Xilinx/PlaceDesign.py +4 -4
  9. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/Xilinx/RouteDesign.py +24 -4
  10. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA/OutputFilter/Xilinx/SynthesizeDesign.py +92 -303
  11. pyedaa_outputfilter-0.9.0/pyEDAA/OutputFilter/Xilinx/__init__.py +4033 -0
  12. pyedaa_outputfilter-0.9.0/pyEDAA/OutputFilter/__init__.py +278 -0
  13. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/PKG-INFO +23 -25
  14. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/SOURCES.txt +2 -4
  15. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/requires.txt +22 -24
  16. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/CLI/Vivado.py +0 -352
  17. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/Xilinx/Commands.py +0 -939
  18. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/Xilinx/Common.py +0 -710
  19. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/Xilinx/Common2.py +0 -1675
  20. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/Xilinx/__init__.py +0 -387
  21. pyedaa_outputfilter-0.7.1/pyEDAA/OutputFilter/__init__.py +0 -52
  22. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/LICENSE.md +0 -0
  23. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/README.md +0 -0
  24. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/dependency_links.txt +0 -0
  25. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/entry_points.txt +0 -0
  26. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyEDAA.OutputFilter.egg-info/top_level.txt +0 -0
  27. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/pyproject.toml +0 -0
  28. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/setup.cfg +0 -0
  29. {pyedaa_outputfilter-0.7.1 → pyedaa_outputfilter-0.9.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyEDAA.OutputFilter
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Post-processing of EDA Tool outputs (log files).
5
5
  Home-page: https://GitHub.com/edaa-org/pyEDAA.OutputFilter
6
6
  Author: Patrick Lehmann
@@ -28,45 +28,43 @@ Description-Content-Type: text/markdown
28
28
  License-File: LICENSE.md
29
29
  Requires-Dist: pyTooling[terminal]~=8.14
30
30
  Provides-Extra: doc
31
- Requires-Dist: sphinxcontrib-mermaid~=2.0; extra == "doc"
31
+ Requires-Dist: docutils_stubs~=0.0.22; extra == "doc"
32
32
  Requires-Dist: sphinxcontrib-autoprogram~=0.1.0; extra == "doc"
33
- Requires-Dist: sphinx_rtd_theme~=3.1; extra == "doc"
34
33
  Requires-Dist: sphinx_autodoc_typehints~=3.10; extra == "doc"
35
- Requires-Dist: docutils~=0.22.0; extra == "doc"
36
34
  Requires-Dist: pyTooling[terminal]~=8.14; extra == "doc"
37
- Requires-Dist: autoapi>=2.0.1; extra == "doc"
35
+ Requires-Dist: sphinx_reports~=0.11.0; extra == "doc"
36
+ Requires-Dist: sphinx~=9.1; extra == "doc"
38
37
  Requires-Dist: sphinx_design~=0.7.0; extra == "doc"
38
+ Requires-Dist: sphinx_rtd_theme~=3.1; extra == "doc"
39
+ Requires-Dist: sphinxcontrib-mermaid~=2.0; extra == "doc"
40
+ Requires-Dist: autoapi>=2.0.1; extra == "doc"
39
41
  Requires-Dist: sphinx-copybutton>=0.5.2; extra == "doc"
40
- Requires-Dist: sphinx_reports~=0.10.0; extra == "doc"
41
- Requires-Dist: docutils_stubs~=0.0.22; extra == "doc"
42
- Requires-Dist: sphinx~=9.1; extra == "doc"
43
42
  Provides-Extra: test
44
- Requires-Dist: typing_extensions~=4.15; extra == "test"
45
- Requires-Dist: pytest~=9.0; extra == "test"
46
- Requires-Dist: pytest-cov~=7.1; extra == "test"
47
- Requires-Dist: mypy[reports]~=1.19; extra == "test"
48
43
  Requires-Dist: lxml<7.0,>=5.4; extra == "test"
49
44
  Requires-Dist: pyTooling[terminal]~=8.14; extra == "test"
50
- Requires-Dist: Coverage~=7.13; extra == "test"
45
+ Requires-Dist: pytest~=9.0; extra == "test"
46
+ Requires-Dist: mypy[reports]~=1.19; extra == "test"
47
+ Requires-Dist: pytest-cov~=7.1; extra == "test"
48
+ Requires-Dist: Coverage~=7.14; extra == "test"
49
+ Requires-Dist: typing_extensions~=4.15; extra == "test"
51
50
  Provides-Extra: all
52
- Requires-Dist: sphinxcontrib-mermaid~=2.0; extra == "all"
53
- Requires-Dist: typing_extensions~=4.15; extra == "all"
54
- Requires-Dist: pytest~=9.0; extra == "all"
55
- Requires-Dist: mypy[reports]~=1.19; extra == "all"
51
+ Requires-Dist: docutils_stubs~=0.0.22; extra == "all"
56
52
  Requires-Dist: sphinxcontrib-autoprogram~=0.1.0; extra == "all"
57
- Requires-Dist: sphinx_rtd_theme~=3.1; extra == "all"
53
+ Requires-Dist: lxml<7.0,>=5.4; extra == "all"
58
54
  Requires-Dist: sphinx_autodoc_typehints~=3.10; extra == "all"
59
- Requires-Dist: docutils_stubs~=0.0.22; extra == "all"
60
- Requires-Dist: docutils~=0.22.0; extra == "all"
55
+ Requires-Dist: pyTooling[terminal]~=8.14; extra == "all"
56
+ Requires-Dist: pytest~=9.0; extra == "all"
57
+ Requires-Dist: sphinx~=9.1; extra == "all"
58
+ Requires-Dist: mypy[reports]~=1.19; extra == "all"
61
59
  Requires-Dist: autoapi>=2.0.1; extra == "all"
62
60
  Requires-Dist: pytest-cov~=7.1; extra == "all"
63
61
  Requires-Dist: sphinx_design~=0.7.0; extra == "all"
62
+ Requires-Dist: Coverage~=7.14; extra == "all"
63
+ Requires-Dist: typing_extensions~=4.15; extra == "all"
64
+ Requires-Dist: sphinx_reports~=0.11.0; extra == "all"
65
+ Requires-Dist: sphinxcontrib-mermaid~=2.0; extra == "all"
66
+ Requires-Dist: sphinx_rtd_theme~=3.1; extra == "all"
64
67
  Requires-Dist: sphinx-copybutton>=0.5.2; extra == "all"
65
- Requires-Dist: sphinx_reports~=0.10.0; extra == "all"
66
- Requires-Dist: lxml<7.0,>=5.4; extra == "all"
67
- Requires-Dist: pyTooling[terminal]~=8.14; extra == "all"
68
- Requires-Dist: Coverage~=7.13; extra == "all"
69
- Requires-Dist: sphinx~=9.1; extra == "all"
70
68
  Dynamic: author
71
69
  Dynamic: author-email
72
70
  Dynamic: classifier
@@ -0,0 +1,617 @@
1
+ # ==================================================================================================================== #
2
+ # _____ ____ _ _ ___ _ _ _____ _ _ _ #
3
+ # _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ #
4
+ # | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| #
5
+ # | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | #
6
+ # | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| #
7
+ # |_| |___/ |_| #
8
+ # ==================================================================================================================== #
9
+ # Authors: #
10
+ # Patrick Lehmann #
11
+ # #
12
+ # License: #
13
+ # ==================================================================================================================== #
14
+ # Copyright 2025-2026 Electronic Design Automation Abstraction (EDA²) #
15
+ # #
16
+ # Licensed under the Apache License, Version 2.0 (the "License"); #
17
+ # you may not use this file except in compliance with the License. #
18
+ # You may obtain a copy of the License at #
19
+ # #
20
+ # http://www.apache.org/licenses/LICENSE-2.0 #
21
+ # #
22
+ # Unless required by applicable law or agreed to in writing, software #
23
+ # distributed under the License is distributed on an "AS IS" BASIS, #
24
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25
+ # See the License for the specific language governing permissions and #
26
+ # limitations under the License. #
27
+ # #
28
+ # SPDX-License-Identifier: Apache-2.0 #
29
+ # ==================================================================================================================== #
30
+ #
31
+ from enum import StrEnum
32
+ from pathlib import Path
33
+ from re import compile as re_compile
34
+ from typing import Optional as Nullable, Dict, List, ClassVar, Set, Self, Any, TextIO, Callable
35
+
36
+ from pyTooling.Decorators import export
37
+ from pyTooling.MetaClasses import ExtendedType, abstractmethod
38
+ from pyTooling.Stopwatch import Stopwatch
39
+ from pyTooling.TerminalUI import TerminalBaseApplication
40
+ from pyTooling.Warning import WarningCollector, Warning
41
+ from ruamel.yaml import YAML, CommentedMap, CommentedSeq
42
+
43
+ from pyEDAA.OutputFilter import OutputFilterException
44
+ from pyEDAA.OutputFilter.Xilinx import VivadoInfoMessage, VivadoWarningMessage, VivadoCriticalWarningMessage, VivadoErrorMessage, LineAction
45
+ from pyEDAA.OutputFilter.Xilinx import Synth_Design, Opt_Design, Link_Design, Place_Design, PhyOpt_Design, Route_Design, Write_Bitstream
46
+ from pyEDAA.OutputFilter.Xilinx import Command, LineKind, VivadoLine
47
+
48
+
49
+ @export
50
+ class ConfigurationException(OutputFilterException):
51
+ pass
52
+
53
+
54
+ @export
55
+ class ConfigurationWarning(Warning):
56
+ pass
57
+
58
+
59
+ @export
60
+ class OutputFormat(StrEnum):
61
+ Plain = "plain"
62
+ JSON = "json"
63
+ JSONLine = "jsonl"
64
+
65
+ @classmethod
66
+ def parse(cls, value: str) -> Self:
67
+ if value not in cls._value2member_map_:
68
+ ex = ValueError(f"'{value}' is not a valid OutputFormat.")
69
+ ex.add_note(f"Allowed values: {', '.join([item.value for item in cls])}")
70
+ raise ex
71
+
72
+ return cls(value)
73
+
74
+
75
+ @export
76
+ class TimestampFormat(StrEnum):
77
+ Undefined = "undefined"
78
+ DateTime = "datetime"
79
+ TimeOnly = "timeonly"
80
+ Runtime = "deltatime"
81
+
82
+ @classmethod
83
+ def parse(cls, value: str) -> Self:
84
+ if value not in cls._value2member_map_:
85
+ ex = ValueError(f"'{value}' is not a valid TimestampFormat.")
86
+ ex.add_note(f"Allowed values: {', '.join([item.value for item in cls])}")
87
+ raise ex
88
+
89
+ return cls(value)
90
+
91
+
92
+ @export
93
+ class Action(StrEnum):
94
+ Default = "default"
95
+ Remove = "remove"
96
+ Keep = "keep"
97
+ Error = "error"
98
+
99
+ @classmethod
100
+ def parse(cls, value: str) -> Self:
101
+ if value not in cls._value2member_map_:
102
+ ex = ValueError(f"'{value}' is not a valid Action.")
103
+ ex.add_note(f"Allowed values: {', '.join([item.value for item in cls])}")
104
+ raise ex
105
+
106
+ return cls(value)
107
+
108
+
109
+ @export
110
+ class Level(StrEnum):
111
+ Unknown = ""
112
+ Info = "info"
113
+ Warning = "warning"
114
+ CriticalWarning = "critical warning"
115
+ Error = "error"
116
+
117
+ @classmethod
118
+ def parse(cls, value: str) -> Self:
119
+ if value not in cls._value2member_map_:
120
+ ex = ValueError(f"'{value}' is not a valid Level.")
121
+ ex.add_note(f"Allowed values: {', '.join([item.value for item in cls])}")
122
+ raise ex
123
+
124
+ return cls(value)
125
+
126
+
127
+ @export
128
+ class Rule(metaclass=ExtendedType, slots=True):
129
+ _optional: bool
130
+ _action: Nullable[Action]
131
+ _level: Nullable[Level]
132
+
133
+ # todo: justification (whitelist/ blacklist / message)
134
+
135
+ def __init__(self, action: Nullable[Action] = None, level: Nullable[Level] = None) -> None:
136
+ self._action = action
137
+ self._level = level
138
+
139
+ @abstractmethod
140
+ def Match(self, line: VivadoLine) -> bool:
141
+ pass
142
+
143
+ def Process(self, line: VivadoLine) -> None:
144
+ if self._action is Action.Remove:
145
+ line._action = LineAction.Remove
146
+ elif self._action is Action.Default or self._action is Action.Keep:
147
+ line._action = LineAction.Default
148
+
149
+
150
+ @export
151
+ class AllRule(Rule):
152
+ def __init__(self, action: Nullable[Action] = None) -> None:
153
+ super().__init__(action)
154
+
155
+ def Match(self, line: VivadoLine) -> bool:
156
+ return True
157
+
158
+ def __str__(self) -> str:
159
+ return "Rule match all lines"
160
+
161
+
162
+ @export
163
+ class ClassificationRule(Rule):
164
+ _lineKind: LineKind
165
+
166
+ def __init__(self, lineKind: LineKind, action: Nullable[Action] = None, level: Nullable[Level] = None) -> None:
167
+ super().__init__(action, level)
168
+
169
+ self._lineKind = lineKind
170
+
171
+ def Match(self, line: VivadoLine) -> bool:
172
+ return self._lineKind == line._kind
173
+
174
+ def __str__(self) -> str:
175
+ if self._lineKind is LineKind.InfoMessage:
176
+ kind = "info"
177
+ elif self._lineKind is LineKind.Warning:
178
+ kind = "warning"
179
+ elif self._lineKind is LineKind.CriticalWarning:
180
+ kind = "critical warning"
181
+ elif self._lineKind is LineKind.Error:
182
+ kind = "error"
183
+
184
+ return f"Rule for all '{kind}' messages: Level={self._level}; Action={self._action}"
185
+
186
+
187
+ @export
188
+ class VivadoMessageRule(Rule):
189
+ _toolID: int
190
+ _messageKindID: int
191
+
192
+ def __init__(self, toolID: int, messageKindID: int, action: Nullable[Action] = None, level: Nullable[Level] = None) -> None:
193
+ super().__init__(action, level)
194
+
195
+ self._toolID = toolID
196
+ self._messageKindID = messageKindID
197
+
198
+ def Match(self, line: VivadoLine) -> bool:
199
+ if isinstance(line, (VivadoInfoMessage, VivadoWarningMessage, VivadoCriticalWarningMessage, VivadoErrorMessage)):
200
+ return line._toolID == self._toolID and line._messageKindID == self._messageKindID
201
+
202
+ return False
203
+
204
+ def __str__(self) -> str:
205
+ return f"Rule for '{self._toolID}-{self._messageKindID}': Level={self._level}; Action={self._action}"
206
+
207
+
208
+ @export
209
+ class Output(metaclass=ExtendedType, slots=True):
210
+ _parent: "ProcessingPipeline"
211
+ _file: TextIO
212
+ _format: OutputFormat
213
+ _commands: Nullable[List[Command]]
214
+ _rules: Nullable[List[Rule]]
215
+
216
+ def __init__(
217
+ self,
218
+ parent: "ProcessingPipeline",
219
+ format: OutputFormat,
220
+ commands: Nullable[List[Command]],
221
+ rules: Nullable[List[Rule]]
222
+ ) -> None:
223
+ self._parent = parent
224
+ self._format = format
225
+ self._commands = commands
226
+ self._rules = rules
227
+
228
+
229
+ @export
230
+ class StdOutOutput(Output):
231
+ _coloring: bool
232
+ _colors: Dict[str, str]
233
+ _lineNumbers: bool
234
+ _timestampFormat: TimestampFormat
235
+
236
+ def __init__(
237
+ self,
238
+ parent: "ProcessingPipeline",
239
+ coloring: bool,
240
+ format: OutputFormat,
241
+ commands: Nullable[List[Command]],
242
+ rules: Nullable[List[Rule]]
243
+ ) -> None:
244
+ super().__init__(parent, format, commands, rules)
245
+
246
+ self._coloring = coloring
247
+ self._colors = parent._parent._colors
248
+ self._lineNumbers = False
249
+ self._timestampFormat = TimestampFormat.Undefined
250
+
251
+
252
+ @export
253
+ class FileOutput(Output):
254
+ _path: Path
255
+
256
+ def __init__(
257
+ self,
258
+ parent: "ProcessingPipeline",
259
+ file: Path,
260
+ format: OutputFormat,
261
+ commands: List[Command],
262
+ rules: List[Rule]
263
+ ) -> None:
264
+ super().__init__(parent, format, commands, rules)
265
+
266
+ self._path = file
267
+
268
+
269
+ @export
270
+ class ProcessingPipeline(metaclass=ExtendedType, slots=True):
271
+ _ALLOWED_BOOL_VALUES: ClassVar[Dict[str, bool]] = {
272
+ "false": False,
273
+ "true": True,
274
+ "off": False,
275
+ "on": True,
276
+ "no": False,
277
+ "yes": True
278
+ }
279
+ _ALLOWED_COMMANDS: ClassVar[Dict[str, Command]] = {
280
+ "synth_design": Synth_Design,
281
+ "link_design": Link_Design,
282
+ "opt_design": Opt_Design,
283
+ "place_design": Place_Design,
284
+ "phyopt_design": PhyOpt_Design,
285
+ "route_design": Route_Design,
286
+ "write_bitstream": Write_Bitstream
287
+ }
288
+
289
+ _parent: "Vivado"
290
+ _preprocessing: Nullable[List[Rule]]
291
+ _outputs: Dict[str, Output]
292
+
293
+ def __init__(self, parent: "Vivado") -> None:
294
+ self._parent = parent
295
+ self._preprocessing = None
296
+ self._outputs = {
297
+ "stdout": StdOutOutput(self, True, OutputFormat.Plain, None, [])
298
+ }
299
+
300
+ def Parse(self, outputsConfig: CommentedMap) -> None:
301
+ if "preprocessing" in outputsConfig and (preprocessing := outputsConfig["preprocessing"]) is not None:
302
+ try:
303
+ self._preprocessing = list(self._parent._ruleSets[preprocessing].values())
304
+ except KeyError:
305
+ warn = ConfigurationWarning(f"tools.vivado.outputs.preprocessing: Unknown rule set '{preprocessing}'.")
306
+ warn.add_note(f"Rule set '{preprocessing}' not found in tools.vivado.rule-sets.")
307
+ WarningCollector.Raise(warn)
308
+
309
+ for outputName, outputConfig in outputsConfig.items():
310
+ if outputName == "preprocessing":
311
+ continue
312
+ elif outputName == "stdout":
313
+ self._ParseStdOutOutput(outputConfig)
314
+ elif outputName == "stderr":
315
+ pass
316
+ # self._ParseStdErrOutput(outputConfig)
317
+ else:
318
+ self._ParseOutput(outputName, outputConfig)
319
+
320
+ def _ParseStdOutOutput(self, outputConfig: CommentedMap) -> None:
321
+ stdout: StdOutOutput = self._outputs["stdout"]
322
+
323
+ stdout._coloring = self._ParseBoolean(outputConfig, "coloring", False, "tools.vivado.outputs.stdout.coloring")
324
+ stdout._lineNumbers = self._ParseBoolean(outputConfig, "lineNumbers", False, "tools.vivado.outputs.stdout.lineNumbers")
325
+ stdout._timestampFormat = self._ParseTimestampFormat(outputConfig, "timestamps", "tools.vivado.outputs.stdout.timestamps")
326
+ stdout._commands = self._ParseCommands(outputConfig, "commands", "tools.vivado.outputs.stdout.commands")
327
+ stdout._rules = self._ParseRuleSets(outputConfig, "rule-sets", "tools.vivado.outputs.stdout.rule-sets")
328
+
329
+ def _ParseOutput(self, outputName: str, outputConfig: CommentedMap) -> None:
330
+ try:
331
+ path = self._ParsePath(outputConfig, "path", f"tools.vivado.outputs.{outputName}.path")
332
+ except ConfigurationException as ex:
333
+ WarningCollector.Raise(ConfigurationWarning(str(ex)))
334
+ return
335
+
336
+ self._outputs[outputName] = FileOutput(
337
+ self,
338
+ path,
339
+ self._ParseOutputFormat(outputConfig, "format", f"tools.vivado.outputs.{outputName}.format"),
340
+ self._ParseCommands(outputConfig,"commands", f"tools.vivado.outputs.{outputName}.commands"),
341
+ self._ParseRuleSets(outputConfig,"rule-sets", f"tools.vivado.outputs.{outputName}.rule-sets")
342
+ )
343
+
344
+ def _ParseBoolean(self, config: CommentedMap, key: str, default: Nullable[bool], configPath: str) -> Nullable[bool]:
345
+ if key not in config:
346
+ return default
347
+ elif isinstance((value := config[key]), bool):
348
+ return value
349
+
350
+ try:
351
+ return self._ALLOWED_BOOL_VALUES[value]
352
+ except KeyError:
353
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown value '{value}'."))
354
+
355
+ def _ParsePath(self, config: CommentedMap, key: str, configPath: str) -> Path:
356
+ if key not in config:
357
+ raise ConfigurationException(f"{configPath}: Doesn't exist. A filename is required.")
358
+ elif (pathConfig := config[key]) is None:
359
+ raise ConfigurationException(f"{configPath}: Is empty. A filename is required.")
360
+ elif not isinstance(pathConfig, str):
361
+ raise ConfigurationException(f"{configPath}: Unknown value '{pathConfig}'.")
362
+
363
+ try:
364
+ return Path(pathConfig)
365
+ except ValueError as ex:
366
+ raise ConfigurationException(f"{configPath}: Value '{pathConfig}' is not a path.") from ex
367
+
368
+ def _ParseOutputFormat(self, config: CommentedMap, key: str, configPath: str) -> OutputFormat:
369
+ if key not in config:
370
+ return OutputFormat.Plain
371
+ elif (formatConfig := config[key]) is None:
372
+ return OutputFormat.Plain
373
+ elif not isinstance(formatConfig, str):
374
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown settings."))
375
+ return OutputFormat.Plain
376
+
377
+ return OutputFormat.parse(formatConfig)
378
+
379
+ def _ParseTimestampFormat(self, config: CommentedMap, key: str, configPath: str) -> TimestampFormat:
380
+ if key not in config:
381
+ return TimestampFormat.Undefined
382
+ elif (formatConfig := config[key]) is None:
383
+ return TimestampFormat.Undefined
384
+ elif not isinstance(formatConfig, str):
385
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown settings."))
386
+ return TimestampFormat.Undefined
387
+
388
+ return TimestampFormat.parse(formatConfig)
389
+
390
+ def _ParseCommands(self, config: CommentedMap, key: str, configPath: str) -> Nullable[List[Command]]:
391
+ if key not in config:
392
+ return None
393
+ elif (commandsConfig := config[key]) is None:
394
+ return None
395
+ elif not isinstance(commandsConfig, CommentedMap):
396
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown settings."))
397
+ return None
398
+
399
+ commands: List[Command] = []
400
+ for commandName in commandsConfig:
401
+ try:
402
+ commands.append(self._ALLOWED_COMMANDS[commandName])
403
+ except KeyError:
404
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown Vivado TCL command '{commandName}'."))
405
+
406
+ if len(commands) > 0:
407
+ return commands
408
+
409
+ return None
410
+
411
+ def _ParseRuleSets(self, config: CommentedMap, key: str, configPath: str) -> Nullable[List[Rule]]:
412
+ if key not in config:
413
+ return None
414
+ elif (ruleSetsConfig := config[key]) is None:
415
+ return None
416
+ elif not isinstance(ruleSetsConfig, CommentedSeq):
417
+ WarningCollector.Raise(ConfigurationWarning(f"{configPath}: Unknown settings."))
418
+ return None
419
+
420
+ rules: List[Rule] = []
421
+ for ruleSetName in ruleSetsConfig:
422
+ try:
423
+ rules.extend(self._parent._ruleSets[ruleSetName].values())
424
+ except KeyError:
425
+ warn = ConfigurationWarning(f"{configPath}: Unknown rule set '{ruleSetName}'.")
426
+ warn.add_note(f"Rule set '{ruleSetName}' not found in tools.vivado.rule-sets.")
427
+ WarningCollector.Raise(warn)
428
+
429
+ if len(rules) > 0:
430
+ return rules
431
+
432
+ return None
433
+
434
+ def __len__(self) -> int:
435
+ return len(self._outputs)
436
+
437
+
438
+ @export
439
+ class Tool(metaclass=ExtendedType, slots=True):
440
+ pass
441
+
442
+
443
+ @export
444
+ class Vivado(Tool):
445
+ _ALLOWED_COLORS: ClassVar[Set[str]] = set(TerminalBaseApplication.Foreground.keys())
446
+
447
+ _parent: "Configuration"
448
+ _colors: Dict[str, str]
449
+ _ruleSets: Dict[str, Dict[str, Rule]]
450
+ _processingPipeline: ProcessingPipeline
451
+ _hasLatches: Action
452
+
453
+ def __init__(self, parent: "Configuration") -> None:
454
+ self._parent = parent
455
+
456
+ self._colors = self._InitializeColors()
457
+ self._ruleSets = {}
458
+ self._processingPipeline = ProcessingPipeline(self)
459
+ self._hasLatches = Action.Default
460
+
461
+ def _InitializeColors(self) -> Dict[str, str]:
462
+ return {
463
+ "normal": "WHITE",
464
+ "info": "GRAY", # "DARK_BLUE",
465
+ "warning": "YELLOW",
466
+ "critical": "MAGENTA",
467
+ "error": "RED",
468
+ "tcl": "CYAN",
469
+ "success": "GREEN",
470
+ "failed": "RED",
471
+ "verbose": "GRAY",
472
+ "unprocessed": "DARK_GRAY",
473
+ "empty": "NOCOLOR",
474
+ "sectionDelimiter": "DARK_GRAY",
475
+ "sectionStart": "DARK_CYAN",
476
+ "sectionEnd": "DARK_CYAN",
477
+ "sectionTime": "DARK_GREEN",
478
+ "subsectionStart": "DARK_CYAN",
479
+ "subsectionEnd": "DARK_CYAN",
480
+ "subsectionTime": "DARK_GREEN",
481
+ "taskStart": "YELLOW",
482
+ "taskEnd": "YELLOW",
483
+ "taskTime": "DARK_GREEN",
484
+ "phaseStart": "BLUE",
485
+ "phaseEnd": "BLUE",
486
+ "phaseTime": "DARK_GREEN",
487
+ "subphaseStart": "DARK_CYAN",
488
+ "subphaseEnd": "DARK_CYAN",
489
+ "subphaseTime": "DARK_GREEN",
490
+ "subsubphaseStart": "DARK_CYAN",
491
+ "subsubphaseEnd": "DARK_CYAN",
492
+ "subsubphaseTime": "DARK_GREEN",
493
+ "subsubsubphaseStart": "DARK_CYAN",
494
+ "subsubsubphaseEnd": "DARK_CYAN",
495
+ "subsubsubphaseTime": "DARK_GREEN",
496
+ "nestedTaskStart": "DARK_CYAN",
497
+ "nestedTaskEnd": "DARK_CYAN",
498
+ "nestedTaskTime": "DARK_GREEN",
499
+ "nestedPhaseStart": "DARK_CYAN",
500
+ "nestedPhaseEnd": "DARK_CYAN",
501
+ "nestedPhaseTime": "DARK_GREEN",
502
+ "paragraphHeadline": "DARK_YELLOW",
503
+ "hierarchyStart": "DARK_CYAN",
504
+ "hierarchyEnd": "DARK_GRAY",
505
+ "xdcStart": "DARK_CYAN",
506
+ "xdcEnd": "DARK_GRAY",
507
+ "table": "GRAY",
508
+ }
509
+
510
+ def Parse(self, toolConfig: CommentedMap) -> None:
511
+ if "colors" in toolConfig:
512
+ self._ParseColors(toolConfig["colors"])
513
+
514
+ if "rule-sets" in toolConfig:
515
+ for ruleSetName, ruleSetConfig in toolConfig["rule-sets"].items():
516
+ if len(ruleSetConfig) > 0:
517
+ self._ParseRuleSet(ruleSetName, ruleSetConfig)
518
+
519
+ if "outputs" in toolConfig:
520
+ self._processingPipeline.Parse(toolConfig["outputs"])
521
+
522
+ if "exports" in toolConfig:
523
+ if "cellUsage" in (exportConfig := toolConfig["exports"]):
524
+ pass
525
+ # self._ParseCellUsage(exportConfig["cellUsage"])
526
+
527
+ if "policies" in toolConfig:
528
+ self._ParsePolicies(toolConfig["policies"])
529
+
530
+ def _ParseColors(self, colors: CommentedMap) -> None:
531
+ for lineKind, color in colors.items():
532
+ if lineKind not in self._colors:
533
+ WarningCollector.Raise(ConfigurationWarning(f"tools.vivado.colors: Item '{lineKind}' not supported for coloring."))
534
+ continue
535
+ elif (col := color.upper()) not in self._ALLOWED_COLORS:
536
+ WarningCollector.Raise(ConfigurationWarning(f"tools.vivado.colors.{lineKind}: Color '{color}' is not supported."))
537
+ continue
538
+
539
+ self._colors[lineKind] = col
540
+
541
+ def _ParseRuleSet(self, ruleSetName: str, ruleSetConfig: CommentedMap) -> None:
542
+ self._ruleSets[ruleSetName] = (ruleSet := {})
543
+
544
+ pattern = re_compile(r"^(?P<toolID>\d+)-(?P<messageID>\d+)$")
545
+
546
+ for ruleName, ruleConfig in ruleSetConfig.items():
547
+ if ruleName == "all":
548
+ ruleSet[ruleName] = AllRule(self._ParseRuleSetAction(ruleConfig["action"]))
549
+ elif ruleName == "info":
550
+ ruleSet[ruleName] = ClassificationRule(LineKind.InfoMessage, self._ParseRuleSetAction(ruleConfig["action"]))
551
+ elif ruleName == "warning":
552
+ ruleSet[ruleName] = ClassificationRule(LineKind.WarningMessage, self._ParseRuleSetAction(ruleConfig["action"]))
553
+ elif ruleName == "criticalWarning":
554
+ ruleSet[ruleName] = ClassificationRule(LineKind.CriticalWarningMessage, self._ParseRuleSetAction(ruleConfig["action"]))
555
+ elif ruleName == "error":
556
+ ruleSet[ruleName] = ClassificationRule(LineKind.ErrorMessage, self._ParseRuleSetAction(ruleConfig["action"]))
557
+ elif (match := pattern.match(ruleName)) is not None:
558
+ toolID = int(match.group("toolID"))
559
+ messageID = int(match.group("messageID"))
560
+ ruleSet[ruleName] = VivadoMessageRule(toolID, messageID, self._ParseRuleSetAction(ruleConfig["action"]))
561
+ else:
562
+ WarningCollector.Raise(ConfigurationWarning(f"tools.vivado.rule-sets.{ruleSetName}: Unknown rule '{ruleName}'."))
563
+
564
+ def _ParsePolicies(self, policies: CommentedMap) -> None:
565
+ if "hasLatches" in policies:
566
+
567
+ if policies["hasLatches"] == "error":
568
+ self._hasLatches = Action.Error
569
+ else:
570
+ WarningCollector.Raise(
571
+ ConfigurationWarning(f"tools.vivado.policies.hasLatches: Unknown value '{policies["hasLatches"]}'."))
572
+
573
+ def _ParseRuleSetAction(self, actionConfig: Any) -> LineAction:
574
+ if not isinstance(actionConfig, str):
575
+ WarningCollector.Raise(ConfigurationWarning(f"tools.vivado.rule-sets.<ruleset>.<rule>.action: Unsupported value '{actionConfig}'."))
576
+ try:
577
+ return Action(actionConfig)
578
+ except ValueError:
579
+ WarningCollector.Raise(ConfigurationWarning(f"tools.vivado.rule-sets.<ruleset>.<rule>.action: Unknown value '{actionConfig}'."))
580
+
581
+
582
+ @export
583
+ class Configuration(metaclass=ExtendedType, slots=True):
584
+ _file: Path
585
+ _yamlDocument: YAML
586
+ _yamlLoadTime: float
587
+
588
+ _tools: Dict[str, Tool]
589
+
590
+ def __init__(self, file: Path) -> None:
591
+ self._file = file
592
+ self._tools = {}
593
+
594
+ with Stopwatch() as sw:
595
+ try:
596
+ yamlReader = YAML()
597
+ self._yamlDocument = yamlReader.load(self._file)
598
+ except Exception as ex:
599
+ raise ConfigurationException(f"Couldn't open '{self._file}'.") from ex
600
+
601
+ self._yamlLoadTime = sw.Duration
602
+
603
+ self.Parse()
604
+
605
+ def Parse(self) ->None:
606
+ if (version := self._yamlDocument["version"]) != "0.1":
607
+ ex = ConfigurationException(f"Configuration file version {version} is not supported.")
608
+ ex.add_note("Supported versions: 0.1")
609
+ raise ex
610
+
611
+ self._Parse_v0_1()
612
+
613
+ def _Parse_v0_1(self) -> None:
614
+ for toolName, toolConfig in self._yamlDocument["tools"].items():
615
+ if toolName == "vivado":
616
+ self._tools["vivado"] = (vivado := Vivado(self))
617
+ vivado.Parse(toolConfig)