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
« 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.
34.. hint::
36 See :ref:`high-level help <DEPENDENCIES>` for explanations and usage examples.
38.. seealso::
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
49from datetime import datetime
50from typing import Optional as Nullable, Union, Iterable, Self, Iterator
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
60@export
61class DependencyException(ToolingException):
62 """Base-exception of all exceptions raised by :mod:`pyTooling.Dependency`."""
65@export
66class NoSessionAvailableException(DependencyException):
67 """
68 The operation needs a session to the package index, but no session was opened.
70 A session is created by the package index and handed to the objects it creates.
71 """
74@export
75class ProjectNotFoundException(DependencyException):
76 """The package index doesn't know a project of that name."""
79@export
80class ReleaseNotFoundException(DependencyException):
81 """The project exists in the package index, but not in the requested version."""
84@export
85class BrokenRequirementWarning(Warning):
86 """
87 A requirement carries an environment marker that matches none of the extras declared by the project.
89 Such a requirement can't be assigned to an extra, so it's not reachable through :attr:`Requirements`.
90 """
93@export
94class ReleaseDetailsWarning(Warning):
95 """Downloading the details of a release failed, therefore the release was dropped from the project."""
98@export
99class PackageVersion(metaclass=ExtendedType, slots=True):
100 """
101 The package's version of a :class:`Package`.
103 A :class:`Package` has multiple available versions. A version can have multiple dependencies to other
104 :class:`PackageVersion`s.
105 """
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.
111 _dependsOn: dict[Package, dict[SemanticVersion, PackageVersion]] #: Versioned dependencies to other packages.
113 def __init__(self, version: SemanticVersion, package: Package, releasedAt: Nullable[datetime] = None) -> None:
114 """
115 Initializes a package version.
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}'.")
132 self._version = version
133 package._versions[version] = self
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
140 self._package = package
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
147 self._releasedAt = releasedAt
149 self._dependsOn = {}
151 @readonly
152 def Package(self) -> Package:
153 """
154 Read-only property to access the associated package.
156 :returns: Associated package.
157 """
158 return self._package
160 @readonly
161 def Version(self) -> SemanticVersion:
162 """
163 Read-only property to access the semantic version of a package.
165 :returns: Semantic version of a package.
166 """
167 return self._version
169 @readonly
170 def ReleasedAt(self) -> Nullable[datetime]:
171 """
172 Read-only property to access the release date and time.
174 :returns: Optional release date and time.
175 """
176 return self._releasedAt
178 @readonly
179 def DependsOn(self) -> dict[Package, dict[SemanticVersion, PackageVersion]]:
180 """
181 Read-only property to access the dictionary of dictionaries referencing dependencies.
183 The outer dictionary key groups dependencies by :class:`Package`. |br|
184 The inner dictionary key accesses dependencies by :class:`~pyTooling.Versioning.SemanticVersion`.
186 :returns: Dictionary of dependencies.
187 """
188 return self._dependsOn
190 def AddDependencyToPackageVersion(self, packageVersion: PackageVersion) -> None:
191 """
192 Add a dependency from current package version to another package version.
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}
205 def AddDependencyToPackageVersions(self, packageVersions: Iterable[PackageVersion]) -> None:
206 """
207 Add multiple dependencies from current package version to a list of other package versions.
209 :param packageVersions: Dependencies to be added.
210 """
211 # TODO: check for iterable
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}
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.
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
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#
253 packageVersion = package._versions[v]
254 self.AddDependencyToPackageVersion(packageVersion)
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
262 packageVersion = package._versions[version]
263 self.AddDependencyToPackageVersion(packageVersion)
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.
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
276 def SolveLatest(self) -> Iterable[PackageVersion]:
277 """
278 Solve the dependency problem, while using preferably latest versions.
280 .. todo::
282 Describe algorithm.
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}
289 def _recursion(currentSolution: dict[Package, PackageVersion]) -> bool:
290 """
291 Nested function for recursion.
293 It adds the latest matching version of every package the current solution requires, and recurses until no
294 package is missing.
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())
304 # 2. Identify which required packages are missing from the solution
305 missingPackages = requiredPackages - currentSolution.keys()
307 # Base Case: If no packages are missing, the graph is complete and valid
308 if len(missingPackages) == 0:
309 return True
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))
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
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())
325 if allowedVersions is None:
326 allowedVersions = parentConstraints
327 else:
328 # Intersect with existing constraints (must satisfy everyone)
329 allowedVersions &= parentConstraints
331 # If the intersection is empty, no version satisfies all parents -> backtrack
332 if not allowedVersions:
333 return False
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]
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
351 if isCompatible:
352 # Tentatively add to solution
353 currentSolution[targetPackage] = candidate
355 # Recurse
356 if _recursion(currentSolution):
357 return True
359 # If recursion failed, remove (backtrack) and try next version
360 del currentSolution[targetPackage]
362 # If we run out of versions for this package, this path is dead
363 return False
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}'.")
371 def __len__(self) -> int:
372 """
373 Returns the number of dependencies.
375 :returns: Number of dependencies.
376 """
377 return len(self._dependsOn)
379 def __str__(self) -> str:
380 """
381 Return a string representation of this package version.
383 :returns: The package's name and version.
384 """
385 return f"{self._package._name} - {self._version}"
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.
396 _versions: dict[SemanticVersion, PackageVersion] #: A dictionary of available versions for this package.
398 def __init__(self, name: str, *, storage: PackageStorage) -> None:
399 """
400 Initializes a package.
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
411 self._name = name
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
418 self._storage = storage
419 storage._packages[name] = self
421 self._versions = {}
423 @readonly
424 def Storage(self) -> PackageStorage:
425 """
426 Read-only property to access the package's storage.
428 :returns: Package storage.
429 """
430 return self._storage
432 @readonly
433 def Name(self) -> str:
434 """
435 Read-only property to access the package name.
437 :returns: Name of the package.
438 """
439 return self._name
441 @readonly
442 def Versions(self) -> dict[SemanticVersion, PackageVersion]:
443 """
444 Read-only property to access the dictionary of available versions.
446 :returns: Available version dictionary.
447 """
448 return self._versions
450 @readonly
451 def VersionCount(self) -> int:
452 """
453 Read-only property to return the number of versions this package has.
455 :returns: Number of versions.
456 """
457 return len(self._versions)
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)}
465 def __len__(self) -> int:
466 """
467 Returns the number of available versions.
469 :returns: Number of versions.
470 """
471 return len(self._versions)
473 def __iter__(self) -> Iterator[PackageVersion]:
474 """
475 Iterate the versions of this package.
477 :returns: An iterator over all versions of this package.
478 """
479 return iter(self._versions.values())
481 def __getitem__(self, version: str | SemanticVersion) -> PackageVersion:
482 """
483 Access a package version in the package by version string or semantic version.
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
497 return self._versions[version]
499 def __str__(self) -> str:
500 """
501 Return a string representation of this package.
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)})"
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.
520 def __init__(self, name: str, graph: PackageDependencyGraph) -> None:
521 """
522 Initializes the package storage.
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
533 self._name = name
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
540 self._graph = graph
541 graph._storages[name] = self
543 self._packages = {}
545 @readonly
546 def Graph(self) -> PackageDependencyGraph:
547 """
548 Read-only property to access the package dependency graph.
550 :returns: Package dependency graph.
551 """
552 return self._graph
554 @readonly
555 def Name(self) -> str:
556 """
557 Read-only property to access the package dependency graph's name.
559 :returns: Name of the package dependency graph.
560 """
561 return self._name
563 @readonly
564 def Packages(self) -> dict[str, Package]:
565 """
566 Read-only property to access the dictionary of known packages.
568 :returns: Known packages dictionary.
569 """
570 return self._packages
572 @readonly
573 def PackageCount(self) -> int:
574 """
575 Read-only property to return the number of packages in this storage.
577 :returns: Number of packages.
578 """
579 return len(self._packages)
581 def CreatePackage(self, packageName: str) -> Package:
582 """
583 Create a new package in the package dependency graph.
585 :param packageName: Name of the new package.
586 :returns: New package's instance.
587 """
588 return Package(packageName, storage=self)
590 def CreatePackages(self, packageNames: Iterable[str]) -> Iterable[Package]:
591 """
592 Create multiple new packages in the package dependency graph.
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]
599 def CreatePackageVersion(self, packageName: str, version: str) -> PackageVersion:
600 """
601 Create a new package and a package version in the package dependency graph.
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)
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.
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]
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()
628 def __len__(self) -> int:
629 """
630 Returns the number of known packages.
632 :returns: Number of packages.
633 """
634 return len(self._packages)
636 def __iter__(self) -> Iterator[Package]:
637 """
638 Iterate the packages in this storage.
640 :returns: An iterator over all packages in this storage.
641 """
642 return iter(self._packages.values())
644 def __getitem__(self, name: str) -> Package:
645 """
646 Access a known package in the package dependency graph by package name.
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]
654 def __str__(self) -> str:
655 """
656 Return a string representation of this graph.
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)})"
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.
674 def __init__(self, name: str) -> None:
675 """
676 Initializes the package dependency graph.
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
686 self._name = name
688 self._storages = {}
690 @readonly
691 def Name(self) -> str:
692 """
693 Read-only property to access the package dependency graph's name.
695 :returns: Name of the package dependency graph.
696 """
697 return self._name
699 @readonly
700 def Storages(self) -> dict[str, PackageStorage]:
701 """
702 Read-only property to access the dictionary of known package storages.
704 :returns: Known package storage dictionary.
705 """
706 return self._storages
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]
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()
755 def __len__(self) -> int:
756 """
757 Returns the number of known packages.
759 :returns: Number of packages.
760 """
761 return len(self._storages)
763 def __iter__(self) -> Iterator[PackageStorage]:
764 """
765 Iterate the storages in this dependency graph.
767 :returns: An iterator over all storages in this dependency graph.
768 """
769 return iter(self._storages.values())
771 def __getitem__(self, name: str) -> PackageStorage:
772 """
773 Access a known package storage in the package dependency graph by storage name.
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]
781 def __str__(self) -> str:
782 """
783 Return a string representation of this graph.
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})"