diff --git a/src/black/files.py b/src/black/files.py index e18381323c0..6509d0dda34 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -18,7 +18,7 @@ ) from mypy_extensions import mypyc_attr -from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet from packaging.version import InvalidVersion, Version from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPatternError @@ -182,10 +182,10 @@ def parse_req_python_specifier(requires_python: str) -> Optional[TargetVersion]: If parsing fails, will raise a packaging.specifiers.InvalidSpecifier error. If the parsed specifier cannot be mapped to a valid TargetVersion, returns None. """ - if not requires_python: - return None + specifier_set = strip_specifier_set(SpecifierSet(requires_python)) - specifier_set = SpecifierSet(requires_python) + if not specifier_set: + return None target_version_map = {f"3.{v.value}": v for v in TargetVersion} compatible_versions = specifier_set.filter(target_version_map) @@ -195,6 +195,36 @@ def parse_req_python_specifier(requires_python: str) -> Optional[TargetVersion]: return None +def strip_specifier_set(specifier_set: SpecifierSet) -> SpecifierSet: + """Strip irrelevant parts of the specifier set. + + Drops some specifiers, and strips minor versions for some others. + + For background on version specifiers, see PEP 440: + https://peps.python.org/pep-0440/#version-specifiers + """ + specifiers = [] + for s in specifier_set: + if "*" in str(s): + specifiers.append(s) + elif s.operator in ["~=", "==", ">=", "==="]: + version = Version(s.version) + stripped = Specifier(f"{s.operator}{version.major}.{version.minor}") + specifiers.append(stripped) + elif s.operator == ">": + version = Version(s.version) + if len(version.release) > 2: + s = Specifier(f">={version.major}.{version.minor}") + specifiers.append(s) + else: + specifiers.append(s) + + if all(s.operator in ["<=", "<"] for s in specifiers): + specifiers = [] + + return SpecifierSet(",".join(str(s) for s in specifiers)) + + @lru_cache() def find_user_pyproject_toml() -> Path: r"""Return the path to the top-level user configuration for black. diff --git a/tests/test_black.py b/tests/test_black.py index a8d6b278bcf..f10e529e9c8 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1370,12 +1370,16 @@ def test_infer_target_version(self) -> None: (">=3.10", TargetVersion.PY310), (">3.6,<3.10", TargetVersion.PY37), ("==3.8.*", TargetVersion.PY38), - # (">=3.8.6", TargetVersion.PY38), # Doesn't work yet + (">=3.8.6", TargetVersion.PY38), + ("> 3.7.4, != 3.8.8", TargetVersion.PY37), + (">3.7,!=3.8", TargetVersion.PY39), (None, None), ("", None), ("invalid", None), ("3", None), ("3.2", None), + ("<3.11", None), + (">3.10,<3.11", None), ]: test_toml = {"project": {"requires-python": version}} result = black.files.infer_target_version(test_toml)