Coverage for pyTooling/Dependency/__init__.py: 81%

276 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-22 21:29 +0000

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

2# _____ _ _ ____ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ # 

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

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, | # 

7# |_| |___/ |___/ |_| |___/ # 

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2025-2026 Patrick Lehmann - Bötzingen, 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""" 

32Implementation of package dependencies. 

33 

34.. hint:: 

35 

36 See :ref:`high-level help <DEPENDENCIES>` for explanations and usage examples. 

37 

38.. seealso:: 

39 

40 :mod:`pyTooling.Dependency.Python` 

41 |rarr| The implementation for Python packages on a package index. 

42 :mod:`pyTooling.Versioning` 

43 |rarr| The version numbers a requirement is resolved against. 

44 :mod:`pyTooling.Graph` 

45 |rarr| The graph data structure a dependency graph is built on. 

46""" 

47from __future__ import annotations 

48 

49from datetime import datetime 

50from typing import Optional as Nullable, Union, Iterable, Self, Iterator 

51 

52from pyTooling.Decorators import export, readonly 

53from pyTooling.MetaClasses import ExtendedType 

54from pyTooling.Exceptions import ToolingException 

55from pyTooling.Common import getFullyQualifiedName, firstKey 

56from pyTooling.Versioning import SemanticVersion 

57from pyTooling.Warning import Warning 

58 

59 

60@export 

61class DependencyException(ToolingException): 

62 """Base-exception of all exceptions raised by :mod:`pyTooling.Dependency`.""" 

63 

64 

65@export 

66class NoSessionAvailableException(DependencyException): 

67 """ 

68 The operation needs a session to the package index, but no session was opened. 

69 

70 A session is created by the package index and handed to the objects it creates. 

71 """ 

72 

73 

74@export 

75class ProjectNotFoundException(DependencyException): 

76 """The package index doesn't know a project of that name.""" 

77 

78 

79@export 

80class ReleaseNotFoundException(DependencyException): 

81 """The project exists in the package index, but not in the requested version.""" 

82 

83 

84@export 

85class BrokenRequirementWarning(Warning): 

86 """ 

87 A requirement carries an environment marker that matches none of the extras declared by the project. 

88 

89 Such a requirement can't be assigned to an extra, so it's not reachable through :attr:`Requirements`. 

90 """ 

91 

92 

93@export 

94class ReleaseDetailsWarning(Warning): 

95 """Downloading the details of a release failed, therefore the release was dropped from the project.""" 

96 

97 

98@export 

99class PackageVersion(metaclass=ExtendedType, slots=True): 

100 """ 

101 The package's version of a :class:`Package`. 

102 

103 A :class:`Package` has multiple available versions. A version can have multiple dependencies to other 

104 :class:`PackageVersion`s. 

105 """ 

106 

107 _package: Package #: Reference to the corresponding package 

108 _version: SemanticVersion #: :class:`SemanticVersion` of this package version. 

109 _releasedAt: Nullable[datetime] #: Time when this package version was released. 

110 

111 _dependsOn: dict[Package, dict[SemanticVersion, PackageVersion]] #: Versioned dependencies to other packages. 

112 

113 def __init__(self, version: SemanticVersion, package: Package, releasedAt: Nullable[datetime] = None) -> None: 

114 """ 

115 Initializes a package version. 

116 

117 :param version: Semantic version of this package. 

118 :param package: Package this version is associated to. 

119 :param releasedAt: Optional, release date and time. 

120 :raises TypeError: When parameter 'version' is not of type :class:`SemanticVersion`. 

121 :raises TypeError: When parameter 'package' is not of type :class:`Package`. 

122 :raises TypeError: When parameter 'releasedAt' is not of type :class:`~datetime.datetime`. 

123 :raises ToolingException: When version already exists for the associated package. 

124 """ 

125 if not isinstance(version, SemanticVersion): 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true

126 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.") 

127 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

128 raise ex 

129 elif version in package._versions: 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 raise ToolingException(f"Version '{version}' is already registered in package '{package._name}'.") 

131 

132 self._version = version 

133 package._versions[version] = self 

134 

135 if not isinstance(package, Package): 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true

136 ex = TypeError("Parameter 'package' is not of type 'Package'.") 

137 ex.add_note(f"Got type '{getFullyQualifiedName(package)}'.") 

138 raise ex 

139 

140 self._package = package 

141 

142 if releasedAt is not None and not isinstance(releasedAt, datetime): 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true

143 ex = TypeError("Parameter 'releasedAt' is not of type 'datetime'.") 

144 ex.add_note(f"Got type '{getFullyQualifiedName(releasedAt)}'.") 

145 raise ex 

146 

147 self._releasedAt = releasedAt 

148 

149 self._dependsOn = {} 

150 

151 @readonly 

152 def Package(self) -> Package: 

153 """ 

154 Read-only property to access the associated package. 

155 

156 :returns: Associated package. 

157 """ 

158 return self._package 

159 

160 @readonly 

161 def Version(self) -> SemanticVersion: 

162 """ 

163 Read-only property to access the semantic version of a package. 

164 

165 :returns: Semantic version of a package. 

166 """ 

167 return self._version 

168 

169 @readonly 

170 def ReleasedAt(self) -> Nullable[datetime]: 

171 """ 

172 Read-only property to access the release date and time. 

173 

174 :returns: Optional release date and time. 

175 """ 

176 return self._releasedAt 

177 

178 @readonly 

179 def DependsOn(self) -> dict[Package, dict[SemanticVersion, PackageVersion]]: 

180 """ 

181 Read-only property to access the dictionary of dictionaries referencing dependencies. 

182 

183 The outer dictionary key groups dependencies by :class:`Package`. |br| 

184 The inner dictionary key accesses dependencies by :class:`~pyTooling.Versioning.SemanticVersion`. 

185 

186 :returns: Dictionary of dependencies. 

187 """ 

188 return self._dependsOn 

189 

190 def AddDependencyToPackageVersion(self, packageVersion: PackageVersion) -> None: 

191 """ 

192 Add a dependency from current package version to another package version. 

193 

194 :param packageVersion: Dependency to be added. 

195 """ 

196 if (package := packageVersion._package) in self._dependsOn: 

197 pack = self._dependsOn[package] 

198 if (version := packageVersion._version) in pack: 198 ↛ 199line 198 didn't jump to line 199 because the condition on line 198 was never true

199 pass 

200 else: 

201 pack[version] = packageVersion 

202 else: 

203 self._dependsOn[package] = {packageVersion._version: packageVersion} 

204 

205 def AddDependencyToPackageVersions(self, packageVersions: Iterable[PackageVersion]) -> None: 

206 """ 

207 Add multiple dependencies from current package version to a list of other package versions. 

208 

209 :param packageVersions: Dependencies to be added. 

210 """ 

211 # TODO: check for iterable 

212 

213 for packageVersion in packageVersions: 

214 if (package := packageVersion._package) in self._dependsOn: 

215 pack = self._dependsOn[package] 

216 if (version := packageVersion._version) in pack: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 pass 

218 else: 

219 pack[version] = packageVersion 

220 else: 

221 self._dependsOn[package] = {packageVersion._version: packageVersion} 

222 

223 def AddDependencyTo( 

224 self, 

225 package: str | Package, 

226 version: str | SemanticVersion | Iterable[str | SemanticVersion] 

227 ) -> None: 

228 """ 

229 Add a dependency from current package version to another package version. 

230 

231 :param package: :class:`Package` object or name of the package. 

232 :param version: :class:`~pyTooling.Versioning.SemanticVersion` object or version string or an iterable thereof. 

233 :raises TypeError: If parameter 'package' is not of type :class:`Package`. 

234 """ 

235 if isinstance(package, str): 

236 package = self._package._storage._packages[package] 

237 elif not isinstance(package, Package): 237 ↛ 238line 237 didn't jump to line 238 because the condition on line 237 was never true

238 ex = TypeError(f"Parameter 'package' is not of type 'str' nor 'Package'.") 

239 ex.add_note(f"Got type '{getFullyQualifiedName(package)}'.") 

240 raise ex 

241 

242 if isinstance(version, str): 

243 version = SemanticVersion.Parse(version) 

244 elif isinstance(version, Iterable): 

245 for v in version: 

246 if isinstance(v, str): 246 ↛ 248line 246 didn't jump to line 248 because the condition on line 246 was always true

247 v = SemanticVersion.Parse(v) 

248 elif not isinstance(v, SemanticVersion): 

249 ex = TypeError(f"Parameter 'version' contains an element, which is not of type 'str' nor 'SemanticVersion'.") 

250 ex.add_note(f"Got type '{getFullyQualifiedName(v)}'.") 

251 raise ex# 

252 

253 packageVersion = package._versions[v] 

254 self.AddDependencyToPackageVersion(packageVersion) 

255 

256 return 

257 elif not isinstance(version, SemanticVersion): 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true

258 ex = TypeError(f"Parameter 'version' is not of type 'str' nor 'SemanticVersion'.") 

259 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

260 raise ex 

261 

262 packageVersion = package._versions[version] 

263 self.AddDependencyToPackageVersion(packageVersion) 

264 

265 def SortDependencies(self) -> Self: 

266 """ 

267 Sort versions of a package and dependencies by version, thus dependency resolution can work on pre-sorted lists and 

268 dictionaries. 

269 

270 :returns: The instance itself (for method-chaining). 

271 """ 

272 for package, versions in self._dependsOn.items(): 

273 self._dependsOn[package] = {version: versions[version] for version in sorted(versions.keys(), reverse=True)} 

274 return self 

275 

276 def SolveLatest(self) -> Iterable[PackageVersion]: 

277 """ 

278 Solve the dependency problem, while using preferably latest versions. 

279 

280 .. todo:: 

281 

282 Describe algorithm. 

283 

284 :returns: A list of :class:`PackageVersion`s fulfilling the constraints of the dependency problem. 

285 :raises ToolingException: When there is no valid solution to the problem. 

286 """ 

287 solution: dict[Package, PackageVersion] = {self._package: self} 

288 

289 def _recursion(currentSolution: dict[Package, PackageVersion]) -> bool: 

290 """ 

291 Nested function for recursion. 

292 

293 It adds the latest matching version of every package the current solution requires, and recurses until no 

294 package is missing. 

295 

296 :param currentSolution: The packages selected so far, by package. 

297 :returns: ``True``, if the solution is complete and consistent. 

298 """ 

299 # 1. Identify all required packages based on current selection 

300 requiredPackages: set[Package] = set() 

301 for packageVersion in currentSolution.values(): 

302 requiredPackages.update(packageVersion.DependsOn.keys()) 

303 

304 # 2. Identify which required packages are missing from the solution 

305 missingPackages = requiredPackages - currentSolution.keys() 

306 

307 # Base Case: If no packages are missing, the graph is complete and valid 

308 if len(missingPackages) == 0: 

309 return True 

310 

311 # 3. Pick the next package to resolve 

312 # (Heuristic: we just pick the first one, but could be optimized) 

313 targetPackage = next(iter(missingPackages)) 

314 

315 # 4. Determine valid candidates 

316 # The candidate version must satisfy the constraints of all parents currently in the solution 

317 allowedVersions: Nullable[set[SemanticVersion]] = None 

318 

319 for parentPackageVersion in currentSolution.values(): 

320 if targetPackage in parentPackageVersion.DependsOn: 

321 # Get the set of versions allowed by this specific parent 

322 # (Keys of the inner dict are SemanticVersion objects) 

323 parentConstraints = set(parentPackageVersion.DependsOn[targetPackage].keys()) 

324 

325 if allowedVersions is None: 

326 allowedVersions = parentConstraints 

327 else: 

328 # Intersect with existing constraints (must satisfy everyone) 

329 allowedVersions &= parentConstraints 

330 

331 # If the intersection is empty, no version satisfies all parents -> backtrack 

332 if not allowedVersions: 

333 return False 

334 

335 # 5. Try candidates (sorted descending to prioritize latest) 

336 # We convert the set to a list and sort it reverse 

337 for version_key in sorted(list(allowedVersions), reverse=True): 

338 candidate = targetPackage.Versions[version_key] 

339 

340 # 6. Check compatibility (reverse dependencies) 

341 # Does the candidate depend on anything we have already selected? 

342 # If so, does the candidate accept the version we already picked? 

343 isCompatible = True 

344 for existingPackage, existingPackageVersion in currentSolution.items(): 

345 if existingPackage in candidate.DependsOn: 

346 # If candidate relies on 'existingPackage', check if 'existingPackageVersion' is in the allowed list 

347 if existingPackageVersion._version not in candidate.DependsOn[existingPackage]: 

348 isCompatible = False 

349 break 

350 

351 if isCompatible: 

352 # Tentatively add to solution 

353 currentSolution[targetPackage] = candidate 

354 

355 # Recurse 

356 if _recursion(currentSolution): 

357 return True 

358 

359 # If recursion failed, remove (backtrack) and try next version 

360 del currentSolution[targetPackage] 

361 

362 # If we run out of versions for this package, this path is dead 

363 return False 

364 

365 # Run the solver 

366 if _recursion(solution): 

367 return list(solution.values()) 

368 else: 

369 raise ToolingException(f"Could not resolve dependencies for '{self}'.") 

370 

371 def __len__(self) -> int: 

372 """ 

373 Returns the number of dependencies. 

374 

375 :returns: Number of dependencies. 

376 """ 

377 return len(self._dependsOn) 

378 

379 def __str__(self) -> str: 

380 """ 

381 Return a string representation of this package version. 

382 

383 :returns: The package's name and version. 

384 """ 

385 return f"{self._package._name} - {self._version}" 

386 

387 

388@export 

389class Package(metaclass=ExtendedType, slots=True): 

390 """ 

391 The package, which exists in multiple versions (:class:`PackageVersion`). 

392 """ 

393 _storage: PackageStorage #: Reference to the package's storage. 

394 _name: str #: Name of the package. 

395 

396 _versions: dict[SemanticVersion, PackageVersion] #: A dictionary of available versions for this package. 

397 

398 def __init__(self, name: str, *, storage: PackageStorage) -> None: 

399 """ 

400 Initializes a package. 

401 

402 :param name: Name of the package. 

403 :param storage: The package's storage. 

404 :raises TypeError: If a parameter is not of the expected type. 

405 """ 

406 if not isinstance(name, str): 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true

407 ex = TypeError("Parameter 'name' is not of type 'str'.") 

408 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

409 raise ex 

410 

411 self._name = name 

412 

413 if not isinstance(storage, PackageStorage): 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true

414 ex = TypeError("Parameter 'storage' is not of type 'PackageStorage'.") 

415 ex.add_note(f"Got type '{getFullyQualifiedName(storage)}'.") 

416 raise ex 

417 

418 self._storage = storage 

419 storage._packages[name] = self 

420 

421 self._versions = {} 

422 

423 @readonly 

424 def Storage(self) -> PackageStorage: 

425 """ 

426 Read-only property to access the package's storage. 

427 

428 :returns: Package storage. 

429 """ 

430 return self._storage 

431 

432 @readonly 

433 def Name(self) -> str: 

434 """ 

435 Read-only property to access the package name. 

436 

437 :returns: Name of the package. 

438 """ 

439 return self._name 

440 

441 @readonly 

442 def Versions(self) -> dict[SemanticVersion, PackageVersion]: 

443 """ 

444 Read-only property to access the dictionary of available versions. 

445 

446 :returns: Available version dictionary. 

447 """ 

448 return self._versions 

449 

450 @readonly 

451 def VersionCount(self) -> int: 

452 """ 

453 Read-only property to return the number of versions this package has. 

454 

455 :returns: Number of versions. 

456 """ 

457 return len(self._versions) 

458 

459 def SortVersions(self) -> None: 

460 """ 

461 Sort versions within this package in reverse order (latest first). 

462 """ 

463 self._versions = {k: self._versions[k].SortDependencies() for k in sorted(self._versions.keys(), reverse=True)} 

464 

465 def __len__(self) -> int: 

466 """ 

467 Returns the number of available versions. 

468 

469 :returns: Number of versions. 

470 """ 

471 return len(self._versions) 

472 

473 def __iter__(self) -> Iterator[PackageVersion]: 

474 """ 

475 Iterate the versions of this package. 

476 

477 :returns: An iterator over all versions of this package. 

478 """ 

479 return iter(self._versions.values()) 

480 

481 def __getitem__(self, version: str | SemanticVersion) -> PackageVersion: 

482 """ 

483 Access a package version in the package by version string or semantic version. 

484 

485 :param version: Version as string or instance. 

486 :returns: The package version. 

487 :raises KeyError: If version is not available for the package. 

488 :raises TypeError: If the given key is not of the expected type. 

489 """ 

490 if isinstance(version, str): 490 ↛ 492line 490 didn't jump to line 492 because the condition on line 490 was always true

491 version = SemanticVersion.Parse(version) 

492 elif not isinstance(version, SemanticVersion): 

493 ex = TypeError("Parameter 'version' is neither a 'str' nor of type 'SemanticVersion'.") 

494 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.") 

495 raise ex 

496 

497 return self._versions[version] 

498 

499 def __str__(self) -> str: 

500 """ 

501 Return a string representation of this package. 

502 

503 :returns: The package's name and latest version. 

504 """ 

505 if len(self._versions) == 0: 

506 return f"{self._name} (empty)" 

507 else: 

508 return f"{self._name} (latest: {firstKey(self._versions)})" 

509 

510 

511@export 

512class PackageStorage(metaclass=ExtendedType, slots=True): 

513 """ 

514 A storage for packages. 

515 """ 

516 _graph: PackageDependencyGraph #: Reference to the overall dependency graph data structure. 

517 _name: str #: Package dependency graph name 

518 _packages: dict[str, Package] #: Dictionary of known packages. 

519 

520 def __init__(self, name: str, graph: PackageDependencyGraph) -> None: 

521 """ 

522 Initializes the package storage. 

523 

524 :param name: Name of the package storage. 

525 :param graph: PackageDependencyGraph instance (parent). 

526 :raises TypeError: If a parameter is not of the expected type. 

527 """ 

528 if not isinstance(name, str): 528 ↛ 529line 528 didn't jump to line 529 because the condition on line 528 was never true

529 ex = TypeError("Parameter 'name' is not of type 'str'.") 

530 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

531 raise ex 

532 

533 self._name = name 

534 

535 if not isinstance(graph, PackageDependencyGraph): 535 ↛ 536line 535 didn't jump to line 536 because the condition on line 535 was never true

536 ex = TypeError("Parameter 'graph' is not of type 'PackageDependencyGraph'.") 

537 ex.add_note(f"Got type '{getFullyQualifiedName(graph)}'.") 

538 raise ex 

539 

540 self._graph = graph 

541 graph._storages[name] = self 

542 

543 self._packages = {} 

544 

545 @readonly 

546 def Graph(self) -> PackageDependencyGraph: 

547 """ 

548 Read-only property to access the package dependency graph. 

549 

550 :returns: Package dependency graph. 

551 """ 

552 return self._graph 

553 

554 @readonly 

555 def Name(self) -> str: 

556 """ 

557 Read-only property to access the package dependency graph's name. 

558 

559 :returns: Name of the package dependency graph. 

560 """ 

561 return self._name 

562 

563 @readonly 

564 def Packages(self) -> dict[str, Package]: 

565 """ 

566 Read-only property to access the dictionary of known packages. 

567 

568 :returns: Known packages dictionary. 

569 """ 

570 return self._packages 

571 

572 @readonly 

573 def PackageCount(self) -> int: 

574 """ 

575 Read-only property to return the number of packages in this storage. 

576 

577 :returns: Number of packages. 

578 """ 

579 return len(self._packages) 

580 

581 def CreatePackage(self, packageName: str) -> Package: 

582 """ 

583 Create a new package in the package dependency graph. 

584 

585 :param packageName: Name of the new package. 

586 :returns: New package's instance. 

587 """ 

588 return Package(packageName, storage=self) 

589 

590 def CreatePackages(self, packageNames: Iterable[str]) -> Iterable[Package]: 

591 """ 

592 Create multiple new packages in the package dependency graph. 

593 

594 :param packageNames: List of package names. 

595 :returns: List of new package instances. 

596 """ 

597 return [Package(packageName, storage=self) for packageName in packageNames] 

598 

599 def CreatePackageVersion(self, packageName: str, version: str) -> PackageVersion: 

600 """ 

601 Create a new package and a package version in the package dependency graph. 

602 

603 :param packageName: Name of the new package. 

604 :param version: Version string. 

605 :returns: New package version instance. 

606 """ 

607 package = Package(packageName, storage=self) 

608 return PackageVersion(SemanticVersion.Parse(version), package) 

609 

610 def CreatePackageVersions(self, packageName: str, versions: Iterable[str]) -> Iterable[PackageVersion]: 

611 """ 

612 Create a new package and multiple package versions in the package dependency graph. 

613 

614 :param packageName: Name of the new package. 

615 :param versions: List of version string.s 

616 :returns: List of new package version instances. 

617 """ 

618 package = Package(packageName, storage=self) 

619 return [PackageVersion(SemanticVersion.Parse(version), package) for version in versions] 

620 

621 def SortPackageVersions(self) -> None: 

622 """ 

623 Sort versions within all known packages in reverse order (latest first). 

624 """ 

625 for package in self._packages.values(): 

626 package.SortVersions() 

627 

628 def __len__(self) -> int: 

629 """ 

630 Returns the number of known packages. 

631 

632 :returns: Number of packages. 

633 """ 

634 return len(self._packages) 

635 

636 def __iter__(self) -> Iterator[Package]: 

637 """ 

638 Iterate the packages in this storage. 

639 

640 :returns: An iterator over all packages in this storage. 

641 """ 

642 return iter(self._packages.values()) 

643 

644 def __getitem__(self, name: str) -> Package: 

645 """ 

646 Access a known package in the package dependency graph by package name. 

647 

648 :param name: Name of the package. 

649 :returns: The package. 

650 :raises KeyError: If package is not known within the package dependency graph. 

651 """ 

652 return self._packages[name] 

653 

654 def __str__(self) -> str: 

655 """ 

656 Return a string representation of this graph. 

657 

658 :returns: The graph's name and number of known packages. 

659 """ 

660 if len(self._packages) == 0: 660 ↛ 663line 660 didn't jump to line 663 because the condition on line 660 was always true

661 return f"{self._name} (empty)" 

662 else: 

663 return f"{self._name} ({len(self._packages)})" 

664 

665 

666@export 

667class PackageDependencyGraph(metaclass=ExtendedType, slots=True): 

668 """ 

669 A package dependency graph collecting all known packages. 

670 """ 

671 _name: str #: Package dependency graph name 

672 _storages: dict[str, PackageStorage] #: Dictionary of known package storages. 

673 

674 def __init__(self, name: str) -> None: 

675 """ 

676 Initializes the package dependency graph. 

677 

678 :param name: Name of the dependency graph. 

679 :raises TypeError: If a parameter is not of the expected type. 

680 """ 

681 if not isinstance(name, str): 681 ↛ 682line 681 didn't jump to line 682 because the condition on line 681 was never true

682 ex = TypeError("Parameter 'name' is not of type 'str'.") 

683 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

684 raise ex 

685 

686 self._name = name 

687 

688 self._storages = {} 

689 

690 @readonly 

691 def Name(self) -> str: 

692 """ 

693 Read-only property to access the package dependency graph's name. 

694 

695 :returns: Name of the package dependency graph. 

696 """ 

697 return self._name 

698 

699 @readonly 

700 def Storages(self) -> dict[str, PackageStorage]: 

701 """ 

702 Read-only property to access the dictionary of known package storages. 

703 

704 :returns: Known package storage dictionary. 

705 """ 

706 return self._storages 

707 

708 # def CreatePackage(self, packageName: str) -> Package: 

709 # """ 

710 # Create a new package in the package dependency graph. 

711 # 

712 # :param packageName: Name of the new package. 

713 # :returns: New package's instance. 

714 # """ 

715 # return Package(packageName, storage=self) 

716 # 

717 # def CreatePackages(self, packageNames: Iterable[str]) -> Iterable[Package]: 

718 # """ 

719 # Create multiple new packages in the package dependency graph. 

720 # 

721 # :param packageNames: List of package names. 

722 # :returns: List of new package instances. 

723 # """ 

724 # return [Package(packageName, storage=self) for packageName in packageNames] 

725 # 

726 # def CreatePackageVersion(self, packageName: str, version: str) -> PackageVersion: 

727 # """ 

728 # Create a new package and a package version in the package dependency graph. 

729 # 

730 # :param packageName: Name of the new package. 

731 # :param version: Version string. 

732 # :returns: New package version instance. 

733 # """ 

734 # package = Package(packageName, storage=self) 

735 # return PackageVersion(SemanticVersion.Parse(version), package) 

736 # 

737 # def CreatePackageVersions(self, packageName: str, versions: Iterable[str]) -> Iterable[PackageVersion]: 

738 # """ 

739 # Create a new package and multiple package versions in the package dependency graph. 

740 # 

741 # :param packageName: Name of the new package. 

742 # :param versions: List of version string.s 

743 # :returns: List of new package version instances. 

744 # """ 

745 # package = Package(packageName, storage=self) 

746 # return [PackageVersion(SemanticVersion.Parse(version), package) for version in versions] 

747 

748 def SortPackageVersions(self) -> None: 

749 """ 

750 Sort versions within all known packages in reverse order (latest first). 

751 """ 

752 for storage in self._storages.values(): 

753 storage.SortPackageVersions() 

754 

755 def __len__(self) -> int: 

756 """ 

757 Returns the number of known packages. 

758 

759 :returns: Number of packages. 

760 """ 

761 return len(self._storages) 

762 

763 def __iter__(self) -> Iterator[PackageStorage]: 

764 """ 

765 Iterate the storages in this dependency graph. 

766 

767 :returns: An iterator over all storages in this dependency graph. 

768 """ 

769 return iter(self._storages.values()) 

770 

771 def __getitem__(self, name: str) -> PackageStorage: 

772 """ 

773 Access a known package storage in the package dependency graph by storage name. 

774 

775 :param name: Name of the package storage. 

776 :returns: The package storage. 

777 :raises KeyError: If package storage is not known within the package dependency graph. 

778 """ 

779 return self._storages[name] 

780 

781 def __str__(self) -> str: 

782 """ 

783 Return a string representation of this graph. 

784 

785 :returns: The graph's name and number of known packages. 

786 """ 

787 count = sum(len(storage) for storage in self._storages.values()) 

788 if count == 0: 788 ↛ 791line 788 didn't jump to line 791 because the condition on line 788 was always true

789 return f"{self._name} (empty)" 

790 else: 

791 return f"{self._name} ({count})"