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

261 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-08 23:46 +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 

41try: 

42 from pyTooling.Decorators import export, readonly 

43 from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride 

44 from pyTooling.Exceptions import ToolingException 

45 from pyTooling.Common import getFullyQualifiedName, firstKey 

46 from pyTooling.Versioning import SemanticVersion 

47except (ImportError, ModuleNotFoundError): # pragma: no cover 

48 print("[pyTooling.Dependency] Could not import from 'pyTooling.*'!") 

49 

50 try: 

51 from Decorators import export, readonly 

52 from MetaClasses import ExtendedType, abstractmethod, mustoverride 

53 from Exceptions import ToolingException 

54 from Common import getFullyQualifiedName, firstKey 

55 from Versioning import SemanticVersion 

56 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

57 print("[pyTooling.Dependency] Could not import directly!") 

58 raise ex 

59 

60 

61@export 

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

63 """ 

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

65 

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

67 :class:`PackageVersion`s. 

68 """ 

69 

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

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

72 _releasedAt: Nullable[datetime] 

73 

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

75 

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

77 """ 

78 Initializes a package version. 

79 

80 :param version: Semantic version of this package. 

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

82 :param releasedAt: Optional release date and time. 

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

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

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

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

87 """ 

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

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

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

91 raise ex 

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

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

94 

95 self._version = version 

96 package._versions[version] = self 

97 

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

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

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

101 raise ex 

102 

103 self._package = package 

104 

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

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

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

108 raise ex 

109 

110 self._releasedAt = releasedAt 

111 

112 self._dependsOn = {} 

113 

114 @readonly 

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

116 """ 

117 Read-only property to access the associated package. 

118 

119 :returns: Associated package. 

120 """ 

121 return self._package 

122 

123 @readonly 

124 def Version(self) -> SemanticVersion: 

125 """ 

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

127 

128 :returns: Semantic version of a package. 

129 """ 

130 return self._version 

131 

132 @readonly 

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

134 """ 

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

136 

137 :returns: Optional release date and time. 

138 """ 

139 return self._releasedAt 

140 

141 @readonly 

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

143 """ 

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

145 

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

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

148 

149 :returns: Dictionary of dependencies. 

150 """ 

151 return self._dependsOn 

152 

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

154 """ 

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

156 

157 :param packageVersion: Dependency to be added. 

158 """ 

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

160 pack = self._dependsOn[package] 

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

162 pass 

163 else: 

164 pack[version] = packageVersion 

165 else: 

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

167 

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

169 """ 

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

171 

172 :param packageVersions: Dependencies to be added. 

173 """ 

174 # TODO: check for iterable 

175 

176 for packageVersion in packageVersions: 

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

178 pack = self._dependsOn[package] 

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

180 pass 

181 else: 

182 pack[version] = packageVersion 

183 else: 

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

185 

186 def AddDependencyTo( 

187 self, 

188 package: Union[str, Package], 

189 version: Union[str, SemanticVersion, Iterable[Union[str, SemanticVersion]]] 

190 ) -> None: 

191 """ 

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

193 

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

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

196 :return: 

197 """ 

198 if isinstance(package, str): 

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

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

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

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

203 raise ex 

204 

205 if isinstance(version, str): 

206 version = SemanticVersion.Parse(version) 

207 elif isinstance(version, Iterable): 

208 for v in version: 

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

210 v = SemanticVersion.Parse(v) 

211 elif not isinstance(v, SemanticVersion): 

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

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

214 raise ex# 

215 

216 packageVersion = package._versions[v] 

217 self.AddDependencyToPackageVersion(packageVersion) 

218 

219 return 

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

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

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

223 raise ex 

224 

225 packageVersion = package._versions[version] 

226 self.AddDependencyToPackageVersion(packageVersion) 

227 

228 def SortDependencies(self) -> Self: 

229 """ 

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

231 dictionaries. 

232 

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

234 """ 

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

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

237 return self 

238 

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

240 """ 

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

242 

243 .. todo:: 

244 

245 Describe algorithm. 

246 

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

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

249 """ 

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

251 

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

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

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

255 for packageVersion in currentSolution.values(): 

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

257 

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

259 missingPackages = requiredPackages - currentSolution.keys() 

260 

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

262 if len(missingPackages) == 0: 

263 return True 

264 

265 # 3. Pick the next package to resolve 

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

267 targetPackage = next(iter(missingPackages)) 

268 

269 # 4. Determine valid candidates 

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

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

272 

273 for parentPackageVersion in currentSolution.values(): 

274 if targetPackage in parentPackageVersion.DependsOn: 

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

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

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

278 

279 if allowedVersions is None: 

280 allowedVersions = parentConstraints 

281 else: 

282 # Intersect with existing constraints (must satisfy everyone) 

283 allowedVersions &= parentConstraints 

284 

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

286 if not allowedVersions: 

287 return False 

288 

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

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

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

292 candidate = targetPackage.Versions[version_key] 

293 

294 # 6. Check compatibility (reverse dependencies) 

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

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

297 isCompatible = True 

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

299 if existingPackage in candidate.DependsOn: 

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

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

302 isCompatible = False 

303 break 

304 

305 if isCompatible: 

306 # Tentatively add to solution 

307 currentSolution[targetPackage] = candidate 

308 

309 # Recurse 

310 if _recursion(currentSolution): 

311 return True 

312 

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

314 del currentSolution[targetPackage] 

315 

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

317 return False 

318 

319 # Run the solver 

320 if _recursion(solution): 

321 return list(solution.values()) 

322 else: 

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

324 

325 def __len__(self) -> int: 

326 """ 

327 Returns the number of dependencies. 

328 

329 :returns: Number of dependencies. 

330 """ 

331 return len(self._dependsOn) 

332 

333 def __str__(self) -> str: 

334 """ 

335 Return a string representation of this package version. 

336 

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

338 """ 

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

340 

341 

342@export 

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

344 """ 

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

346 """ 

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

348 _name: str #: Name of the package. 

349 

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

351 

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

353 """ 

354 Initializes a package. 

355 

356 :param name: Name of the package. 

357 :param storage: The package's storage. 

358 """ 

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

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

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

362 raise ex 

363 

364 self._name = name 

365 

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

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

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

369 raise ex 

370 

371 self._storage = storage 

372 storage._packages[name] = self 

373 

374 self._versions = {} 

375 

376 @readonly 

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

378 """ 

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

380 

381 :returns: Package storage. 

382 """ 

383 return self._storage 

384 

385 @readonly 

386 def Name(self) -> str: 

387 """ 

388 Read-only property to access the package name. 

389 

390 :returns: Name of the package. 

391 """ 

392 return self._name 

393 

394 @readonly 

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

396 """ 

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

398 

399 :returns: Available version dictionary. 

400 """ 

401 return self._versions 

402 

403 @readonly 

404 def VersionCount(self) -> int: 

405 return len(self._versions) 

406 

407 def SortVersions(self) -> None: 

408 """ 

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

410 """ 

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

412 

413 def __len__(self) -> int: 

414 """ 

415 Returns the number of available versions. 

416 

417 :returns: Number of versions. 

418 """ 

419 return len(self._versions) 

420 

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

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

423 

424 def __getitem__(self, version: Union[str, SemanticVersion]) -> PackageVersion: 

425 """ 

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

427 

428 :param version: Version as string or instance. 

429 :returns: The package version. 

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

431 """ 

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

433 version = SemanticVersion.Parse(version) 

434 elif not isinstance(version, SemanticVersion): 

435 # TODO: raise proper type error 

436 raise TypeError() 

437 

438 return self._versions[version] 

439 

440 def __str__(self) -> str: 

441 """ 

442 Return a string representation of this package. 

443 

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

445 """ 

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

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

448 else: 

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

450 

451 

452@export 

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

454 """ 

455 A storage for packages. 

456 """ 

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

458 _name: str #: Package dependency graph name 

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

460 

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

462 """ 

463 Initializes the package storage. 

464 

465 :param name: Name of the package storage. 

466 :param graph: PackageDependencyGraph instance (parent). 

467 """ 

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

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

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

471 raise ex 

472 

473 self._name = name 

474 

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

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

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

478 raise ex 

479 

480 self._graph = graph 

481 graph._storages[name] = self 

482 

483 self._packages = {} 

484 

485 @readonly 

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

487 """ 

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

489 

490 :returns: Package dependency graph. 

491 """ 

492 return self._graph 

493 

494 @readonly 

495 def Name(self) -> str: 

496 """ 

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

498 

499 :returns: Name of the package dependency graph. 

500 """ 

501 return self._name 

502 

503 @readonly 

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

505 """ 

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

507 

508 :returns: Known packages dictionary. 

509 """ 

510 return self._packages 

511 

512 @readonly 

513 def PackageCount(self) -> int: 

514 return len(self._packages) 

515 

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

517 """ 

518 Create a new package in the package dependency graph. 

519 

520 :param packageName: Name of the new package. 

521 :returns: New package's instance. 

522 """ 

523 return Package(packageName, storage=self) 

524 

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

526 """ 

527 Create multiple new packages in the package dependency graph. 

528 

529 :param packageNames: List of package names. 

530 :returns: List of new package instances. 

531 """ 

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

533 

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

535 """ 

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

537 

538 :param packageName: Name of the new package. 

539 :param version: Version string. 

540 :returns: New package version instance. 

541 """ 

542 package = Package(packageName, storage=self) 

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

544 

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

546 """ 

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

548 

549 :param packageName: Name of the new package. 

550 :param versions: List of version string.s 

551 :returns: List of new package version instances. 

552 """ 

553 package = Package(packageName, storage=self) 

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

555 

556 def SortPackageVersions(self) -> None: 

557 """ 

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

559 """ 

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

561 package.SortVersions() 

562 

563 def __len__(self) -> int: 

564 """ 

565 Returns the number of known packages. 

566 

567 :returns: Number of packages. 

568 """ 

569 return len(self._packages) 

570 

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

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

573 

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

575 """ 

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

577 

578 :param name: Name of the package. 

579 :returns: The package. 

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

581 """ 

582 return self._packages[name] 

583 

584 def __str__(self) -> str: 

585 """ 

586 Return a string representation of this graph. 

587 

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

589 """ 

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

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

592 else: 

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

594 

595 

596@export 

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

598 """ 

599 A package dependency graph collecting all known packages. 

600 """ 

601 _name: str #: Package dependency graph name 

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

603 

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

605 """ 

606 Initializes the package dependency graph. 

607 

608 :param name: Name of the dependency graph. 

609 """ 

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

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

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

613 raise ex 

614 

615 self._name = name 

616 

617 self._storages = {} 

618 

619 @readonly 

620 def Name(self) -> str: 

621 """ 

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

623 

624 :returns: Name of the package dependency graph. 

625 """ 

626 return self._name 

627 

628 @readonly 

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

630 """ 

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

632 

633 :returns: Known package storage dictionary. 

634 """ 

635 return self._storages 

636 

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

638 # """ 

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

640 # 

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

642 # :returns: New package's instance. 

643 # """ 

644 # return Package(packageName, storage=self) 

645 # 

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

647 # """ 

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

649 # 

650 # :param packageNames: List of package names. 

651 # :returns: List of new package instances. 

652 # """ 

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

654 # 

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

656 # """ 

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

658 # 

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

660 # :param version: Version string. 

661 # :returns: New package version instance. 

662 # """ 

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

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

665 # 

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

667 # """ 

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

669 # 

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

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

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

673 # """ 

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

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

676 

677 def SortPackageVersions(self) -> None: 

678 """ 

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

680 """ 

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

682 storage.SortPackageVersions() 

683 

684 def __len__(self) -> int: 

685 """ 

686 Returns the number of known packages. 

687 

688 :returns: Number of packages. 

689 """ 

690 return len(self._storages) 

691 

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

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

694 

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

696 """ 

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

698 

699 :param name: Name of the package storage. 

700 :returns: The package storage. 

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

702 """ 

703 return self._storages[name] 

704 

705 def __str__(self) -> str: 

706 """ 

707 Return a string representation of this graph. 

708 

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

710 """ 

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

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

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

714 else: 

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