33from collections .abc import Generator
44
55from packaging .requirements import Requirement
6+ from packaging .specifiers import SpecifierSet
67from packaging .utils import NormalizedName , canonicalize_name
78from packaging .version import Version
89
910from . import requirements_file
1011
12+ #: Specifiers that block a package entirely (no version can satisfy them).
13+ #: Used as a convention to exclude packages from builds.
14+ _BLOCKED_SPECIFIERS : frozenset [str ] = frozenset ({"<0" , "<0.0" , "<0.0.0" })
15+
1116logger = logging .getLogger (__name__ )
1217
1318
19+ def _is_blocked_specifier (specifier : SpecifierSet ) -> bool :
20+ """Return True if specifier blocks a package entirely.
21+
22+ The convention ``<0``, ``<0.0``, or ``<0.0.0`` is used to mark a
23+ package as blocked so that no version can satisfy the constraint.
24+ """
25+ return str (specifier ) in _BLOCKED_SPECIFIERS
26+
27+
1428class InvalidConstraintError (ValueError ):
1529 pass
1630
@@ -47,23 +61,34 @@ def add_constraint(self, unparsed: str) -> None:
4761 if not req .specifier :
4862 raise InvalidConstraintError (f"Constraint { unparsed !r} has no specifiers" )
4963
64+ # A "blocked" specifier (<0, <0.0, <0.0.0) is intentionally
65+ # unsatisfiable and used to exclude a package from builds.
66+ blocked = _is_blocked_specifier (req .specifier )
67+
5068 # verify that incoming constraint is okay by itself
51- if req .specifier .is_unsatisfiable ():
69+ if not blocked and req .specifier .is_unsatisfiable ():
5270 raise InvalidConstraintError (f"Constraint { unparsed !r} is unsatisfiable" )
5371
5472 if not requirements_file .evaluate_marker (req , req ):
5573 logger .debug (f"Constraint { req } does not match environment" )
5674 return
5775
5876 if previous is not None :
59- logger .debug ("combining constraints %s and %s" , previous , req )
60- new_specifier = req .specifier & previous .specifier
61- if new_specifier .is_unsatisfiable ():
77+ prev_blocked = _is_blocked_specifier (previous .specifier )
78+ if blocked != prev_blocked :
6279 raise InvalidConstraintError (
63- f"Combined specifier ' { new_specifier } ' is not satisfiable "
80+ f"Cannot combine blocked and non-blocked constraints "
6481 f"(existing: { previous } , new: { req } )"
6582 )
66- req .specifier = new_specifier
83+ if not blocked :
84+ logger .debug ("combining constraints %s and %s" , previous , req )
85+ new_specifier = req .specifier & previous .specifier
86+ if new_specifier .is_unsatisfiable ():
87+ raise InvalidConstraintError (
88+ f"Combined specifier '{ new_specifier } ' is not satisfiable "
89+ f"(existing: { previous } , new: { req } )"
90+ )
91+ req .specifier = new_specifier
6792 else :
6893 logger .debug (f"adding constraint { req } " )
6994
@@ -85,6 +110,13 @@ def allow_prerelease(self, pkg_name: str) -> bool:
85110 return bool (constraint .specifier .prereleases )
86111 return False
87112
113+ def is_blocked (self , pkg_name : str ) -> bool :
114+ """Return True if the package is blocked by a ``<0`` constraint."""
115+ constraint = self .get_constraint (pkg_name )
116+ if constraint :
117+ return _is_blocked_specifier (constraint .specifier )
118+ return False
119+
88120 def is_satisfied_by (self , pkg_name : str , version : Version ) -> bool :
89121 constraint = self .get_constraint (pkg_name )
90122 if constraint :
0 commit comments