Coverage for pyEDAA / OSVVM / Project / TCL.py: 75%
162 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-13 22:31 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-13 22:31 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ___ ______ ____ ____ __ #
3# _ __ _ _| ____| _ \ / \ / \ / _ \/ ___\ \ / /\ \ / / \/ | #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | \___ \\ \ / / \ \ / /| |\/| | #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| |___) |\ V / \ V / | | | | #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/|____/ \_/ \_/ |_| |_| #
7# |_| |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2025-2026 Patrick Lehmann - Boetzingen, Germany #
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"""
32A TCL execution environment for OSVVM's ``*.pro`` files.
33"""
34from pathlib import Path
35from textwrap import dedent
36from tkinter import Tk, Tcl, TclError
37from typing import Any, Dict, Callable, Optional as Nullable
39from pyTooling.Decorators import export, readonly
40from pyTooling.MetaClasses import ExtendedType
41from pyVHDLModel import VHDLVersion
43from pyEDAA.OSVVM import OSVVMException
44from pyEDAA.OSVVM.Project import Context, osvvmContext, Build, Project
45from pyEDAA.OSVVM.Project.Procedures import noop, NoNullRangeWarning
46from pyEDAA.OSVVM.Project.Procedures import FileExists, DirectoryExists, FindOsvvmSettingsDirectory
47from pyEDAA.OSVVM.Project.Procedures import build, BuildName, include, library, analyze, simulate, generic
48from pyEDAA.OSVVM.Project.Procedures import TestSuite, TestName, RunTest
49from pyEDAA.OSVVM.Project.Procedures import ChangeWorkingDirectory, CreateOsvvmScriptSettingsPkg
50from pyEDAA.OSVVM.Project.Procedures import SetVHDLVersion, GetVHDLVersion
51from pyEDAA.OSVVM.Project.Procedures import SetCoverageAnalyzeEnable, SetCoverageSimulateEnable
52from pyEDAA.OSVVM.Project.Procedures import ConstraintFile, ScopeToRef, ScopeToCell
55@export
56class TclEnvironment(metaclass=ExtendedType, slots=True):
57 """
58 A TCL execution environment wrapping an embedded TCL interpreter based on :class:`tkinter.Tcl`.
59 """
60 _tcl: Tk #: The embedded TCL interpreter instance.
61 _procedures: Dict[str, Callable] #: A dictionary of registered TCL procedures implemented by Python functions.
62 _context: Context #: The TCL execution context.
64 def __init__(self, context: Context) -> None:
65 """
66 Initialize a TCL execution environment.
68 :param context: The TCL execution context.
69 """
70 self._context = context
71 context._processor = self
73 self._tcl = Tcl()
74 self._procedures = {}
76 @readonly
77 def TCL(self) -> Tk:
78 """
79 Read-only property to access the embedded TCL interpreter instance (:attr:`_tcl`).
81 :returns: TCL interpreter instance.
82 """
83 return self._tcl
85 @readonly
86 def Procedures(self) -> Dict[str, Callable]:
87 """
88 Read-only property to access the dictionary of registered TCL procedures implemented by Python functions (:attr:`_procedures`).
90 :returns: The dictionary of registered procedures.
91 """
92 return self._procedures
94 @readonly
95 def Context(self) -> Context:
96 """
97 Read-only property to access the TCL execution context (:attr:`_context`).
99 :returns: The TCL execution context.
100 """
101 return self._context
103 def RegisterPythonFunctionAsTclProcedure(self, pythonFunction: Callable, tclProcedureName: Nullable[str] = None) -> None:
104 """
105 Register a Python function as TCL procedure.
107 :param pythonFunction: The Python function to be registered.
108 :param tclProcedureName: Optional, name of the TCl procedure. |br|
109 Default: derived the TCL procedure name from Python function name.
110 """
111 if tclProcedureName is None:
112 tclProcedureName = pythonFunction.__name__
114 self._tcl.createcommand(tclProcedureName, pythonFunction)
115 self._procedures[tclProcedureName] = pythonFunction
117 def EvaluateTclCode(self, tclCode: str) -> None:
118 """
119 Evaluate TCL source code.
121 :param tclCode: TCL source code to evaluate.
122 :raises OSVVMException: When a :exc:`~tkinter.TclError` is caught while executing the TCL source code. |br|
123 In case the error is unspecific, :func:`~pyEDAA.OSVVM.Project.TCL.getException` is used to
124 look up and restore an exception, potentially coming from Python code called within TCL
125 code.
126 """
127 try:
128 self._tcl.eval(tclCode)
129 except TclError as e:
130 e = getException(e, self._context)
131 ex = OSVVMException(f"Caught TclError while evaluating TCL code.")
132 ex.add_note(tclCode)
133 raise ex from e
135 def EvaluateProFile(self, path: Path) -> None:
136 """
137 Evaluate TCL source file.
139 :param path: Path to a TCL source file for evaluation.
140 :raises OSVVMException: When a :exc:`~tkinter.TclError` is caught while executing the TCL source code. |br|
141 In case the error is unspecific, :func:`~pyEDAA.OSVVM.Project.TCL.getException` is used to
142 look up and restore an exception, potentially coming from Python code called within TCL
143 code.
144 """
145 try:
146 self._tcl.evalfile(str(path))
147 except TclError as e:
148 ex = getException(e, self._context)
149 raise OSVVMException(f"Caught TclError while processing '{path}'.") from ex
151 def __setitem__(self, tclVariableName: str, value: Any) -> None:
152 """
153 Set a TCL variable to a specific value.
155 :param tclVariableName: Name of the TCL variable.
156 :param value: Value to be set.
157 """
158 self._tcl.setvar(tclVariableName, value)
160 def __getitem__(self, tclVariableName: str) -> None:
161 """
162 Return a TCL variable's value.
164 :param tclVariableName: Name of the TCL variable.
165 :returns: TCL variable's value.
166 """
167 return self._tcl.getvar(tclVariableName)
169 def __delitem__(self, tclVariableName: str) -> None:
170 """
171 Unset a TCL variable.
173 :param tclVariableName: Name of the TCL variable.
174 """
175 self._tcl.unsetvar(tclVariableName)
178@export
179class OsvvmVariables(metaclass=ExtendedType, slots=True):
180 """
181 A class representing OSVVM's setting variables.
182 """
183 _vhdlVersion: VHDLVersion #: Default VHDL language revision.
184 _toolVendor: str #: Name of the tool vendor.
185 _toolName: str #: Name of the tool.
186 _toolVersion: str #: Version of the tool.
188 def __init__(
189 self,
190 vhdlVersion: Nullable[VHDLVersion] = None,
191 toolVendor: Nullable[str] = None,
192 toolName: Nullable[str] = None,
193 toolVersion: Nullable[str] = None
194 ) -> None:
195 """
196 Initialize OSVVM's setting variables.
198 :param vhdlVersion: Optional, default VHDL language revision.
199 :param toolVendor: Optional, name of the tool vendor.
200 :param toolName: Optional, name of the tool.
201 :param toolVersion: Optional, version of the tool.
203 .. note::
205 If not specified, the following values are used:
207 * VHDL version = :pycode:`VHDLVersion.VHDL2008`
208 * Tool vendor = :pycode:`"EDA²"`
209 * Tool name = :pycode:`"pyEDAA.ProjectModel"`
210 * Tool version = :pycode:`"0.1"`
211 """
212 self._vhdlVersion = vhdlVersion if vhdlVersion is not None else VHDLVersion.VHDL2008
213 self._toolVendor = toolVendor if toolVendor is not None else "EDA²"
214 self._toolName = toolName if toolName is not None else "pyEDAA.ProjectModel"
215 self._toolVersion = toolVersion if toolVersion is not None else "0.1"
217 @readonly
218 def VHDLVersion(self) -> VHDLVersion:
219 """
220 Read-only property to access the default VHDL language revision (:attr:`_vhdlVersion`).
222 :returns: The default VHDL language revision.
223 """
224 return self._vhdlVersion
226 @readonly
227 def ToolVendor(self) -> str:
228 """
229 Read-only property to access the tool vendor name (:attr:`_toolVendor`).
231 :returns: The tool vendor name.
232 """
233 return self._toolVendor
235 @readonly
236 def ToolName(self) -> str:
237 """
238 Read-only property to access the tool's' name (:attr:`_toolName`).
240 :returns: The tool's name.
241 """
242 return self._toolName
244 @readonly
245 def ToolVersion(self) -> str:
246 """
247 Read-only property to access the tool's version (:attr:`_toolVersion`).
249 :returns: The tool's version.
250 """
251 return self._toolVersion
254@export
255class OsvvmProFileProcessor(TclEnvironment):
256 """
257 An OSVVM-specific TCL execution environment for ``*.pro`` files.
258 """
260 def __init__(
261 self,
262 context: Nullable[Context] = None,
263 osvvmVariables: Nullable[OsvvmVariables] = None
264 ) -> None:
265 """
266 Initialize an OSVVM-specific TCL execution environment.
268 :param context: The TCL execution context.
269 :param osvvmVariables: OSVVM default settings.
271 .. rubric:: Initialization steps:
273 1. Initialize base-class.
274 2. Load OSVVM default value into ``::osvvm::`` namespace variables.
275 3. Overwrite predefined TCL procedures. |br|
276 Avoid harmful or disturbing actions caused by these procedures.
277 4. Register Python functions as TCL procedures.
278 """
279 if context is None: 279 ↛ 282line 279 didn't jump to line 282 because the condition on line 279 was always true
280 context = osvvmContext
282 super().__init__(context)
284 if osvvmVariables is None: 284 ↛ 287line 284 didn't jump to line 287 because the condition on line 284 was always true
285 osvvmVariables = OsvvmVariables()
287 self.LoadOsvvmDefaults(osvvmVariables)
288 self.OverwriteTclProcedures()
289 self.RegisterTclProcedures()
291 def LoadOsvvmDefaults(self, osvvmVariables: OsvvmVariables) -> None:
292 """
293 Create an OSVVM namespace and declare variables with default values.
295 :param osvvmVariables: OSVVM settings object.
297 .. code-block:: TCL
299 namespace eval ::osvvm {
300 variable VhdlVersion <Version>
301 variable ToolVendor "<ToolVendor>"
302 variable ToolName "<ToolName>"
303 variable ToolNameVersion "<ToolVersion>"
304 variable ToolSupportsDeferredConstants 1
305 variable ToolSupportsGenericPackages 1
306 variable FunctionalCoverageIntegratedInSimulator "default"
307 variable Support2019FilePath 1
309 variable ClockResetVersion 0
310 }
311 """
312 match osvvmVariables.VHDLVersion:
313 case VHDLVersion.VHDL2002: 313 ↛ 314line 313 didn't jump to line 314 because the pattern on line 313 never matched
314 version = "2002"
315 case VHDLVersion.VHDL2008: 315 ↛ 317line 315 didn't jump to line 317 because the pattern on line 315 always matched
316 version = "2008"
317 case VHDLVersion.VHDL2019:
318 version = "2019"
319 case _:
320 version = "unsupported"
322 code = dedent(f"""\
323 namespace eval ::osvvm {{
324 variable VhdlVersion {version}
325 variable ToolVendor "{osvvmVariables.ToolVendor}"
326 variable ToolName "{osvvmVariables.ToolName}"
327 variable ToolNameVersion "{osvvmVariables.ToolVersion}"
328 variable ToolSupportsDeferredConstants 1
329 variable ToolSupportsGenericPackages 1
330 variable FunctionalCoverageIntegratedInSimulator "default"
331 variable Support2019FilePath 1
333 variable ClockResetVersion 0
334 }}
335 """)
337 try:
338 self._tcl.eval(code)
339 except TclError as ex:
340 raise OSVVMException(f"TCL error occurred, when initializing OSVVM variables.") from ex
342 def OverwriteTclProcedures(self) -> None:
343 """
344 Overwrite predefined TCL procedures.
346 .. rubric:: List of overwritten procedures:
348 * `puts` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
349 """
350 self.RegisterPythonFunctionAsTclProcedure(noop, "puts")
352 def RegisterTclProcedures(self) -> None:
353 """
354 Register Python functions as TCL procedures.
356 .. rubric:: List of registered procedures:
358 * ``build`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.build`
359 * ``include`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.include`
360 * ``library`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.library`
361 * ``analyze`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.analyze`
362 * ``simulate`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.simulate`
363 * ``generic`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.generic`
364 * ``BuildName`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.BuildName`
365 * ``NoNullRangeWarning`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.NoNullRangeWarning`
366 * ``TestSuite`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.TestSuite`
367 * ``TestName`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.TestName`
368 * ``RunTest`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.RunTest`
369 * ``SetVHDLVersion`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.SetVHDLVersion`
370 * ``GetVHDLVersion`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.GetVHDLVersion`
371 * ``SetCoverageAnalyzeEnable`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.SetCoverageAnalyzeEnable`
372 * ``SetCoverageSimulateEnable`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.SetCoverageSimulateEnable`
373 * ``FileExists`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.FileExists`
374 * ``DirectoryExists`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.DirectoryExists`
375 * ``ChangeWorkingDirectory`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.ChangeWorkingDirectory`
376 * ``FindOsvvmSettingsDirectory`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.FindOsvvmSettingsDirectory`
377 * ``CreateOsvvmScriptSettingsPkg`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.CreateOsvvmScriptSettingsPkg`
378 * ``ConstraintFile`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.ConstraintFile`
379 * ``ScopeToRef`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.ScopeToRef`
380 * ``ScopeToCell`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.ScopeToCell`
381 * ``OpenBuildHtml`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
382 * ``SetTranscriptType`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
383 * ``GetTranscriptType`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
384 * ``SetSimulatorResolution`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
385 * ``GetSimulatorResolution`` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop`
386 """
387 self.RegisterPythonFunctionAsTclProcedure(build)
388 self.RegisterPythonFunctionAsTclProcedure(include)
389 self.RegisterPythonFunctionAsTclProcedure(library)
390 self.RegisterPythonFunctionAsTclProcedure(analyze)
391 self.RegisterPythonFunctionAsTclProcedure(simulate)
392 self.RegisterPythonFunctionAsTclProcedure(generic)
394 self.RegisterPythonFunctionAsTclProcedure(BuildName)
395 self.RegisterPythonFunctionAsTclProcedure(NoNullRangeWarning)
397 self.RegisterPythonFunctionAsTclProcedure(TestSuite)
398 self.RegisterPythonFunctionAsTclProcedure(TestName)
399 self.RegisterPythonFunctionAsTclProcedure(RunTest)
401 self.RegisterPythonFunctionAsTclProcedure(SetVHDLVersion)
402 self.RegisterPythonFunctionAsTclProcedure(GetVHDLVersion)
403 self.RegisterPythonFunctionAsTclProcedure(SetCoverageAnalyzeEnable)
404 self.RegisterPythonFunctionAsTclProcedure(SetCoverageSimulateEnable)
406 self.RegisterPythonFunctionAsTclProcedure(FileExists)
407 self.RegisterPythonFunctionAsTclProcedure(DirectoryExists)
408 self.RegisterPythonFunctionAsTclProcedure(ChangeWorkingDirectory)
410 self.RegisterPythonFunctionAsTclProcedure(FindOsvvmSettingsDirectory)
411 self.RegisterPythonFunctionAsTclProcedure(CreateOsvvmScriptSettingsPkg)
413 self.RegisterPythonFunctionAsTclProcedure(ConstraintFile)
414 self.RegisterPythonFunctionAsTclProcedure(ScopeToRef)
415 self.RegisterPythonFunctionAsTclProcedure(ScopeToCell)
417 self.RegisterPythonFunctionAsTclProcedure(noop, "OpenBuildHtml")
418 self.RegisterPythonFunctionAsTclProcedure(noop, "SetTranscriptType")
419 self.RegisterPythonFunctionAsTclProcedure(noop, "GetTranscriptType")
420 self.RegisterPythonFunctionAsTclProcedure(noop, "SetSimulatorResolution")
421 self.RegisterPythonFunctionAsTclProcedure(noop, "GetSimulatorResolution")
423 def LoadIncludeFile(self, path: Path) -> None:
424 """
425 Load an OSVVM ``*.pro`` file for inclusion (not as a root level build, see :meth:`LoadBuildFile`).
427 :param path: Path to the ``*.pro`` file.
429 .. seealso::
431 * :meth:`LoadBuildFile`
432 """
433 # TODO: should a context be used with _context to restore _currentDirectory?
434 includeFile = self._context.IncludeFile(path)
435 self.EvaluateProFile(includeFile)
437 def LoadBuildFile(self, buildFile: Path, buildName: Nullable[str] = None) -> Build:
438 """
439 Load an OSVVM ``*.pro`` file as build creating a new build context.
441 .. rubric:: inferring the build name:
443 1. From optional parameter ``buildName``.
444 2. From ``*.pro`` file's filename.
446 :param path: Path to the ``*.pro`` file.
447 :returns: The created build object.
449 .. seealso::
451 * :meth:`LoadIncludeFile`
452 """
453 if buildName is None:
454 buildName = buildFile.stem
456 self._context.BeginBuild(buildName)
457 includeFile = self._context.IncludeFile(buildFile)
458 self.EvaluateProFile(includeFile)
460 # TODO: should a context be used with _context to restore _currentDirectory?
461 return self._context.EndBuild()
463 def LoadRegressionFile(self, regressionFile: Path, projectName: Nullable[str] = None) -> Project:
464 """
465 Load a TCL file as a regression file and create a project from it.
467 .. rubric:: inferring the project name:
469 1. From optional parameter ``projectName``.
470 2. From ``*.pro`` file's filename.
472 :param regressionFile:
473 :param projectName:
474 :return:
475 """
476 if projectName is None:
477 projectName = regressionFile.stem
479 self.EvaluateProFile(regressionFile)
481 return self._context.ToProject(projectName)
484@export
485def getException(ex: Exception, context: Context) -> Exception:
486 """
487 Restore Python exceptions if known by the execution context.
489 :param ex: Original exception (usually a :exc:`~tkinter.TclError`).
490 :param context: The TCL execution context.
491 :returns: The original Python exception, if the context preserved an exception, otherwise the given TCLError.
493 .. note::
495 When executing Python code within TCL, where TCL again is run within Python, TCL doesn't forward Python exceptions
496 through the TCL layer back into Python. Therefore, last seen Python exceptions are caught in the Python-TCL
497 interfacing procedures and preserved in the TCL execution context.
499 This helper function restores these preserved exception objects.
500 """
501 if str(ex) == "": 501 ↛ 505line 501 didn't jump to line 505 because the condition on line 501 was always true
502 if (lastException := context.LastException) is not None: 502 ↛ 505line 502 didn't jump to line 505 because the condition on line 502 was always true
503 return lastException
505 return ex