diff --git a/src/python/pants/backend/python/macros/poetry_requirements.py b/src/python/pants/backend/python/macros/poetry_requirements.py index 3f2ae51516f..131e4199d17 100644 --- a/src/python/pants/backend/python/macros/poetry_requirements.py +++ b/src/python/pants/backend/python/macros/poetry_requirements.py @@ -104,6 +104,16 @@ def get_max_tilde(parsed_version: Version) -> str: return f"{major}.{minor}.0" +def get_max_wildcard(parsed_version: Version) -> str: + # Note: Assumes this is not a global wildcard, so parsed_version.release has + # at least two components. + release = list(parsed_version.release) + release[-2] += 1 + major = release[0] + minor = release[1] + return f"{major}.{minor}.0" + + def parse_str_version(attributes: str, **kwargs: str) -> str: valid_specifiers = "<>!~=" pep440_reqs = [] @@ -112,12 +122,10 @@ def parse_str_version(attributes: str, **kwargs: str) -> str: extras_str = kwargs["extras_str"] comma_split_reqs = (i.strip() for i in attributes.split(",")) for req in comma_split_reqs: - is_caret = req[0] == "^" - # ~= is an acceptable default operator; however, ~ is not, and IS NOT the same as ~= - is_tilde = req[0] == "~" and req[1] != "=" - if is_caret or is_tilde: + + def parse_version(version_str: str) -> Version: try: - parsed_version = Version(req[1:]) + return Version(version_str) except InvalidVersion: raise InvalidVersion( f'Failed to parse requirement {proj_name} = "{req}" in {fp} loaded by the ' @@ -126,12 +134,29 @@ def parse_str_version(attributes: str, **kwargs: str) -> str: "that we can update Pants' Poetry macro to support this." ) - max_ver = get_max_caret(parsed_version) if is_caret else get_max_tilde(parsed_version) + if not req: + continue + if req[0] == "^": + parsed_version = parse_version(req[1:]) + max_ver = get_max_caret(parsed_version) + min_ver = f"{parsed_version.public}" + pep440_reqs.append(f">={min_ver},<{max_ver}") + elif req[0] == "~" and req[1] != "=": + # ~= is an acceptable default operator; however, ~ is not, and IS NOT the same as ~= + parsed_version = parse_version(req[1:]) + max_ver = get_max_tilde(parsed_version) min_ver = f"{parsed_version.public}" pep440_reqs.append(f">={min_ver},<{max_ver}") + elif req[-1] == "*": + if req != "*": # This is not a global wildcard. + # To parse we replace the * with a 0. + parsed_version = parse_version(f"{req[:-1]}0") + max_ver = get_max_wildcard(parsed_version) + min_ver = f"{parsed_version.public}" + pep440_reqs.append(f">={min_ver},<{max_ver}") else: pep440_reqs.append(req if req[0] in valid_specifiers else f"=={req}") - return f"{proj_name}{extras_str} {','.join(pep440_reqs)}" + return f"{proj_name}{extras_str} {','.join(pep440_reqs)}".rstrip() def parse_python_constraint(constr: str | None, fp: str) -> str: diff --git a/src/python/pants/backend/python/macros/poetry_requirements_test.py b/src/python/pants/backend/python/macros/poetry_requirements_test.py index c8408e9bb47..818d4310430 100644 --- a/src/python/pants/backend/python/macros/poetry_requirements_test.py +++ b/src/python/pants/backend/python/macros/poetry_requirements_test.py @@ -72,6 +72,21 @@ def test_max_tilde(test, exp) -> None: assert get_max_tilde(version) == exp +@pytest.mark.parametrize( + "test, exp", + [ + ("*", ""), + ("1.*", ">=1.0,<2.0.0"), + ("1.2.*", ">=1.2.0,<1.3.0"), + ], +) +def test_wildcard(test, exp) -> None: + assert ( + parse_str_version(test, proj_name="foo", file_path="", extras_str="") + == f"foo {exp}".rstrip() + ) + + @pytest.mark.parametrize( "test, exp", [ @@ -362,6 +377,9 @@ def test_parse_multi_reqs() -> None: [tool.poetry.group.mygroup.dependencies] myrequirement = "1.2.3" + awildcard = "6.7.*" + anotherwildcard = "44.*" + aglobalwildcard = "*" [tool.poetry.group.mygroup2.dependencies] myrequirement2 = "1.2.3" @@ -378,6 +396,9 @@ def test_parse_multi_reqs() -> None: actual_reqs = { PipRequirement.parse("junk[security]@ https://github.com/myrepo/junk.whl"), PipRequirement.parse("myrequirement==1.2.3"), + PipRequirement.parse("awildcard>=6.7.0,<6.8.0"), + PipRequirement.parse("anotherwildcard>=44.0,<45.0.0"), + PipRequirement.parse("aglobalwildcard"), PipRequirement.parse("myrequirement2==1.2.3"), PipRequirement.parse("poetry@ git+https://github.com/python-poetry/poetry.git@v1.1.1"), PipRequirement.parse('requests[security, random]<3.0.0,>=2.25.1; python_version > "2.7"'),