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

261 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-31 07:24 +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""" 

38from datetime import datetime 

39from typing import Optional as Nullable, Dict, Union, Iterable, Set, Self, Iterator 

40 

41from pyTooling.Decorators import export, readonly 

42from pyTooling.MetaClasses import ExtendedType 

43from pyTooling.Exceptions import ToolingException 

44from pyTooling.Common import getFullyQualifiedName, firstKey 

45from pyTooling.Versioning import SemanticVersion 

46from pyTooling.Warning import Warning 

47 

48 

49@export 

50class DependencyException(ToolingException): 

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

52 

53 

54@export 

55class NoSessionAvailableException(DependencyException): 

56 """ 

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

58 

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

60 """ 

61 

62 

63@export 

64class ProjectNotFoundException(DependencyException): 

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

66 

67 

68@export 

69class ReleaseNotFoundException(DependencyException): 

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

71 

72 

73@export 

74class BrokenRequirementWarning(Warning): 

75 """ 

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

77 

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

79 """ 

80 

81 

82@export 

83class ReleaseDetailsWarning(Warning): 

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

85 

86 

87@export 

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

89 """ 

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

91 

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

93 :class:`PackageVersion`s. 

94 """ 

95 

96 _package: "Package" #: Reference to the corresponding package 

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

98 _releasedAt: Nullable[datetime] 

99 

100 _dependsOn: Dict["Package", Dict[SemanticVersion, "PackageVersion"]] #: Versioned dependencies to other packages. 

101 

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

103 """ 

104 Initializes a package version. 

105 

106 :param version: Semantic version of this package. 

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

108 :param releasedAt: Optional release date and time. 

109 :raises TypeError: When parameter 'version' is not of type 'SemanticVersion'. 

110 :raises TypeError: When parameter 'package' is not of type 'Package'. 

111 :raises TypeError: When parameter 'releasedAt' is not of type 'datetime'. 

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

113 """ 

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

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

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

117 raise ex 

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

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

120 

121 self._version = version 

122 package._versions[version] = self 

123 

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

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

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

127 raise ex 

128 

129 self._package = package 

130 

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

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

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

134 raise ex 

135 

136 self._releasedAt = releasedAt 

137 

138 self._dependsOn = {} 

139 

140 @readonly 

141 def Package(self) -> "Package": 

142 """ 

143 Read-only property to access the associated package. 

144 

145 :returns: Associated package. 

146 """ 

147 return self._package 

148 

149 @readonly 

150 def Version(self) -> SemanticVersion: 

151 """ 

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

153 

154 :returns: Semantic version of a package. 

155 """ 

156 return self._version 

157 

158 @readonly 

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

160 """ 

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

162 

163 :returns: Optional release date and time. 

164 """ 

165 return self._releasedAt 

166 

167 @readonly 

168 def DependsOn(self) -> Dict["Package", Dict[SemanticVersion, "PackageVersion"]]: 

169 """ 

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

171 

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

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

174 

175 :returns: Dictionary of dependencies. 

176 """ 

177 return self._dependsOn 

178 

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

180 """ 

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

182 

183 :param packageVersion: Dependency to be added. 

184 """ 

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

186 pack = self._dependsOn[package] 

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

188 pass 

189 else: 

190 pack[version] = packageVersion 

191 else: 

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

193 

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

195 """ 

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

197 

198 :param packageVersions: Dependencies to be added. 

199 """ 

200 # TODO: check for iterable 

201 

202 for packageVersion in packageVersions: 

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

204 pack = self._dependsOn[package] 

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

206 pass 

207 else: 

208 pack[version] = packageVersion 

209 else: 

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

211 

212 def AddDependencyTo( 

213 self, 

214 package: "str | Package", 

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

216 ) -> None: 

217 """ 

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

219 

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

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

222 """ 

223 if isinstance(package, str): 

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

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

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

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

228 raise ex 

229 

230 if isinstance(version, str): 

231 version = SemanticVersion.Parse(version) 

232 elif isinstance(version, Iterable): 

233 for v in version: 

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

235 v = SemanticVersion.Parse(v) 

236 elif not isinstance(v, SemanticVersion): 

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

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

239 raise ex# 

240 

241 packageVersion = package._versions[v] 

242 self.AddDependencyToPackageVersion(packageVersion) 

243 

244 return 

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

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

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

248 raise ex 

249 

250 packageVersion = package._versions[version] 

251 self.AddDependencyToPackageVersion(packageVersion) 

252 

253 def SortDependencies(self) -> Self: 

254 """ 

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

256 dictionaries. 

257 

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

259 """ 

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

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

262 return self 

263 

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

265 """ 

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

267 

268 .. todo:: 

269 

270 Describe algorithm. 

271 

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

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

274 """ 

275 solution: Dict["Package", "PackageVersion"] = {self._package: self} 

276 

277 def _recursion(currentSolution: Dict["Package", "PackageVersion"]) -> bool: 

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

279 requiredPackages: Set["Package"] = set() 

280 for packageVersion in currentSolution.values(): 

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

282 

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

284 missingPackages = requiredPackages - currentSolution.keys() 

285 

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

287 if len(missingPackages) == 0: 

288 return True 

289 

290 # 3. Pick the next package to resolve 

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

292 targetPackage = next(iter(missingPackages)) 

293 

294 # 4. Determine valid candidates 

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

296 allowedVersions: Nullable[Set[SemanticVersion]] = None 

297 

298 for parentPackageVersion in currentSolution.values(): 

299 if targetPackage in parentPackageVersion.DependsOn: 

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

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

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

303 

304 if allowedVersions is None: 

305 allowedVersions = parentConstraints 

306 else: 

307 # Intersect with existing constraints (must satisfy everyone) 

308 allowedVersions &= parentConstraints 

309 

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

311 if not allowedVersions: 

312 return False 

313 

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

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

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

317 candidate = targetPackage.Versions[version_key] 

318 

319 # 6. Check compatibility (reverse dependencies) 

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

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

322 isCompatible = True 

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

324 if existingPackage in candidate.DependsOn: 

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

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

327 isCompatible = False 

328 break 

329 

330 if isCompatible: 

331 # Tentatively add to solution 

332 currentSolution[targetPackage] = candidate 

333 

334 # Recurse 

335 if _recursion(currentSolution): 

336 return True 

337 

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

339 del currentSolution[targetPackage] 

340 

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

342 return False 

343 

344 # Run the solver 

345 if _recursion(solution): 

346 return list(solution.values()) 

347 else: 

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

349 

350 def __len__(self) -> int: 

351 """ 

352 Returns the number of dependencies. 

353 

354 :returns: Number of dependencies. 

355 """ 

356 return len(self._dependsOn) 

357 

358 def __str__(self) -> str: 

359 """ 

360 Return a string representation of this package version. 

361 

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

363 """ 

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

365 

366 

367@export 

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

369 """ 

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

371 """ 

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

373 _name: str #: Name of the package. 

374 

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

376 

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

378 """ 

379 Initializes a package. 

380 

381 :param name: Name of the package. 

382 :param storage: The package's storage. 

383 """ 

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

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

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

387 raise ex 

388 

389 self._name = name 

390 

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

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

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

394 raise ex 

395 

396 self._storage = storage 

397 storage._packages[name] = self 

398 

399 self._versions = {} 

400 

401 @readonly 

402 def Storage(self) -> "PackageStorage": 

403 """ 

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

405 

406 :returns: Package storage. 

407 """ 

408 return self._storage 

409 

410 @readonly 

411 def Name(self) -> str: 

412 """ 

413 Read-only property to access the package name. 

414 

415 :returns: Name of the package. 

416 """ 

417 return self._name 

418 

419 @readonly 

420 def Versions(self) -> Dict[SemanticVersion, PackageVersion]: 

421 """ 

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

423 

424 :returns: Available version dictionary. 

425 """ 

426 return self._versions 

427 

428 @readonly 

429 def VersionCount(self) -> int: 

430 """ 

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

432 

433 :returns: Number of versions. 

434 """ 

435 return len(self._versions) 

436 

437 def SortVersions(self) -> None: 

438 """ 

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

440 """ 

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

442 

443 def __len__(self) -> int: 

444 """ 

445 Returns the number of available versions. 

446 

447 :returns: Number of versions. 

448 """ 

449 return len(self._versions) 

450 

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

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

453 

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

455 """ 

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

457 

458 :param version: Version as string or instance. 

459 :returns: The package version. 

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

461 """ 

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

463 version = SemanticVersion.Parse(version) 

464 elif not isinstance(version, SemanticVersion): 

465 # TODO: raise proper type error 

466 raise TypeError() 

467 

468 return self._versions[version] 

469 

470 def __str__(self) -> str: 

471 """ 

472 Return a string representation of this package. 

473 

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

475 """ 

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

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

478 else: 

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

480 

481 

482@export 

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

484 """ 

485 A storage for packages. 

486 """ 

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

488 _name: str #: Package dependency graph name 

489 _packages: Dict[str, Package] #: Dictionary of known packages. 

490 

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

492 """ 

493 Initializes the package storage. 

494 

495 :param name: Name of the package storage. 

496 :param graph: PackageDependencyGraph instance (parent). 

497 """ 

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

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

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

501 raise ex 

502 

503 self._name = name 

504 

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

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

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

508 raise ex 

509 

510 self._graph = graph 

511 graph._storages[name] = self 

512 

513 self._packages = {} 

514 

515 @readonly 

516 def Graph(self) -> "PackageDependencyGraph": 

517 """ 

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

519 

520 :returns: Package dependency graph. 

521 """ 

522 return self._graph 

523 

524 @readonly 

525 def Name(self) -> str: 

526 """ 

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

528 

529 :returns: Name of the package dependency graph. 

530 """ 

531 return self._name 

532 

533 @readonly 

534 def Packages(self) -> Dict[str, Package]: 

535 """ 

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

537 

538 :returns: Known packages dictionary. 

539 """ 

540 return self._packages 

541 

542 @readonly 

543 def PackageCount(self) -> int: 

544 """ 

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

546 

547 :returns: Number of packages. 

548 """ 

549 return len(self._packages) 

550 

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

552 """ 

553 Create a new package in the package dependency graph. 

554 

555 :param packageName: Name of the new package. 

556 :returns: New package's instance. 

557 """ 

558 return Package(packageName, storage=self) 

559 

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

561 """ 

562 Create multiple new packages in the package dependency graph. 

563 

564 :param packageNames: List of package names. 

565 :returns: List of new package instances. 

566 """ 

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

568 

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

570 """ 

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

572 

573 :param packageName: Name of the new package. 

574 :param version: Version string. 

575 :returns: New package version instance. 

576 """ 

577 package = Package(packageName, storage=self) 

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

579 

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

581 """ 

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

583 

584 :param packageName: Name of the new package. 

585 :param versions: List of version string.s 

586 :returns: List of new package version instances. 

587 """ 

588 package = Package(packageName, storage=self) 

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

590 

591 def SortPackageVersions(self) -> None: 

592 """ 

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

594 """ 

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

596 package.SortVersions() 

597 

598 def __len__(self) -> int: 

599 """ 

600 Returns the number of known packages. 

601 

602 :returns: Number of packages. 

603 """ 

604 return len(self._packages) 

605 

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

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

608 

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

610 """ 

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

612 

613 :param name: Name of the package. 

614 :returns: The package. 

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

616 """ 

617 return self._packages[name] 

618 

619 def __str__(self) -> str: 

620 """ 

621 Return a string representation of this graph. 

622 

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

624 """ 

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

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

627 else: 

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

629 

630 

631@export 

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

633 """ 

634 A package dependency graph collecting all known packages. 

635 """ 

636 _name: str #: Package dependency graph name 

637 _storages: Dict[str, PackageStorage] #: Dictionary of known package storages. 

638 

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

640 """ 

641 Initializes the package dependency graph. 

642 

643 :param name: Name of the dependency graph. 

644 """ 

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

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

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

648 raise ex 

649 

650 self._name = name 

651 

652 self._storages = {} 

653 

654 @readonly 

655 def Name(self) -> str: 

656 """ 

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

658 

659 :returns: Name of the package dependency graph. 

660 """ 

661 return self._name 

662 

663 @readonly 

664 def Storages(self) -> Dict[str, PackageStorage]: 

665 """ 

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

667 

668 :returns: Known package storage dictionary. 

669 """ 

670 return self._storages 

671 

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

673 # """ 

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

675 # 

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

677 # :returns: New package's instance. 

678 # """ 

679 # return Package(packageName, storage=self) 

680 # 

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

682 # """ 

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

684 # 

685 # :param packageNames: List of package names. 

686 # :returns: List of new package instances. 

687 # """ 

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

689 # 

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

691 # """ 

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

693 # 

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

695 # :param version: Version string. 

696 # :returns: New package version instance. 

697 # """ 

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

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

700 # 

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

702 # """ 

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

704 # 

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

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

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

708 # """ 

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

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

711 

712 def SortPackageVersions(self) -> None: 

713 """ 

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

715 """ 

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

717 storage.SortPackageVersions() 

718 

719 def __len__(self) -> int: 

720 """ 

721 Returns the number of known packages. 

722 

723 :returns: Number of packages. 

724 """ 

725 return len(self._storages) 

726 

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

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

729 

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

731 """ 

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

733 

734 :param name: Name of the package storage. 

735 :returns: The package storage. 

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

737 """ 

738 return self._storages[name] 

739 

740 def __str__(self) -> str: 

741 """ 

742 Return a string representation of this graph. 

743 

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

745 """ 

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

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

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

749 else: 

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