From b148bf741602e4fcee8531d9bd8d7da479ab564e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:54:41 +0100 Subject: [PATCH 1/2] feat: add `pylock.toml` as export format --- README.md | 7 +- docs/_index.md | 2 +- poetry.lock | 4 +- pyproject.toml | 1 + src/poetry_plugin_export/command.py | 20 +- src/poetry_plugin_export/exporter.py | 216 ++++- src/poetry_plugin_export/walker.py | 2 + tests/command/test_command_export.py | 63 +- tests/test_exporter.py | 2 +- tests/test_exporter_pylock_toml.py | 1083 ++++++++++++++++++++++++++ 10 files changed, 1383 insertions(+), 17 deletions(-) create mode 100644 tests/test_exporter_pylock_toml.py diff --git a/README.md b/README.md index 932b546..49ca30d 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,14 @@ poetry export -f requirements.txt --output requirements.txt > which are exported with their resolved hashes, are included. > [!NOTE] -> Only the `constraints.txt` and `requirements.txt` formats are currently supported. +> The following formats are currently supported: +> * `requirements.txt` +> * `constraints.txt` +> * `pylock.toml` ### Available options -* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `constraints.txt` and `requirements.txt` are supported. +* `--format (-f)`: The format to export to (default: `requirements.txt`). Additionally, `constraints.txt` and `pylock.toml` are supported. * `--output (-o)`: The name of the output file. If omitted, print to standard output. * `--with`: The optional and non-optional dependency groups to include. By default, only the main dependencies are included. * `--only`: The only dependency groups to include. It is possible to exclude the `main` group this way. diff --git a/docs/_index.md b/docs/_index.md index fc9251c..22ea85d 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -65,7 +65,7 @@ poetry export --only test,docs ### Available options -* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `constraints.txt` and `requirements.txt` are supported. +* `--format (-f)`: The format to export to (default: `requirements.txt`). Additionally, `constraints.txt` and `pylock.toml` are supported. * `--output (-o)`: The name of the output file. If omitted, print to standard output. * `--with`: The optional and non-optional dependency groups to include. By default, only the main dependencies are included. * `--only`: The only dependency groups to include. It is possible to exclude the `main` group this way. diff --git a/poetry.lock b/poetry.lock index 4f5f487..e80714d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "anyio" @@ -2140,4 +2140,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "9d3737621fb95fb1048deaf04626cbf1b731d899a527e1b8fe650b376cd100e8" +content-hash = "04588f581e9016fe28653baa5b49bc87e15db54dae0dd85e8ec161817fab33e8" diff --git a/pyproject.toml b/pyproject.toml index 113f4b9..c27fc40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ requires-python = ">=3.10,<4.0" dependencies = [ "poetry>=2.1.0,<3.0.0", "poetry-core>=2.1.0,<3.0.0", + "tomlkit (>=0.11.4,<1.0.0)", ] dynamic = ["classifiers"] diff --git a/src/poetry_plugin_export/command.py b/src/poetry_plugin_export/command.py index 51db1e7..3f838bd 100644 --- a/src/poetry_plugin_export/command.py +++ b/src/poetry_plugin_export/command.py @@ -1,5 +1,7 @@ from __future__ import annotations +import re + from pathlib import Path from typing import TYPE_CHECKING @@ -24,8 +26,7 @@ class ExportCommand(GroupCommand): option( "format", "f", - "Format to export to. Currently, only constraints.txt and" - " requirements.txt are supported.", + "Format to export to: constraints.txt, requirements.txt, pylock.toml", flag=False, default=Exporter.FORMAT_REQUIREMENTS_TXT, ), @@ -89,6 +90,21 @@ def handle(self) -> int: output = self.option("output") + pylock_pattern = r"^pylock\.([^.]+)\.toml$" + if ( + fmt == Exporter.FORMAT_PYLOCK_TOML + and output + and Path(output).name != "pylock.toml" + and not re.match(pylock_pattern, Path(output).name) + ): + self.line_error( + "" + 'The output file for pylock.toml export must be named "pylock.toml"' + f' or must follow the regex "{pylock_pattern}", e.g. "pylock.dev.toml"' + "" + ) + return 1 + locker = self.poetry.locker if not locker.is_locked(): self.line_error("The lock file does not exist. Locking.") diff --git a/src/poetry_plugin_export/exporter.py b/src/poetry_plugin_export/exporter.py index 38753ff..e030275 100644 --- a/src/poetry_plugin_export/exporter.py +++ b/src/poetry_plugin_export/exporter.py @@ -1,13 +1,23 @@ from __future__ import annotations +import contextlib +import itertools import urllib.parse +from datetime import datetime from functools import partialmethod +from importlib import metadata from typing import TYPE_CHECKING +from typing import Any from cleo.io.io import IO +from poetry.core.constraints.version.version import Version from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.core.packages.directory_dependency import DirectoryDependency +from poetry.core.packages.file_dependency import FileDependency +from poetry.core.packages.url_dependency import URLDependency from poetry.core.packages.utils.utils import create_nested_marker +from poetry.core.packages.vcs_dependency import VCSDependency from poetry.core.version.markers import parse_marker from poetry.repositories.http_repository import HTTPRepository @@ -22,6 +32,7 @@ from typing import ClassVar from packaging.utils import NormalizedName + from poetry.core.packages.package import PackageFile from poetry.poetry import Poetry @@ -32,11 +43,13 @@ class Exporter: FORMAT_CONSTRAINTS_TXT = "constraints.txt" FORMAT_REQUIREMENTS_TXT = "requirements.txt" + FORMAT_PYLOCK_TOML = "pylock.toml" ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512") EXPORT_METHODS: ClassVar[dict[str, str]] = { FORMAT_CONSTRAINTS_TXT: "_export_constraints_txt", FORMAT_REQUIREMENTS_TXT: "_export_requirements_txt", + FORMAT_PYLOCK_TOML: "_export_pylock_toml", } def __init__(self, poetry: Poetry, io: IO) -> None: @@ -81,11 +94,20 @@ def export(self, fmt: str, cwd: Path, output: IO | str) -> None: if not self.is_format_supported(fmt): raise ValueError(f"Invalid export format: {fmt}") - getattr(self, self.EXPORT_METHODS[fmt])(cwd, output) + out_dir = cwd + if isinstance(output, str): + out_dir = (cwd / output).parent + content = getattr(self, self.EXPORT_METHODS[fmt])(out_dir) + + if isinstance(output, IO): + output.write(content) + else: + with (cwd / output).open("w", encoding="utf-8") as txt: + txt.write(content) def _export_generic_txt( - self, cwd: Path, output: IO | str, with_extras: bool, allow_editable: bool - ) -> None: + self, out_dir: Path, with_extras: bool, allow_editable: bool + ) -> str: from poetry.core.packages.utils.utils import path_to_url indexes = set() @@ -219,11 +241,7 @@ def _export_generic_txt( content = indexes_header + "\n" + content - if isinstance(output, IO): - output.write(content) - else: - with (cwd / output).open("w", encoding="utf-8") as txt: - txt.write(content) + return content _export_constraints_txt = partialmethod( _export_generic_txt, with_extras=False, allow_editable=False @@ -232,3 +250,185 @@ def _export_generic_txt( _export_requirements_txt = partialmethod( _export_generic_txt, with_extras=True, allow_editable=True ) + + def _get_poetry_version(self) -> str: + return metadata.version("poetry") + + def _export_pylock_toml(self, out_dir: Path) -> str: + from tomlkit import aot + from tomlkit import array + from tomlkit import document + from tomlkit import inline_table + from tomlkit import table + + min_poetry_version = "2.3.0" + if Version.parse(self._get_poetry_version()) < Version.parse( + min_poetry_version + ): + raise RuntimeError( + "Exporting pylock.toml requires Poetry version" + f" {min_poetry_version} or higher." + ) + + if not self._poetry.locker.is_locked_groups_and_markers(): + raise RuntimeError( + "Cannot export pylock.toml because the lock file is not at least version 2.1" + ) + + def add_file_info( + archive: dict[str, Any], + locked_file_info: PackageFile, + additional_file_info: PackageFile | None = None, + ) -> None: + # We only use additional_file_info for url, upload_time and size + # because they are not in locked_file_info. + if additional_file_info: + archive["name"] = locked_file_info["file"] + url = additional_file_info.get("url") + assert url, "url must be present in additional_file_info" + archive["url"] = url + if upload_time := additional_file_info.get("upload_time"): + with contextlib.suppress(ValueError): + # Python < 3.11 does not support 'Z' suffix for UTC, replace it with '+00:00' + archive["upload-time"] = datetime.fromisoformat( + upload_time.replace("Z", "+00:00") + ) + if size := additional_file_info.get("size"): + archive["size"] = size + archive["hashes"] = dict([locked_file_info["hash"].split(":", 1)]) + + python_constraint = self._poetry.package.python_constraint + python_marker = parse_marker( + create_nested_marker("python_version", python_constraint) + ) + + lock = document() + lock["lock-version"] = "1.0" + if self._poetry.package.python_versions != "*": + lock["environments"] = [str(python_marker)] + lock["requires-python"] = str(python_constraint) + lock["created-by"] = "poetry-plugin-export" + + packages = aot() + for dependency_package in get_project_dependency_packages2( + self._poetry.locker, + groups=set(self._groups), + extras=self._extras, + ): + dependency = dependency_package.dependency + package = dependency_package.package + data = table() + data["name"] = package.name + data["version"] = str(package.version) + if not package.marker.is_any(): + data["marker"] = str(package.marker) + if not package.python_constraint.is_any(): + data["requires-python"] = str(package.python_constraint) + packages.append(data) + match dependency: + case VCSDependency(): + vcs = {} + vcs["type"] = "git" + vcs["url"] = dependency.source + vcs["requested-revision"] = dependency.reference + assert dependency.source_resolved_reference, ( + "VCSDependency must have a resolved reference" + ) + vcs["commit-id"] = dependency.source_resolved_reference + if dependency.directory: + vcs["subdirectory"] = dependency.directory + data["vcs"] = vcs + case DirectoryDependency(): + # The version MUST NOT be included when it cannot be guaranteed + # to be consistent with the code used + del data["version"] + dir_: dict[str, Any] = {} + try: + dir_["path"] = dependency.full_path.relative_to( + out_dir + ).as_posix() + except ValueError: + dir_["path"] = dependency.full_path.as_posix() + if package.develop: + dir_["editable"] = package.develop + data["directory"] = dir_ + case FileDependency(): + archive = inline_table() + try: + archive["path"] = dependency.full_path.relative_to( + out_dir + ).as_posix() + except ValueError: + archive["path"] = dependency.full_path.as_posix() + assert len(package.files) == 1, ( + "FileDependency must have exactly one file" + ) + add_file_info(archive, package.files[0]) + if dependency.directory: + archive["subdirectory"] = dependency.directory + data["archive"] = archive + case URLDependency(): + archive = inline_table() + archive["url"] = dependency.url + assert len(package.files) == 1, ( + "URLDependency must have exactly one file" + ) + add_file_info(archive, package.files[0]) + if dependency.directory: + archive["subdirectory"] = dependency.directory + data["archive"] = archive + case _: + data["index"] = package.source_url or "https://pypi.org/simple" + pool_info = { + p["file"]: p + for p in self._poetry.pool.package( + package.name, + package.version, + package.source_reference or "PyPI", + ).files + } + artifacts = { + k: list(v) + for k, v in itertools.groupby( + package.files, + key=( + lambda x: "wheel" + if x["file"].endswith(".whl") + else "sdist" + ), + ) + } + + sdist_files = list(artifacts.get("sdist", [])) + for sdist in sdist_files: + sdist_table = inline_table() + data["sdist"] = sdist_table + add_file_info(sdist_table, sdist, pool_info[sdist["file"]]) + if wheels := list(artifacts.get("wheel", [])): + wheel_array = array() + data["wheels"] = wheel_array + wheel_array.multiline(True) + for wheel in wheels: + wheel_table = inline_table() + add_file_info(wheel_table, wheel, pool_info[wheel["file"]]) + wheel_array.append(wheel_table) + + lock["packages"] = packages if packages else [] + + lock["tool"] = {} + lock["tool"]["poetry-plugin-export"] = {} # type: ignore[index] + lock["tool"]["poetry-plugin-export"]["groups"] = sorted( # type: ignore[index] + self._groups, key=lambda x: (x != "main", x) + ) + lock["tool"]["poetry-plugin-export"]["extras"] = sorted(self._extras) # type: ignore[index] + + # Poetry writes invalid requires-python for "or" relations. + # Though Poetry could parse it, other tools would fail. + # Since requires-python is redundant with markers, we just comment it out. + lock_lines = [ + f"# {line}" + if line.startswith("requires-python = ") and "||" in line + else line + for line in lock.as_string().splitlines() + ] + return "\n".join(lock_lines) + "\n" diff --git a/src/poetry_plugin_export/walker.py b/src/poetry_plugin_export/walker.py index c2b63d4..9921755 100644 --- a/src/poetry_plugin_export/walker.py +++ b/src/poetry_plugin_export/walker.py @@ -276,6 +276,8 @@ def get_project_dependency_packages2( if not marker.validate({"extra": extras}): continue + marker = marker.without_extras() + if project_python_marker: marker = project_python_marker.intersect(marker) diff --git a/tests/command/test_command_export.py b/tests/command/test_command_export.py index 792265b..fb8674c 100644 --- a/tests/command/test_command_export.py +++ b/tests/command/test_command_export.py @@ -9,6 +9,7 @@ from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.packages.package import Package +from poetry.repositories import Repository from poetry_plugin_export.exporter import Exporter from tests.markers import MARKER_PY @@ -20,7 +21,6 @@ from _pytest.monkeypatch import MonkeyPatch from cleo.testers.command_tester import CommandTester from poetry.poetry import Poetry - from poetry.repositories import Repository from pytest_mock import MockerFixture from tests.types import CommandTesterFactory @@ -323,3 +323,64 @@ def test_export_exports_constraints_txt_with_warnings( assert develop_warning in tester.io.fetch_error() assert tester.io.fetch_output() == expected + + +def test_export_pylock_toml( + mocker: MockerFixture, poetry: Poetry, tester: CommandTester, do_lock: None +) -> None: + mocker.patch( + "poetry_plugin_export.exporter.Exporter._get_poetry_version", + return_value="2.3.0", + ) + poetry.package.python_versions = "*" + repo = Repository("PyPI") + poetry.pool.add_repository(repo) + repo.add_package(Package("foo", "1.0")) + + assert tester.execute("--format pylock.toml") == 0 + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0.0" +index = "https://pypi.org/simple" + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + assert tester.io.fetch_output() == expected + + +@pytest.mark.parametrize("name", ["pylock.toml", "pylock.dev.toml"]) +def test_export_pylock_toml_valid_file_names( + mocker: MockerFixture, + poetry: Poetry, + tester: CommandTester, + do_lock: None, + name: str, +) -> None: + export_mock = mocker.patch("poetry_plugin_export.command.Exporter.export") + + assert tester.execute(f"--format pylock.toml --output somedir/{name}") == 0 + assert export_mock.call_count == 1 + + +@pytest.mark.parametrize("name", ["pylock-dev.toml", "pylock.dev.test.toml"]) +def test_export_pylock_toml_invalid_file_names( + mocker: MockerFixture, + poetry: Poetry, + tester: CommandTester, + do_lock: None, + name: str, +) -> None: + export_mock = mocker.patch("poetry_plugin_export.command.Exporter.export") + + assert tester.execute(f"--format pylock.toml --output somedir/{name}") == 1 + assert export_mock.call_count == 0 + assert ( + "The output file for pylock.toml export must be named" + in tester.io.fetch_error() + ) diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 7563f73..b56c976 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -72,7 +72,7 @@ def is_locked(self) -> bool: def is_fresh(self) -> bool: return True - def _get_content_hash(self) -> str: + def _get_content_hash(self, *, with_dependency_groups: bool = True) -> str: return "123456789" diff --git a/tests/test_exporter_pylock_toml.py b/tests/test_exporter_pylock_toml.py new file mode 100644 index 0000000..df40a7a --- /dev/null +++ b/tests/test_exporter_pylock_toml.py @@ -0,0 +1,1083 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +import pytest + +from cleo.io.null_io import NullIO +from packaging.utils import canonicalize_name +from poetry.core.constraints.version import Version +from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.core.packages.package import Package +from poetry.factory import Factory +from poetry.packages import Locker as BaseLocker +from poetry.repositories import Repository + +from poetry_plugin_export.exporter import Exporter + + +if TYPE_CHECKING: + from pathlib import Path + + from poetry.poetry import Poetry + from pytest_mock import MockerFixture + + +DEV_GROUP = canonicalize_name("dev") + + +class Locker(BaseLocker): + def __init__(self, fixture_root: Path) -> None: + super().__init__(fixture_root / "poetry.lock", {}) + self._locked = True + + def locked(self, is_locked: bool = True) -> Locker: + self._locked = is_locked + + return self + + def mock_lock_data(self, data: dict[str, Any]) -> None: + self._lock_data = data + + def is_locked(self) -> bool: + return self._locked + + def is_fresh(self) -> bool: + return True + + def _get_content_hash(self, *, with_dependency_groups: bool = True) -> str: + return "123456789" + + +@pytest.fixture +def locker(fixture_root: Path) -> Locker: + return Locker(fixture_root) + + +@pytest.fixture +def pypi_repo() -> Repository: + repo = Repository("PyPI") + foo = Package("foo", "1.0") + foo.files = [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + "upload_time": "2025-12-28T12:34:56.789Z", + "size": 12345, + }, + { + "file": "foo-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + "url": "https://example.org/foo-1.0.tar.gz", + }, + ] + repo.add_package(foo) + return repo + + +@pytest.fixture +def legacy_repositories() -> list[Repository]: + repos = [] + for repo_name in ("legacy1", "legacy2"): + repo = Repository(repo_name) + repos.append(repo) + for package_name in ("foo", "bar"): + package = Package( + package_name, + "1.0", + source_type="legacy", + source_url=f"https://{repo_name}.org/simple", + source_reference=repo_name, + ) + package.files = [ + { + "file": f"{package_name}-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + "url": f"https://{repo_name}.org/{package_name}-1.0-py3-none-any.whl", + }, + { + "file": f"{package_name}-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + "url": f"https://{repo_name}.org/{package_name}-1.0.tar.gz", + "upload_time": "2025-12-27T12:34:56.789Z", + "size": 42, + }, + ] + repo.add_package(package) + return repos + + +@pytest.fixture +def poetry( + fixture_root: Path, + locker: Locker, + pypi_repo: Repository, + legacy_repositories: list[Repository], +) -> Poetry: + p = Factory().create_poetry(fixture_root / "sample_project") + p.package.python_versions = "*" + p._locker = locker + p.pool.remove_repository("PyPI") + p.pool.add_repository(pypi_repo) + for repo in legacy_repositories: + p.pool.add_repository(repo) + + return p + + +@pytest.fixture(autouse=True) +def mock_poetry_version(mocker: MockerFixture) -> None: + mocker.patch( + "poetry_plugin_export.exporter.Exporter._get_poetry_version", + return_value="2.3.0", + ) + + +def test_exporter_raises_error_on_old_poetry_version( + mocker: MockerFixture, tmp_path: Path, poetry: Poetry +) -> None: + mocker.patch( + "poetry_plugin_export.exporter.Exporter._get_poetry_version", + return_value="2.2.1", + ) + + lock_data = {"metadata": {"lock-version": "2.0"}} + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + with pytest.raises(RuntimeError) as exc_info: + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + assert str(exc_info.value) == ( + "Exporting pylock.toml requires Poetry version 2.3.0 or higher." + ) + + +def test_exporter_raises_error_on_old_lock_version( + tmp_path: Path, poetry: Poetry +) -> None: + lock_data = {"metadata": {"lock-version": "2.0"}} + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + with pytest.raises(RuntimeError) as exc_info: + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + assert str(exc_info.value) == ( + "Cannot export pylock.toml because the lock file is not at least version 2.1" + ) + + +def test_exporter_locks_exported_groups_and_extras( + tmp_path: Path, poetry: Poetry +) -> None: + lock_data = {"package": [], "metadata": {"lock-version": "2.1"}} + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + exporter.only_groups([DEV_GROUP]) + exporter.with_extras([canonicalize_name("extra1"), canonicalize_name("extra2")]) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" +packages = [] + +[tool.poetry-plugin-export] +groups = ["dev"] +extras = ["extra1", "extra2"] +""" + + assert content == expected + + +@pytest.mark.parametrize( + ("python_versions", "expected_python", "expected_marker"), + [ + (">=3.9", ">=3.9", 'python_version >= "3.9"'), + ("~3.9", ">=3.9,<3.10", 'python_version == "3.9"'), + ("^3.9", ">=3.9,<4.0", 'python_version >= "3.9" and python_version < "4.0"'), + ], +) +def test_exporter_python_constraint( + tmp_path: Path, + poetry: Poetry, + python_versions: str, + expected_python: str, + expected_marker: str, +) -> None: + poetry.package.python_versions = python_versions + lock_data = {"package": [], "metadata": {"lock-version": "2.1"}} + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected_marker = expected_marker.replace('"', '\\"') + expected = f"""\ +lock-version = "1.0" +environments = ["{expected_marker}"] +requires-python = "{expected_python}" +created-by = "poetry-plugin-export" +packages = [] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +@pytest.mark.parametrize( + ("python_versions", "expected_python", "expected_marker"), + [ + ( + "~2.7 | ^3.9", + ">=2.7,<2.8 || >=3.9,<4.0", + 'python_version == "2.7" or python_version >= "3.9" and python_version < "4.0"', + ), + ], +) +def test_exporter_does_not_write_invalid_python_constraint( + tmp_path: Path, + poetry: Poetry, + python_versions: str, + expected_python: str, + expected_marker: str, +) -> None: + poetry.package.python_versions = python_versions + lock_data = {"package": [], "metadata": {"lock-version": "2.1"}} + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected_marker = expected_marker.replace('"', '\\"') + expected = f"""\ +lock-version = "1.0" +environments = ["{expected_marker}"] +# requires-python = "{expected_python}" +created-by = "poetry-plugin-export" +packages = [] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_vcs_dependencies(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", + "resolved_reference": "abcdef", + }, + }, + { + "name": "bar", + "version": "2.3", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "git", + "url": "https://github.com/bar/bar.git", + "reference": "123456", + "resolved_reference": "abcdef", + "subdirectory": "subdir", + }, + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.2.3" + +[packages.vcs] +type = "git" +url = "https://github.com/foo/foo.git" +requested-revision = "123456" +commit-id = "abcdef" + +[[packages]] +name = "bar" +version = "2.3" + +[packages.vcs] +type = "git" +url = "https://github.com/bar/bar.git" +requested-revision = "123456" +commit-id = "abcdef" +subdirectory = "subdir" + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_directory_dependencies(tmp_path: Path, poetry: Poetry) -> None: + tmp_project = tmp_path / "tmp_project" + tmp_project.mkdir() + lock_data = { + "package": [ + { + "name": "simple_project", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "directory", + "url": "simple_project", + }, + }, + { + "name": "tmp-project", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "develop": True, + "groups": [MAIN_GROUP], + "source": { + "type": "directory", + "url": tmp_project.as_posix(), + }, + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = f"""\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "simple-project" + +[packages.directory] +path = "{(poetry.locker.lock.parent / "simple_project").as_posix()}" + +[[packages]] +name = "tmp-project" + +[packages.directory] +path = "tmp_project" +editable = true + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_file_dependencies( + tmp_path: Path, poetry: Poetry, fixture_root: Path +) -> None: + tmp_project = tmp_path / "files" / "tmp_project.zip" + tmp_project.parent.mkdir() + tmp_project.touch() + lock_data = { + "package": [ + { + "name": "demo", + "version": "0.1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "file", + "url": "distributions/demo-0.2.0-py3-none-any.whl", + }, + "files": [ + { + "file": "demo-0.2.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + } + ], + }, + { + "name": "simple-project", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "develop": True, + "groups": [MAIN_GROUP], + "source": { + "type": "directory", + "url": "simple_project/dist/simple_project-0.1.0.tar.gz", + }, + "files": [ + { + "file": "simple_project-0.1.0.tar.gz", + "hash": "sha256:1234567890abcdef", + } + ], + }, + { + "name": "tmp-project", + "version": "3", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "file", + "url": f"{tmp_project.as_posix()}", + "subdirectory": "sub", + }, + "files": [ + { + "file": "tmp_project.zip", + "hash": "sha256:fedcba0987654321", + } + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = f"""\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "demo" +version = "0.1.0" +archive = {{path = "{fixture_root.as_posix()}/distributions/demo-0.2.0-py3-none-any.whl", hashes = {{sha256 = "abcdef1234567890"}}}} + +[[packages]] +name = "simple-project" + +[packages.directory] +path = "{(poetry.locker.lock.parent / "simple_project" / "dist" / "simple_project-0.1.0.tar.gz").as_posix()}" +editable = true + +[[packages]] +name = "tmp-project" +version = "3" +archive = {{path = "files/tmp_project.zip", hashes = {{sha256 = "fedcba0987654321"}}, subdirectory = "sub"}} + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_url_dependencies(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "url", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + }, + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + } + ], + }, + { + "name": "bar", + "version": "3", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "url", + "url": "https://example.org/bar.zip#subdir=sub", + "subdirectory": "sub", + }, + "files": [ + { + "file": "bar.zip", + "hash": "sha256:fedcba0987654321", + } + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +archive = {url = "https://example.org/foo-1.0-py3-none-any.whl", hashes = {sha256 = "abcdef1234567890"}} + +[[packages]] +name = "bar" +version = "3" +archive = {url = "https://example.org/bar.zip#subdir=sub", hashes = {sha256 = "fedcba0987654321"}, subdirectory = "sub"} + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_pypi_dependencies(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + }, + { + "file": "foo-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + }, + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +index = "https://pypi.org/simple" +sdist = {name = "foo-1.0.tar.gz", url = "https://example.org/foo-1.0.tar.gz", hashes = {sha256 = "0123456789abcdef"}} +wheels = [ + {name = "foo-1.0-py3-none-any.whl", url = "https://example.org/foo-1.0-py3-none-any.whl", upload-time = 2025-12-28T12:34:56.789000Z, size = 12345, hashes = {sha256 = "abcdef1234567890"}}, +] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_pypi_dependencies_sdist_only(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "files": [ + { + "file": "foo-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + }, + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + poetry.pool.repository("PyPI").package("foo", Version.parse("1.0")).files = [ + { + "file": "foo-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + "url": "https://example.org/foo-1.0.tar.gz", + }, + ] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +index = "https://pypi.org/simple" +sdist = {name = "foo-1.0.tar.gz", url = "https://example.org/foo-1.0.tar.gz", hashes = {sha256 = "0123456789abcdef"}} + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_pypi_dependencies_wheel_only(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + }, + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + poetry.pool.repository("PyPI").package("foo", Version.parse("1.0")).files = [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + }, + ] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +index = "https://pypi.org/simple" +wheels = [ + {name = "foo-1.0-py3-none-any.whl", url = "https://example.org/foo-1.0-py3-none-any.whl", hashes = {sha256 = "abcdef1234567890"}}, +] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_pypi_dependencies_multiple_wheels( + tmp_path: Path, poetry: Poetry +) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "files": [ + { + "file": "foo-1.0-py2-none-any.whl", + "hash": "sha256:abcdef1234567891", + }, + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + }, + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + poetry.pool.repository("PyPI").package("foo", Version.parse("1.0")).files = [ + { + "file": "foo-1.0-py2-none-any.whl", + "hash": "sha256:abcdef1234567891", + "url": "https://example.org/foo-1.0-py2-none-any.whl", + }, + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + }, + ] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +index = "https://pypi.org/simple" +wheels = [ + {name = "foo-1.0-py2-none-any.whl", url = "https://example.org/foo-1.0-py2-none-any.whl", hashes = {sha256 = "abcdef1234567891"}}, + {name = "foo-1.0-py3-none-any.whl", url = "https://example.org/foo-1.0-py3-none-any.whl", hashes = {sha256 = "abcdef1234567890"}}, +] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +def test_export_legacy_repo_dependencies(tmp_path: Path, poetry: Poetry) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "legacy", + "url": "https://legacy1.org/simple", + "reference": "legacy1", + }, + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + }, + { + "file": "foo-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + }, + ], + }, + { + "name": "bar", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP], + "source": { + "type": "legacy", + "url": "https://legacy2.org/simple", + "reference": "legacy2", + }, + "files": [ + { + "file": "bar-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + }, + { + "file": "bar-1.0.tar.gz", + "hash": "sha256:0123456789abcdef", + }, + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +lock-version = "1.0" +created-by = "poetry-plugin-export" + +[[packages]] +name = "foo" +version = "1.0" +index = "https://legacy1.org/simple" +sdist = {name = "foo-1.0.tar.gz", url = "https://legacy1.org/foo-1.0.tar.gz", upload-time = 2025-12-27T12:34:56.789000Z, size = 42, hashes = {sha256 = "0123456789abcdef"}} +wheels = [ + {name = "foo-1.0-py3-none-any.whl", url = "https://legacy1.org/foo-1.0-py3-none-any.whl", hashes = {sha256 = "abcdef1234567890"}}, +] + +[[packages]] +name = "bar" +version = "1.0" +index = "https://legacy2.org/simple" +sdist = {name = "bar-1.0.tar.gz", url = "https://legacy2.org/bar-1.0.tar.gz", upload-time = 2025-12-27T12:34:56.789000Z, size = 42, hashes = {sha256 = "0123456789abcdef"}} +wheels = [ + {name = "bar-1.0-py3-none-any.whl", url = "https://legacy2.org/bar-1.0-py3-none-any.whl", hashes = {sha256 = "abcdef1234567890"}}, +] + +[tool.poetry-plugin-export] +groups = ["main"] +extras = [] +""" + + assert content == expected + + +@pytest.mark.parametrize( + ("groups", "extras", "marker", "expected"), + [ + ({"main"}, set(), 'python_version >= "3.6"', 'python_version >= "3.6"'), + ({"other"}, set(), 'python_version >= "3.6"', ""), + ( + {"main"}, + set(), + {"main": 'python_version >= "3.6"'}, + 'python_version >= "3.6"', + ), + ({"dev"}, set(), {"main": 'python_version >= "3.6"'}, "*"), + ( + {"dev"}, + set(), + {"main": 'python_version >= "3.6"', "dev": 'python_version < "3.6"'}, + 'python_version < "3.6"', + ), + ( + {"main", "dev"}, + set(), + {"main": 'python_version >= "3.6"', "dev": 'python_version < "3.6"'}, + "*", + ), + ( + {"main", "dev"}, + set(), + {"main": 'python_version >= "3.6"', "dev": 'sys_platform == "linux"'}, + 'python_version >= "3.6" or sys_platform == "linux"', + ), + # extras + ({"main"}, {}, 'python_version >= "3.6" and extra == "extra1"', ""), + ( + {"main"}, + {}, + 'python_version >= "3.6" or extra == "extra1"', + 'python_version >= "3.6"', + ), + ( + {"main"}, + {}, + 'python_version >= "3.6" and extra != "extra1"', + 'python_version >= "3.6"', + ), + ( + {"main"}, + {"extra1"}, + 'python_version >= "3.6" and extra == "extra1"', + 'python_version >= "3.6"', + ), + ( + {"main"}, + {"extra1"}, + 'python_version >= "3.6" or extra == "extra1"', + 'python_version >= "3.6"', + ), + ({"main"}, {"extra1"}, 'python_version >= "3.6" and extra != "extra1"', ""), + ], +) +def test_export_markers( + tmp_path: Path, + poetry: Poetry, + groups: set[str], + extras: set[str], + marker: str | dict[str, str], + expected: str, +) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": "*", + "groups": [MAIN_GROUP, DEV_GROUP], + "markers": marker, + "source": { + "type": "url", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + }, + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + } + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + exporter.only_groups({canonicalize_name(g) for g in groups}) + if extras: + exporter.with_extras({canonicalize_name(e) for e in extras}) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + match expected: + case "": + assert 'name = "foo"' not in content + case "*": + assert 'name = "foo"' in content.splitlines() + assert "marker = " not in content + case _: + expected = expected.replace('"', '\\"') + assert f'marker = "{expected}"' in content.splitlines() + + +@pytest.mark.parametrize( + ("python_versions", "expected"), + [ + (">=3.9", ">=3.9"), + ("*", None), + ("~2.7 | ^3.9", "# >=2.7,<2.8 || >=3.9,<4.0"), + ], +) +def test_export_requires_python( + tmp_path: Path, poetry: Poetry, python_versions: str, expected: str | None +) -> None: + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.0", + "optional": False, + "python-versions": python_versions, + "groups": [MAIN_GROUP], + "source": { + "type": "url", + "url": "https://example.org/foo-1.0-py3-none-any.whl", + }, + "files": [ + { + "file": "foo-1.0-py3-none-any.whl", + "hash": "sha256:abcdef1234567890", + } + ], + }, + ], + "metadata": {"lock-version": "2.1"}, + } + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + + exporter = Exporter(poetry, NullIO()) + + exporter.export("pylock.toml", tmp_path, "pylock.toml") + + with (tmp_path / "pylock.toml").open(encoding="utf-8") as f: + content = f.read() + + if expected is None: + assert "requires-python" not in content + else: + prefix = "# " if expected.startswith("#") else "" + expected = expected.removeprefix("# ") + assert f'{prefix}requires-python = "{expected}"' in content.splitlines() From a7d9d4b316313d7f4c2fdbdd3c47ea9dd176666a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:31:06 +0100 Subject: [PATCH 2/2] temp: use suitable poetry version --- poetry.lock | 101 +++++++++++++++++++++++++------------------------ pyproject.toml | 2 +- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/poetry.lock b/poetry.lock index e80714d..e7d84be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -569,43 +569,38 @@ files = [ [[package]] name = "dulwich" -version = "0.24.10" +version = "0.25.0" description = "Python Git Library" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "dulwich-0.24.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1f511f7afe1f36e37193214e4e069685d7d0378e756cc96a2fcb138bdf9fefca"}, - {file = "dulwich-0.24.10-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2a56f9838e5d2414a2b57bab370b73b9803fefd98836ef841f0fd489b5cc1349"}, - {file = "dulwich-0.24.10-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90b24c0827299cfb53c4f4d4fedc811be5c4b10c11172ff6e5a5c52277fe0b3a"}, - {file = "dulwich-0.24.10-cp310-cp310-win32.whl", hash = "sha256:0dfae8c59b97964a907fdf4c5809154a18fd8c55f2eb6d8fd1607464165a9aa2"}, - {file = "dulwich-0.24.10-cp310-cp310-win_amd64.whl", hash = "sha256:0e1601789554e3d15b294356c78a5403521c27d5460e64dbbc44ffd5b10af4c3"}, - {file = "dulwich-0.24.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf94fa73211d2f029751a72e1ca3a2fd35c6f5d9bb434acdf10a4a79ca322dd"}, - {file = "dulwich-0.24.10-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b715a9f85ed71bef8027275c1bded064e4925071ae8c8a8d9a20c67b31faf3cd"}, - {file = "dulwich-0.24.10-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:858fae0c7121715282a993abb1919385a28e1a9c4f136f568748d283c2ba874f"}, - {file = "dulwich-0.24.10-cp311-cp311-win32.whl", hash = "sha256:393e9c3cdd382cff20b5beb66989376d6da69e3b0dfec046a884707ab5d27ac9"}, - {file = "dulwich-0.24.10-cp311-cp311-win_amd64.whl", hash = "sha256:470d6cd8207e1a5ff1fb34c4c6fac2ec9a96d618f7062e5fb96c5260927bb9a7"}, - {file = "dulwich-0.24.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c724e5fc67c45f3c813f2630795ac388e3e6310534212f799a7a6bf230648c8"}, - {file = "dulwich-0.24.10-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6a25ca1605a94090514af408f9df64427281aefbb726f542e97d86d3a7c8ec18"}, - {file = "dulwich-0.24.10-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d9793fc1e42149a650a017dc8ce38485368a41729b9937e1dfcfedd0591ebe9d"}, - {file = "dulwich-0.24.10-cp312-cp312-win32.whl", hash = "sha256:1601bfea3906b52c924fae5b6ba32a0b087fb8fae927607e6b5381e6f7559611"}, - {file = "dulwich-0.24.10-cp312-cp312-win_amd64.whl", hash = "sha256:f7bfa9f0bfae57685754b163eef6641609047460939d28052e3beeb63efa6795"}, - {file = "dulwich-0.24.10-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:843de5f678436a27b33aea0f2b87fd0453afdd0135f885a3ca44bc3147846dd2"}, - {file = "dulwich-0.24.10-cp313-cp313-android_21_x86_64.whl", hash = "sha256:4914abb6408a719b7a1f7d9a182d1efd92c326e178b440faf582df50f9f032db"}, - {file = "dulwich-0.24.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ce6e05ec50f258ccd14d83114eb32cc5bb241ae4a8c7199d014fd7568de285b1"}, - {file = "dulwich-0.24.10-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3581ae0af33f28e6c0834d2f41ca67ca81cd92a589e6a5f985e6c64373232958"}, - {file = "dulwich-0.24.10-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:019af16c850ae85254289f9633a29dea02f45351c4182ea20b0c1394c074a13b"}, - {file = "dulwich-0.24.10-cp313-cp313-win32.whl", hash = "sha256:4b5c225477a529e1d4a2b5e51272a418177e34803938391ce41b7573b2e5b0d0"}, - {file = "dulwich-0.24.10-cp313-cp313-win_amd64.whl", hash = "sha256:752c32d517dc608dbb8414061eaaec8ac8a05591b29531f81a83336b018b26c6"}, - {file = "dulwich-0.24.10-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:44f62e0244531a8c43ca7771e201ec9e7f6a2fb27f8c3c623939bc03c1f50423"}, - {file = "dulwich-0.24.10-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e2eda4a634d6f1ac4c0d4786f8772495c8840dfc2b3e595507376bf5e5b0f9c5"}, - {file = "dulwich-0.24.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b19af8a3ab051003ba05f15fc5c0d6f0d427e795639490790f34ec0558e99e3"}, - {file = "dulwich-0.24.10-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:90028182b9a47ea4efed51c81298f3a98e279d7bf5c1f91c47101927a309ee45"}, - {file = "dulwich-0.24.10-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8df79c8080471f363e4dfcfc4e0d2e61e6da73af1fd7d31cb6ae0d34af45a6b4"}, - {file = "dulwich-0.24.10-cp39-cp39-win32.whl", hash = "sha256:f102c38207540fa485e85e0b763ce3725a2d49d846dbf316ed271e27fd85ff21"}, - {file = "dulwich-0.24.10-cp39-cp39-win_amd64.whl", hash = "sha256:c262ffc94338999e7808b434dccafaccd572d03b42d4ef140059d4b7cad765a3"}, - {file = "dulwich-0.24.10-py3-none-any.whl", hash = "sha256:15b32f8c3116a1c0a042dde8da96f65a607e263e860ee42b3d4a98ce2c2f4a06"}, - {file = "dulwich-0.24.10.tar.gz", hash = "sha256:30e028979b6fa7220c913da9c786026611c10746c06496149742602b36a11f6b"}, + {file = "dulwich-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7e9233686fd49c7fa311e1a9f769ce0fa9eb57e546b6ccd88d2dafb5d7cb6bd"}, + {file = "dulwich-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47f0328af2c0e5149f356b27d1ac5b2860049c29bf32d2e5994d33f879909dd6"}, + {file = "dulwich-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6ca746bd4f8a6a7b849a759c34e960dd7b6fa573225a571e23ea9c73377175d2"}, + {file = "dulwich-0.25.0-cp310-cp310-win32.whl", hash = "sha256:4a98628ae4150f5084e0e0eab884c967d9f499304ff220f558ebe523868fd564"}, + {file = "dulwich-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:db89094df6567721ec1eae8a70f85afd22e07eefa86a1b11194247407a3426ee"}, + {file = "dulwich-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d342daf24cc544f1ccc7e6cf6b8b22d10a4381c1c7ed2bf0e2024a48be9218f"}, + {file = "dulwich-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1575e7bf93cbc9ae93d6653fe29962357b96a1f5943275ff55cbb772e61359e2"}, + {file = "dulwich-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:63846a66254dd89bec7b3df75dda61fc37f9c53aa93cddf46d063a9e1f832634"}, + {file = "dulwich-0.25.0-cp311-cp311-win32.whl", hash = "sha256:92cc60a9cfd027b0bbaeb588ab06577d58e2b1a41c824f069bd53544f0cccdf3"}, + {file = "dulwich-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9d5710c8dbaefe6254bbefb409c612485e32d983df9a1299459987b13f2ac3f"}, + {file = "dulwich-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866dcf6103ca4dddf9db5c307700b5b47dd49ddadb63423d957bb24d438a87d2"}, + {file = "dulwich-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b074a82f40a3ab4068e2f01697a65b6239db55a3335e5c2e9b2a630601c1aa05"}, + {file = "dulwich-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8ad390efed25a4fad288f80449a2180bfdb17db19bed4916630c01d20084c4b"}, + {file = "dulwich-0.25.0-cp312-cp312-win32.whl", hash = "sha256:14c9aba34e1ac262806174304a5a17a78a0f83d0a6960e506005d3aa1cf9004e"}, + {file = "dulwich-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:caeb9740f6e0d5a3fa48e1a009dee2f99f47be1836c6bc252022aa25327fcb0e"}, + {file = "dulwich-0.25.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:c1731f45fd24b05a01ac32dc0f7e96337a3bd78ab33a230b2035a82f624d112e"}, + {file = "dulwich-0.25.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c0bbe69be332d4cee36f628ba5feaf731c6a53dbe1ea1cf40324a4954a92093a"}, + {file = "dulwich-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b88ef0402ce2a94db5ae926e6be8048e59e8cdcc889a71e332d0e7bcc59f8b7"}, + {file = "dulwich-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ae6f4c99a3978ff4fb1f537d16435d75a17f97ec84f61e3a9ac2b7b879b4dae8"}, + {file = "dulwich-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4b46836c467bd898fd2ff1d4ebe511d2956f7f3f181dccbdde8631d4031cd0fa"}, + {file = "dulwich-0.25.0-cp313-cp313-win32.whl", hash = "sha256:757ab788d2d87d96e4b5e84eaddc32d7b8e5b57a221f43b8cbb694787a9c1b80"}, + {file = "dulwich-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:97f05e8a38f0e1a872b623e094bd270760318c9ab947ff65359192c9a692bda1"}, + {file = "dulwich-0.25.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:b2eb2c727cfa173a48b65fbfc67b170f47c5b28d483759a1fc26886b01770345"}, + {file = "dulwich-0.25.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:83e1cbff47ce1dc7d44a20f624c0d2fcbc6a70a458c5fe8e0f8bbf84f32aeb1c"}, + {file = "dulwich-0.25.0-py3-none-any.whl", hash = "sha256:b5459ed202fcc7bdaaf619b4bd2718fc7ac7c5dea9c0be682f7e64bf145749e5"}, + {file = "dulwich-0.25.0.tar.gz", hash = "sha256:baa84b539fea0e6a925a9159c3e0a1d08cceeea5260732b84200e077444a4b0e"}, ] [package.dependencies] @@ -614,7 +609,7 @@ urllib3 = ">=2.2.2" [package.extras] colordiff = ["rich"] -dev = ["codespell (==2.4.1)", "dissolve (>=0.1.1)", "mypy (==1.18.2)", "ruff (==0.13.2)"] +dev = ["codespell (==2.4.1)", "dissolve (>=0.1.1)", "mypy (==1.19.0)", "ruff (==0.14.7)"] fastimport = ["fastimport"] fuzzing = ["atheris"] https = ["urllib3 (>=2.2.2)"] @@ -1300,27 +1295,25 @@ name = "poetry" version = "2.2.1" description = "Python dependency management and packaging made easy." optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.10,<4.0" groups = ["main"] -files = [ - {file = "poetry-2.2.1-py3-none-any.whl", hash = "sha256:f5958b908b96c5824e2acbb8b19cdef8a3351c62142d7ecff2d705396c8ca34c"}, - {file = "poetry-2.2.1.tar.gz", hash = "sha256:bef9aa4bb00ce4c10b28b25e7bac724094802d6958190762c45df6c12749b37c"}, -] +files = [] +develop = false [package.dependencies] build = ">=1.2.1,<2.0.0" cachecontrol = {version = ">=0.14.0,<0.15.0", extras = ["filecache"]} cleo = ">=2.1.0,<3.0.0" -dulwich = ">=0.24.0,<0.25.0" +dulwich = ">=0.25.0,<0.26.0" fastjsonschema = ">=2.18.0,<3.0.0" findpython = ">=0.6.2,<0.8.0" installer = ">=0.7.0,<0.8.0" keyring = ">=25.1.0,<26.0.0" packaging = ">=24.2" -pbs-installer = {version = ">=2025.1.6,<2026.0.0", extras = ["download", "install"]} +pbs-installer = {version = ">=2025.6.10", extras = ["download", "install"]} pkginfo = ">=1.12,<2.0" platformdirs = ">=3.0.0,<5" -poetry-core = "2.2.1" +poetry-core = {git = "https://github.com/radoering/poetry-core.git", rev = "file-url-size-upload-time"} pyproject-hooks = ">=1.0.0,<2.0.0" requests = ">=2.26,<3.0" requests-toolbelt = ">=1.0.0,<2.0.0" @@ -1331,17 +1324,27 @@ trove-classifiers = ">=2022.5.19" virtualenv = ">=20.26.6" xattr = {version = ">=1.0.0,<2.0.0", markers = "sys_platform == \"darwin\""} +[package.source] +type = "git" +url = "https://github.com/radoering/poetry.git" +reference = "repo-file-url-size-upload-time" +resolved_reference = "aa6647e3502944cec3950d972fba60ebc249b091" + [[package]] name = "poetry-core" version = "2.2.1" description = "Poetry PEP 517 Build Backend" optional = false -python-versions = "<4.0,>=3.9" +python-versions = ">=3.10, <4.0" groups = ["main"] -files = [ - {file = "poetry_core-2.2.1-py3-none-any.whl", hash = "sha256:bdfce710edc10bfcf9ab35041605c480829be4ab23f5bc01202cfe5db8f125ab"}, - {file = "poetry_core-2.2.1.tar.gz", hash = "sha256:97e50d8593c8729d3f49364b428583e044087ee3def1e010c6496db76bd65ac5"}, -] +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/radoering/poetry-core.git" +reference = "file-url-size-upload-time" +resolved_reference = "c206e05be1ecbadfa90b17b0dc244581bf51aafd" [[package]] name = "pre-commit" @@ -2135,9 +2138,9 @@ files = [ ] [package.extras] -cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b0) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""] +cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "04588f581e9016fe28653baa5b49bc87e15db54dae0dd85e8ec161817fab33e8" +content-hash = "71851a4fd525096ec87b433f7e31e3bbb06b1aeea7603d3369ce533d36f26393" diff --git a/pyproject.toml b/pyproject.toml index c27fc40..2c029e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" readme = "README.md" requires-python = ">=3.10,<4.0" dependencies = [ - "poetry>=2.1.0,<3.0.0", + "poetry @ git+https://github.com/radoering/poetry.git@repo-file-url-size-upload-time", "poetry-core>=2.1.0,<3.0.0", "tomlkit (>=0.11.4,<1.0.0)", ]