diff --git a/src/poetry_plugin_export/exporter.py b/src/poetry_plugin_export/exporter.py index 7ff5f08..3add86b 100644 --- a/src/poetry_plugin_export/exporter.py +++ b/src/poetry_plugin_export/exporter.py @@ -11,6 +11,7 @@ from poetry.repositories.http_repository import HTTPRepository from poetry_plugin_export.walker import get_project_dependency_packages +from poetry_plugin_export.walker import get_project_dependency_packages2 if TYPE_CHECKING: @@ -89,17 +90,26 @@ def _export_generic_txt( content = "" dependency_lines = set() - root = self._poetry.package.with_dependency_groups( - list(self._groups), only=True - ) - - for dependency_package in get_project_dependency_packages( - self._poetry.locker, - project_requires=root.all_requires, - root_package_name=root.name, - project_python_marker=root.python_marker, - extras=self._extras, - ): + if self._poetry.locker.is_locked_groups_and_markers(): + dependency_package_iterator = get_project_dependency_packages2( + self._poetry.locker, + project_python_marker=self._poetry.package.python_marker, + groups=set(self._groups), + extras=self._extras, + ) + else: + root = self._poetry.package.with_dependency_groups( + list(self._groups), only=True + ) + dependency_package_iterator = get_project_dependency_packages( + self._poetry.locker, + project_requires=root.all_requires, + root_package_name=root.name, + project_python_marker=root.python_marker, + extras=self._extras, + ) + + for dependency_package in dependency_package_iterator: line = "" if not with_extras: diff --git a/src/poetry_plugin_export/walker.py b/src/poetry_plugin_export/walker.py index 028fbb6..1161ab6 100644 --- a/src/poetry_plugin_export/walker.py +++ b/src/poetry_plugin_export/walker.py @@ -264,5 +264,27 @@ def get_locked_package( return next(iter(compatible_candidates), None) +def get_project_dependency_packages2( + locker: Locker, + project_python_marker: BaseMarker | None = None, + groups: Collection[str] = (), + extras: Collection[NormalizedName] = (), +) -> Iterator[DependencyPackage]: + for package, info in locker.locked_packages().items(): + if not info.groups.intersection(groups): + continue + + marker = info.get_marker(groups) + if not marker.validate({"extra": extras}): + continue + + if project_python_marker: + marker = project_python_marker.intersect(marker) + + package.marker = marker + + yield DependencyPackage(dependency=package.to_dependency(), package=package) + + class DependencyWalkerError(Exception): pass diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 7cbda52..e46e5ca 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -7,6 +7,7 @@ from cleo.io.buffered_io import BufferedIO from cleo.io.null_io import NullIO +from poetry.core.constraints.version import Version from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.core.version.markers import parse_marker @@ -16,6 +17,7 @@ from poetry.repositories.repository_pool import Priority from poetry_plugin_export.exporter import Exporter +from poetry_plugin_export.walker import DependencyWalkerError from tests.markers import MARKER_CPYTHON from tests.markers import MARKER_DARWIN from tests.markers import MARKER_LINUX @@ -103,32 +105,44 @@ def set_package_requires( poetry._package = package +def fix_lock_data(lock_data: dict[str, Any]) -> None: + if Version.parse(lock_data["metadata"]["lock-version"]) >= Version.parse("2.1"): + for locked_package in lock_data["package"]: + locked_package["groups"] = ["main"] + locked_package["files"] = lock_data["metadata"]["files"][ + locked_package["name"] + ] + del lock_data["metadata"]["files"] + + +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_standard_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": []}, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -145,38 +159,43 @@ def test_exporter_can_export_requirements_txt_with_standard_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + { + "name": "baz", + "version": "7.8.9", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + lock_data["package"][2]["markers"] = "sys_platform == 'win32'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] markers = { "foo": "python_version < '3.7'", "bar": "extra =='foo'", @@ -199,75 +218,80 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_poetry( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: """Regression test for #3254""" - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "poetry", - "version": "1.1.4", - "optional": False, - "python-versions": "*", - "dependencies": {"keyring": "*"}, - }, - { - "name": "junit-xml", - "version": "1.9", - "optional": False, - "python-versions": "*", - "dependencies": {"six": "*"}, - }, - { - "name": "keyring", - "version": "21.8.0", - "optional": False, - "python-versions": "*", - "dependencies": { - "SecretStorage": { - "version": "*", - "markers": "sys_platform == 'linux'", - } - }, - }, - { - "name": "secretstorage", - "version": "3.3.0", - "optional": False, - "python-versions": "*", - "dependencies": {"cryptography": "*"}, - }, - { - "name": "cryptography", - "version": "3.2", - "optional": False, - "python-versions": "*", - "dependencies": {"six": "*"}, - }, - { - "name": "six", - "version": "1.15.0", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { - "python-versions": "*", - "content-hash": "123456789", - "files": { - "poetry": [], - "keyring": [], - "secretstorage": [], - "cryptography": [], - "six": [], - "junit-xml": [], + lock_data: dict[str, Any] = { + "package": [ + { + "name": "poetry", + "version": "1.1.4", + "optional": False, + "python-versions": "*", + "dependencies": {"keyring": "*"}, + }, + { + "name": "junit-xml", + "version": "1.9", + "optional": False, + "python-versions": "*", + "dependencies": {"six": "*"}, + }, + { + "name": "keyring", + "version": "21.8.0", + "optional": False, + "python-versions": "*", + "dependencies": { + "SecretStorage": { + "version": "*", + "markers": "sys_platform == 'linux'", + } }, }, - } - ) + { + "name": "secretstorage", + "version": "3.3.0", + "optional": False, + "python-versions": "*", + "dependencies": {"cryptography": "*"}, + }, + { + "name": "cryptography", + "version": "3.2", + "optional": False, + "python-versions": "*", + "dependencies": {"six": "*"}, + }, + { + "name": "six", + "version": "1.15.0", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "poetry": [], + "keyring": [], + "secretstorage": [], + "cryptography": [], + "six": [], + "junit-xml": [], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][3]["markers"] = "sys_platform == 'linux'" + lock_data["package"][4]["markers"] = "sys_platform == 'linux'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires( poetry, skip={"keyring", "secretstorage", "cryptography", "six"} ) @@ -314,48 +338,52 @@ def test_exporter_can_export_requirements_txt_poetry( assert dependency.marker == expected_dependency.marker +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_pyinstaller( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: """Regression test for #3254""" - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "pyinstaller", - "version": "4.0", - "optional": False, - "python-versions": "*", - "dependencies": { - "altgraph": "*", - "macholib": { - "version": "*", - "markers": "sys_platform == 'darwin'", - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "pyinstaller", + "version": "4.0", + "optional": False, + "python-versions": "*", + "dependencies": { + "altgraph": "*", + "macholib": { + "version": "*", + "markers": "sys_platform == 'darwin'", }, }, - { - "name": "altgraph", - "version": "0.17", - "optional": False, - "python-versions": "*", - }, - { - "name": "macholib", - "version": "1.8", - "optional": False, - "python-versions": "*", - "dependencies": {"altgraph": ">=0.15"}, - }, - ], - "metadata": { + }, + { + "name": "altgraph", + "version": "0.17", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"pyinstaller": [], "altgraph": [], "macholib": []}, }, - } - ) + { + "name": "macholib", + "version": "1.8", + "optional": False, + "python-versions": "*", + "dependencies": {"altgraph": ">=0.15"}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"pyinstaller": [], "altgraph": [], "macholib": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][2]["markers"] = "sys_platform == 'darwin'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, skip={"altgraph", "macholib"}) exporter = Exporter(poetry, NullIO()) @@ -393,56 +421,70 @@ def test_exporter_can_export_requirements_txt_pyinstaller( assert dependency.marker == expected_dependency.marker +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "a", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": { - "b": { - "version": ">=0.0.0", - "markers": "platform_system == 'Windows'", - }, - "c": { - "version": ">=0.0.0", - "markers": "sys_platform == 'win32'", - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "a", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "dependencies": { + "b": { + "version": ">=0.0.0", + "markers": "platform_system == 'Windows'", + }, + "c": { + "version": ">=0.0.0", + "markers": "sys_platform == 'win32'", }, }, - { - "name": "b", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "dependencies": {"d": ">=0.0.0"}, - }, - { - "name": "c", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - "dependencies": {"d": ">=0.0.0"}, - }, - { - "name": "d", - "version": "0.0.1", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + }, + { + "name": "b", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"a": [], "b": [], "c": [], "d": []}, + "dependencies": {"d": ">=0.0.0"}, }, - } - ) + { + "name": "c", + "version": "7.8.9", + "optional": False, + "python-versions": "*", + "dependencies": {"d": ">=0.0.0"}, + }, + { + "name": "d", + "version": "0.0.1", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"a": [], "b": [], "c": [], "d": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + lock_data["package"][1]["markers"] = ( + "python_version < '3.7' and platform_system == 'Windows'" + ) + lock_data["package"][2]["markers"] = ( + "python_version < '3.7' and sys_platform == 'win32'" + ) + lock_data["package"][3]["markers"] = ( + "python_version < '3.7' and platform_system == 'Windows'" + " or python_version < '3.7' and sys_platform == 'win32'" + ) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires( poetry, skip={"b", "c", "d"}, markers={"a": "python_version < '3.7'"} ) @@ -492,33 +534,39 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any( - tmp_path: Path, poetry: Poetry, dev: bool, lines: list[str] + tmp_path: Path, poetry: Poetry, dev: bool, lines: list[str], lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "a", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "b", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "dependencies": {"a": ">=1.2.3"}, - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "a", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"a": [], "b": []}, }, - } - ) + { + "name": "b", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "dependencies": {"a": ">=1.2.3"}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"a": [], "b": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["groups"] = ["main", "dev"] + lock_data["package"][0]["markers"] = {"main": "python_version < '3.8'"} + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.add_dependency( @@ -544,35 +592,37 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_a assert content.strip() == "\n".join(lines) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -591,41 +641,43 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_standard_packages_and_sorted_hashes( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [ - {"name": "foo1.whl", "hash": "67890"}, - {"name": "foo2.whl", "hash": "12345"}, - ], - "bar": [ - {"name": "bar1.whl", "hash": "67890"}, - {"name": "bar2.whl", "hash": "12345"}, - ], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [ + {"name": "foo1.whl", "hash": "67890"}, + {"name": "foo2.whl", "hash": "12345"}, + ], + "bar": [ + {"name": "bar1.whl", "hash": "67890"}, + {"name": "bar2.whl", "hash": "12345"}, + ], + }, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -646,35 +698,37 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_sorted_ assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_disabled( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -692,35 +746,39 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_ assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_without_dev_packages_by_default( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -737,35 +795,39 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -785,35 +847,39 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_without_groups_if_set_explicitly( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -826,35 +892,40 @@ def test_exporter_exports_requirements_txt_without_groups_if_set_explicitly( assert content == "\n" +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_without_optional_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": True, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - }, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": True, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + lock_data["package"][1]["markers"] = 'extra == "feature-bar"' + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -885,47 +956,53 @@ def test_exporter_exports_requirements_txt_without_optional_packages( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_optional_packages( tmp_path: Path, poetry: Poetry, extras: Collection[NormalizedName], lines: list[str], + lock_version: str, ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": True, - "python-versions": "*", - "dependencies": {"spam": ">=0.1"}, - }, - { - "name": "spam", - "version": "0.1.0", - "optional": True, - "python-versions": "*", - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - "spam": [{"name": "spam.whl", "hash": "abcde"}], - }, }, - "extras": {"feature_bar": ["bar"]}, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": True, + "python-versions": "*", + "dependencies": {"spam": ">=0.1"}, + }, + { + "name": "spam", + "version": "0.1.0", + "optional": True, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + "spam": [{"name": "spam.whl", "hash": "abcde"}], + }, + }, + "extras": {"feature_bar": ["bar"]}, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["markers"] = 'extra == "feature-bar"' + lock_data["package"][2]["markers"] = 'extra == "feature-bar"' + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -946,32 +1023,34 @@ def test_exporter_exports_requirements_txt_with_optional_packages( assert content.strip() == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_git_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "git", - "url": "https://github.com/foo/foo.git", - "reference": "123456", - "resolved_reference": "abcdef", - }, - } - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", + "resolved_reference": "abcdef", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -987,44 +1066,46 @@ def test_exporter_can_export_requirements_txt_with_git_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "git", - "url": "https://github.com/foo/foo.git", - "reference": "123456", - "resolved_reference": "abcdef", - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "dependencies": { - "foo": { - "git": "https://github.com/foo/foo.git", - "rev": "123456", - } - }, + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", + "resolved_reference": "abcdef", }, - ], - "metadata": { + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": []}, + "dependencies": { + "foo": { + "git": "https://github.com/foo/foo.git", + "rev": "123456", + } + }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, skip={"foo"}) exporter = Exporter(poetry, NullIO()) @@ -1041,41 +1122,43 @@ def test_exporter_can_export_requirements_txt_with_nested_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": {"bar": {"version": "4.5.6"}}, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "dependencies": {"baz": {"version": "7.8.9"}}, - }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - "dependencies": {"foo": {"version": "1.2.3"}}, - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, + "dependencies": {"bar": {"version": "4.5.6"}}, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "dependencies": {"baz": {"version": "7.8.9"}}, + }, + { + "name": "baz", + "version": "7.8.9", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": {"version": "1.2.3"}}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, skip={"bar", "baz"}) exporter = Exporter(poetry, NullIO()) @@ -1093,27 +1176,29 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_circular_root_dependency( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": {poetry.package.pretty_name: {"version": "1.2.3"}}, - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, + "dependencies": {poetry.package.pretty_name: {"version": "1.2.3"}}, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1129,56 +1214,60 @@ def test_exporter_can_export_requirements_txt_with_circular_root_dependency( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_markers( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": { - "bar": [ - { - "version": ">=1.2.3,<7.8.10", - "markers": 'platform_system != "Windows"', - }, - { - "version": ">=4.5.6,<7.8.10", - "markers": 'platform_system == "Windows"', - }, - ] - }, - }, - { - "name": "bar", - "version": "7.8.9", - "optional": True, - "python-versions": "*", - "dependencies": { - "baz": { - "version": "!=10.11.12", + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "dependencies": { + "bar": [ + { + "version": ">=1.2.3,<7.8.10", + "markers": 'platform_system != "Windows"', + }, + { + "version": ">=4.5.6,<7.8.10", "markers": 'platform_system == "Windows"', - } - }, + }, + ] }, - { - "name": "baz", - "version": "10.11.13", - "optional": True, - "python-versions": "*", + }, + { + "name": "bar", + "version": "7.8.9", + "optional": True, + "python-versions": "*", + "dependencies": { + "baz": { + "version": "!=10.11.12", + "markers": 'platform_system == "Windows"', + } }, - ], - "metadata": { + }, + { + "name": "baz", + "version": "10.11.13", + "optional": True, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][2]["markers"] = 'platform_system == "Windows"' + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1200,32 +1289,36 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_ assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "git", - "url": "https://github.com/foo/foo.git", - "reference": "123456", - "resolved_reference": "abcdef", - }, - } - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", + "resolved_reference": "abcdef", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, markers={"foo": "python_version < '3.7'"}) exporter = Exporter(poetry, NullIO()) @@ -1241,31 +1334,33 @@ def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_directory_packages( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project", - "reference": "", - }, - } - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "directory", + "url": "sample_project", + "reference": "", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1281,32 +1376,34 @@ def test_exporter_can_export_requirements_txt_with_directory_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_directory_packages_editable( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "develop": True, - "source": { - "type": "directory", - "url": "sample_project", - "reference": "", - }, - } - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "develop": True, + "source": { + "type": "directory", + "url": "sample_project", + "reference": "", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1322,53 +1419,55 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_editable( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_nested_directory_packages( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project", - "reference": "", - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project/../project_with_nested_local/bar", - "reference": "", - }, + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "sample_project", + "reference": "", }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project/../project_with_nested_local/bar/..", - "reference": "", - }, + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "sample_project/../project_with_nested_local/bar", + "reference": "", }, - ], - "metadata": { + }, + { + "name": "baz", + "version": "7.8.9", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, + "source": { + "type": "directory", + "url": "sample_project/../project_with_nested_local/bar/..", + "reference": "", + }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1386,31 +1485,35 @@ def test_exporter_can_export_requirements_txt_with_nested_directory_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_directory_packages_and_markers( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project", - "reference": "", - }, - } - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "directory", + "url": "sample_project", + "reference": "", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, markers={"foo": "python_version < '3.7'"}) exporter = Exporter(poetry, NullIO()) @@ -1427,31 +1530,33 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_file_packages( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "file", - "url": "distributions/demo-0.1.0.tar.gz", - "reference": "", - }, - } - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "file", + "url": "distributions/demo-0.1.0.tar.gz", + "reference": "", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -1468,31 +1573,35 @@ def test_exporter_can_export_requirements_txt_with_file_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( - tmp_path: Path, poetry: Poetry, fixture_root_uri: str + tmp_path: Path, poetry: Poetry, fixture_root_uri: str, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "file", - "url": "distributions/demo-0.1.0.tar.gz", - "reference": "", - }, - } - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": []}, - }, - } - ) + "source": { + "type": "file", + "url": "distributions/demo-0.1.0.tar.gz", + "reference": "", + }, + } + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, markers={"foo": "python_version < '3.7'"}) exporter = Exporter(poetry, NullIO()) @@ -1509,8 +1618,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_legacy_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.add_repository( LegacyRepository( @@ -1518,37 +1628,40 @@ def test_exporter_exports_requirements_txt_with_legacy_packages( "https://example.com/simple", ) ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://example.com/simple", - "reference": "", - }, - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], + "source": { + "type": "legacy", + "url": "https://example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -1570,8 +1683,9 @@ def test_exporter_exports_requirements_txt_with_legacy_packages( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_url_false( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.add_repository( LegacyRepository( @@ -1579,37 +1693,40 @@ def test_exporter_exports_requirements_txt_with_url_false( "https://example.com/simple", ) ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://example.com/simple", - "reference": "", - }, - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -1630,8 +1747,9 @@ def test_exporter_exports_requirements_txt_with_url_false( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.add_repository( LegacyRepository( @@ -1639,30 +1757,33 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( "http://example.com/simple", ) ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "http://example.com/simple", - "reference": "", - }, - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "bar": [{"name": "bar.whl", "hash": "67890"}], + "source": { + "type": "legacy", + "url": "http://example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) exporter.only_groups([MAIN_GROUP, "dev"]) @@ -1702,46 +1823,50 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_dev_extras( - tmp_path: Path, poetry: Poetry, dev: bool, expected: list[str] -) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.1", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "1.2.2", - "optional": False, - "python-versions": "*", - "dependencies": { - "baz": { - "version": ">=0.1.0", - "optional": True, - "markers": "extra == 'baz'", - } - }, - "extras": {"baz": ["baz (>=0.1.0)"]}, - }, - { - "name": "baz", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + tmp_path: Path, poetry: Poetry, dev: bool, expected: list[str], lock_version: str +) -> None: + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.1", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, }, - } - ) + { + "name": "bar", + "version": "1.2.2", + "optional": False, + "python-versions": "*", + "dependencies": { + "baz": { + "version": ">=0.1.0", + "optional": True, + "markers": "extra == 'baz'", + } + }, + "extras": {"baz": ["baz (>=0.1.0)"]}, + }, + { + "name": "baz", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][2]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"baz"}) exporter = Exporter(poetry, NullIO()) @@ -1755,8 +1880,9 @@ def test_exporter_exports_requirements_txt_with_dev_extras( assert content == "\n".join(expected) + "\n" +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_sources( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.add_repository( LegacyRepository( @@ -1770,54 +1896,58 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so "https://foobaz.com/simple", ) ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://example.com/simple", - "reference": "", - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://example.com/simple", - "reference": "", - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://example.com/simple", + "reference": "", }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://foobaz.com/simple", - "reference": "", - }, + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://example.com/simple", + "reference": "", }, - ], - "metadata": { + }, + { + "name": "baz", + "version": "7.8.9", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - "baz": [{"name": "baz.whl", "hash": "24680"}], + "source": { + "type": "legacy", + "url": "https://foobaz.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + "baz": [{"name": "baz.whl", "hash": "24680"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + lock_data["package"][2]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar", "baz"}) exporter = Exporter(poetry, NullIO()) @@ -1842,8 +1972,9 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_two_primary_sources( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.remove_repository("PyPI") poetry.config.merge( @@ -1872,54 +2003,58 @@ def test_exporter_exports_requirements_txt_with_two_primary_sources( config=poetry.config, ), ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://a.example.com/simple", - "reference": "", - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://b.example.com/simple", - "reference": "", - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://a.example.com/simple", + "reference": "", }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://b.example.com/simple", - "reference": "", - }, + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://b.example.com/simple", + "reference": "", }, - ], - "metadata": { + }, + { + "name": "baz", + "version": "7.8.9", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], - "baz": [{"name": "baz.whl", "hash": "24680"}], + "source": { + "type": "legacy", + "url": "https://b.example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + "baz": [{"name": "baz.whl", "hash": "24680"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + lock_data["package"][2]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar", "baz"}) exporter = Exporter(poetry, NullIO()) @@ -1945,8 +2080,9 @@ def test_exporter_exports_requirements_txt_with_two_primary_sources( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( - tmp_path: Path, poetry: Poetry, config: Config + tmp_path: Path, poetry: Poetry, config: Config, lock_version: str ) -> None: poetry.config.merge( { @@ -1957,37 +2093,40 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( poetry.pool.add_repository( LegacyRepository("custom", "https://example.com/simple", config=poetry.config) ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://example.com/simple", - "reference": "", - }, - }, - ], - "metadata": { + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], + "source": { + "type": "legacy", + "url": "https://example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][1]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -2014,32 +2153,34 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_requirements_txt_to_standard_output( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": []}, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -2054,85 +2195,93 @@ def test_exporter_exports_requirements_txt_to_standard_output( assert io.fetch_output() == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_doesnt_confuse_repeated_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: # Testcase derived from . - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "celery", - "version": "5.1.2", - "optional": False, - "python-versions": "<3.7", - "dependencies": { - "click": ">=7.0,<8.0", - "click-didyoumean": ">=0.0.3", - "click-plugins": ">=1.1.1", - }, - }, - { - "name": "celery", - "version": "5.2.3", - "optional": False, - "python-versions": ">=3.7", - "dependencies": { - "click": ">=8.0.3,<9.0", - "click-didyoumean": ">=0.0.3", - "click-plugins": ">=1.1.1", - }, - }, - { - "name": "click", - "version": "7.1.2", - "optional": False, - "python-versions": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - }, - { - "name": "click", - "version": "8.0.3", - "optional": False, - "python-versions": ">=3.6", - "dependencies": {}, - }, - { - "name": "click-didyoumean", - "version": "0.0.3", - "optional": False, - "python-versions": "*", - "dependencies": {"click": "*"}, - }, - { - "name": "click-didyoumean", - "version": "0.3.0", - "optional": False, - "python-versions": ">=3.6.2,<4.0.0", - "dependencies": {"click": ">=7"}, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "celery", + "version": "5.1.2", + "optional": False, + "python-versions": "<3.7", + "dependencies": { + "click": ">=7.0,<8.0", + "click-didyoumean": ">=0.0.3", + "click-plugins": ">=1.1.1", }, - { - "name": "click-plugins", - "version": "1.1.1", - "optional": False, - "python-versions": "*", - "dependencies": {"click": ">=4.0"}, - }, - ], - "metadata": { - "lock-version": "1.1", - "python-versions": "^3.6", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), - "files": { - "celery": [], - "click-didyoumean": [], - "click-plugins": [], - "click": [], + }, + { + "name": "celery", + "version": "5.2.3", + "optional": False, + "python-versions": ">=3.7", + "dependencies": { + "click": ">=8.0.3,<9.0", + "click-didyoumean": ">=0.0.3", + "click-plugins": ">=1.1.1", }, }, - } - ) + { + "name": "click", + "version": "7.1.2", + "optional": False, + "python-versions": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", + }, + { + "name": "click", + "version": "8.0.3", + "optional": False, + "python-versions": ">=3.6", + "dependencies": {}, + }, + { + "name": "click-didyoumean", + "version": "0.0.3", + "optional": False, + "python-versions": "*", + "dependencies": {"click": "*"}, + }, + { + "name": "click-didyoumean", + "version": "0.3.0", + "optional": False, + "python-versions": ">=3.6.2,<4.0.0", + "dependencies": {"click": ">=7"}, + }, + { + "name": "click-plugins", + "version": "1.1.1", + "optional": False, + "python-versions": "*", + "dependencies": {"click": ">=4.0"}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.6", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + "files": { + "celery": [], + "click-didyoumean": [], + "click-plugins": [], + "click": [], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.7'" + lock_data["package"][1]["markers"] = "python_version >= '3.7'" + lock_data["package"][2]["markers"] = "python_version < '3.7'" + lock_data["package"][3]["markers"] = "python_version >= '3.7'" + lock_data["package"][4]["markers"] = "python_full_version < '3.6.2'" + lock_data["package"][5]["markers"] = "python_full_version >= '3.6.2'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.6" root.add_dependency( @@ -2165,89 +2314,83 @@ def test_exporter_doesnt_confuse_repeated_packages( assert io.fetch_output() == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_handles_extras_next_to_non_extras( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: # Testcase similar to the solver testcase added at #5305. - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "localstack", - "python-versions": "*", - "version": "1.0.0", - "optional": False, - "dependencies": { - "localstack-ext": [ - {"version": ">=1.0.0"}, - { - "version": ">=1.0.0", - "extras": ["bar"], - "markers": 'extra == "foo"', - }, - ] - }, - "extras": {"foo": ["localstack-ext[bar] (>=1.0.0)"]}, - }, - { - "name": "localstack-ext", - "python-versions": "*", - "version": "1.0.0", - "optional": False, - "dependencies": { - "something": "*", - "something-else": { - "version": ">=1.0.0", - "markers": 'extra == "bar"', - }, - "another-thing": { + lock_data = { + "package": [ + { + "name": "localstack", + "python-versions": "*", + "version": "1.0.0", + "optional": False, + "dependencies": { + "localstack-ext": [ + {"version": ">=1.0.0"}, + { "version": ">=1.0.0", - "markers": 'extra == "baz"', + "extras": ["bar"], + "markers": 'extra == "foo"', }, + ] + }, + "extras": {"foo": ["localstack-ext[bar] (>=1.0.0)"]}, + }, + { + "name": "localstack-ext", + "python-versions": "*", + "version": "1.0.0", + "optional": False, + "dependencies": { + "something": "*", + "something-else": { + "version": ">=1.0.0", + "markers": 'extra == "bar"', }, - "extras": { - "bar": ["something-else (>=1.0.0)"], - "baz": ["another-thing (>=1.0.0)"], + "another-thing": { + "version": ">=1.0.0", + "markers": 'extra == "baz"', }, }, - { - "name": "something", - "python-versions": "*", - "version": "1.0.0", - "optional": False, - "dependencies": {}, - }, - { - "name": "something-else", - "python-versions": "*", - "version": "1.0.0", - "optional": False, - "dependencies": {}, - }, - { - "name": "another-thing", - "python-versions": "*", - "version": "1.0.0", - "optional": False, - "dependencies": {}, - }, - ], - "metadata": { - "lock-version": "1.1", - "python-versions": "^3.6", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), - "files": { - "localstack": [], - "localstack-ext": [], - "something": [], - "something-else": [], - "another-thing": [], + "extras": { + "bar": ["something-else (>=1.0.0)"], + "baz": ["another-thing (>=1.0.0)"], }, }, - } - ) + { + "name": "something", + "python-versions": "*", + "version": "1.0.0", + "optional": False, + "dependencies": {}, + }, + { + "name": "something-else", + "python-versions": "*", + "version": "1.0.0", + "optional": False, + "dependencies": {}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.6", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + "files": { + "localstack": [], + "localstack-ext": [], + "something": [], + "something-else": [], + "another-thing": [], + }, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.6" root.add_dependency( @@ -2261,77 +2404,94 @@ def test_exporter_handles_extras_next_to_non_extras( io = BufferedIO() exporter.export("requirements.txt", tmp_path, io) - expected = f"""\ + # It does not matter whether packages are exported with extras or not + # because all dependencies are listed explicitly. + if lock_version == "1.1": + expected = f"""\ localstack-ext==1.0.0 ; {MARKER_PY36} localstack-ext[bar]==1.0.0 ; {MARKER_PY36} localstack[foo]==1.0.0 ; {MARKER_PY36} something-else==1.0.0 ; {MARKER_PY36} something==1.0.0 ; {MARKER_PY36} +""" + else: + expected = f"""\ +localstack-ext==1.0.0 ; {MARKER_PY36} +localstack==1.0.0 ; {MARKER_PY36} +something-else==1.0.0 ; {MARKER_PY36} +something==1.0.0 ; {MARKER_PY36} """ assert io.fetch_output() == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_handles_overlapping_python_versions( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: # Testcase derived from # https://github.com/python-poetry/poetry-plugin-export/issues/32. - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "ipython", - "python-versions": ">=3.6", - "version": "7.16.3", - "optional": False, - "dependencies": {}, - }, - { - "name": "ipython", - "python-versions": ">=3.7", - "version": "7.34.0", - "optional": False, - "dependencies": {}, - }, - { - "name": "slash", - "python-versions": ">=3.6.*", - "version": "1.13.0", - "optional": False, - "dependencies": { - "ipython": [ - { - "version": "*", - "markers": ( - 'python_version >= "3.6" and implementation_name !=' - ' "pypy"' - ), - }, - { - "version": "<7.17.0", - "markers": ( - 'python_version < "3.6" and implementation_name !=' - ' "pypy"' - ), - }, - ], - }, - }, - ], - "metadata": { - "lock-version": "1.1", - "python-versions": "^3.6", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), - "files": { - "ipython": [], - "slash": [], + lock_data: dict[str, Any] = { + "package": [ + { + "name": "ipython", + "python-versions": ">=3.6", + "version": "7.16.3", + "optional": False, + "dependencies": {}, + }, + { + "name": "ipython", + "python-versions": ">=3.7", + "version": "7.34.0", + "optional": False, + "dependencies": {}, + }, + { + "name": "slash", + "python-versions": ">=3.6.*", + "version": "1.13.0", + "optional": False, + "dependencies": { + "ipython": [ + { + "version": "*", + "markers": ( + 'python_version >= "3.6" and implementation_name !=' + ' "pypy"' + ), + }, + { + "version": "<7.17.0", + "markers": ( + 'python_version < "3.6" and implementation_name !=' + ' "pypy"' + ), + }, + ], }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.6", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + "files": { + "ipython": [], + "slash": [], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = ( + "python_version >= '3.6' and python_version < '3.7'" + ) + lock_data["package"][1]["markers"] = "python_version >= '3.7'" + lock_data["package"][2]["markers"] = "implementation_name == 'cpython'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.6" root.add_dependency( @@ -2383,43 +2543,51 @@ def test_exporter_handles_overlapping_python_versions( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_omits_unwanted_extras( - tmp_path: Path, poetry: Poetry, with_extras: bool, expected: list[str] + tmp_path: Path, + poetry: Poetry, + with_extras: bool, + expected: list[str], + lock_version: str, ) -> None: # Testcase derived from # https://github.com/python-poetry/poetry/issues/5779 - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "python-versions": ">=3.6", - "version": "1.0.0", - "optional": False, - "dependencies": {"pytest": {"version": "^6.2.4", "optional": True}}, - "extras": {"test": ["pytest (>=6.2.4,<7.0.0)"]}, - }, - { - "name": "pytest", - "python-versions": ">=3.6", - "version": "6.24.0", - "optional": False, - "dependencies": {}, - }, - ], - "metadata": { - "lock-version": "1.1", - "python-versions": "^3.6", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), - "files": { - "foo": [], - "pytest": [], - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "python-versions": ">=3.6", + "version": "1.0.0", + "optional": False, + "dependencies": {"pytest": {"version": "^6.2.4", "optional": True}}, + "extras": {"test": ["pytest (>=6.2.4,<7.0.0)"]}, }, - } - ) + { + "name": "pytest", + "python-versions": ">=3.6", + "version": "6.24.0", + "optional": False, + "dependencies": {}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.6", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + "files": { + "foo": [], + "pytest": [], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["groups"] = ["main", "with-extras"] + lock_data["package"][1]["groups"] = ["with-extras"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.6" root.add_dependency( @@ -2441,6 +2609,10 @@ def test_exporter_omits_unwanted_extras( exporter = Exporter(poetry, NullIO()) if with_extras: exporter.only_groups(["with-extras"]) + # It does not matter whether packages are exported with extras or not + # because all dependencies are listed explicitly. + if lock_version == "2.1": + expected = [req.replace("foo[test]", "foo") for req in expected] exporter.export("requirements.txt", tmp_path, io) assert io.fetch_output() == "\n".join(expected) + "\n" @@ -2468,52 +2640,54 @@ def test_exporter_omits_unwanted_extras( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_omits_and_includes_extras_for_txt_formats( - tmp_path: Path, poetry: Poetry, fmt: str, expected: list[str] + tmp_path: Path, poetry: Poetry, fmt: str, expected: list[str], lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": { - "bar": { - "extras": ["baz"], - "version": ">=0.1.0", - } - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "dependencies": { - "baz": { - "version": ">=0.1.0", - "optional": True, - "markers": "extra == 'baz'", - } - }, - "extras": {"baz": ["baz (>=0.1.0)"]}, - }, - { - "name": "baz", - "version": "7.8.9", - "optional": False, - "python-versions": "*", + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "dependencies": { + "bar": { + "extras": ["baz"], + "version": ">=0.1.0", + } }, - ], - "metadata": { + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, + "dependencies": { + "baz": { + "version": ">=0.1.0", + "optional": True, + "markers": "extra == 'baz'", + } + }, + "extras": {"baz": ["baz (>=0.1.0)"]}, }, - } - ) + { + "name": "baz", + "version": "7.8.9", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -2522,53 +2696,59 @@ def test_exporter_omits_and_includes_extras_for_txt_formats( with (tmp_path / "exported.txt").open(encoding="utf-8") as f: content = f.read() + # It does not matter whether packages are exported with extras or not + # because all dependencies are listed explicitly. + if lock_version == "2.1": + expected = [req for req in expected if not req.startswith("bar[baz]")] assert content == "\n".join(expected) + "\n" +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_prints_warning_for_constraints_txt_with_editable_packages( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "git", - "url": "https://github.com/foo/foo.git", - "reference": "123456", - }, - "develop": True, - }, - { - "name": "bar", - "version": "7.8.9", - "optional": False, - "python-versions": "*", - }, - { - "name": "baz", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "directory", - "url": "sample_project", - "reference": "", - }, - "develop": True, + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "git", + "url": "https://github.com/foo/foo.git", + "reference": "123456", }, - ], - "metadata": { + "develop": True, + }, + { + "name": "bar", + "version": "7.8.9", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": [], "baz": []}, }, - } - ) + { + "name": "baz", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "directory", + "url": "sample_project", + "reference": "", + }, + "develop": True, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": [], "baz": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) io = BufferedIO() @@ -2590,45 +2770,51 @@ def test_exporter_prints_warning_for_constraints_txt_with_editable_packages( assert content == f"bar==7.8.9 ; {MARKER_PY}\n" -def test_exporter_respects_package_sources(tmp_path: Path, poetry: Poetry) -> None: - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "python-versions": ">=3.6", - "version": "1.0.0", - "optional": False, - "dependencies": {}, - "source": { - "type": "url", - "url": "https://example.com/foo-darwin.whl", - }, - }, - { - "name": "foo", - "python-versions": ">=3.6", - "version": "1.0.0", - "optional": False, - "dependencies": {}, - "source": { - "type": "url", - "url": "https://example.com/foo-linux.whl", - }, +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) +def test_exporter_respects_package_sources( + tmp_path: Path, poetry: Poetry, lock_version: str +) -> None: + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "python-versions": ">=3.6", + "version": "1.0.0", + "optional": False, + "dependencies": {}, + "source": { + "type": "url", + "url": "https://example.com/foo-darwin.whl", }, - ], - "metadata": { - "lock-version": "1.1", - "python-versions": "^3.6", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), - "files": { - "foo": [], + }, + { + "name": "foo", + "python-versions": ">=3.6", + "version": "1.0.0", + "optional": False, + "dependencies": {}, + "source": { + "type": "url", + "url": "https://example.com/foo-linux.whl", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.6", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + "files": { + "foo": [], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "sys_platform == 'darwin'" + lock_data["package"][1]["markers"] = "sys_platform == 'linux'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.6" root.add_dependency( @@ -2663,40 +2849,38 @@ def test_exporter_respects_package_sources(tmp_path: Path, poetry: Poetry) -> No assert io.fetch_output() == expected -def test_exporter_tolerates_non_existent_extra(tmp_path: Path, poetry: Poetry) -> None: +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) +def test_exporter_tolerates_non_existent_extra( + tmp_path: Path, poetry: Poetry, lock_version: str +) -> None: # foo actually has a 'bar' extra, but pyproject.toml mistakenly references a 'baz' # extra. - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": { - "bar": { - "version": ">=0.1.0", - "optional": True, - "markers": "extra == 'bar'", - } - }, - "extras": {"bar": ["bar (>=0.1.0)"]}, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": []}, + "dependencies": { + "bar": { + "version": ">=0.1.0", + "optional": True, + "markers": "extra == 'bar'", + } + }, + "extras": {"bar": ["bar (>=0.1.0)"]}, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.add_dependency( Factory.create_dependency( @@ -2711,14 +2895,20 @@ def test_exporter_tolerates_non_existent_extra(tmp_path: Path, poetry: Poetry) - with (tmp_path / "requirements.txt").open(encoding="utf-8") as f: content = f.read() - expected = f"""\ + if lock_version == "1.1": + expected = f"""\ foo[baz]==1.2.3 ; {MARKER_PY27} or {MARKER_PY36} +""" + else: + expected = f"""\ +foo==1.2.3 ; {MARKER_PY27} or {MARKER_PY36} """ assert content == expected +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_exports_extra_index_url_and_trusted_host( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: poetry.pool.add_repository( LegacyRepository( @@ -2727,35 +2917,36 @@ def test_exporter_exports_extra_index_url_and_trusted_host( ), priority=Priority.EXPLICIT, ) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "dependencies": {"bar": "*"}, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "http://example.com/simple", - "reference": "", - }, - }, - ], - "metadata": { + lock_data = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": {"foo": [], "bar": []}, + "dependencies": {"bar": "*"}, }, - } - ) + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "http://example.com/simple", + "reference": "", + }, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry) exporter = Exporter(poetry, NullIO()) @@ -2774,60 +2965,63 @@ def test_exporter_exports_extra_index_url_and_trusted_host( assert content == expected +@pytest.mark.parametrize("lock_version", ("2.0", "2.1")) def test_exporter_not_confused_by_extras_in_sub_dependencies( - tmp_path: Path, poetry: Poetry + tmp_path: Path, poetry: Poetry, lock_version: str ) -> None: # Testcase derived from # https://github.com/python-poetry/poetry-plugin-export/issues/208 - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "typer", - "python-versions": ">=3.6", - "version": "0.9.0", - "optional": False, - "files": [], - "dependencies": { - "click": ">=7.1.1,<9.0.0", - "colorama": { - "version": ">=0.4.3,<0.5.0", - "optional": True, - "markers": 'extra == "all"', - }, - }, - "extras": {"all": ["colorama (>=0.4.3,<0.5.0)"]}, - }, - { - "name": "click", - "python-versions": ">=3.7", - "version": "8.1.3", - "optional": False, - "files": [], - "dependencies": { - "colorama": { - "version": "*", - "markers": 'platform_system == "Windows"', - } + lock_data: dict[str, Any] = { + "package": [ + { + "name": "typer", + "python-versions": ">=3.6", + "version": "0.9.0", + "optional": False, + "files": [], + "dependencies": { + "click": ">=7.1.1,<9.0.0", + "colorama": { + "version": ">=0.4.3,<0.5.0", + "optional": True, + "markers": 'extra == "all"', }, }, - { - "name": "colorama", - "python-versions": ">=3.7", - "version": "0.4.6", - "optional": False, - "files": [], + "extras": {"all": ["colorama (>=0.4.3,<0.5.0)"]}, + }, + { + "name": "click", + "python-versions": ">=3.7", + "version": "8.1.3", + "optional": False, + "files": [], + "dependencies": { + "colorama": { + "version": "*", + "markers": 'platform_system == "Windows"', + } }, - ], - "metadata": { - "lock-version": "2.0", - "python-versions": "^3.11", - "content-hash": ( - "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" - ), }, - } - ) + { + "name": "colorama", + "python-versions": ">=3.7", + "version": "0.4.6", + "optional": False, + "files": [], + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "^3.11", + "content-hash": ( + "832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6" + ), + }, + } + if lock_version == "2.1": + for locked_package in lock_data["package"]: + locked_package["groups"] = ["main"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] root = poetry.package.with_dependency_groups([], only=True) root.python_versions = "^3.11" root.add_dependency( @@ -2842,10 +3036,17 @@ def test_exporter_not_confused_by_extras_in_sub_dependencies( exporter = Exporter(poetry, NullIO()) exporter.export("requirements.txt", tmp_path, io) - expected = """\ + if lock_version == "2.0": + expected = """\ click==8.1.3 ; python_version >= "3.11" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" typer[all]==0.9.0 ; python_version >= "3.11" and python_version < "4.0" +""" + else: + expected = """\ +click==8.1.3 ; python_version >= "3.11" and python_version < "4.0" +colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" +typer==0.9.0 ; python_version >= "3.11" and python_version < "4.0" """ assert io.fetch_output() == expected @@ -2886,11 +3087,13 @@ def test_exporter_not_confused_by_extras_in_sub_dependencies( ), ], ) +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) def test_exporter_index_urls( tmp_path: Path, poetry: Poetry, priorities: list[tuple[str, Priority]], expected: tuple[str, ...], + lock_version: str, ) -> None: pypi = poetry.pool.repository("PyPI") poetry.pool.remove_repository("PyPI") @@ -2901,42 +3104,45 @@ def test_exporter_index_urls( repo = LegacyRepository(name, f"https://{name[-1]}.example.com/simple") poetry.pool.add_repository(repo, priority=prio) - poetry.locker.mock_lock_data( # type: ignore[attr-defined] - { - "package": [ - { - "name": "foo", - "version": "1.2.3", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://a.example.com/simple", - "reference": "", - }, - }, - { - "name": "bar", - "version": "4.5.6", - "optional": False, - "python-versions": "*", - "source": { - "type": "legacy", - "url": "https://b.example.com/simple", - "reference": "", - }, + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://a.example.com/simple", + "reference": "", }, - ], - "metadata": { + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, "python-versions": "*", - "content-hash": "123456789", - "files": { - "foo": [{"name": "foo.whl", "hash": "12345"}], - "bar": [{"name": "bar.whl", "hash": "67890"}], + "source": { + "type": "legacy", + "url": "https://b.example.com/simple", + "reference": "", }, }, - } - ) + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["groups"] = ["dev"] + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] set_package_requires(poetry, dev={"bar"}) exporter = Exporter(poetry, NullIO()) @@ -2967,3 +3173,109 @@ def test_exporter_index_urls( """ assert content == expected_content + + +@pytest.mark.parametrize("lock_version", ("1.1", "2.1")) +def test_dependency_walk_error( + tmp_path: Path, poetry: Poetry, lock_version: str +) -> None: + """ + With lock file version 2.1 we can export lock files + that resulted in a DependencyWalkerError with lower lock file versions. + + root + ├── foo >=0 ; python_version < "3.9" + ├── foo >=1 ; python_version >= "3.9" + ├── bar ==1 ; python_version < "3.9" + │ └── foo ==1 ; python_version < "3.9" + └── bar ==2 ; python_version >= "3.9" + └── foo ==2 ; python_version >= "3.9" + + Only considering the root dependency, foo 2 is a valid solution + for all environments. However, due to bar depending on foo, + foo 1 must be chosen for Python 3.8 and lower. + """ + lock_data: dict[str, Any] = { + "package": [ + { + "name": "foo", + "version": "1", + "optional": False, + "python-versions": "*", + }, + { + "name": "foo", + "version": "2", + "optional": False, + "python-versions": "*", + }, + { + "name": "bar", + "version": "1", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": "1"}, + }, + { + "name": "bar", + "version": "2", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": "2"}, + }, + ], + "metadata": { + "lock-version": lock_version, + "python-versions": "*", + "content-hash": "123456789", + "files": {"foo": [], "bar": []}, + }, + } + fix_lock_data(lock_data) + if lock_version == "2.1": + lock_data["package"][0]["markers"] = "python_version < '3.9'" + lock_data["package"][1]["markers"] = "python_version >= '3.9'" + lock_data["package"][2]["markers"] = "python_version < '3.9'" + lock_data["package"][3]["markers"] = "python_version >= '3.9'" + poetry.locker.mock_lock_data(lock_data) # type: ignore[attr-defined] + poetry.package.python_versions = "^3.8" + poetry.package.add_dependency( + Factory.create_dependency( + name="foo", constraint={"version": ">=0", "python": "<3.9"} + ) + ) + poetry.package.add_dependency( + Factory.create_dependency( + name="foo", constraint={"version": ">=1", "python": ">=3.9"} + ) + ) + poetry.package.add_dependency( + Factory.create_dependency( + name="bar", constraint={"version": "1", "python": "<3.9"} + ) + ) + poetry.package.add_dependency( + Factory.create_dependency( + name="bar", constraint={"version": "2", "python": ">=3.9"} + ) + ) + + exporter = Exporter(poetry, NullIO()) + if lock_version == "1.1": + with pytest.raises(DependencyWalkerError): + exporter.export("requirements.txt", tmp_path, "requirements.txt") + return + + exporter.export("requirements.txt", tmp_path, "requirements.txt") + + with (tmp_path / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected = """\ +bar==1 ; python_version >= "3.8" and python_version < "3.9" +bar==2 ; python_version >= "3.9" and python_version < "4.0" +foo==1 ; python_version >= "3.8" and python_version < "3.9" +foo==2 ; python_version >= "3.9" and python_version < "4.0" +""" + + assert content == expected