pip 25.1__py3-none-any.whl → 25.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pip/__init__.py +3 -3
- pip/_internal/__init__.py +2 -2
- pip/_internal/build_env.py +118 -94
- pip/_internal/cache.py +16 -14
- pip/_internal/cli/autocompletion.py +13 -4
- pip/_internal/cli/base_command.py +18 -7
- pip/_internal/cli/cmdoptions.py +14 -9
- pip/_internal/cli/command_context.py +4 -3
- pip/_internal/cli/index_command.py +11 -9
- pip/_internal/cli/main.py +3 -2
- pip/_internal/cli/main_parser.py +4 -3
- pip/_internal/cli/parser.py +26 -22
- pip/_internal/cli/progress_bars.py +19 -12
- pip/_internal/cli/req_command.py +16 -12
- pip/_internal/cli/spinners.py +81 -5
- pip/_internal/commands/__init__.py +5 -3
- pip/_internal/commands/cache.py +18 -15
- pip/_internal/commands/check.py +1 -2
- pip/_internal/commands/completion.py +1 -2
- pip/_internal/commands/configuration.py +26 -18
- pip/_internal/commands/debug.py +8 -6
- pip/_internal/commands/download.py +2 -3
- pip/_internal/commands/freeze.py +2 -3
- pip/_internal/commands/hash.py +1 -2
- pip/_internal/commands/help.py +1 -2
- pip/_internal/commands/index.py +15 -9
- pip/_internal/commands/inspect.py +4 -4
- pip/_internal/commands/install.py +45 -40
- pip/_internal/commands/list.py +35 -26
- pip/_internal/commands/lock.py +1 -2
- pip/_internal/commands/search.py +14 -12
- pip/_internal/commands/show.py +14 -11
- pip/_internal/commands/uninstall.py +1 -2
- pip/_internal/commands/wheel.py +2 -3
- pip/_internal/configuration.py +39 -25
- pip/_internal/distributions/base.py +6 -4
- pip/_internal/distributions/installed.py +8 -4
- pip/_internal/distributions/sdist.py +20 -13
- pip/_internal/distributions/wheel.py +6 -4
- pip/_internal/exceptions.py +58 -39
- pip/_internal/index/collector.py +24 -29
- pip/_internal/index/package_finder.py +70 -61
- pip/_internal/index/sources.py +17 -14
- pip/_internal/locations/__init__.py +18 -16
- pip/_internal/locations/_distutils.py +12 -11
- pip/_internal/locations/_sysconfig.py +5 -4
- pip/_internal/locations/base.py +4 -3
- pip/_internal/main.py +2 -2
- pip/_internal/metadata/__init__.py +8 -6
- pip/_internal/metadata/_json.py +5 -4
- pip/_internal/metadata/base.py +22 -27
- pip/_internal/metadata/importlib/_compat.py +6 -4
- pip/_internal/metadata/importlib/_dists.py +12 -17
- pip/_internal/metadata/importlib/_envs.py +9 -6
- pip/_internal/metadata/pkg_resources.py +11 -14
- pip/_internal/models/direct_url.py +24 -21
- pip/_internal/models/format_control.py +5 -5
- pip/_internal/models/installation_report.py +4 -3
- pip/_internal/models/link.py +39 -34
- pip/_internal/models/pylock.py +27 -22
- pip/_internal/models/search_scope.py +6 -7
- pip/_internal/models/selection_prefs.py +3 -3
- pip/_internal/models/target_python.py +10 -9
- pip/_internal/models/wheel.py +7 -5
- pip/_internal/network/auth.py +20 -22
- pip/_internal/network/cache.py +22 -6
- pip/_internal/network/download.py +169 -141
- pip/_internal/network/lazy_wheel.py +10 -7
- pip/_internal/network/session.py +32 -27
- pip/_internal/network/utils.py +2 -2
- pip/_internal/network/xmlrpc.py +2 -2
- pip/_internal/operations/build/build_tracker.py +10 -8
- pip/_internal/operations/build/wheel.py +3 -2
- pip/_internal/operations/build/wheel_editable.py +3 -2
- pip/_internal/operations/build/wheel_legacy.py +9 -8
- pip/_internal/operations/check.py +21 -26
- pip/_internal/operations/freeze.py +12 -9
- pip/_internal/operations/install/editable_legacy.py +5 -3
- pip/_internal/operations/install/wheel.py +53 -44
- pip/_internal/operations/prepare.py +35 -30
- pip/_internal/pyproject.py +7 -10
- pip/_internal/req/__init__.py +12 -10
- pip/_internal/req/constructors.py +33 -31
- pip/_internal/req/req_dependency_group.py +9 -8
- pip/_internal/req/req_file.py +32 -35
- pip/_internal/req/req_install.py +37 -34
- pip/_internal/req/req_set.py +4 -5
- pip/_internal/req/req_uninstall.py +20 -17
- pip/_internal/resolution/base.py +3 -3
- pip/_internal/resolution/legacy/resolver.py +21 -20
- pip/_internal/resolution/resolvelib/base.py +16 -13
- pip/_internal/resolution/resolvelib/candidates.py +29 -26
- pip/_internal/resolution/resolvelib/factory.py +41 -50
- pip/_internal/resolution/resolvelib/found_candidates.py +11 -9
- pip/_internal/resolution/resolvelib/provider.py +15 -20
- pip/_internal/resolution/resolvelib/reporter.py +5 -3
- pip/_internal/resolution/resolvelib/requirements.py +8 -6
- pip/_internal/resolution/resolvelib/resolver.py +39 -23
- pip/_internal/self_outdated_check.py +8 -6
- pip/_internal/utils/appdirs.py +1 -2
- pip/_internal/utils/compat.py +7 -1
- pip/_internal/utils/compatibility_tags.py +17 -16
- pip/_internal/utils/deprecation.py +11 -9
- pip/_internal/utils/direct_url_helpers.py +2 -2
- pip/_internal/utils/egg_link.py +6 -5
- pip/_internal/utils/entrypoints.py +3 -2
- pip/_internal/utils/filesystem.py +8 -5
- pip/_internal/utils/filetypes.py +4 -6
- pip/_internal/utils/glibc.py +6 -5
- pip/_internal/utils/hashes.py +9 -6
- pip/_internal/utils/logging.py +8 -5
- pip/_internal/utils/misc.py +54 -44
- pip/_internal/utils/packaging.py +3 -2
- pip/_internal/utils/retry.py +7 -4
- pip/_internal/utils/setuptools_build.py +12 -10
- pip/_internal/utils/subprocess.py +20 -17
- pip/_internal/utils/temp_dir.py +10 -12
- pip/_internal/utils/unpacking.py +6 -4
- pip/_internal/utils/urls.py +1 -1
- pip/_internal/utils/virtualenv.py +3 -2
- pip/_internal/utils/wheel.py +3 -4
- pip/_internal/vcs/bazaar.py +26 -8
- pip/_internal/vcs/git.py +59 -24
- pip/_internal/vcs/mercurial.py +34 -11
- pip/_internal/vcs/subversion.py +27 -16
- pip/_internal/vcs/versioncontrol.py +56 -51
- pip/_internal/wheel_builder.py +14 -12
- pip/_vendor/cachecontrol/__init__.py +1 -1
- pip/_vendor/certifi/__init__.py +1 -1
- pip/_vendor/certifi/cacert.pem +102 -221
- pip/_vendor/certifi/core.py +1 -32
- pip/_vendor/dependency_groups/_implementation.py +7 -11
- pip/_vendor/distlib/__init__.py +2 -2
- pip/_vendor/distlib/scripts.py +1 -1
- pip/_vendor/msgpack/__init__.py +2 -2
- pip/_vendor/pkg_resources/__init__.py +1 -1
- pip/_vendor/platformdirs/version.py +2 -2
- pip/_vendor/pygments/__init__.py +1 -1
- pip/_vendor/requests/__version__.py +2 -2
- pip/_vendor/requests/compat.py +12 -0
- pip/_vendor/requests/models.py +3 -1
- pip/_vendor/requests/utils.py +6 -16
- pip/_vendor/resolvelib/__init__.py +3 -3
- pip/_vendor/resolvelib/reporters.py +1 -1
- pip/_vendor/resolvelib/resolvers/__init__.py +4 -4
- pip/_vendor/resolvelib/resolvers/resolution.py +91 -10
- pip/_vendor/rich/__main__.py +12 -40
- pip/_vendor/rich/_inspect.py +1 -1
- pip/_vendor/rich/_ratio.py +1 -7
- pip/_vendor/rich/align.py +1 -7
- pip/_vendor/rich/box.py +1 -7
- pip/_vendor/rich/console.py +25 -20
- pip/_vendor/rich/control.py +1 -7
- pip/_vendor/rich/diagnose.py +1 -0
- pip/_vendor/rich/emoji.py +1 -6
- pip/_vendor/rich/live.py +32 -7
- pip/_vendor/rich/live_render.py +1 -7
- pip/_vendor/rich/logging.py +1 -1
- pip/_vendor/rich/panel.py +3 -4
- pip/_vendor/rich/progress.py +15 -15
- pip/_vendor/rich/spinner.py +7 -13
- pip/_vendor/rich/syntax.py +24 -5
- pip/_vendor/rich/traceback.py +32 -17
- pip/_vendor/truststore/_api.py +1 -1
- pip/_vendor/vendor.txt +10 -11
- {pip-25.1.dist-info → pip-25.2.dist-info}/METADATA +26 -4
- {pip-25.1.dist-info → pip-25.2.dist-info}/RECORD +194 -181
- {pip-25.1.dist-info → pip-25.2.dist-info}/WHEEL +1 -1
- {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/AUTHORS.txt +12 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +13 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/certifi/LICENSE +20 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +9 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/distlib/LICENSE.txt +284 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/distro/LICENSE +202 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/idna/LICENSE.md +31 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/msgpack/COPYING +14 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE +3 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +177 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.BSD +23 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/pkg_resources/LICENSE +17 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/platformdirs/LICENSE +21 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/pygments/LICENSE +25 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +21 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/requests/LICENSE +175 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/resolvelib/LICENSE +13 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/rich/LICENSE +19 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE +21 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER +3 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/tomli_w/LICENSE +21 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/truststore/LICENSE +21 -0
- pip-25.2.dist-info/licenses/src/pip/_vendor/urllib3/LICENSE.txt +21 -0
- pip/_vendor/distlib/database.py +0 -1329
- pip/_vendor/distlib/index.py +0 -508
- pip/_vendor/distlib/locators.py +0 -1295
- pip/_vendor/distlib/manifest.py +0 -384
- pip/_vendor/distlib/markers.py +0 -162
- pip/_vendor/distlib/metadata.py +0 -1031
- pip/_vendor/distlib/version.py +0 -750
- pip/_vendor/distlib/wheel.py +0 -1100
- pip/_vendor/typing_extensions.py +0 -4584
- {pip-25.1.dist-info → pip-25.2.dist-info}/entry_points.txt +0 -0
- {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/LICENSE.txt +0 -0
- {pip-25.1.dist-info → pip-25.2.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
import functools
|
|
3
5
|
import logging
|
|
6
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
4
7
|
from typing import (
|
|
5
8
|
TYPE_CHECKING,
|
|
6
9
|
Callable,
|
|
7
|
-
Dict,
|
|
8
|
-
FrozenSet,
|
|
9
|
-
Iterable,
|
|
10
|
-
Iterator,
|
|
11
|
-
List,
|
|
12
|
-
Mapping,
|
|
13
10
|
NamedTuple,
|
|
14
|
-
Optional,
|
|
15
11
|
Protocol,
|
|
16
|
-
Sequence,
|
|
17
|
-
Set,
|
|
18
|
-
Tuple,
|
|
19
12
|
TypeVar,
|
|
20
13
|
cast,
|
|
21
14
|
)
|
|
@@ -84,13 +77,13 @@ if TYPE_CHECKING:
|
|
|
84
77
|
logger = logging.getLogger(__name__)
|
|
85
78
|
|
|
86
79
|
C = TypeVar("C")
|
|
87
|
-
Cache =
|
|
80
|
+
Cache = dict[Link, C]
|
|
88
81
|
|
|
89
82
|
|
|
90
83
|
class CollectedRootRequirements(NamedTuple):
|
|
91
|
-
requirements:
|
|
92
|
-
constraints:
|
|
93
|
-
user_requested:
|
|
84
|
+
requirements: list[Requirement]
|
|
85
|
+
constraints: dict[str, Constraint]
|
|
86
|
+
user_requested: dict[str, int]
|
|
94
87
|
|
|
95
88
|
|
|
96
89
|
class Factory:
|
|
@@ -99,12 +92,12 @@ class Factory:
|
|
|
99
92
|
finder: PackageFinder,
|
|
100
93
|
preparer: RequirementPreparer,
|
|
101
94
|
make_install_req: InstallRequirementProvider,
|
|
102
|
-
wheel_cache:
|
|
95
|
+
wheel_cache: WheelCache | None,
|
|
103
96
|
use_user_site: bool,
|
|
104
97
|
force_reinstall: bool,
|
|
105
98
|
ignore_installed: bool,
|
|
106
99
|
ignore_requires_python: bool,
|
|
107
|
-
py_version_info:
|
|
100
|
+
py_version_info: tuple[int, ...] | None = None,
|
|
108
101
|
) -> None:
|
|
109
102
|
self._finder = finder
|
|
110
103
|
self.preparer = preparer
|
|
@@ -118,9 +111,9 @@ class Factory:
|
|
|
118
111
|
self._build_failures: Cache[InstallationError] = {}
|
|
119
112
|
self._link_candidate_cache: Cache[LinkCandidate] = {}
|
|
120
113
|
self._editable_candidate_cache: Cache[EditableCandidate] = {}
|
|
121
|
-
self._installed_candidate_cache:
|
|
122
|
-
self._extras_candidate_cache:
|
|
123
|
-
|
|
114
|
+
self._installed_candidate_cache: dict[str, AlreadyInstalledCandidate] = {}
|
|
115
|
+
self._extras_candidate_cache: dict[
|
|
116
|
+
tuple[int, frozenset[NormalizedName]], ExtrasCandidate
|
|
124
117
|
] = {}
|
|
125
118
|
self._supported_tags_cache = get_supported()
|
|
126
119
|
|
|
@@ -149,9 +142,9 @@ class Factory:
|
|
|
149
142
|
def _make_extras_candidate(
|
|
150
143
|
self,
|
|
151
144
|
base: BaseCandidate,
|
|
152
|
-
extras:
|
|
145
|
+
extras: frozenset[str],
|
|
153
146
|
*,
|
|
154
|
-
comes_from:
|
|
147
|
+
comes_from: InstallRequirement | None = None,
|
|
155
148
|
) -> ExtrasCandidate:
|
|
156
149
|
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
|
|
157
150
|
try:
|
|
@@ -164,7 +157,7 @@ class Factory:
|
|
|
164
157
|
def _make_candidate_from_dist(
|
|
165
158
|
self,
|
|
166
159
|
dist: BaseDistribution,
|
|
167
|
-
extras:
|
|
160
|
+
extras: frozenset[str],
|
|
168
161
|
template: InstallRequirement,
|
|
169
162
|
) -> Candidate:
|
|
170
163
|
try:
|
|
@@ -179,12 +172,12 @@ class Factory:
|
|
|
179
172
|
def _make_candidate_from_link(
|
|
180
173
|
self,
|
|
181
174
|
link: Link,
|
|
182
|
-
extras:
|
|
175
|
+
extras: frozenset[str],
|
|
183
176
|
template: InstallRequirement,
|
|
184
|
-
name:
|
|
185
|
-
version:
|
|
186
|
-
) ->
|
|
187
|
-
base:
|
|
177
|
+
name: NormalizedName | None,
|
|
178
|
+
version: Version | None,
|
|
179
|
+
) -> Candidate | None:
|
|
180
|
+
base: BaseCandidate | None = self._make_base_candidate_from_link(
|
|
188
181
|
link, template, name, version
|
|
189
182
|
)
|
|
190
183
|
if not extras or base is None:
|
|
@@ -195,9 +188,9 @@ class Factory:
|
|
|
195
188
|
self,
|
|
196
189
|
link: Link,
|
|
197
190
|
template: InstallRequirement,
|
|
198
|
-
name:
|
|
199
|
-
version:
|
|
200
|
-
) ->
|
|
191
|
+
name: NormalizedName | None,
|
|
192
|
+
version: Version | None,
|
|
193
|
+
) -> BaseCandidate | None:
|
|
201
194
|
# TODO: Check already installed candidate, and use it if the link and
|
|
202
195
|
# editable flag match.
|
|
203
196
|
|
|
@@ -254,7 +247,7 @@ class Factory:
|
|
|
254
247
|
specifier: SpecifierSet,
|
|
255
248
|
hashes: Hashes,
|
|
256
249
|
prefers_installed: bool,
|
|
257
|
-
incompatible_ids:
|
|
250
|
+
incompatible_ids: set[int],
|
|
258
251
|
) -> Iterable[Candidate]:
|
|
259
252
|
if not ireqs:
|
|
260
253
|
return ()
|
|
@@ -267,14 +260,14 @@ class Factory:
|
|
|
267
260
|
assert template.req, "Candidates found on index must be PEP 508"
|
|
268
261
|
name = canonicalize_name(template.req.name)
|
|
269
262
|
|
|
270
|
-
extras:
|
|
263
|
+
extras: frozenset[str] = frozenset()
|
|
271
264
|
for ireq in ireqs:
|
|
272
265
|
assert ireq.req, "Candidates found on index must be PEP 508"
|
|
273
266
|
specifier &= ireq.req.specifier
|
|
274
267
|
hashes &= ireq.hashes(trust_internet=False)
|
|
275
268
|
extras |= frozenset(ireq.extras)
|
|
276
269
|
|
|
277
|
-
def _get_installed_candidate() ->
|
|
270
|
+
def _get_installed_candidate() -> Candidate | None:
|
|
278
271
|
"""Get the candidate for the currently-installed version."""
|
|
279
272
|
# If --force-reinstall is set, we want the version from the index
|
|
280
273
|
# instead, so we "pretend" there is nothing installed.
|
|
@@ -353,7 +346,7 @@ class Factory:
|
|
|
353
346
|
def _iter_explicit_candidates_from_base(
|
|
354
347
|
self,
|
|
355
348
|
base_requirements: Iterable[Requirement],
|
|
356
|
-
extras:
|
|
349
|
+
extras: frozenset[str],
|
|
357
350
|
) -> Iterator[Candidate]:
|
|
358
351
|
"""Produce explicit candidates from the base given an extra-ed package.
|
|
359
352
|
|
|
@@ -404,8 +397,8 @@ class Factory:
|
|
|
404
397
|
is_satisfied_by: Callable[[Requirement, Candidate], bool],
|
|
405
398
|
) -> Iterable[Candidate]:
|
|
406
399
|
# Collect basic lookup information from the requirements.
|
|
407
|
-
explicit_candidates:
|
|
408
|
-
ireqs:
|
|
400
|
+
explicit_candidates: set[Candidate] = set()
|
|
401
|
+
ireqs: list[InstallRequirement] = []
|
|
409
402
|
for req in requirements[identifier]:
|
|
410
403
|
cand, ireq = req.get_candidate_lookup()
|
|
411
404
|
if cand is not None:
|
|
@@ -524,7 +517,7 @@ class Factory:
|
|
|
524
517
|
)
|
|
525
518
|
|
|
526
519
|
def collect_root_requirements(
|
|
527
|
-
self, root_ireqs:
|
|
520
|
+
self, root_ireqs: list[InstallRequirement]
|
|
528
521
|
) -> CollectedRootRequirements:
|
|
529
522
|
collected = CollectedRootRequirements([], {}, {})
|
|
530
523
|
for i, ireq in enumerate(root_ireqs):
|
|
@@ -573,7 +566,7 @@ class Factory:
|
|
|
573
566
|
def make_requirements_from_spec(
|
|
574
567
|
self,
|
|
575
568
|
specifier: str,
|
|
576
|
-
comes_from:
|
|
569
|
+
comes_from: InstallRequirement | None,
|
|
577
570
|
requested_extras: Iterable[str] = (),
|
|
578
571
|
) -> Iterator[Requirement]:
|
|
579
572
|
"""
|
|
@@ -591,7 +584,7 @@ class Factory:
|
|
|
591
584
|
def make_requires_python_requirement(
|
|
592
585
|
self,
|
|
593
586
|
specifier: SpecifierSet,
|
|
594
|
-
) ->
|
|
587
|
+
) -> Requirement | None:
|
|
595
588
|
if self._ignore_requires_python:
|
|
596
589
|
return None
|
|
597
590
|
# Don't bother creating a dependency for an empty Requires-Python.
|
|
@@ -599,9 +592,7 @@ class Factory:
|
|
|
599
592
|
return None
|
|
600
593
|
return RequiresPythonRequirement(specifier, self._python_candidate)
|
|
601
594
|
|
|
602
|
-
def get_wheel_cache_entry(
|
|
603
|
-
self, link: Link, name: Optional[str]
|
|
604
|
-
) -> Optional[CacheEntry]:
|
|
595
|
+
def get_wheel_cache_entry(self, link: Link, name: str | None) -> CacheEntry | None:
|
|
605
596
|
"""Look up the link in the wheel cache.
|
|
606
597
|
|
|
607
598
|
If ``preparer.require_hashes`` is True, don't use the wheel cache,
|
|
@@ -618,7 +609,7 @@ class Factory:
|
|
|
618
609
|
supported_tags=self._supported_tags_cache,
|
|
619
610
|
)
|
|
620
611
|
|
|
621
|
-
def get_dist_to_uninstall(self, candidate: Candidate) ->
|
|
612
|
+
def get_dist_to_uninstall(self, candidate: Candidate) -> BaseDistribution | None:
|
|
622
613
|
# TODO: Are there more cases this needs to return True? Editable?
|
|
623
614
|
dist = self._installed_dists.get(candidate.project_name)
|
|
624
615
|
if dist is None: # Not installed, no uninstallation required.
|
|
@@ -647,7 +638,7 @@ class Factory:
|
|
|
647
638
|
return None
|
|
648
639
|
|
|
649
640
|
def _report_requires_python_error(
|
|
650
|
-
self, causes: Sequence[
|
|
641
|
+
self, causes: Sequence[ConflictCause]
|
|
651
642
|
) -> UnsupportedPythonVersion:
|
|
652
643
|
assert causes, "Requires-Python error reported with no cause"
|
|
653
644
|
|
|
@@ -669,7 +660,7 @@ class Factory:
|
|
|
669
660
|
return UnsupportedPythonVersion(message)
|
|
670
661
|
|
|
671
662
|
def _report_single_requirement_conflict(
|
|
672
|
-
self, req: Requirement, parent:
|
|
663
|
+
self, req: Requirement, parent: Candidate | None
|
|
673
664
|
) -> DistributionNotFound:
|
|
674
665
|
if parent is None:
|
|
675
666
|
req_disp = str(req)
|
|
@@ -679,8 +670,8 @@ class Factory:
|
|
|
679
670
|
cands = self._finder.find_all_candidates(req.project_name)
|
|
680
671
|
skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
|
|
681
672
|
|
|
682
|
-
versions_set:
|
|
683
|
-
yanked_versions_set:
|
|
673
|
+
versions_set: set[Version] = set()
|
|
674
|
+
yanked_versions_set: set[Version] = set()
|
|
684
675
|
for c in cands:
|
|
685
676
|
is_yanked = c.link.is_yanked if c.link else False
|
|
686
677
|
if is_yanked:
|
|
@@ -722,8 +713,8 @@ class Factory:
|
|
|
722
713
|
|
|
723
714
|
def get_installation_error(
|
|
724
715
|
self,
|
|
725
|
-
e:
|
|
726
|
-
constraints:
|
|
716
|
+
e: ResolutionImpossible[Requirement, Candidate],
|
|
717
|
+
constraints: dict[str, Constraint],
|
|
727
718
|
) -> InstallationError:
|
|
728
719
|
assert e.causes, "Installation error reported with no cause"
|
|
729
720
|
|
|
@@ -756,7 +747,7 @@ class Factory:
|
|
|
756
747
|
# satisfied at once.
|
|
757
748
|
|
|
758
749
|
# A couple of formatting helpers
|
|
759
|
-
def text_join(parts:
|
|
750
|
+
def text_join(parts: list[str]) -> str:
|
|
760
751
|
if len(parts) == 1:
|
|
761
752
|
return parts[0]
|
|
762
753
|
|
|
@@ -8,9 +8,11 @@ absolutely need, and not "download the world" when we only need one version of
|
|
|
8
8
|
something.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
11
13
|
import logging
|
|
12
|
-
from collections.abc import Sequence
|
|
13
|
-
from typing import Any, Callable,
|
|
14
|
+
from collections.abc import Iterator, Sequence
|
|
15
|
+
from typing import Any, Callable, Optional
|
|
14
16
|
|
|
15
17
|
from pip._vendor.packaging.version import _BaseVersion
|
|
16
18
|
|
|
@@ -20,7 +22,7 @@ from .base import Candidate
|
|
|
20
22
|
|
|
21
23
|
logger = logging.getLogger(__name__)
|
|
22
24
|
|
|
23
|
-
IndexCandidateInfo =
|
|
25
|
+
IndexCandidateInfo = tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
|
|
@@ -29,7 +31,7 @@ def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
|
|
|
29
31
|
This iterator is used when the package is not already installed. Candidates
|
|
30
32
|
from index come later in their normal ordering.
|
|
31
33
|
"""
|
|
32
|
-
versions_found:
|
|
34
|
+
versions_found: set[_BaseVersion] = set()
|
|
33
35
|
for version, func in infos:
|
|
34
36
|
if version in versions_found:
|
|
35
37
|
continue
|
|
@@ -65,7 +67,7 @@ def _iter_built_with_prepended(
|
|
|
65
67
|
normal ordering, except skipped when the version is already installed.
|
|
66
68
|
"""
|
|
67
69
|
yield installed
|
|
68
|
-
versions_found:
|
|
70
|
+
versions_found: set[_BaseVersion] = {installed.version}
|
|
69
71
|
for version, func in infos:
|
|
70
72
|
if version in versions_found:
|
|
71
73
|
continue
|
|
@@ -89,7 +91,7 @@ def _iter_built_with_inserted(
|
|
|
89
91
|
the installed candidate exactly once before we start yielding older or
|
|
90
92
|
equivalent candidates, or after all other candidates if they are all newer.
|
|
91
93
|
"""
|
|
92
|
-
versions_found:
|
|
94
|
+
versions_found: set[_BaseVersion] = set()
|
|
93
95
|
for version, func in infos:
|
|
94
96
|
if version in versions_found:
|
|
95
97
|
continue
|
|
@@ -120,15 +122,15 @@ class FoundCandidates(Sequence[Candidate]):
|
|
|
120
122
|
def __init__(
|
|
121
123
|
self,
|
|
122
124
|
get_infos: Callable[[], Iterator[IndexCandidateInfo]],
|
|
123
|
-
installed:
|
|
125
|
+
installed: Candidate | None,
|
|
124
126
|
prefers_installed: bool,
|
|
125
|
-
incompatible_ids:
|
|
127
|
+
incompatible_ids: set[int],
|
|
126
128
|
):
|
|
127
129
|
self._get_infos = get_infos
|
|
128
130
|
self._installed = installed
|
|
129
131
|
self._prefers_installed = prefers_installed
|
|
130
132
|
self._incompatible_ids = incompatible_ids
|
|
131
|
-
self._bool:
|
|
133
|
+
self._bool: bool | None = None
|
|
132
134
|
|
|
133
135
|
def __getitem__(self, index: Any) -> Any:
|
|
134
136
|
# Implemented to satisfy the ABC check. This is not needed by the
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import math
|
|
2
|
-
from
|
|
4
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
5
|
+
from functools import cache
|
|
3
6
|
from typing import (
|
|
4
7
|
TYPE_CHECKING,
|
|
5
|
-
Dict,
|
|
6
|
-
Iterable,
|
|
7
|
-
Iterator,
|
|
8
|
-
Mapping,
|
|
9
|
-
Optional,
|
|
10
|
-
Sequence,
|
|
11
|
-
Tuple,
|
|
12
8
|
TypeVar,
|
|
13
|
-
Union,
|
|
14
9
|
)
|
|
15
10
|
|
|
16
11
|
from pip._vendor.resolvelib.providers import AbstractProvider
|
|
@@ -59,7 +54,7 @@ def _get_with_identifier(
|
|
|
59
54
|
mapping: Mapping[str, V],
|
|
60
55
|
identifier: str,
|
|
61
56
|
default: D,
|
|
62
|
-
) ->
|
|
57
|
+
) -> D | V:
|
|
63
58
|
"""Get item from a package name lookup mapping with a resolver identifier.
|
|
64
59
|
|
|
65
60
|
This extra logic is needed when the target mapping is keyed by package
|
|
@@ -94,10 +89,10 @@ class PipProvider(_ProviderBase):
|
|
|
94
89
|
def __init__(
|
|
95
90
|
self,
|
|
96
91
|
factory: Factory,
|
|
97
|
-
constraints:
|
|
92
|
+
constraints: dict[str, Constraint],
|
|
98
93
|
ignore_dependencies: bool,
|
|
99
94
|
upgrade_strategy: str,
|
|
100
|
-
user_requested:
|
|
95
|
+
user_requested: dict[str, int],
|
|
101
96
|
) -> None:
|
|
102
97
|
self._factory = factory
|
|
103
98
|
self._constraints = constraints
|
|
@@ -105,7 +100,7 @@ class PipProvider(_ProviderBase):
|
|
|
105
100
|
self._upgrade_strategy = upgrade_strategy
|
|
106
101
|
self._user_requested = user_requested
|
|
107
102
|
|
|
108
|
-
def identify(self, requirement_or_candidate:
|
|
103
|
+
def identify(self, requirement_or_candidate: Requirement | Candidate) -> str:
|
|
109
104
|
return requirement_or_candidate.name
|
|
110
105
|
|
|
111
106
|
def narrow_requirement_selection(
|
|
@@ -113,8 +108,8 @@ class PipProvider(_ProviderBase):
|
|
|
113
108
|
identifiers: Iterable[str],
|
|
114
109
|
resolutions: Mapping[str, Candidate],
|
|
115
110
|
candidates: Mapping[str, Iterator[Candidate]],
|
|
116
|
-
information: Mapping[str, Iterator[
|
|
117
|
-
backtrack_causes: Sequence[
|
|
111
|
+
information: Mapping[str, Iterator[PreferenceInformation]],
|
|
112
|
+
backtrack_causes: Sequence[PreferenceInformation],
|
|
118
113
|
) -> Iterable[str]:
|
|
119
114
|
"""Produce a subset of identifiers that should be considered before others.
|
|
120
115
|
|
|
@@ -156,9 +151,9 @@ class PipProvider(_ProviderBase):
|
|
|
156
151
|
identifier: str,
|
|
157
152
|
resolutions: Mapping[str, Candidate],
|
|
158
153
|
candidates: Mapping[str, Iterator[Candidate]],
|
|
159
|
-
information: Mapping[str, Iterable[
|
|
160
|
-
backtrack_causes: Sequence[
|
|
161
|
-
) ->
|
|
154
|
+
information: Mapping[str, Iterable[PreferenceInformation]],
|
|
155
|
+
backtrack_causes: Sequence[PreferenceInformation],
|
|
156
|
+
) -> Preference:
|
|
162
157
|
"""Produce a sort key for given requirement based on preference.
|
|
163
158
|
|
|
164
159
|
The lower the return value is, the more preferred this group of
|
|
@@ -192,7 +187,7 @@ class PipProvider(_ProviderBase):
|
|
|
192
187
|
|
|
193
188
|
if not has_information:
|
|
194
189
|
direct = False
|
|
195
|
-
ireqs:
|
|
190
|
+
ireqs: tuple[InstallRequirement | None, ...] = ()
|
|
196
191
|
else:
|
|
197
192
|
# Go through the information and for each requirement,
|
|
198
193
|
# check if it's explicit (e.g., a direct link) and get the
|
|
@@ -271,7 +266,7 @@ class PipProvider(_ProviderBase):
|
|
|
271
266
|
)
|
|
272
267
|
|
|
273
268
|
@staticmethod
|
|
274
|
-
@
|
|
269
|
+
@cache
|
|
275
270
|
def is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
|
|
276
271
|
return requirement.is_satisfied_by(candidate)
|
|
277
272
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from collections import defaultdict
|
|
2
4
|
from logging import getLogger
|
|
3
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
4
6
|
|
|
5
7
|
from pip._vendor.resolvelib.reporters import BaseReporter
|
|
6
8
|
|
|
@@ -11,7 +13,7 @@ logger = getLogger(__name__)
|
|
|
11
13
|
|
|
12
14
|
class PipReporter(BaseReporter[Requirement, Candidate, str]):
|
|
13
15
|
def __init__(self) -> None:
|
|
14
|
-
self.reject_count_by_package:
|
|
16
|
+
self.reject_count_by_package: defaultdict[str, int] = defaultdict(int)
|
|
15
17
|
|
|
16
18
|
self._messages_at_reject_count = {
|
|
17
19
|
1: (
|
|
@@ -72,7 +74,7 @@ class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]):
|
|
|
72
74
|
logger.info("Reporter.ending(%r)", state)
|
|
73
75
|
|
|
74
76
|
def adding_requirement(
|
|
75
|
-
self, requirement: Requirement, parent:
|
|
77
|
+
self, requirement: Requirement, parent: Candidate | None
|
|
76
78
|
) -> None:
|
|
77
79
|
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
|
|
78
80
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
5
|
from pip._vendor.packaging.specifiers import SpecifierSet
|
|
4
6
|
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
|
@@ -51,8 +53,8 @@ class SpecifierRequirement(Requirement):
|
|
|
51
53
|
def __init__(self, ireq: InstallRequirement) -> None:
|
|
52
54
|
assert ireq.link is None, "This is a link, not a specifier"
|
|
53
55
|
self._ireq = ireq
|
|
54
|
-
self._equal_cache:
|
|
55
|
-
self._hash:
|
|
56
|
+
self._equal_cache: str | None = None
|
|
57
|
+
self._hash: int | None = None
|
|
56
58
|
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
|
57
59
|
|
|
58
60
|
@property
|
|
@@ -128,8 +130,8 @@ class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
|
|
|
128
130
|
def __init__(self, ireq: InstallRequirement) -> None:
|
|
129
131
|
assert ireq.link is None, "This is a link, not a specifier"
|
|
130
132
|
self._ireq = install_req_drop_extras(ireq)
|
|
131
|
-
self._equal_cache:
|
|
132
|
-
self._hash:
|
|
133
|
+
self._equal_cache: str | None = None
|
|
134
|
+
self._hash: int | None = None
|
|
133
135
|
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
|
134
136
|
|
|
135
137
|
@property
|
|
@@ -159,7 +161,7 @@ class RequiresPythonRequirement(Requirement):
|
|
|
159
161
|
def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
|
|
160
162
|
self.specifier = specifier
|
|
161
163
|
self._specifier_string = str(specifier) # for faster __eq__
|
|
162
|
-
self._hash:
|
|
164
|
+
self._hash: int | None = None
|
|
163
165
|
self._candidate = match
|
|
164
166
|
|
|
165
167
|
def __str__(self) -> str:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
import functools
|
|
3
5
|
import logging
|
|
4
6
|
import os
|
|
5
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, cast
|
|
6
8
|
|
|
7
9
|
from pip._vendor.packaging.utils import canonicalize_name
|
|
8
10
|
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible, ResolutionTooDeep
|
|
@@ -43,7 +45,7 @@ class Resolver(BaseResolver):
|
|
|
43
45
|
self,
|
|
44
46
|
preparer: RequirementPreparer,
|
|
45
47
|
finder: PackageFinder,
|
|
46
|
-
wheel_cache:
|
|
48
|
+
wheel_cache: WheelCache | None,
|
|
47
49
|
make_install_req: InstallRequirementProvider,
|
|
48
50
|
use_user_site: bool,
|
|
49
51
|
ignore_dependencies: bool,
|
|
@@ -51,7 +53,7 @@ class Resolver(BaseResolver):
|
|
|
51
53
|
ignore_requires_python: bool,
|
|
52
54
|
force_reinstall: bool,
|
|
53
55
|
upgrade_strategy: str,
|
|
54
|
-
py_version_info:
|
|
56
|
+
py_version_info: tuple[int, ...] | None = None,
|
|
55
57
|
):
|
|
56
58
|
super().__init__()
|
|
57
59
|
assert upgrade_strategy in self._allowed_strategies
|
|
@@ -69,10 +71,10 @@ class Resolver(BaseResolver):
|
|
|
69
71
|
)
|
|
70
72
|
self.ignore_dependencies = ignore_dependencies
|
|
71
73
|
self.upgrade_strategy = upgrade_strategy
|
|
72
|
-
self._result:
|
|
74
|
+
self._result: Result | None = None
|
|
73
75
|
|
|
74
76
|
def resolve(
|
|
75
|
-
self, root_reqs:
|
|
77
|
+
self, root_reqs: list[InstallRequirement], check_supported_wheels: bool
|
|
76
78
|
) -> RequirementSet:
|
|
77
79
|
collected = self.factory.collect_root_requirements(root_reqs)
|
|
78
80
|
provider = PipProvider(
|
|
@@ -187,7 +189,7 @@ class Resolver(BaseResolver):
|
|
|
187
189
|
|
|
188
190
|
def get_installation_order(
|
|
189
191
|
self, req_set: RequirementSet
|
|
190
|
-
) ->
|
|
192
|
+
) -> list[InstallRequirement]:
|
|
191
193
|
"""Get order for installation of requirements in RequirementSet.
|
|
192
194
|
|
|
193
195
|
The returned list contains a requirement before another that depends on
|
|
@@ -218,8 +220,8 @@ class Resolver(BaseResolver):
|
|
|
218
220
|
|
|
219
221
|
|
|
220
222
|
def get_topological_weights(
|
|
221
|
-
graph:
|
|
222
|
-
) ->
|
|
223
|
+
graph: DirectedGraph[str | None], requirement_keys: set[str]
|
|
224
|
+
) -> dict[str | None, int]:
|
|
223
225
|
"""Assign weights to each node based on how "deep" they are.
|
|
224
226
|
|
|
225
227
|
This implementation may change at any point in the future without prior
|
|
@@ -245,14 +247,25 @@ def get_topological_weights(
|
|
|
245
247
|
We are only interested in the weights of packages that are in the
|
|
246
248
|
requirement_keys.
|
|
247
249
|
"""
|
|
248
|
-
path:
|
|
249
|
-
weights:
|
|
250
|
+
path: set[str | None] = set()
|
|
251
|
+
weights: dict[str | None, list[int]] = {}
|
|
250
252
|
|
|
251
|
-
def visit(node:
|
|
253
|
+
def visit(node: str | None) -> None:
|
|
252
254
|
if node in path:
|
|
253
255
|
# We hit a cycle, so we'll break it here.
|
|
254
256
|
return
|
|
255
257
|
|
|
258
|
+
# The walk is exponential and for pathologically connected graphs (which
|
|
259
|
+
# are the ones most likely to contain cycles in the first place) it can
|
|
260
|
+
# take until the heat-death of the universe. To counter this we limit
|
|
261
|
+
# the number of attempts to visit (i.e. traverse through) any given
|
|
262
|
+
# node. We choose a value here which gives decent enough coverage for
|
|
263
|
+
# fairly well behaved graphs, and still limits the walk complexity to be
|
|
264
|
+
# linear in nature.
|
|
265
|
+
cur_weights = weights.get(node, [])
|
|
266
|
+
if len(cur_weights) >= 5:
|
|
267
|
+
return
|
|
268
|
+
|
|
256
269
|
# Time to visit the children!
|
|
257
270
|
path.add(node)
|
|
258
271
|
for child in graph.iter_children(node):
|
|
@@ -262,14 +275,14 @@ def get_topological_weights(
|
|
|
262
275
|
if node not in requirement_keys:
|
|
263
276
|
return
|
|
264
277
|
|
|
265
|
-
|
|
266
|
-
weights[node] =
|
|
278
|
+
cur_weights.append(len(path))
|
|
279
|
+
weights[node] = cur_weights
|
|
267
280
|
|
|
268
|
-
# Simplify the graph, pruning leaves that have no dependencies.
|
|
269
|
-
#
|
|
270
|
-
#
|
|
281
|
+
# Simplify the graph, pruning leaves that have no dependencies. This is
|
|
282
|
+
# needed for large graphs (say over 200 packages) because the `visit`
|
|
283
|
+
# function is slower for large/densely connected graphs, taking minutes.
|
|
271
284
|
# See https://github.com/pypa/pip/issues/10557
|
|
272
|
-
# We
|
|
285
|
+
# We repeat the pruning step until we have no more leaves to remove.
|
|
273
286
|
while True:
|
|
274
287
|
leaves = set()
|
|
275
288
|
for key in graph:
|
|
@@ -289,12 +302,13 @@ def get_topological_weights(
|
|
|
289
302
|
for leaf in leaves:
|
|
290
303
|
if leaf not in requirement_keys:
|
|
291
304
|
continue
|
|
292
|
-
weights[leaf] = weight
|
|
305
|
+
weights[leaf] = [weight]
|
|
293
306
|
# Remove the leaves from the graph, making it simpler.
|
|
294
307
|
for leaf in leaves:
|
|
295
308
|
graph.remove(leaf)
|
|
296
309
|
|
|
297
|
-
# Visit the remaining graph
|
|
310
|
+
# Visit the remaining graph, this will only have nodes to handle if the
|
|
311
|
+
# graph had a cycle in it, which the pruning step above could not handle.
|
|
298
312
|
# `None` is guaranteed to be the root node by resolvelib.
|
|
299
313
|
visit(None)
|
|
300
314
|
|
|
@@ -303,13 +317,15 @@ def get_topological_weights(
|
|
|
303
317
|
difference = set(weights.keys()).difference(requirement_keys)
|
|
304
318
|
assert not difference, difference
|
|
305
319
|
|
|
306
|
-
|
|
320
|
+
# Now give back all the weights, choosing the largest ones from what we
|
|
321
|
+
# accumulated.
|
|
322
|
+
return {node: max(wgts) for (node, wgts) in weights.items()}
|
|
307
323
|
|
|
308
324
|
|
|
309
325
|
def _req_set_item_sorter(
|
|
310
|
-
item:
|
|
311
|
-
weights:
|
|
312
|
-
) ->
|
|
326
|
+
item: tuple[str, InstallRequirement],
|
|
327
|
+
weights: dict[str | None, int],
|
|
328
|
+
) -> tuple[int, str]:
|
|
313
329
|
"""Key function used to sort install requirements for installation.
|
|
314
330
|
|
|
315
331
|
Based on the "weight" mapping calculated in ``get_installation_order()``.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
import functools
|
|
3
5
|
import hashlib
|
|
@@ -7,7 +9,7 @@ import optparse
|
|
|
7
9
|
import os.path
|
|
8
10
|
import sys
|
|
9
11
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Callable
|
|
12
|
+
from typing import Any, Callable
|
|
11
13
|
|
|
12
14
|
from pip._vendor.packaging.version import Version
|
|
13
15
|
from pip._vendor.packaging.version import parse as parse_version
|
|
@@ -54,7 +56,7 @@ def _convert_date(isodate: str) -> datetime.datetime:
|
|
|
54
56
|
|
|
55
57
|
class SelfCheckState:
|
|
56
58
|
def __init__(self, cache_dir: str) -> None:
|
|
57
|
-
self._state:
|
|
59
|
+
self._state: dict[str, Any] = {}
|
|
58
60
|
self._statefile_path = None
|
|
59
61
|
|
|
60
62
|
# Try to load the existing state
|
|
@@ -74,7 +76,7 @@ class SelfCheckState:
|
|
|
74
76
|
def key(self) -> str:
|
|
75
77
|
return sys.prefix
|
|
76
78
|
|
|
77
|
-
def get(self, current_time: datetime.datetime) ->
|
|
79
|
+
def get(self, current_time: datetime.datetime) -> str | None:
|
|
78
80
|
"""Check if we have a not-outdated version loaded already."""
|
|
79
81
|
if not self._state:
|
|
80
82
|
return None
|
|
@@ -165,7 +167,7 @@ def was_installed_by_pip(pkg: str) -> bool:
|
|
|
165
167
|
|
|
166
168
|
def _get_current_remote_pip_version(
|
|
167
169
|
session: PipSession, options: optparse.Values
|
|
168
|
-
) ->
|
|
170
|
+
) -> str | None:
|
|
169
171
|
# Lets use PackageFinder to see what the latest pip version is
|
|
170
172
|
link_collector = LinkCollector.create(
|
|
171
173
|
session,
|
|
@@ -196,8 +198,8 @@ def _self_version_check_logic(
|
|
|
196
198
|
state: SelfCheckState,
|
|
197
199
|
current_time: datetime.datetime,
|
|
198
200
|
local_version: Version,
|
|
199
|
-
get_remote_version: Callable[[],
|
|
200
|
-
) ->
|
|
201
|
+
get_remote_version: Callable[[], str | None],
|
|
202
|
+
) -> UpgradePrompt | None:
|
|
201
203
|
remote_version_str = state.get(current_time)
|
|
202
204
|
if remote_version_str is None:
|
|
203
205
|
remote_version_str = get_remote_version()
|