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

260 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-13 22:36 +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 

46 

47 

48@export 

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

50 """ 

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

52 

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

54 :class:`PackageVersion`s. 

55 """ 

56 

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

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

59 _releasedAt: Nullable[datetime] 

60 

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

62 

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

64 """ 

65 Initializes a package version. 

66 

67 :param version: Semantic version of this package. 

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

69 :param releasedAt: Optional release date and time. 

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

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

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

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

74 """ 

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

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

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

78 raise ex 

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

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

81 

82 self._version = version 

83 package._versions[version] = self 

84 

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

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

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

88 raise ex 

89 

90 self._package = package 

91 

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

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

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

95 raise ex 

96 

97 self._releasedAt = releasedAt 

98 

99 self._dependsOn = {} 

100 

101 @readonly 

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

103 """ 

104 Read-only property to access the associated package. 

105 

106 :returns: Associated package. 

107 """ 

108 return self._package 

109 

110 @readonly 

111 def Version(self) -> SemanticVersion: 

112 """ 

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

114 

115 :returns: Semantic version of a package. 

116 """ 

117 return self._version 

118 

119 @readonly 

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

121 """ 

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

123 

124 :returns: Optional release date and time. 

125 """ 

126 return self._releasedAt 

127 

128 @readonly 

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

130 """ 

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

132 

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

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

135 

136 :returns: Dictionary of dependencies. 

137 """ 

138 return self._dependsOn 

139 

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

141 """ 

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

143 

144 :param packageVersion: Dependency to be added. 

145 """ 

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

147 pack = self._dependsOn[package] 

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

149 pass 

150 else: 

151 pack[version] = packageVersion 

152 else: 

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

154 

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

156 """ 

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

158 

159 :param packageVersions: Dependencies to be added. 

160 """ 

161 # TODO: check for iterable 

162 

163 for packageVersion in packageVersions: 

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

165 pack = self._dependsOn[package] 

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

167 pass 

168 else: 

169 pack[version] = packageVersion 

170 else: 

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

172 

173 def AddDependencyTo( 

174 self, 

175 package: Union[str, Package], 

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

177 ) -> None: 

178 """ 

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

180 

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

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

183 :return: 

184 """ 

185 if isinstance(package, str): 

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

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

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

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

190 raise ex 

191 

192 if isinstance(version, str): 

193 version = SemanticVersion.Parse(version) 

194 elif isinstance(version, Iterable): 

195 for v in version: 

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

197 v = SemanticVersion.Parse(v) 

198 elif not isinstance(v, SemanticVersion): 

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

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

201 raise ex# 

202 

203 packageVersion = package._versions[v] 

204 self.AddDependencyToPackageVersion(packageVersion) 

205 

206 return 

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

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

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

210 raise ex 

211 

212 packageVersion = package._versions[version] 

213 self.AddDependencyToPackageVersion(packageVersion) 

214 

215 def SortDependencies(self) -> Self: 

216 """ 

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

218 dictionaries. 

219 

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

221 """ 

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

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

224 return self 

225 

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

227 """ 

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

229 

230 .. todo:: 

231 

232 Describe algorithm. 

233 

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

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

236 """ 

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

238 

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

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

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

242 for packageVersion in currentSolution.values(): 

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

244 

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

246 missingPackages = requiredPackages - currentSolution.keys() 

247 

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

249 if len(missingPackages) == 0: 

250 return True 

251 

252 # 3. Pick the next package to resolve 

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

254 targetPackage = next(iter(missingPackages)) 

255 

256 # 4. Determine valid candidates 

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

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

259 

260 for parentPackageVersion in currentSolution.values(): 

261 if targetPackage in parentPackageVersion.DependsOn: 

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

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

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

265 

266 if allowedVersions is None: 

267 allowedVersions = parentConstraints 

268 else: 

269 # Intersect with existing constraints (must satisfy everyone) 

270 allowedVersions &= parentConstraints 

271 

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

273 if not allowedVersions: 

274 return False 

275 

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

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

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

279 candidate = targetPackage.Versions[version_key] 

280 

281 # 6. Check compatibility (reverse dependencies) 

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

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

284 isCompatible = True 

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

286 if existingPackage in candidate.DependsOn: 

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

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

289 isCompatible = False 

290 break 

291 

292 if isCompatible: 

293 # Tentatively add to solution 

294 currentSolution[targetPackage] = candidate 

295 

296 # Recurse 

297 if _recursion(currentSolution): 

298 return True 

299 

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

301 del currentSolution[targetPackage] 

302 

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

304 return False 

305 

306 # Run the solver 

307 if _recursion(solution): 

308 return list(solution.values()) 

309 else: 

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

311 

312 def __len__(self) -> int: 

313 """ 

314 Returns the number of dependencies. 

315 

316 :returns: Number of dependencies. 

317 """ 

318 return len(self._dependsOn) 

319 

320 def __str__(self) -> str: 

321 """ 

322 Return a string representation of this package version. 

323 

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

325 """ 

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

327 

328 

329@export 

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

331 """ 

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

333 """ 

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

335 _name: str #: Name of the package. 

336 

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

338 

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

340 """ 

341 Initializes a package. 

342 

343 :param name: Name of the package. 

344 :param storage: The package's storage. 

345 """ 

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

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

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

349 raise ex 

350 

351 self._name = name 

352 

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

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

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

356 raise ex 

357 

358 self._storage = storage 

359 storage._packages[name] = self 

360 

361 self._versions = {} 

362 

363 @readonly 

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

365 """ 

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

367 

368 :returns: Package storage. 

369 """ 

370 return self._storage 

371 

372 @readonly 

373 def Name(self) -> str: 

374 """ 

375 Read-only property to access the package name. 

376 

377 :returns: Name of the package. 

378 """ 

379 return self._name 

380 

381 @readonly 

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

383 """ 

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

385 

386 :returns: Available version dictionary. 

387 """ 

388 return self._versions 

389 

390 @readonly 

391 def VersionCount(self) -> int: 

392 return len(self._versions) 

393 

394 def SortVersions(self) -> None: 

395 """ 

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

397 """ 

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

399 

400 def __len__(self) -> int: 

401 """ 

402 Returns the number of available versions. 

403 

404 :returns: Number of versions. 

405 """ 

406 return len(self._versions) 

407 

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

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

410 

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

412 """ 

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

414 

415 :param version: Version as string or instance. 

416 :returns: The package version. 

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

418 """ 

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

420 version = SemanticVersion.Parse(version) 

421 elif not isinstance(version, SemanticVersion): 

422 # TODO: raise proper type error 

423 raise TypeError() 

424 

425 return self._versions[version] 

426 

427 def __str__(self) -> str: 

428 """ 

429 Return a string representation of this package. 

430 

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

432 """ 

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

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

435 else: 

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

437 

438 

439@export 

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

441 """ 

442 A storage for packages. 

443 """ 

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

445 _name: str #: Package dependency graph name 

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

447 

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

449 """ 

450 Initializes the package storage. 

451 

452 :param name: Name of the package storage. 

453 :param graph: PackageDependencyGraph instance (parent). 

454 """ 

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

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

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

458 raise ex 

459 

460 self._name = name 

461 

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

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

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

465 raise ex 

466 

467 self._graph = graph 

468 graph._storages[name] = self 

469 

470 self._packages = {} 

471 

472 @readonly 

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

474 """ 

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

476 

477 :returns: Package dependency graph. 

478 """ 

479 return self._graph 

480 

481 @readonly 

482 def Name(self) -> str: 

483 """ 

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

485 

486 :returns: Name of the package dependency graph. 

487 """ 

488 return self._name 

489 

490 @readonly 

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

492 """ 

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

494 

495 :returns: Known packages dictionary. 

496 """ 

497 return self._packages 

498 

499 @readonly 

500 def PackageCount(self) -> int: 

501 return len(self._packages) 

502 

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

504 """ 

505 Create a new package in the package dependency graph. 

506 

507 :param packageName: Name of the new package. 

508 :returns: New package's instance. 

509 """ 

510 return Package(packageName, storage=self) 

511 

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

513 """ 

514 Create multiple new packages in the package dependency graph. 

515 

516 :param packageNames: List of package names. 

517 :returns: List of new package instances. 

518 """ 

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

520 

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

522 """ 

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

524 

525 :param packageName: Name of the new package. 

526 :param version: Version string. 

527 :returns: New package version instance. 

528 """ 

529 package = Package(packageName, storage=self) 

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

531 

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

533 """ 

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

535 

536 :param packageName: Name of the new package. 

537 :param versions: List of version string.s 

538 :returns: List of new package version instances. 

539 """ 

540 package = Package(packageName, storage=self) 

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

542 

543 def SortPackageVersions(self) -> None: 

544 """ 

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

546 """ 

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

548 package.SortVersions() 

549 

550 def __len__(self) -> int: 

551 """ 

552 Returns the number of known packages. 

553 

554 :returns: Number of packages. 

555 """ 

556 return len(self._packages) 

557 

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

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

560 

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

562 """ 

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

564 

565 :param name: Name of the package. 

566 :returns: The package. 

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

568 """ 

569 return self._packages[name] 

570 

571 def __str__(self) -> str: 

572 """ 

573 Return a string representation of this graph. 

574 

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

576 """ 

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

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

579 else: 

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

581 

582 

583@export 

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

585 """ 

586 A package dependency graph collecting all known packages. 

587 """ 

588 _name: str #: Package dependency graph name 

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

590 

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

592 """ 

593 Initializes the package dependency graph. 

594 

595 :param name: Name of the dependency graph. 

596 """ 

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

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

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

600 raise ex 

601 

602 self._name = name 

603 

604 self._storages = {} 

605 

606 @readonly 

607 def Name(self) -> str: 

608 """ 

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

610 

611 :returns: Name of the package dependency graph. 

612 """ 

613 return self._name 

614 

615 @readonly 

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

617 """ 

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

619 

620 :returns: Known package storage dictionary. 

621 """ 

622 return self._storages 

623 

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

625 # """ 

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

627 # 

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

629 # :returns: New package's instance. 

630 # """ 

631 # return Package(packageName, storage=self) 

632 # 

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

634 # """ 

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

636 # 

637 # :param packageNames: List of package names. 

638 # :returns: List of new package instances. 

639 # """ 

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

641 # 

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

643 # """ 

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

645 # 

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

647 # :param version: Version string. 

648 # :returns: New package version instance. 

649 # """ 

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

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

652 # 

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

654 # """ 

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

656 # 

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

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

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

660 # """ 

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

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

663 

664 def SortPackageVersions(self) -> None: 

665 """ 

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

667 """ 

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

669 storage.SortPackageVersions() 

670 

671 def __len__(self) -> int: 

672 """ 

673 Returns the number of known packages. 

674 

675 :returns: Number of packages. 

676 """ 

677 return len(self._storages) 

678 

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

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

681 

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

683 """ 

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

685 

686 :param name: Name of the package storage. 

687 :returns: The package storage. 

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

689 """ 

690 return self._storages[name] 

691 

692 def __str__(self) -> str: 

693 """ 

694 Return a string representation of this graph. 

695 

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

697 """ 

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

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

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

701 else: 

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