|
4 | 4 | from collections.abc import Generator |
5 | 5 |
|
6 | 6 | from packaging.requirements import Requirement |
| 7 | +from packaging.specifiers import SpecifierSet |
7 | 8 | from packaging.utils import NormalizedName, canonicalize_name |
8 | 9 | from packaging.version import Version |
9 | 10 |
|
|
12 | 13 | logger = logging.getLogger(__name__) |
13 | 14 |
|
14 | 15 |
|
| 16 | +def _is_blocked_specifier(specifier: SpecifierSet) -> bool: |
| 17 | + """Return True if specifier blocks a package entirely. |
| 18 | +
|
| 19 | + The convention ``<0``, ``<0.0``, or ``<0.0.0`` is used to mark a |
| 20 | + package as blocked so that no version can satisfy the constraint. |
| 21 | + """ |
| 22 | + specs = list(specifier) |
| 23 | + return ( |
| 24 | + len(specs) == 1 |
| 25 | + and specs[0].operator == "<" |
| 26 | + and Version(specs[0].version) == Version("0") |
| 27 | + ) |
| 28 | + |
| 29 | + |
15 | 30 | class InvalidConstraintError(ValueError): |
16 | 31 | pass |
17 | 32 |
|
@@ -51,23 +66,34 @@ def add_constraint(self, unparsed: str) -> None: |
51 | 66 | if not req.specifier: |
52 | 67 | raise InvalidConstraintError(f"Constraint {unparsed!r} has no specifiers") |
53 | 68 |
|
| 69 | + # A "blocked" specifier (<0, <0.0, <0.0.0) is intentionally |
| 70 | + # unsatisfiable and used to exclude a package from builds. |
| 71 | + blocked = _is_blocked_specifier(req.specifier) |
| 72 | + |
54 | 73 | # verify that incoming constraint is okay by itself |
55 | | - if req.specifier.is_unsatisfiable(): |
| 74 | + if not blocked and req.specifier.is_unsatisfiable(): |
56 | 75 | raise InvalidConstraintError(f"Constraint {unparsed!r} is unsatisfiable") |
57 | 76 |
|
58 | 77 | if not requirements_file.evaluate_marker(req, req): |
59 | 78 | logger.debug(f"Constraint {req} does not match environment") |
60 | 79 | return |
61 | 80 |
|
62 | 81 | if previous is not None: |
63 | | - logger.debug("combining constraints %s and %s", previous, req) |
64 | | - new_specifier = req.specifier & previous.specifier |
65 | | - if new_specifier.is_unsatisfiable(): |
| 82 | + prev_blocked = _is_blocked_specifier(previous.specifier) |
| 83 | + if blocked != prev_blocked: |
66 | 84 | raise InvalidConstraintError( |
67 | | - f"Combined specifier '{new_specifier}' is not satisfiable " |
| 85 | + f"Cannot combine blocked and non-blocked constraints " |
68 | 86 | f"(existing: {previous}, new: {req})" |
69 | 87 | ) |
70 | | - req.specifier = new_specifier |
| 88 | + if not blocked: |
| 89 | + logger.debug("combining constraints %s and %s", previous, req) |
| 90 | + new_specifier = req.specifier & previous.specifier |
| 91 | + if new_specifier.is_unsatisfiable(): |
| 92 | + raise InvalidConstraintError( |
| 93 | + f"Combined specifier '{new_specifier}' is not satisfiable " |
| 94 | + f"(existing: {previous}, new: {req})" |
| 95 | + ) |
| 96 | + req.specifier = new_specifier |
71 | 97 | else: |
72 | 98 | logger.debug(f"adding constraint {req}") |
73 | 99 |
|
@@ -97,6 +123,13 @@ def allow_prerelease(self, pkg_name: str) -> bool: |
97 | 123 | return bool(constraint.specifier.prereleases) |
98 | 124 | return False |
99 | 125 |
|
| 126 | + def is_blocked(self, pkg_name: str) -> bool: |
| 127 | + """Return True if the package is blocked by a ``<0`` constraint.""" |
| 128 | + constraint = self.get_constraint(pkg_name) |
| 129 | + if constraint: |
| 130 | + return _is_blocked_specifier(constraint.specifier) |
| 131 | + return False |
| 132 | + |
100 | 133 | def is_satisfied_by(self, pkg_name: str, version: Version) -> bool: |
101 | 134 | constraint = self.get_constraint(pkg_name) |
102 | 135 | if constraint: |
|
0 commit comments