Coverage for pyVHDLModel/Namespace.py: 95%

190 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 10:05 +0000

1# ==================================================================================================================== # 

2# __ ___ _ ____ _ __ __ _ _ # 

3# _ __ _ \ \ / / | | | _ \| | | \/ | ___ __| | ___| | # 

4# | '_ \| | | \ \ / /| |_| | | | | | | |\/| |/ _ \ / _` |/ _ \ | # 

5# | |_) | |_| |\ V / | _ | |_| | |___| | | | (_) | (_| | __/ | # 

6# | .__/ \__, | \_/ |_| |_|____/|_____|_| |_|\___/ \__,_|\___|_| # 

7# |_| |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2026 Patrick Lehmann - Boetzingen, Germany # 

15# Copyright 2016-2017 Patrick Lehmann - Dresden, Germany # 

16# # 

17# Licensed under the Apache License, Version 2.0 (the "License"); # 

18# you may not use this file except in compliance with the License. # 

19# You may obtain a copy of the License at # 

20# # 

21# http://www.apache.org/licenses/LICENSE-2.0 # 

22# # 

23# Unless required by applicable law or agreed to in writing, software # 

24# distributed under the License is distributed on an "AS IS" BASIS, # 

25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

26# See the License for the specific language governing permissions and # 

27# limitations under the License. # 

28# # 

29# SPDX-License-Identifier: Apache-2.0 # 

30# ==================================================================================================================== # 

31# 

32""" 

33This module contains parts of an abstract document language model for VHDL. 

34 

35A helper class to implement namespaces and scopes. 

36""" 

37from __future__ import annotations 

38 

39from typing import TYPE_CHECKING, TypeVar, Generic, Optional as Nullable 

40 

41from pyTooling.Common import getFullyQualifiedName 

42from pyTooling.Decorators import readonly 

43from pyTooling.Warning import WarningCollector 

44 

45from pyVHDLModel.Base import identifiersOf, normalizedIdentifiersOf 

46from pyVHDLModel.Object import Obj, Signal, Constant, Variable 

47from pyVHDLModel.Symbol import ComponentInstantiationSymbol, Symbol, PossibleReference 

48from pyVHDLModel.Exception import DuplicateDeclarationWarning, NotInNamespaceError 

49if TYPE_CHECKING: 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true

50 from pyVHDLModel.Type import BaseType 

51 

52K = TypeVar("K") 

53O = TypeVar("O") 

54 

55 

56class ExtendedKeyError(KeyError): 

57 """ 

58 A :exc:`KeyError` reporting which namespaces were searched. 

59 

60 Raised when a name cannot be resolved. Besides the key (:data:`key`), it carries every namespace 

61 visited while walking outwards (:data:`searchedNamespaces`). 

62 """ 

63 key: str #: The key that was not found. 

64 searchedNamespaces: tuple[Namespace, ...] #: The namespaces that were searched for the key. 

65 

66 def __init__(self, key: str, searchedNamespaces: tuple[Namespace, ...], message: str) -> None: 

67 """ 

68 Initializes an extended key error. 

69 

70 :param key: The key that was not found. 

71 :param searchedNamespaces: The namespaces that were searched for the key. 

72 :param message: The error message. 

73 """ 

74 super().__init__(message) 

75 

76 self.key = key 

77 self.searchedNamespaces = searchedNamespaces 

78 

79 

80class Namespace(Generic[K, O]): 

81 """ 

82 Represents a namespace: the declared items visible in one declarative region. 

83 

84 Namespaces nest, so a lookup that misses locally continues in the parent namespace 

85 (:data:`ParentNamespace`). That is what makes an entity's ports visible inside its architecture, 

86 and lets a process variable hide an outer signal. 

87 

88 .. seealso:: 

89 

90 * :class:`Concurrent declaration region <pyVHDLModel.Regions.ConcurrentDeclarationRegionMixin>` 

91 * :class:`Sequential declaration region <pyVHDLModel.Regions.SequentialDeclarationRegionMixin>` 

92 """ 

93 _name: str #: The namespace's name. 

94 _parentNamespace: Nullable[Namespace] #: Reference to the enclosing namespace, ``None`` if outermost. 

95 _subNamespaces: list[Namespace] #: List of all nested namespaces, in the order they were linked. 

96 _elements: dict[K, O] #: All elements declared in this namespace, indexed by name. 

97 _imports: dict[K, O] #: All elements imported into this namespace, indexed by name. 

98 _sharesRegionWithParent: bool #: ``True`` if the parent namespace is the same declarative region. 

99 

100 def __init__( 

101 self, 

102 name: str, 

103 parentNamespace: Nullable[Namespace] = None, 

104 sharesRegionWithParent: bool = False 

105 ) -> None: 

106 """ 

107 Initializes a namespace. 

108 

109 :param name: The namespace's name. 

110 :param parentNamespace: Optional, reference to the enclosing namespace, or ``None`` for the outermost one. 

111 Default: ``None``. |br| 

112 This namespace is appended to the parent namespace's sub-namespaces. 

113 :param sharesRegionWithParent: Optional, ``True`` if the parent namespace is the same declarative region. 

114 Default: ``False``. 

115 :raises TypeError: If parameter 'parentNamespace' is not of type :class:`Namespace`. 

116 """ 

117 self._name = name 

118 self._parentNamespace = None 

119 self._subNamespaces = [] 

120 self._elements = {} 

121 self._imports = {} 

122 self._sharesRegionWithParent = sharesRegionWithParent 

123 

124 if parentNamespace is not None: 

125 self.ParentNamespace = parentNamespace 

126 

127 @readonly 

128 def Name(self) -> str: 

129 """ 

130 Read-only property to access the name (:attr:`_name`). 

131 

132 :returns: The name. 

133 """ 

134 return self._name 

135 

136 @property 

137 def ParentNamespace(self) -> Nullable[Namespace]: 

138 """ 

139 Property to access the parent namespace (:attr:`_parentNamespace`). 

140 

141 Assigning a namespace links both sides: this namespace leaves the sub-namespaces (:attr:`SubNamespaces`) of its 

142 previous parent namespace and is appended to the sub-namespaces of the new one. Assigning ``None`` unlinks this 

143 namespace from its parent namespace. Assigning the current parent namespace changes nothing. 

144 

145 :returns: The parent namespace, or ``None`` if this namespace is the outermost one. 

146 :raises TypeError: If the assigned value is not of type :class:`Namespace`. 

147 """ 

148 return self._parentNamespace 

149 

150 @ParentNamespace.setter 

151 def ParentNamespace(self, parentNamespace: Nullable[Namespace]) -> None: 

152 if parentNamespace is not None and not isinstance(parentNamespace, Namespace): 

153 ex = TypeError("Parameter 'parentNamespace' is not of type 'Namespace'.") 

154 ex.add_note(f"Got type '{getFullyQualifiedName(parentNamespace)}'.") 

155 raise ex 

156 

157 if parentNamespace is self._parentNamespace: 

158 return 

159 

160 if self._parentNamespace is not None: 

161 self._parentNamespace._subNamespaces.remove(self) 

162 

163 self._parentNamespace = parentNamespace 

164 if parentNamespace is not None: 

165 parentNamespace._subNamespaces.append(self) 

166 

167 @readonly 

168 def SharesRegionWithParent(self) -> bool: 

169 """ 

170 Read-only property to access whether this namespace continues its parent's declarative region 

171 (:attr:`_sharesRegionWithParent`). 

172 

173 .. hint:: 

174 

175 An entity and its architecture form one VHDL declarative region, as do a package and its body, 

176 even though each owns a namespace. A duplicate declaration is reported across such a link, while 

177 a genuinely nested region - a process, a block - hides instead. 

178 

179 :returns: ``True`` if the parent namespace is the same declarative region. 

180 """ 

181 return self._sharesRegionWithParent 

182 

183 @readonly 

184 def SubNamespaces(self) -> list[Namespace]: 

185 """ 

186 Read-only property to access the sub namespaces (:attr:`_subNamespaces`). 

187 

188 A sub-namespace is linked by assigning this namespace to its :attr:`ParentNamespace`. Several sub-namespaces may 

189 share a name, e.g. ``None`` for unlabeled processes. 

190 

191 :returns: List of sub namespaces, in the order they were linked. 

192 """ 

193 return self._subNamespaces 

194 

195 def AddElement(self, normalizedIdentifier: K, element: O, overloadable: bool = False) -> None: 

196 """ 

197 Add a declared item to this namespace, reporting a duplicate declaration. 

198 

199 VHDL rejects two declarations sharing an identifier in one declarative region. A region can span 

200 more than one namespace - an entity and its architecture form one, as do a package and its body - 

201 so enclosing namespaces are searched too, but only while they share this one's region. 

202 

203 Overloadable declarations are exempt: several subprograms may share a name as long as their 

204 signatures differ. Signatures are not compared yet, so two subprograms sharing a name are always 

205 accepted (see the overload-resolution finding). 

206 

207 An item imported into a namespace (e.g. by a use clause) is no declaration of that namespace's region: it's kept 

208 apart (:meth:`AddImport`), so it never clashes with a declaration, and a lookup finds the declaration first. 

209 

210 :param normalizedIdentifier: The normalized (lower case) identifier being declared. 

211 :param element: The declared item. 

212 :param overloadable: ``True`` if this declaration may legally share its name. 

213 :raises UnhandledCriticalWarningError: If the identifier is already declared in the declarative region and no 

214 :class:`~pyTooling.Warning.WarningCollector` collects the 

215 :exc:`~pyVHDLModel.Exception.DuplicateDeclarationWarning`. 

216 """ 

217 from pyVHDLModel.Subprogram import Function, Procedure 

218 

219 namespace = self 

220 while namespace is not None: 

221 existing = namespace._elements.get(normalizedIdentifier) 

222 # `existing is element` means the region is being re-indexed, not that the name is declared twice. 

223 isReindex = existing is element 

224 isOverload = overloadable and isinstance(existing, (Function, Procedure)) 

225 if existing is not None and not isReindex and not isOverload: 

226 WarningCollector.Raise(DuplicateDeclarationWarning( 

227 f"Identifier '{normalizedIdentifier}' is already used for a declaration in '{namespace._name}'.", 

228 identifier=self._IdentifierOf(element, normalizedIdentifier), 

229 namespace=namespace, 

230 element=element, 

231 existing=existing 

232 )) 

233 break 

234 

235 namespace = namespace._parentNamespace if namespace._sharesRegionWithParent else None 

236 

237 self._elements[normalizedIdentifier] = element 

238 

239 def RemoveElement(self, normalizedIdentifier: K, element: O) -> None: 

240 """ 

241 Remove a declared item from this namespace. 

242 

243 :param normalizedIdentifier: The normalized (lower case) identifier of the declared item. 

244 :param element: The declared item. 

245 :raises NotInNamespaceError: If the identifier isn't declared in this namespace. 

246 :raises NotInNamespaceError: If the identifier refers to another element, e.g. a duplicate declaration. 

247 """ 

248 if (existing := self._elements.get(normalizedIdentifier)) is None: 

249 raise NotInNamespaceError( 

250 f"Namespace '{self._name}' has no element '{normalizedIdentifier}'.", 

251 identifier=self._IdentifierOf(element, normalizedIdentifier), 

252 namespace=self 

253 ) 

254 elif existing is not element: 

255 ex = NotInNamespaceError( 

256 f"Namespace '{self._name}' has another element for '{normalizedIdentifier}'.", 

257 identifier=self._IdentifierOf(element, normalizedIdentifier), 

258 namespace=self 

259 ) 

260 ex.add_note(f"Found element of type '{getFullyQualifiedName(existing)}'.") 

261 raise ex 

262 

263 del self._elements[normalizedIdentifier] 

264 

265 def AddImport(self, normalizedIdentifier: K, element: O) -> None: 

266 """ 

267 Add an item imported by a use clause to this namespace. 

268 

269 An imported item is declared elsewhere, so it's kept apart from the declarations (:meth:`AddElement`): it never 

270 clashes with a declaration, and a lookup finds a declaration of the same identifier first. Importing another item 

271 for the same identifier replaces the imported item. 

272 

273 :param normalizedIdentifier: The normalized (lower case) identifier of the imported item. 

274 :param element: The imported item, e.g. a package or a component. 

275 """ 

276 self._imports[normalizedIdentifier] = element 

277 

278 def RemoveImport(self, normalizedIdentifier: K, element: O) -> None: 

279 """ 

280 Remove an imported item from this namespace. 

281 

282 Declarations of the same identifier are kept. 

283 

284 :param normalizedIdentifier: The normalized (lower case) identifier of the imported item. 

285 :param element: The imported item. 

286 :raises NotInNamespaceError: If the identifier isn't imported into this namespace. 

287 :raises NotInNamespaceError: If the identifier refers to another imported item. 

288 """ 

289 if (existing := self._imports.get(normalizedIdentifier)) is None: 

290 raise NotInNamespaceError( 

291 f"Namespace '{self._name}' has no import '{normalizedIdentifier}'.", 

292 identifier=self._IdentifierOf(element, normalizedIdentifier), 

293 namespace=self 

294 ) 

295 elif existing is not element: 

296 ex = NotInNamespaceError( 

297 f"Namespace '{self._name}' has another import for '{normalizedIdentifier}'.", 

298 identifier=self._IdentifierOf(element, normalizedIdentifier), 

299 namespace=self 

300 ) 

301 ex.add_note(f"Found element of type '{getFullyQualifiedName(existing)}'.") 

302 raise ex 

303 

304 del self._imports[normalizedIdentifier] 

305 

306 def ClearImports(self) -> None: 

307 """ 

308 Remove all imported items from this namespace. 

309 

310 The declarations (:meth:`Elements`) are kept. 

311 """ 

312 self._imports.clear() 

313 

314 @staticmethod 

315 def _IdentifierOf(element: O, normalizedIdentifier: K) -> str: 

316 """ 

317 Return the identifier of an element as declared, for its normalized identifier. 

318 

319 :param element: The named element. 

320 :param normalizedIdentifier: One of the element's normalized (lower case) identifiers. 

321 :returns: The identifier in its declared spelling. 

322 """ 

323 return next( 

324 identifier 

325 for identifier, normalized in zip(identifiersOf(element), normalizedIdentifiersOf(element)) 

326 if normalized == normalizedIdentifier 

327 ) 

328 

329 def Elements(self) -> dict[K, O]: 

330 """ 

331 Return the elements declared in this namespace (:attr:`_elements`). 

332 

333 :returns: Dictionary of declared elements, indexed by normalized identifier. 

334 """ 

335 return self._elements 

336 

337 def Imports(self) -> dict[K, O]: 

338 """ 

339 Return the elements imported into this namespace (:attr:`_imports`). 

340 

341 :returns: Dictionary of imported elements, indexed by normalized identifier. 

342 """ 

343 return self._imports 

344 

345 def _LookupElement(self, normalizedIdentifier: K) -> O: 

346 """ 

347 Return the element of an identifier in this namespace: the declared element, else the imported element. 

348 

349 :param normalizedIdentifier: The normalized (lower case) identifier. 

350 :returns: The declared or imported element. 

351 :raises KeyError: If the identifier is neither declared nor imported in this namespace. 

352 """ 

353 if (element := self._elements.get(normalizedIdentifier)) is not None: 

354 return element 

355 

356 return self._imports[normalizedIdentifier] 

357 

358 def FindComponent(self, componentSymbol: ComponentInstantiationSymbol) -> Component: 

359 from pyVHDLModel.DesignUnit import Component 

360 

361 try: 

362 element = self._LookupElement(componentSymbol._name._normalizedIdentifier) 

363 if isinstance(element, Component): 

364 return element 

365 else: 

366 ex = TypeError(f"Found element '{componentSymbol._name._identifier}', but it is not a component.") 

367 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

368 raise ex 

369 except KeyError: 

370 key = componentSymbol._name._identifier 

371 

372 if (parentNamespace := self._parentNamespace) is None: 

373 raise ExtendedKeyError(key, (self, ), f"Component '{key}' not found in '{self._name}'.") 

374 

375 try: 

376 return parentNamespace.FindComponent(componentSymbol) 

377 except ExtendedKeyError as ex: 

378 searchedNamespaces = (self, *ex.searchedNamespaces) 

379 raise ExtendedKeyError(key, searchedNamespaces, f"Component '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex 

380 

381 def FindSubtype(self, subtypeSymbol: Symbol) -> BaseType: 

382 from pyVHDLModel.Type import Subtype, FullType 

383 

384 try: 

385 element = self._LookupElement(subtypeSymbol._name._normalizedIdentifier) 

386 if isinstance(element, Subtype): 

387 if PossibleReference.Subtype in subtypeSymbol._possibleReferences: 

388 return element 

389 else: 

390 ex = TypeError(f"Found subtype '{subtypeSymbol._name._identifier}', but it was not expected.") 

391 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

392 ex.add_note(f"Expected one of: {subtypeSymbol._possibleReferences}.") 

393 raise ex 

394 elif isinstance(element, FullType): 

395 if PossibleReference.Type in subtypeSymbol._possibleReferences: 

396 return element 

397 else: 

398 ex = TypeError(f"Found type '{subtypeSymbol._name._identifier}', but it was not expected.") 

399 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

400 ex.add_note(f"Expected one of: {subtypeSymbol._possibleReferences}.") 

401 raise ex 

402 else: 

403 ex = TypeError(f"Found element '{subtypeSymbol._name._identifier}', but it is not a type or subtype.") 

404 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

405 raise ex 

406 except KeyError: 

407 key = subtypeSymbol._name._identifier 

408 

409 if (parentNamespace := self._parentNamespace) is None: 

410 raise ExtendedKeyError(key, (self, ), f"Subtype '{key}' not found in '{self._name}'.") 

411 

412 try: 

413 return parentNamespace.FindSubtype(subtypeSymbol) 

414 except ExtendedKeyError as ex: 

415 searchedNamespaces = (self, *ex.searchedNamespaces) 

416 raise ExtendedKeyError(key, searchedNamespaces, f"Subtype '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex 

417 

418 def FindObject(self, objectSymbol: Symbol) -> Obj: 

419 try: 

420 element = self._LookupElement(objectSymbol._name._normalizedIdentifier) 

421 if isinstance(element, Signal): 

422 if PossibleReference.Signal in objectSymbol._possibleReferences: 

423 return element 

424 elif PossibleReference.SignalAttribute in objectSymbol._possibleReferences: 

425 return element 

426 else: 

427 ex = TypeError(f"Found signal '{objectSymbol._name._identifier}', but it was not expected.") 

428 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

429 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.") 

430 raise ex 

431 elif isinstance(element, Constant): 

432 if PossibleReference.Constant in objectSymbol._possibleReferences: 432 ↛ 435line 432 didn't jump to line 435 because the condition on line 432 was always true

433 return element 

434 else: 

435 ex = TypeError(f"Found constant '{objectSymbol._name._identifier}', but it was not expected.") 

436 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

437 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.") 

438 raise ex 

439 elif isinstance(element, Variable): 

440 if PossibleReference.Variable in objectSymbol._possibleReferences: 440 ↛ 443line 440 didn't jump to line 443 because the condition on line 440 was always true

441 return element 

442 else: 

443 ex = TypeError(f"Found variable '{objectSymbol._name._identifier}', but it was not expected.") 

444 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

445 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.") 

446 raise ex 

447 else: 

448 ex = TypeError(f"Found element '{objectSymbol._name._identifier}', but it is not an object.") 

449 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.") 

450 raise ex 

451 except KeyError: 

452 key = objectSymbol._name._identifier 

453 

454 if (parentNamespace := self._parentNamespace) is None: 

455 raise ExtendedKeyError(key, (self, ), f"Object '{key}' not found in '{self._name}'.") 

456 

457 try: 

458 return parentNamespace.FindObject(objectSymbol) 

459 except ExtendedKeyError as ex: 

460 searchedNamespaces = (self, *ex.searchedNamespaces) 

461 raise ExtendedKeyError(key, searchedNamespaces, f"Object '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex