From 66632513d851b1b86d84f2373567add4b70eb4b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
<30527984+radoering@users.noreply.github.com>
Date: Sat, 8 Oct 2022 17:00:23 +0200
Subject: [PATCH] exporter: print warning instead of raising an error when
exporting a constraints.txt with editable dependencies
---
src/poetry_plugin_export/command.py | 2 +-
src/poetry_plugin_export/exporter.py | 11 ++-
tests/command/test_command_export.py | 43 +++++++++++
tests/test_exporter.py | 110 ++++++++++++++++-----------
4 files changed, 116 insertions(+), 50 deletions(-)
diff --git a/src/poetry_plugin_export/command.py b/src/poetry_plugin_export/command.py
index 9254527..1909bf6 100644
--- a/src/poetry_plugin_export/command.py
+++ b/src/poetry_plugin_export/command.py
@@ -100,7 +100,7 @@ def handle(self) -> None:
f"Extra [{', '.join(sorted(invalid_extras))}] is not specified."
)
- exporter = Exporter(self.poetry)
+ exporter = Exporter(self.poetry, self.io)
exporter.only_groups(list(self.activated_groups))
exporter.with_extras(list(extras))
exporter.with_hashes(not self.option("without-hashes"))
diff --git a/src/poetry_plugin_export/exporter.py b/src/poetry_plugin_export/exporter.py
index bb7db07..1f97c41 100644
--- a/src/poetry_plugin_export/exporter.py
+++ b/src/poetry_plugin_export/exporter.py
@@ -34,8 +34,9 @@ class Exporter:
FORMAT_REQUIREMENTS_TXT: "_export_requirements_txt",
}
- def __init__(self, poetry: Poetry) -> None:
+ def __init__(self, poetry: Poetry, io: IO) -> None:
self._poetry = poetry
+ self._io = io
self._with_hashes = True
self._with_credentials = False
self._with_urls = True
@@ -106,10 +107,12 @@ def _export_generic_txt(
if package.develop:
if not allow_editable:
- raise RuntimeError(
- f"{package.pretty_name} is locked in develop (editable) mode,"
- " which is incompatible with the constraints.txt format."
+ self._io.write_error_line(
+ f"Warning: {package.pretty_name} is locked in develop"
+ " (editable) mode, which is incompatible with the"
+ " constraints.txt format."
)
+ continue
line += "-e "
requirement = dependency.to_pep_508(with_extras=False)
diff --git a/tests/command/test_command_export.py b/tests/command/test_command_export.py
index e2d4124..6f37fe3 100644
--- a/tests/command/test_command_export.py
+++ b/tests/command/test_command_export.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import shutil
+
from typing import TYPE_CHECKING
from unittest.mock import Mock
@@ -229,3 +231,44 @@ def test_export_with_urls(
monkeypatch.setattr(Exporter, "with_urls", mock_export)
tester.execute("--without-urls")
mock_export.assert_called_once_with(False)
+
+
+def test_export_exports_constraints_txt_with_warnings(
+ tmp_path: Path,
+ fixture_root: Path,
+ project_factory: ProjectFactory,
+ command_tester_factory: CommandTesterFactory,
+) -> None:
+ # On Windows we have to make sure that the path dependency and the pyproject.toml
+ # are on the same drive, otherwise locking fails.
+ # (in our CI fixture_root is on D:\ but temp_path is on C:\)
+ editable_dep_path = tmp_path / "project_with_nested_local"
+ shutil.copytree(fixture_root / "project_with_nested_local", editable_dep_path)
+
+ pyproject_content = f"""\
+[tool.poetry]
+name = "simple-project"
+version = "1.2.3"
+description = "Some description."
+authors = [
+ "Sébastien Eustace "
+]
+
+[tool.poetry.dependencies]
+python = "^3.6"
+baz = ">1.0"
+project-with-nested-local = {{ path = "{editable_dep_path.as_posix()}", \
+develop = true }}
+"""
+ poetry = project_factory(name="export", pyproject_content=pyproject_content)
+ tester = command_tester_factory("export", poetry=poetry)
+ tester.execute("--format constraints.txt")
+
+ develop_warning = (
+ "Warning: project-with-nested-local is locked in develop (editable) mode, which"
+ " is incompatible with the constraints.txt format.\n"
+ )
+ expected = 'baz==2.0.0 ; python_version >= "3.6" and python_version < "4.0"\n'
+
+ assert develop_warning in tester.io.fetch_error()
+ assert tester.io.fetch_output() == expected
diff --git a/tests/test_exporter.py b/tests/test_exporter.py
index 4714125..9dd4375 100644
--- a/tests/test_exporter.py
+++ b/tests/test_exporter.py
@@ -6,6 +6,7 @@
import pytest
from cleo.io.buffered_io import BufferedIO
+from cleo.io.null_io import NullIO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.toml.file import TOMLFile
@@ -124,7 +125,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -178,7 +179,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -272,7 +273,7 @@ def test_exporter_can_export_requirements_txt_poetry(
poetry, skip={"keyring", "secretstorage", "cryptography", "six"}
)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -361,7 +362,7 @@ def test_exporter_can_export_requirements_txt_pyinstaller(
)
set_package_requires(poetry, skip={"altgraph", "macholib"})
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -446,7 +447,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
)
set_package_requires(poetry, skip={"b", "c", "d"})
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -534,7 +535,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_a
)
poetry._package = root
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
if dev:
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -578,7 +579,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -633,7 +634,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_sorted_
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -684,7 +685,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.with_hashes(False)
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -732,7 +733,7 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -779,7 +780,7 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -829,7 +830,7 @@ def test_exporter_exports_requirements_txt_without_groups_if_set_explicitly(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -872,7 +873,7 @@ def test_exporter_exports_requirements_txt_without_optional_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -962,7 +963,7 @@ def test_exporter_exports_requirements_txt_with_optional_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.with_hashes(False)
exporter.with_extras(extras)
@@ -1008,7 +1009,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1062,7 +1063,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages(
)
set_package_requires(poetry, skip={"foo"})
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1116,7 +1117,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic(
)
set_package_requires(poetry, skip={"bar", "baz"})
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1186,7 +1187,7 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.with_hashes(False)
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1234,7 +1235,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages_and_markers(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1275,7 +1276,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1340,7 +1341,7 @@ def test_exporter_can_export_requirements_txt_with_nested_directory_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1384,7 +1385,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1426,7 +1427,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1469,7 +1470,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f:
@@ -1527,7 +1528,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1590,7 +1591,7 @@ def test_exporter_exports_requirements_txt_with_url_false(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.with_urls(False)
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1643,7 +1644,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host(
}
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1726,7 +1727,7 @@ def test_exporter_exports_requirements_txt_with_dev_extras(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
if dev:
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1805,7 +1806,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1912,7 +1913,7 @@ def test_exporter_exports_requirements_txt_with_default_and_secondary_sources(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.with_credentials()
exporter.export("requirements.txt", tmp_path, "requirements.txt")
@@ -1982,7 +1983,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
exporter.with_credentials()
exporter.export(
@@ -2036,7 +2037,7 @@ def test_exporter_exports_requirements_txt_to_standard_output(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
io = BufferedIO()
exporter.export("requirements.txt", tmp_path, io)
@@ -2150,7 +2151,7 @@ def test_exporter_doesnt_confuse_repeated_packages(
)
poetry._package = root
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.only_groups([MAIN_GROUP, "dev"])
io = BufferedIO()
exporter.export("requirements.txt", tmp_path, io)
@@ -2265,7 +2266,7 @@ def test_exporter_handles_extras_next_to_non_extras(
)
poetry._package = root
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
io = BufferedIO()
exporter.export("requirements.txt", tmp_path, io)
@@ -2368,7 +2369,7 @@ def test_exporter_handles_overlapping_python_versions(
)
poetry._package = root
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
io = BufferedIO()
exporter.export("requirements.txt", tmp_path, io)
@@ -2451,7 +2452,7 @@ def test_exporter_omits_unwanted_extras(
poetry._package = root
io = BufferedIO()
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
if with_extras:
exporter.only_groups(["with-extras"])
exporter.export("requirements.txt", tmp_path, io)
@@ -2532,7 +2533,7 @@ def test_exporter_omits_and_includes_extras_for_txt_formats(
)
set_package_requires(poetry)
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export(fmt, tmp_path, "exported.txt")
with (tmp_path / "exported.txt").open(encoding="utf-8") as f:
@@ -2541,7 +2542,7 @@ def test_exporter_omits_and_includes_extras_for_txt_formats(
assert content == "\n".join(expected) + "\n"
-def test_exporter_raises_exception_for_constraints_txt_with_editable_packages(
+def test_exporter_prints_warning_for_constraints_txt_with_editable_packages(
tmp_path: Path, poetry: Poetry
) -> None:
poetry.locker.mock_lock_data( # type: ignore[attr-defined]
@@ -2562,6 +2563,13 @@ def test_exporter_raises_exception_for_constraints_txt_with_editable_packages(
},
{
"name": "bar",
+ "version": "7.8.9",
+ "category": "main",
+ "optional": False,
+ "python-versions": "*",
+ },
+ {
+ "name": "baz",
"version": "4.5.6",
"category": "main",
"optional": False,
@@ -2577,17 +2585,29 @@ def test_exporter_raises_exception_for_constraints_txt_with_editable_packages(
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
- "files": {"foo": [], "bar": []},
+ "files": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)
- with pytest.raises(RuntimeError):
- exporter = Exporter(poetry)
- exporter.export("constraints.txt", tmp_path, "constraints.txt")
+ io = BufferedIO()
+ exporter = Exporter(poetry, io)
+ exporter.export("constraints.txt", tmp_path, "constraints.txt")
+
+ expected_error_out = (
+ "Warning: foo is locked in develop (editable) mode, which is "
+ "incompatible with the constraints.txt format.\n"
+ "Warning: baz is locked in develop (editable) mode, which is "
+ "incompatible with the constraints.txt format.\n"
+ )
+
+ assert io.fetch_error() == expected_error_out
+
+ with (tmp_path / "constraints.txt").open(encoding="utf-8") as f:
+ content = f.read()
- assert not (tmp_path / "constraints.txt").exists()
+ assert content == f"bar==7.8.9 ; {MARKER_PY}\n"
def test_exporter_respects_package_sources(tmp_path: Path, poetry: Poetry) -> None:
@@ -2654,7 +2674,7 @@ def test_exporter_respects_package_sources(tmp_path: Path, poetry: Poetry) -> No
poetry._package = root
io = BufferedIO()
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, io)
expected = f"""\
@@ -2709,7 +2729,7 @@ def test_exporter_tolerates_non_existent_extra(tmp_path: Path, poetry: Poetry) -
)
poetry._package = root
- exporter = Exporter(poetry)
+ exporter = Exporter(poetry, NullIO())
exporter.export("requirements.txt", tmp_path, "requirements.txt")
with (tmp_path / "requirements.txt").open(encoding="utf-8") as f: