Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/3848.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Convert a Poetry caret constraint on a `0.x` version to the upper bound Poetry documents, so `^0.2.3` becomes `<0.3.0` and `^0.0.3` becomes `<0.0.4` instead of `<1.0.0`.
18 changes: 15 additions & 3 deletions src/pdm/formats/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,27 @@ def check_fingerprint(project: Project | None, filename: Path | str) -> bool:
VERSION_RE = re.compile(r"([^\d\s]*)\s*(\d.*?)\s*(?=,|$)")


def _caret_upper_bound(version: str) -> str:
"""Return the exclusive upper bound of a Poetry caret requirement.

A caret requirement allows changes that do not modify the leftmost non-zero
component, so ``^0.2.3`` means ``<0.3.0`` and ``^0.0.3`` means ``<0.0.4``.
If every component is zero, the last one is bumped, matching ``^0.0`` -> ``<0.1``.
"""
numbers = [int(match.group()) if (match := re.match(r"\d+", part)) else 0 for part in version.split(".")]
index = next((i for i, number in enumerate(numbers) if number), len(numbers) - 1)
bumped = numbers[: index + 1]
bumped[-1] += 1
return ".".join(str(number) for number in bumped + [0] * (len(numbers) - index - 1))


def _convert_specifier(version: str) -> str:
parts = []
for op, ver in VERSION_RE.findall(str(version)):
if op == "~":
op += "="
elif op == "^":
major, *vparts = ver.split(".")
next_major = ".".join([str(int(major) + 1)] + ["0"] * len(vparts))
parts.append(f">={ver},<{next_major}")
parts.append(f">={ver},<{_caret_upper_bound(ver)}")
continue
elif not op:
op = "=="
Expand Down
28 changes: 26 additions & 2 deletions tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ def test_convert_poetry(project):
assert result["license"] == {"text": "MIT"}
assert "repository" in result["urls"]
assert result["requires-python"] == "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,<4.0,>=2.7"
assert 'cleo<1.0.0,>=0.7.6; python_version ~= "2.7"' in result["dependencies"]
assert 'cachecontrol[filecache]<1.0.0,>=0.12.4; python_version ~= "3.4"' in result["dependencies"]
assert 'cleo<0.8.0,>=0.7.6; python_version ~= "2.7"' in result["dependencies"]
assert 'cachecontrol[filecache]<0.13.0,>=0.12.4; python_version ~= "3.4"' in result["dependencies"]
assert "babel==2.9.0" in result["dependencies"]
assert "mysql" in result["optional-dependencies"]
assert "psycopg2<3.0,>=2.7" in result["optional-dependencies"]["pgsql"]
Expand All @@ -211,6 +211,30 @@ def test_convert_poetry_optional_dependency_in_multiple_extras(project):
assert result["optional-dependencies"]["all"] == ["psycopg2<3.0,>=2.7", "mysqlclient<2.0,>=1.3"]


@pytest.mark.parametrize(
"constraint,expected",
[
# Poetry's caret keeps the leftmost non-zero component unchanged.
("^1.2.3", "<2.0.0,>=1.2.3"),
("^1.2", "<2.0,>=1.2"),
("^1", "<2,>=1"),
("^0.2.3", "<0.3.0,>=0.2.3"),
("^0.0.3", "<0.0.4,>=0.0.3"),
("^0.0", "<0.1,>=0.0"),
("^0", "<1,>=0"),
],
)
def test_convert_poetry_caret_constraint(project, constraint, expected):
pyproject = project.root / "pyproject.toml"
pyproject.write_text(
f'[tool.poetry]\nname = "demo"\nversion = "0.1.0"\n[tool.poetry.dependencies]\nfoo = "{constraint}"\n',
encoding="utf-8",
)
result, _ = poetry.convert(project, pyproject, ns())

assert result["dependencies"] == [f"foo{expected}"]


def test_convert_poetry_12(project):
golden_file = FIXTURES / "poetry-new.toml"
with cd(FIXTURES):
Expand Down
Loading