|
5 | 5 | from sysconfig import get_config_var, get_platform |
6 | 6 |
|
7 | 7 | from ._compat import REPODATA_PACKAGES |
8 | | -from ._vendored.packaging.src.packaging.requirements import Requirement |
| 8 | +from ._vendored.packaging.src.packaging.requirements import ( |
| 9 | + InvalidRequirement, |
| 10 | + Requirement, |
| 11 | +) |
9 | 12 | from ._vendored.packaging.src.packaging.tags import Tag |
10 | 13 | from ._vendored.packaging.src.packaging.tags import sys_tags as sys_tags_orig |
11 | 14 | from ._vendored.packaging.src.packaging.utils import ( |
@@ -273,3 +276,91 @@ def fix_package_dependencies( |
273 | 276 | (get_dist_info(dist) / "PYODIDE_REQUIRES").write_text( |
274 | 277 | json.dumps(sorted(x for x in depends)) |
275 | 278 | ) |
| 279 | + |
| 280 | + |
| 281 | +def validate_constraints( |
| 282 | + constraints: list[str] | None, |
| 283 | + environment: dict[str, str] | None = None, |
| 284 | +) -> tuple[dict[str, Requirement], dict[str, list[str]]]: |
| 285 | + """Build a validated ``Requirement`` dictionary from raw constraint strings. |
| 286 | +
|
| 287 | + Parameters |
| 288 | + ---------- |
| 289 | + constraints (list): |
| 290 | + A list of PEP-508 dependency specs, expected to contain both a package |
| 291 | + name and at least one specifier. |
| 292 | +
|
| 293 | + environment (optional dict): |
| 294 | + The markers for the current environment, such as OS, Python implementation. |
| 295 | + If ``None``, the current execution environment will be used. |
| 296 | +
|
| 297 | + Returns |
| 298 | + ------- |
| 299 | + A 2-tuple of: |
| 300 | + - a dictionary of ``Requirement`` objects, keyed by canonical name |
| 301 | + - a dictionary of message strings, keyed by constraint |
| 302 | + """ |
| 303 | + reqs: dict[str, Requirement] = {} |
| 304 | + all_messages: dict[str, list[str]] = {} |
| 305 | + |
| 306 | + for raw_constraint in constraints or []: |
| 307 | + messages: list[str] = [] |
| 308 | + |
| 309 | + try: |
| 310 | + req = Requirement(raw_constraint) |
| 311 | + req.name = canonicalize_name(req.name) |
| 312 | + except InvalidRequirement as err: |
| 313 | + all_messages[raw_constraint] = [f"failed to parse: {err}"] |
| 314 | + continue |
| 315 | + |
| 316 | + if req.extras: |
| 317 | + messages.append("may not provide [extras]") |
| 318 | + |
| 319 | + if not (req.url or len(req.specifier)): |
| 320 | + messages.append("no version or URL") |
| 321 | + |
| 322 | + if req.marker and not req.marker.evaluate(environment): |
| 323 | + messages.append(f"not applicable: {req.marker}") |
| 324 | + |
| 325 | + if messages: |
| 326 | + all_messages[raw_constraint] = messages |
| 327 | + elif req.name in reqs: |
| 328 | + all_messages[raw_constraint] = [ |
| 329 | + f"updated existing constraint for {req.name}" |
| 330 | + ] |
| 331 | + reqs[req.name] = constrain_requirement(req, reqs) |
| 332 | + else: |
| 333 | + reqs[req.name] = req |
| 334 | + |
| 335 | + return reqs, all_messages |
| 336 | + |
| 337 | + |
| 338 | +def constrain_requirement( |
| 339 | + requirement: Requirement, constrained_requirements: dict[str, Requirement] |
| 340 | +) -> Requirement: |
| 341 | + """Modify or replace a requirement based on a set of constraints. |
| 342 | +
|
| 343 | + Parameters |
| 344 | + ---------- |
| 345 | + requirement (Requirement): |
| 346 | + A ``Requirement`` to constrain. |
| 347 | +
|
| 348 | + constrained_requirements (dict): |
| 349 | + A dictionary of ``Requirement`` objects, keyed by canonical name. |
| 350 | +
|
| 351 | + Returns |
| 352 | + ------- |
| 353 | + A constrained ``Requirement``. |
| 354 | + """ |
| 355 | + # URLs cannot be merged |
| 356 | + if requirement.url: |
| 357 | + return requirement |
| 358 | + |
| 359 | + constrained = constrained_requirements.get(canonicalize_name(requirement.name)) |
| 360 | + |
| 361 | + if constrained: |
| 362 | + if constrained.url: |
| 363 | + return constrained |
| 364 | + requirement.specifier = requirement.specifier & constrained.specifier |
| 365 | + |
| 366 | + return requirement |
0 commit comments