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

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 

38 

39from pyTooling.Decorators import export, readonly 

40from pyTooling.MetaClasses import ExtendedType 

41from pyVHDLModel import VHDLVersion 

42 

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 

53 

54 

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. 

63 

64 def __init__(self, context: Context) -> None: 

65 """ 

66 Initialize a TCL execution environment. 

67 

68 :param context: The TCL execution context. 

69 """ 

70 self._context = context 

71 context._processor = self 

72 

73 self._tcl = Tcl() 

74 self._procedures = {} 

75 

76 @readonly 

77 def TCL(self) -> Tk: 

78 """ 

79 Read-only property to access the embedded TCL interpreter instance (:attr:`_tcl`). 

80 

81 :returns: TCL interpreter instance. 

82 """ 

83 return self._tcl 

84 

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`). 

89 

90 :returns: The dictionary of registered procedures. 

91 """ 

92 return self._procedures 

93 

94 @readonly 

95 def Context(self) -> Context: 

96 """ 

97 Read-only property to access the TCL execution context (:attr:`_context`). 

98 

99 :returns: The TCL execution context. 

100 """ 

101 return self._context 

102 

103 def RegisterPythonFunctionAsTclProcedure(self, pythonFunction: Callable, tclProcedureName: Nullable[str] = None) -> None: 

104 """ 

105 Register a Python function as TCL procedure. 

106 

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__ 

113 

114 self._tcl.createcommand(tclProcedureName, pythonFunction) 

115 self._procedures[tclProcedureName] = pythonFunction 

116 

117 def EvaluateTclCode(self, tclCode: str) -> None: 

118 """ 

119 Evaluate TCL source code. 

120 

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 

134 

135 def EvaluateProFile(self, path: Path) -> None: 

136 """ 

137 Evaluate TCL source file. 

138 

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 

150 

151 def __setitem__(self, tclVariableName: str, value: Any) -> None: 

152 """ 

153 Set a TCL variable to a specific value. 

154 

155 :param tclVariableName: Name of the TCL variable. 

156 :param value: Value to be set. 

157 """ 

158 self._tcl.setvar(tclVariableName, value) 

159 

160 def __getitem__(self, tclVariableName: str) -> None: 

161 """ 

162 Return a TCL variable's value. 

163 

164 :param tclVariableName: Name of the TCL variable. 

165 :returns: TCL variable's value. 

166 """ 

167 return self._tcl.getvar(tclVariableName) 

168 

169 def __delitem__(self, tclVariableName: str) -> None: 

170 """ 

171 Unset a TCL variable. 

172 

173 :param tclVariableName: Name of the TCL variable. 

174 """ 

175 self._tcl.unsetvar(tclVariableName) 

176 

177 

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. 

187 

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. 

197 

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. 

202 

203 .. note:: 

204 

205 If not specified, the following values are used: 

206 

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" 

216 

217 @readonly 

218 def VHDLVersion(self) -> VHDLVersion: 

219 """ 

220 Read-only property to access the default VHDL language revision (:attr:`_vhdlVersion`). 

221 

222 :returns: The default VHDL language revision. 

223 """ 

224 return self._vhdlVersion 

225 

226 @readonly 

227 def ToolVendor(self) -> str: 

228 """ 

229 Read-only property to access the tool vendor name (:attr:`_toolVendor`). 

230 

231 :returns: The tool vendor name. 

232 """ 

233 return self._toolVendor 

234 

235 @readonly 

236 def ToolName(self) -> str: 

237 """ 

238 Read-only property to access the tool's' name (:attr:`_toolName`). 

239 

240 :returns: The tool's name. 

241 """ 

242 return self._toolName 

243 

244 @readonly 

245 def ToolVersion(self) -> str: 

246 """ 

247 Read-only property to access the tool's version (:attr:`_toolVersion`). 

248 

249 :returns: The tool's version. 

250 """ 

251 return self._toolVersion 

252 

253 

254@export 

255class OsvvmProFileProcessor(TclEnvironment): 

256 """ 

257 An OSVVM-specific TCL execution environment for ``*.pro`` files. 

258 """ 

259 

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. 

267 

268 :param context: The TCL execution context. 

269 :param osvvmVariables: OSVVM default settings. 

270 

271 .. rubric:: Initialization steps: 

272 

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 

281 

282 super().__init__(context) 

283 

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() 

286 

287 self.LoadOsvvmDefaults(osvvmVariables) 

288 self.OverwriteTclProcedures() 

289 self.RegisterTclProcedures() 

290 

291 def LoadOsvvmDefaults(self, osvvmVariables: OsvvmVariables) -> None: 

292 """ 

293 Create an OSVVM namespace and declare variables with default values. 

294 

295 :param osvvmVariables: OSVVM settings object. 

296 

297 .. code-block:: TCL 

298 

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 

308 

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" 

321 

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 

332 

333 variable ClockResetVersion 0 

334 }} 

335 """) 

336 

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 

341 

342 def OverwriteTclProcedures(self) -> None: 

343 """ 

344 Overwrite predefined TCL procedures. 

345 

346 .. rubric:: List of overwritten procedures: 

347 

348 * `puts` |rarr| :func:`~pyEDAA.OSVVM.Project.Procedures.noop` 

349 """ 

350 self.RegisterPythonFunctionAsTclProcedure(noop, "puts") 

351 

352 def RegisterTclProcedures(self) -> None: 

353 """ 

354 Register Python functions as TCL procedures. 

355 

356 .. rubric:: List of registered procedures: 

357 

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) 

393 

394 self.RegisterPythonFunctionAsTclProcedure(BuildName) 

395 self.RegisterPythonFunctionAsTclProcedure(NoNullRangeWarning) 

396 

397 self.RegisterPythonFunctionAsTclProcedure(TestSuite) 

398 self.RegisterPythonFunctionAsTclProcedure(TestName) 

399 self.RegisterPythonFunctionAsTclProcedure(RunTest) 

400 

401 self.RegisterPythonFunctionAsTclProcedure(SetVHDLVersion) 

402 self.RegisterPythonFunctionAsTclProcedure(GetVHDLVersion) 

403 self.RegisterPythonFunctionAsTclProcedure(SetCoverageAnalyzeEnable) 

404 self.RegisterPythonFunctionAsTclProcedure(SetCoverageSimulateEnable) 

405 

406 self.RegisterPythonFunctionAsTclProcedure(FileExists) 

407 self.RegisterPythonFunctionAsTclProcedure(DirectoryExists) 

408 self.RegisterPythonFunctionAsTclProcedure(ChangeWorkingDirectory) 

409 

410 self.RegisterPythonFunctionAsTclProcedure(FindOsvvmSettingsDirectory) 

411 self.RegisterPythonFunctionAsTclProcedure(CreateOsvvmScriptSettingsPkg) 

412 

413 self.RegisterPythonFunctionAsTclProcedure(ConstraintFile) 

414 self.RegisterPythonFunctionAsTclProcedure(ScopeToRef) 

415 self.RegisterPythonFunctionAsTclProcedure(ScopeToCell) 

416 

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") 

422 

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`). 

426 

427 :param path: Path to the ``*.pro`` file. 

428 

429 .. seealso:: 

430 

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) 

436 

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. 

440 

441 .. rubric:: inferring the build name: 

442 

443 1. From optional parameter ``buildName``. 

444 2. From ``*.pro`` file's filename. 

445 

446 :param path: Path to the ``*.pro`` file. 

447 :returns: The created build object. 

448 

449 .. seealso:: 

450 

451 * :meth:`LoadIncludeFile` 

452 """ 

453 if buildName is None: 

454 buildName = buildFile.stem 

455 

456 self._context.BeginBuild(buildName) 

457 includeFile = self._context.IncludeFile(buildFile) 

458 self.EvaluateProFile(includeFile) 

459 

460 # TODO: should a context be used with _context to restore _currentDirectory? 

461 return self._context.EndBuild() 

462 

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. 

466 

467 .. rubric:: inferring the project name: 

468 

469 1. From optional parameter ``projectName``. 

470 2. From ``*.pro`` file's filename. 

471 

472 :param regressionFile: 

473 :param projectName: 

474 :return: 

475 """ 

476 if projectName is None: 

477 projectName = regressionFile.stem 

478 

479 self.EvaluateProFile(regressionFile) 

480 

481 return self._context.ToProject(projectName) 

482 

483 

484@export 

485def getException(ex: Exception, context: Context) -> Exception: 

486 """ 

487 Restore Python exceptions if known by the execution context. 

488 

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. 

492 

493 .. note:: 

494 

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. 

498 

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 

504 

505 return ex