diff --git a/poetry.lock b/poetry.lock index 787235b..026ec87 100644 --- a/poetry.lock +++ b/poetry.lock @@ -361,6 +361,7 @@ packaging = "^20.4" pexpect = "^4.7.0" pkginfo = "^1.5" poetry-core = "^1.1.0a7" +poetry-plugin-export = "^1.0" requests = "^2.18" requests-toolbelt = "^0.9.1" shellingham = "^1.1" @@ -372,7 +373,7 @@ virtualenv = "(>=20.4.3,<20.4.5 || >=20.4.7)" type = "git" url = "https://github.com/abn/poetry.git" reference = "use-export-plugin" -resolved_reference = "c5f74f605f0eecd5aedcfb75d06e5a7230ab1b16" +resolved_reference = "642775bf9c7ad2ac74e9495fd8290525bb9060c9" [[package]] name = "poetry-core" @@ -641,7 +642,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "90f3e97cd2d184279fb3e5c0ebf6cba349fbf615158e98817e27ad1925e895a2" +content-hash = "47488c6d0154828f63ebf2f95d6be02314fbd3e3e060fdbfd0b1bf526b8cfb84" [metadata.files] atomicwrites = [ diff --git a/src/poetry_plugin_export/command.py b/src/poetry_plugin_export/command.py index 150c86d..5cf81b0 100644 --- a/src/poetry_plugin_export/command.py +++ b/src/poetry_plugin_export/command.py @@ -41,6 +41,10 @@ class ExportCommand(InstallerCommand): option("with-credentials", None, "Include credentials for extra indices."), ] + @property + def non_optional_groups(self) -> set[str]: + return {"default"} + def handle(self) -> None: fmt = self.option("format") diff --git a/src/poetry_plugin_export/exporter.py b/src/poetry_plugin_export/exporter.py index 562aad5..dff0bbb 100644 --- a/src/poetry_plugin_export/exporter.py +++ b/src/poetry_plugin_export/exporter.py @@ -30,7 +30,7 @@ def __init__(self, poetry: Poetry) -> None: self._with_credentials = False self._with_urls = True self._extras: list[str] = [] - self._groups: Iterable[str] | None = None + self._groups: Iterable[str] = ["default"] @classmethod def is_format_supported(cls, fmt: str) -> bool: @@ -78,14 +78,11 @@ def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None: content = "" dependency_lines = set() - if self._groups is not None: - root = self._poetry.package.with_dependency_groups( - list(self._groups), only=True - ) - else: - root = self._poetry.package.without_optional_dependency_groups() + root = self._poetry.package.with_dependency_groups( + list(self._groups), only=True + ) - locked_repository = self._poetry.locker.locked_repository(True) + locked_repository = self._poetry.locker.locked_repository() pool = Pool(ignore_repository_names=True) pool.add_repository(locked_repository) @@ -98,17 +95,9 @@ def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None: ops = solver.solve().calculate_operations() packages = sorted((op.package for op in ops), key=lambda pkg: pkg.name) - # Get project dependencies. - if self._groups is not None: - root_package = self._poetry.package.with_dependency_groups( - list(self._groups), only=True - ) - else: - root_package = self._poetry.package.without_optional_dependency_groups() - for dependency_package in self._poetry.locker.get_project_dependency_packages( - project_requires=root_package.all_requires, - project_python_marker=root_package.python_marker, + project_requires=root.all_requires, + project_python_marker=root.python_marker, extras=self._extras, ): line = "" @@ -161,11 +150,9 @@ def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None: hashes.append(f"{algorithm}:{h}") - if hashes: - line += " \\\n" - for i, h in enumerate(hashes): - suffix = " \\\n" if i < len(hashes) - 1 else "" - line += f" --hash={h}{suffix}" + for h in hashes: + line += f" \\\n --hash={h}" + dependency_lines.add(line) content += "\n".join(sorted(dependency_lines)) diff --git a/tests/command/test_command_export.py b/tests/command/test_command_export.py index 9ceec95..848d3cf 100644 --- a/tests/command/test_command_export.py +++ b/tests/command/test_command_export.py @@ -50,6 +50,16 @@ foo = "^1.0" bar = { version = "^1.1", optional = true } +[tool.poetry.group.dev.dependencies] +baz = "^2.0" + +[tool.poetry.group.opt] +optional = true + +[tool.poetry.group.opt.dependencies] +opt = "^2.2" + + [tool.poetry.extras] feature_bar = ["bar"] """ @@ -59,6 +69,8 @@ def setup(repo: Repository) -> None: repo.add_package(Package("foo", "1.0.0")) repo.add_package(Package("bar", "1.1.0")) + repo.add_package(Package("baz", "2.0.0")) + repo.add_package(Package("opt", "2.2.0")) @pytest.fixture @@ -88,7 +100,7 @@ def _export_requirements(tester: CommandTester, poetry: Poetry) -> None: foo==1.0.0 ; {MARKER_PY} """ - assert expected == content + assert content == expected def test_export_exports_requirements_txt_file_locks_if_no_lock_file( @@ -116,7 +128,7 @@ def test_export_prints_to_stdout_by_default(tester: CommandTester, do_lock: None expected = f"""\ foo==1.0.0 ; {MARKER_PY} """ - assert expected == tester.io.fetch_output() + assert tester.io.fetch_output() == expected def test_export_uses_requirements_txt_format_by_default( @@ -126,7 +138,34 @@ def test_export_uses_requirements_txt_format_by_default( expected = f"""\ foo==1.0.0 ; {MARKER_PY} """ - assert expected == tester.io.fetch_output() + assert tester.io.fetch_output() == expected + + +@pytest.mark.parametrize( + "options, expected", + [ + ("", f"foo==1.0.0 ; {MARKER_PY}\n"), + ("--with dev", f"baz==2.0.0 ; {MARKER_PY}\nfoo==1.0.0 ; {MARKER_PY}\n"), + ("--with opt", f"foo==1.0.0 ; {MARKER_PY}\nopt==2.2.0 ; {MARKER_PY}\n"), + ( + "--with dev,opt", + f"baz==2.0.0 ; {MARKER_PY}\nfoo==1.0.0 ; {MARKER_PY}\nopt==2.2.0 ;" + f" {MARKER_PY}\n", + ), + ("--without default", "\n"), + ("--without dev", f"foo==1.0.0 ; {MARKER_PY}\n"), + ("--without opt", f"foo==1.0.0 ; {MARKER_PY}\n"), + ("--without default,dev,opt", "\n"), + ("--only default", f"foo==1.0.0 ; {MARKER_PY}\n"), + ("--only dev", f"baz==2.0.0 ; {MARKER_PY}\n"), + ("--only default,dev", f"baz==2.0.0 ; {MARKER_PY}\nfoo==1.0.0 ; {MARKER_PY}\n"), + ], +) +def test_export_groups( + tester: CommandTester, do_lock: None, options: str, expected: str +): + tester.execute(options) + assert tester.io.fetch_output() == expected def test_export_includes_extras_by_flag(tester: CommandTester, do_lock: None): @@ -135,7 +174,7 @@ def test_export_includes_extras_by_flag(tester: CommandTester, do_lock: None): bar==1.1.0 ; {MARKER_PY} foo==1.0.0 ; {MARKER_PY} """ - assert expected == tester.io.fetch_output() + assert tester.io.fetch_output() == expected def test_export_with_urls( diff --git a/tests/test_exporter.py b/tests/test_exporter.py index cd5ea74..e512079 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -1,7 +1,5 @@ from __future__ import annotations -import sys - from pathlib import Path from typing import TYPE_CHECKING from typing import Any @@ -9,14 +7,15 @@ import pytest +from cleo.io.buffered_io import BufferedIO from poetry.core.packages.dependency import Dependency from poetry.core.toml.file import TOMLFile from poetry.core.version.markers import parse_marker from poetry.factory import Factory from poetry.packages import Locker as BaseLocker from poetry.repositories.legacy_repository import LegacyRepository -from poetry.utils.exporter import Exporter +from poetry_plugin_export.exporter import Exporter from tests.markers import MARKER_PY from tests.markers import MARKER_PY27 from tests.markers import MARKER_PY36 @@ -33,7 +32,6 @@ if TYPE_CHECKING: - from _pytest.capture import CaptureFixture from poetry.poetry import Poetry from pytest_mock import MockerFixture @@ -92,11 +90,14 @@ def poetry(fixture_dir: FixtureDirGetter, locker: Locker) -> Poetry: def set_package_requires(poetry: Poetry, skip: set[str] | None = None) -> None: skip = skip or set() - packages = poetry.locker.locked_repository(with_dev_reqs=True).packages + packages = poetry.locker.locked_repository().packages package = poetry.package.with_dependency_groups([], only=True) for pkg in packages: if pkg.name not in skip: - package.add_dependency(pkg.to_dependency()) + dep = pkg.to_dependency() + if pkg.category == "dev": + dep._groups = frozenset(["dev"]) + package.add_dependency(dep) poetry._package = package @@ -538,7 +539,9 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_a poetry._package = root exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev) + if dev: + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -623,9 +626,8 @@ def test_exporter_requirements_txt_with_standard_packages_and_hashes_disabled( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export( - "requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False - ) + exporter.with_hashes(False) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -713,7 +715,8 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -728,6 +731,46 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( assert content == expected +def test_exporter_exports_requirements_txt_without_groups_if_set_explicity( + tmp_dir: str, poetry: Poetry +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + }, + { + "name": "bar", + "version": "4.5.6", + "category": "dev", + "optional": False, + "python-versions": "*", + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": ["12345"], "bar": ["67890"]}, + }, + } + ) + set_package_requires(poetry) + + exporter = Exporter(poetry) + exporter.only_groups([]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") + + with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + assert content == "\n" + + def test_exporter_exports_requirements_txt_without_optional_packages( tmp_dir: str, poetry: Poetry ): @@ -759,7 +802,8 @@ def test_exporter_exports_requirements_txt_without_optional_packages( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -844,13 +888,13 @@ def test_exporter_exports_requirements_txt_with_optional_packages( set_package_requires(poetry) exporter = Exporter(poetry) + exporter.only_groups(["default", "dev"]) + exporter.with_hashes(False) + exporter.with_extras(extras) exporter.export( "requirements.txt", Path(tmp_dir), "requirements.txt", - dev=True, - with_hashes=False, - extras=extras, ) with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: @@ -926,7 +970,12 @@ def test_exporter_can_export_requirements_txt_with_nested_packages( "category": "main", "optional": False, "python-versions": "*", - "dependencies": {"foo": "rev 123456"}, + "dependencies": { + "foo": { + "git": "https://github.com/foo/foo.git", + "rev": "123456", + } + }, }, ], "metadata": { @@ -1063,9 +1112,8 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_ set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export( - "requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False - ) + exporter.with_hashes(False) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1409,7 +1457,8 @@ def test_exporter_exports_requirements_txt_with_legacy_packages( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1466,9 +1515,9 @@ def test_exporter_exports_requirements_txt_with_url_false(tmp_dir: str, poetry: set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export( - "requirements.txt", Path(tmp_dir), "requirements.txt", dev=True, with_urls=False - ) + exporter.only_groups(["default", "dev"]) + exporter.with_urls(False) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1517,7 +1566,8 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( ) set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1599,7 +1649,9 @@ def test_exporter_exports_requirements_txt_with_dev_extras( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev) + if dev: + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1672,7 +1724,8 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True) + exporter.only_groups(["default", "dev"]) + exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt") with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() @@ -1737,12 +1790,12 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( set_package_requires(poetry) exporter = Exporter(poetry) + exporter.only_groups(["default", "dev"]) + exporter.with_credentials() exporter.export( "requirements.txt", Path(tmp_dir), "requirements.txt", - dev=True, - with_credentials=True, ) with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: @@ -1761,7 +1814,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( def test_exporter_exports_requirements_txt_to_standard_output( - tmp_dir: str, poetry: Poetry, capsys: CaptureFixture + tmp_dir: str, poetry: Poetry ): poetry.locker.mock_lock_data( { @@ -1791,20 +1844,18 @@ def test_exporter_exports_requirements_txt_to_standard_output( set_package_requires(poetry) exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), sys.stdout) + io = BufferedIO() + exporter.export("requirements.txt", Path(tmp_dir), io) - out, err = capsys.readouterr() expected = f"""\ bar==4.5.6 ; {MARKER_PY} foo==1.2.3 ; {MARKER_PY} """ - assert out == expected + assert io.fetch_output() == expected -def test_exporter_doesnt_confuse_repeated_packages( - tmp_dir: str, poetry: Poetry, capsys: CaptureFixture -): +def test_exporter_doesnt_confuse_repeated_packages(tmp_dir: str, poetry: Poetry): # Testcase derived from . poetry.locker.mock_lock_data( { @@ -1905,9 +1956,10 @@ def test_exporter_doesnt_confuse_repeated_packages( poetry._package = root exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), sys.stdout) + exporter.only_groups(["default", "dev"]) + io = BufferedIO() + exporter.export("requirements.txt", Path(tmp_dir), io) - out, err = capsys.readouterr() expected = f"""\ celery==5.1.2 ; {MARKER_PY36_ONLY} celery==5.2.3 ; {MARKER_PY37} @@ -1918,12 +1970,10 @@ def test_exporter_doesnt_confuse_repeated_packages( click==8.0.3 ; {MARKER_PY37.union(MARKER_PY37_PY400)} """ - assert out == expected + assert io.fetch_output() == expected -def test_exporter_handles_extras_next_to_non_extras( - tmp_dir: str, poetry: Poetry, capsys: CaptureFixture -): +def test_exporter_handles_extras_next_to_non_extras(tmp_dir: str, poetry: Poetry): # Testcase similar to the solver testcase added at #5305. poetry.locker.mock_lock_data( { @@ -2019,9 +2069,9 @@ def test_exporter_handles_extras_next_to_non_extras( poetry._package = root exporter = Exporter(poetry) - exporter.export("requirements.txt", Path(tmp_dir), sys.stdout) + io = BufferedIO() + exporter.export("requirements.txt", Path(tmp_dir), io) - out, err = capsys.readouterr() expected = f"""\ localstack-ext==1.0.0 ; {MARKER_PY36} localstack==1.0.0 ; {MARKER_PY36} @@ -2029,4 +2079,4 @@ def test_exporter_handles_extras_next_to_non_extras( something==1.0.0 ; {MARKER_PY36} """ - assert out == expected + assert io.fetch_output() == expected