nab-python 0.0.1__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.
Files changed (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,508 @@
1
+ """Universal resolution loop.
2
+
3
+ Runs one specific resolve per matrix tuple, sharing a single
4
+ FetchCoordinator so metadata is fetched once across tuples. Returns a
5
+ merged result keyed by package name with the markers under which each
6
+ pin applies.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import time
13
+ from collections import defaultdict
14
+ from dataclasses import dataclass, field
15
+ from typing import TYPE_CHECKING
16
+
17
+ from nab_index.multi_index import IndexConfig
18
+ from nab_index.urllib3_async_transport import Urllib3AsyncTransport
19
+ from nab_resolver.errors import ResolutionError
20
+ from nab_resolver.resolver import Resolver
21
+
22
+ from .._vendor.packaging.markers import Marker
23
+ from .._vendor.packaging.ranges import VersionRange
24
+ from .._vendor.packaging.requirements import InvalidRequirement, Requirement
25
+ from .._vendor.packaging.utils import canonicalize_name
26
+ from ..fetch import (
27
+ DEFAULT_INDEX_NAME,
28
+ DEFAULT_INDEX_URL,
29
+ FetchCoordinator,
30
+ IndexOverride,
31
+ )
32
+ from ..lockfile import (
33
+ LockInput,
34
+ MissingHashError,
35
+ PinShape,
36
+ build_lock_input_from_provider,
37
+ )
38
+ from ..provider import (
39
+ BuildPolicy,
40
+ DistPolicy,
41
+ LocalSource,
42
+ VcsConfig,
43
+ VcsSource,
44
+ split_extra,
45
+ )
46
+ from .provider import UniversalProvider
47
+
48
+ __all__ = [
49
+ "TupleResult",
50
+ "UniversalResult",
51
+ "merge_universal_lock_inputs",
52
+ "resolve_universal",
53
+ ]
54
+
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+ # Cap on the per-tuple ResolutionError message stashed in
59
+ # :class:`TupleResult.error`, so a runaway report message never
60
+ # bloats the failure summary.
61
+ _ERROR_MESSAGE_LIMIT = 200
62
+
63
+
64
+ if TYPE_CHECKING:
65
+ from collections.abc import Mapping, Sequence
66
+ from datetime import datetime
67
+ from pathlib import Path
68
+
69
+ from nab_index.transport import AsyncHttpTransport
70
+
71
+ from .._vendor.packaging.version import Version
72
+ from ..config import NabProjectConfig
73
+ from .matrix import Matrix, MatrixTuple
74
+
75
+
76
+ @dataclass
77
+ class TupleResult:
78
+ """Result of one specific resolve."""
79
+
80
+ tuple_: MatrixTuple
81
+ success: bool
82
+ pins: dict[str, Version] = field(default_factory=dict)
83
+ error: str | None = None
84
+ decisions: int = 0
85
+ rounds: int = 0
86
+ wall_time: float = 0.0
87
+ lock_input: LockInput | None = None
88
+
89
+
90
+ @dataclass
91
+ class UniversalResult:
92
+ """Merged result across all tuples."""
93
+
94
+ matrix: Matrix
95
+ tuple_results: list[TupleResult] = field(default_factory=list)
96
+
97
+ @property
98
+ def success(self) -> bool:
99
+ """Return True iff every tuple resolved successfully."""
100
+ return all(tr.success for tr in self.tuple_results)
101
+
102
+ def merged_lock(self) -> dict[str, list[tuple[str, str]]]:
103
+ """Collapse per-tuple pins into ``{package: [(version, label), ...]}``.
104
+
105
+ Adjacent labels picking the same version stay together; the
106
+ labels are tuple ids, not PEP 508 markers.
107
+ """
108
+ out: defaultdict[str, list[tuple[str, str]]] = defaultdict(list)
109
+ for tr in self.tuple_results:
110
+ if not tr.success:
111
+ continue
112
+ for pkg, version in tr.pins.items():
113
+ out[pkg].append((str(version), tr.tuple_.label))
114
+ return out
115
+
116
+
117
+ def merge_universal_lock_inputs(
118
+ result: UniversalResult,
119
+ *,
120
+ requires_python: str | None = None,
121
+ created_by: str = "nab",
122
+ extras: Sequence[str] = (),
123
+ dependency_groups: Sequence[str] = (),
124
+ default_groups: Sequence[str] = (),
125
+ ) -> LockInput:
126
+ """Merge per-tuple :class:`LockInput` objects into one universal lock.
127
+
128
+ Each successful tuple's pins are stored under ``per_tuple_pins``
129
+ keyed by the tuple's ``label``; the matching PEP 508 marker
130
+ expression is recorded in ``tuple_markers``. The downstream pylock
131
+ writer collapses these into one or more ``Package`` entries per
132
+ name with markers attached.
133
+
134
+ Tuples with ``lock_input is None`` (resolution succeeded but the
135
+ artefact set is missing a ``sha256`` somewhere) are skipped, which
136
+ means the resulting lock omits those tuples. Callers that want
137
+ every tuple represented should check ``UniversalResult.success``
138
+ before calling.
139
+
140
+ ``dependency_groups`` and ``default_groups`` are recorded at the
141
+ lockfile top level for PEP 735 multi-use locks. Per-package
142
+ group attribution (markers gated by
143
+ ``'group-x' in dependency_groups``) is not emitted.
144
+ """
145
+ per_tuple_pins: dict[str, dict[str, PinShape]] = {}
146
+ tuple_markers: dict[str, Marker] = {}
147
+ tuple_environments: dict[str, dict[str, str]] = {}
148
+ environments: list[Marker] = []
149
+ for tr in result.tuple_results:
150
+ if not tr.success or tr.lock_input is None:
151
+ continue
152
+ label = tr.tuple_.label
153
+ per_tuple_pins[label] = dict(tr.lock_input.pins)
154
+ marker = Marker(tr.tuple_.marker_string)
155
+ tuple_markers[label] = marker
156
+ tuple_environments[label] = dict(tr.tuple_.environment)
157
+ environments.append(marker)
158
+ return LockInput(
159
+ per_tuple_pins=per_tuple_pins,
160
+ tuple_markers=tuple_markers,
161
+ tuple_environments=tuple_environments,
162
+ environments=environments,
163
+ requires_python=requires_python,
164
+ created_by=created_by,
165
+ extras=tuple(extras),
166
+ dependency_groups=tuple(dependency_groups),
167
+ default_groups=tuple(default_groups),
168
+ )
169
+
170
+
171
+ def resolve_universal( # noqa: PLR0913 - surface area mirrors uv's resolution knobs; bundling all flags into a config object hides the call-site documentation
172
+ matrix: Matrix,
173
+ requirements: list[str],
174
+ *,
175
+ transport: AsyncHttpTransport | None = None,
176
+ offline: bool = False,
177
+ constraints: list[str] | None = None,
178
+ cache_dir: Path | None = None,
179
+ uploaded_prior_to: datetime | None = None,
180
+ uploaded_prior_to_overrides: Mapping[str, datetime | None] | None = None,
181
+ dist_policy: DistPolicy = DistPolicy.WHEEL_OR_SDIST,
182
+ dist_policy_overrides: Mapping[str, DistPolicy] | None = None,
183
+ build_policy: BuildPolicy = BuildPolicy.BUILD_LOCAL,
184
+ build_policy_overrides: Mapping[str, BuildPolicy] | None = None,
185
+ vcs_config: VcsConfig | None = None,
186
+ local_sources: list[LocalSource] | None = None,
187
+ vcs_sources: list[VcsSource] | None = None,
188
+ vcs_cache_dir: Path | None = None,
189
+ build_config: NabProjectConfig | None = None,
190
+ indexes: list[IndexConfig] | None = None,
191
+ index_overrides: list[IndexOverride] | None = None,
192
+ resolution_strategy: str = "highest",
193
+ align_across_tuples: bool = True,
194
+ preferences: dict[str, Version] | None = None,
195
+ ) -> UniversalResult:
196
+ """Run a universal resolve for ``matrix``.
197
+
198
+ Returns a :class:`UniversalResult` with one :class:`TupleResult`
199
+ per (python_version, platform) combination.
200
+
201
+ ``resolution_strategy``: ``"highest"`` (default), ``"lowest"``, or
202
+ ``"lowest-direct"``. Mirrors uv's ``--resolution`` flag.
203
+
204
+ ``align_across_tuples``: when True, after each tuple's resolve we
205
+ accumulate its pins as preferences for subsequent tuples.
206
+
207
+ ``preferences``: a starting set of preferred ``{name: Version}``,
208
+ e.g. read from a previous lock.
209
+ """
210
+ if indexes is None:
211
+ indexes = [IndexConfig(DEFAULT_INDEX_NAME, DEFAULT_INDEX_URL)]
212
+ effective_transport: AsyncHttpTransport = (
213
+ transport if transport is not None else Urllib3AsyncTransport()
214
+ )
215
+ with FetchCoordinator(
216
+ effective_transport,
217
+ indexes=indexes,
218
+ cache_dir=cache_dir,
219
+ offline=offline,
220
+ index_overrides=index_overrides,
221
+ ) as coordinator:
222
+ return resolve_with_coordinator(
223
+ coordinator,
224
+ matrix,
225
+ requirements,
226
+ constraints=constraints,
227
+ uploaded_prior_to=uploaded_prior_to,
228
+ uploaded_prior_to_overrides=uploaded_prior_to_overrides,
229
+ dist_policy=dist_policy,
230
+ dist_policy_overrides=dist_policy_overrides,
231
+ build_policy=build_policy,
232
+ build_policy_overrides=build_policy_overrides,
233
+ vcs_config=vcs_config,
234
+ local_sources=local_sources,
235
+ vcs_sources=vcs_sources,
236
+ vcs_cache_dir=vcs_cache_dir,
237
+ build_config=build_config,
238
+ resolution_strategy=resolution_strategy,
239
+ align_across_tuples=align_across_tuples,
240
+ preferences=preferences,
241
+ )
242
+
243
+
244
+ def resolve_with_coordinator( # noqa: PLR0913 - mirrors resolve_universal's surface
245
+ coordinator: FetchCoordinator,
246
+ matrix: Matrix,
247
+ requirements: list[str],
248
+ *,
249
+ constraints: list[str] | None = None,
250
+ uploaded_prior_to: datetime | None = None,
251
+ uploaded_prior_to_overrides: (Mapping[str, datetime | None] | None) = None,
252
+ dist_policy: DistPolicy = DistPolicy.WHEEL_OR_SDIST,
253
+ dist_policy_overrides: Mapping[str, DistPolicy] | None = None,
254
+ build_policy: BuildPolicy = BuildPolicy.BUILD_LOCAL,
255
+ build_policy_overrides: Mapping[str, BuildPolicy] | None = None,
256
+ vcs_config: VcsConfig | None = None,
257
+ local_sources: list[LocalSource] | None = None,
258
+ vcs_sources: list[VcsSource] | None = None,
259
+ vcs_cache_dir: Path | None = None,
260
+ build_config: NabProjectConfig | None = None,
261
+ resolution_strategy: str = "highest",
262
+ align_across_tuples: bool = True,
263
+ preferences: dict[str, Version] | None = None,
264
+ ) -> UniversalResult:
265
+ """Run a universal resolve against an already-open coordinator.
266
+
267
+ Splitting the entry point lets callers (and tests) reuse a single
268
+ :class:`FetchCoordinator` across multiple resolves and avoids
269
+ transport setup in unit tests. See :func:`resolve_universal` for
270
+ the full parameter documentation.
271
+ """
272
+ tuples = matrix.expand()
273
+ _warn_extra_marker_at_root(requirements)
274
+ direct_packages = frozenset(_direct_package_names(requirements))
275
+ initial_preferences: dict[str, Version] = dict(preferences or {})
276
+
277
+ return UniversalResult(
278
+ matrix=matrix,
279
+ tuple_results=_run_pass(
280
+ tuples,
281
+ requirements,
282
+ constraints,
283
+ coordinator=coordinator,
284
+ uploaded_prior_to=uploaded_prior_to,
285
+ uploaded_prior_to_overrides=uploaded_prior_to_overrides,
286
+ dist_policy=dist_policy,
287
+ dist_policy_overrides=dist_policy_overrides,
288
+ build_policy=build_policy,
289
+ build_policy_overrides=build_policy_overrides,
290
+ vcs_config=vcs_config,
291
+ local_sources=local_sources,
292
+ vcs_sources=vcs_sources,
293
+ vcs_cache_dir=vcs_cache_dir,
294
+ build_config=build_config,
295
+ resolution_strategy=resolution_strategy,
296
+ direct_packages=direct_packages,
297
+ preferences=initial_preferences,
298
+ align_serial=align_across_tuples,
299
+ ),
300
+ )
301
+
302
+
303
+ def _run_pass( # noqa: PLR0913
304
+ tuples: list[MatrixTuple],
305
+ requirements: list[str],
306
+ constraints: list[str] | None,
307
+ *,
308
+ coordinator: FetchCoordinator,
309
+ uploaded_prior_to: datetime | None,
310
+ uploaded_prior_to_overrides: Mapping[str, datetime | None] | None = None,
311
+ dist_policy: DistPolicy,
312
+ dist_policy_overrides: Mapping[str, DistPolicy] | None = None,
313
+ build_policy: BuildPolicy,
314
+ build_policy_overrides: Mapping[str, BuildPolicy] | None = None,
315
+ vcs_config: VcsConfig | None = None,
316
+ local_sources: list[LocalSource] | None = None,
317
+ vcs_sources: list[VcsSource] | None = None,
318
+ vcs_cache_dir: Path | None = None,
319
+ build_config: NabProjectConfig | None = None,
320
+ resolution_strategy: str,
321
+ direct_packages: frozenset[str],
322
+ preferences: dict[str, Version],
323
+ align_serial: bool,
324
+ ) -> list[TupleResult]:
325
+ """Run one serial pass of resolution across ``tuples``.
326
+
327
+ When ``align_serial=True``, each tuple's pins are threaded forward
328
+ as preferences for the next so the per-Python pins stay aligned
329
+ where the matrix admits it.
330
+ """
331
+
332
+ def resolve(t: MatrixTuple, current_prefs: dict[str, Version]) -> TupleResult:
333
+ tuple_requirements = _parse_requirements(requirements, t.environment)
334
+ tuple_constraints = (
335
+ _parse_requirements(constraints, t.environment) if constraints else None
336
+ )
337
+ return _resolve_one_tuple(
338
+ coordinator,
339
+ t,
340
+ tuple_requirements,
341
+ tuple_constraints,
342
+ uploaded_prior_to=uploaded_prior_to,
343
+ uploaded_prior_to_overrides=uploaded_prior_to_overrides,
344
+ dist_policy=dist_policy,
345
+ dist_policy_overrides=dist_policy_overrides,
346
+ build_policy=build_policy,
347
+ build_policy_overrides=build_policy_overrides,
348
+ vcs_config=vcs_config,
349
+ local_sources=local_sources,
350
+ vcs_sources=vcs_sources,
351
+ vcs_cache_dir=vcs_cache_dir,
352
+ build_config=build_config,
353
+ resolution_strategy=resolution_strategy,
354
+ preferences=dict(current_prefs),
355
+ direct_packages=direct_packages,
356
+ )
357
+
358
+ out: list[TupleResult] = []
359
+ accumulated = dict(preferences)
360
+ for t in tuples:
361
+ tr = resolve(t, accumulated)
362
+ out.append(tr)
363
+ if align_serial and tr.success:
364
+ accumulated.update(tr.pins)
365
+ return out
366
+
367
+
368
+ def _warn_extra_marker_at_root(requirements: list[str]) -> list[str]:
369
+ """Warn when a root requirement uses an ``extra ==`` marker.
370
+
371
+ Hole 1.6 plug. ``packaging`` defaults the ``extra`` variable to
372
+ ``""`` at root, so a marker like ``pkg ; extra == "test"`` evaluates
373
+ False during root parsing and the dep is silently dropped. The
374
+ user almost certainly meant the ``parent[test]`` extra-of-parent
375
+ syntax, not a self-referential extra marker.
376
+
377
+ Returns the list of requirement strings that triggered the warning
378
+ so callers can write tests against the diagnostic without parsing
379
+ the log output.
380
+ """
381
+ flagged: list[str] = []
382
+ for req_str in requirements:
383
+ try:
384
+ req = Requirement(req_str)
385
+ except InvalidRequirement: # pragma: no cover - re-raised at resolve time
386
+ continue
387
+ marker_text = str(req.marker or "")
388
+ if "extra ==" in marker_text or "extra==" in marker_text:
389
+ flagged.append(req_str)
390
+ logger.warning(
391
+ "Root requirement %r uses an ``extra`` marker; the dep will be "
392
+ "silently dropped because root has no parent extra. Did you "
393
+ "mean ``pkg[extra]`` (extras-of-package) instead?",
394
+ req_str,
395
+ )
396
+ return flagged
397
+
398
+
399
+ def _direct_package_names(requirements: list[str]) -> set[str]:
400
+ """Return the canonical names of the user's direct dependencies.
401
+
402
+ Malformed requirement strings raise downstream during the actual
403
+ resolve; here we just collect names for the lowest-direct strategy
404
+ and let them surface naturally.
405
+ """
406
+ return {canonicalize_name(Requirement(req_str).name) for req_str in requirements}
407
+
408
+
409
+ def _parse_requirements(
410
+ reqs: list[str],
411
+ environment: dict[str, str],
412
+ ) -> dict[str, VersionRange]:
413
+ """Convert PEP 508 strings to resolver requirements for ``environment``.
414
+
415
+ Marker-gated requirements whose marker evaluates to False in
416
+ ``environment`` are dropped. This matches what
417
+ ``Provider._classify_requirement`` does for transitive deps;
418
+ we just apply the same rule at the root.
419
+ """
420
+ out: dict[str, VersionRange] = {}
421
+ for req_str in reqs:
422
+ req = Requirement(req_str)
423
+ if req.marker is not None and not req.marker.evaluate(environment):
424
+ continue
425
+ name = canonicalize_name(req.name)
426
+ out[name] = req.specifier.to_range()
427
+ for extra in req.extras:
428
+ out[f"{name}[{extra}]"] = VersionRange.full()
429
+ return out
430
+
431
+
432
+ def _resolve_one_tuple( # noqa: PLR0913
433
+ coordinator: FetchCoordinator,
434
+ t: MatrixTuple,
435
+ requirements: dict[str, VersionRange],
436
+ constraints: dict[str, VersionRange] | None,
437
+ *,
438
+ uploaded_prior_to: datetime | None,
439
+ uploaded_prior_to_overrides: Mapping[str, datetime | None] | None = None,
440
+ dist_policy: DistPolicy,
441
+ dist_policy_overrides: Mapping[str, DistPolicy] | None = None,
442
+ build_policy: BuildPolicy,
443
+ build_policy_overrides: Mapping[str, BuildPolicy] | None = None,
444
+ vcs_config: VcsConfig | None = None,
445
+ local_sources: list[LocalSource] | None = None,
446
+ vcs_sources: list[VcsSource] | None = None,
447
+ vcs_cache_dir: Path | None = None,
448
+ build_config: NabProjectConfig | None = None,
449
+ resolution_strategy: str = "highest",
450
+ preferences: dict[str, Version] | None = None,
451
+ direct_packages: frozenset[str] = frozenset(),
452
+ ) -> TupleResult:
453
+ """Run one single-environment resolve for ``t``."""
454
+ provider = UniversalProvider(
455
+ coordinator,
456
+ marker_environment=t.environment,
457
+ root_requirements=requirements,
458
+ uploaded_prior_to=uploaded_prior_to,
459
+ uploaded_prior_to_overrides=uploaded_prior_to_overrides,
460
+ dist_policy=dist_policy,
461
+ dist_policy_overrides=dist_policy_overrides,
462
+ build_policy_overrides=build_policy_overrides,
463
+ vcs_config=vcs_config,
464
+ local_sources=local_sources,
465
+ vcs_sources=vcs_sources,
466
+ vcs_cache_dir=vcs_cache_dir,
467
+ build_config=build_config,
468
+ build_policy=build_policy,
469
+ preferences=preferences,
470
+ resolution_strategy=resolution_strategy,
471
+ direct_packages=direct_packages,
472
+ platform_spec=t.platform_spec,
473
+ )
474
+ resolver: Resolver[str, Version] = Resolver(
475
+ provider,
476
+ range_type=VersionRange,
477
+ root_version="0",
478
+ max_iterations=50_000,
479
+ )
480
+ start = time.monotonic()
481
+ try:
482
+ raw = resolver.resolve(requirements, constraints=constraints)
483
+ except ResolutionError as exc:
484
+ return TupleResult(
485
+ tuple_=t,
486
+ success=False,
487
+ error=f"{type(exc).__name__}: {exc}"[:_ERROR_MESSAGE_LIMIT],
488
+ wall_time=time.monotonic() - start,
489
+ rounds=resolver.stats.rounds,
490
+ decisions=resolver.stats.decisions,
491
+ )
492
+ elapsed = time.monotonic() - start
493
+ pins = {k: v for k, v in raw.items() if split_extra(k)[1] is None}
494
+ try:
495
+ lock_input = build_lock_input_from_provider(
496
+ provider, pins, indexes=coordinator.indexes
497
+ )
498
+ except MissingHashError:
499
+ lock_input = None
500
+ return TupleResult(
501
+ tuple_=t,
502
+ success=True,
503
+ pins=pins,
504
+ wall_time=elapsed,
505
+ rounds=resolver.stats.rounds,
506
+ decisions=resolver.stats.decisions,
507
+ lock_input=lock_input,
508
+ )