Commit bd64610e by Arun Babu Neelicattu Committed by Randy Döring

export: remove in-tree export logic

parent d52780c5
......@@ -53,7 +53,6 @@ COMMANDS = [
"build",
"check",
"config",
"export",
"init",
"install",
"lock",
......
from __future__ import annotations
from cleo.helpers import option
from poetry.console.commands.command import Command
from poetry.utils.exporter import Exporter
class ExportCommand(Command):
name = "export"
description = "Exports the lock file to alternative formats."
options = [
option(
"format",
"f",
"Format to export to. Currently, only requirements.txt is supported.",
flag=False,
default=Exporter.FORMAT_REQUIREMENTS_TXT,
),
option("output", "o", "The name of the output file.", flag=False),
option("without-hashes", None, "Exclude hashes from the exported file."),
option(
"without-urls",
None,
"Exclude source repository urls from the exported file.",
),
option("dev", None, "Include development dependencies."),
option(
"extras",
"E",
"Extra sets of dependencies to include.",
flag=False,
multiple=True,
),
option("with-credentials", None, "Include credentials for extra indices."),
]
def handle(self) -> None:
fmt = self.option("format")
if fmt not in Exporter.ACCEPTED_FORMATS:
raise ValueError(f"Invalid export format: {fmt}")
output = self.option("output")
locker = self.poetry.locker
if not locker.is_locked():
self.line_error("<comment>The lock file does not exist. Locking.</comment>")
options = []
if self.io.is_debug():
options.append("-vvv")
elif self.io.is_very_verbose():
options.append("-vv")
elif self.io.is_verbose():
options.append("-v")
self.call("lock", " ".join(options))
if not locker.is_fresh():
self.line_error(
"<warning>"
"Warning: poetry.lock is not consistent with pyproject.toml. "
"You may be getting improper dependencies. "
"Run `poetry lock [--no-update]` to fix it."
"</warning>"
)
exporter = Exporter(self.poetry)
exporter.export(
fmt,
self.poetry.file.parent,
output or self.io,
with_hashes=not self.option("without-hashes"),
dev=self.option("dev"),
extras=self.option("extras"),
with_credentials=self.option("with-credentials"),
with_urls=not self.option("without-urls"),
)
from __future__ import annotations
import urllib.parse
from typing import TYPE_CHECKING
from typing import Sequence
from poetry.core.packages.utils.utils import path_to_url
from poetry.utils._compat import decode
if TYPE_CHECKING:
from pathlib import Path
from cleo.io.io import IO
from poetry.poetry import Poetry
class Exporter:
"""
Exporter class to export a lock file to alternative formats.
"""
FORMAT_REQUIREMENTS_TXT = "requirements.txt"
#: The names of the supported export formats.
ACCEPTED_FORMATS = (FORMAT_REQUIREMENTS_TXT,)
ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512")
def __init__(self, poetry: Poetry) -> None:
self._poetry = poetry
def export(
self,
fmt: str,
cwd: Path,
output: IO | str,
with_hashes: bool = True,
dev: bool = False,
extras: bool | Sequence[str] | None = None,
with_credentials: bool = False,
with_urls: bool = True,
) -> None:
if fmt not in self.ACCEPTED_FORMATS:
raise ValueError(f"Invalid export format: {fmt}")
getattr(self, "_export_" + fmt.replace(".", "_"))(
cwd,
output,
with_hashes=with_hashes,
dev=dev,
extras=extras,
with_credentials=with_credentials,
with_urls=with_urls,
)
def _export_requirements_txt(
self,
cwd: Path,
output: IO | str,
with_hashes: bool = True,
dev: bool = False,
extras: bool | Sequence[str] | None = None,
with_credentials: bool = False,
with_urls: bool = True,
) -> None:
indexes = set()
content = ""
dependency_lines = set()
# Get project dependencies.
root_package = (
self._poetry.package.clone()
if dev
else self._poetry.package.with_dependency_groups(["default"], only=True)
)
for dependency_package in self._poetry.locker.get_project_dependency_packages(
project_requires=root_package.all_requires,
project_python_marker=root_package.python_marker,
dev=dev,
extras=extras,
):
line = ""
dependency = dependency_package.dependency
package = dependency_package.package
if package.develop:
line += "-e "
requirement = dependency.to_pep_508(with_extras=False)
is_direct_local_reference = (
dependency.is_file() or dependency.is_directory()
)
is_direct_remote_reference = dependency.is_vcs() or dependency.is_url()
if is_direct_remote_reference:
line = requirement
elif is_direct_local_reference:
dependency_uri = path_to_url(dependency.source_url)
line = f"{dependency.name} @ {dependency_uri}"
else:
line = f"{package.name}=={package.version}"
if not is_direct_remote_reference and ";" in requirement:
markers = requirement.split(";", 1)[1].strip()
if markers:
line += f" ; {markers}"
if (
not is_direct_remote_reference
and not is_direct_local_reference
and package.source_url
):
indexes.add(package.source_url)
if package.files and with_hashes:
hashes = []
for f in package.files:
h = f["hash"]
algorithm = "sha256"
if ":" in h:
algorithm, h = h.split(":")
if algorithm not in self.ALLOWED_HASH_ALGORITHMS:
continue
hashes.append(f"{algorithm}:{h}")
if hashes:
sep = " \\\n"
line += sep + sep.join(f" --hash={h}" for h in hashes)
dependency_lines.add(line)
content += "\n".join(sorted(dependency_lines))
content += "\n"
if indexes and with_urls:
# If we have extra indexes, we add them to the beginning of the output
indexes_header = ""
for index in sorted(indexes):
repositories = [
r
for r in self._poetry.pool.repositories
if r.url == index.rstrip("/")
]
if not repositories:
continue
repository = repositories[0]
if (
self._poetry.pool.has_default()
and repository is self._poetry.pool.repositories[0]
):
url = (
repository.authenticated_url
if with_credentials
else repository.url
)
indexes_header = f"--index-url {url}\n"
continue
url = (
repository.authenticated_url if with_credentials else repository.url
)
parsed_url = urllib.parse.urlsplit(url)
if parsed_url.scheme == "http":
indexes_header += f"--trusted-host {parsed_url.netloc}\n"
indexes_header += f"--extra-index-url {url}\n"
content = indexes_header + "\n" + content
self._output(content, cwd, output)
def _output(self, content: str, cwd: Path, output: IO | str) -> None:
decoded = decode(content)
try:
output.write(decoded)
except AttributeError:
filepath = cwd / output
with filepath.open("w", encoding="utf-8") as f:
f.write(decoded)
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import Mock
import pytest
from poetry_plugin_export.exporter import Exporter
from tests.helpers import get_package
if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
from cleo.testers.command_tester import CommandTester
from poetry.poetry import Poetry
from tests.helpers import TestRepository
from tests.types import CommandTesterFactory
from tests.types import ProjectFactory
PYPROJECT_CONTENT = """\
[tool.poetry]
name = "simple-project"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://python-poetry.org"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
foo = "^1.0"
bar = { version = "^1.1", optional = true }
[tool.poetry.extras]
feature_bar = ["bar"]
"""
@pytest.fixture(autouse=True)
def setup(repo: TestRepository) -> None:
repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
@pytest.fixture
def poetry(project_factory: ProjectFactory) -> Poetry:
return project_factory(name="export", pyproject_content=PYPROJECT_CONTENT)
@pytest.fixture
def tester(
command_tester_factory: CommandTesterFactory, poetry: Poetry
) -> CommandTester:
return command_tester_factory("export", poetry=poetry)
def _export_requirements(tester: CommandTester, poetry: Poetry) -> None:
tester.execute("--format requirements.txt --output requirements.txt")
requirements = poetry.file.parent / "requirements.txt"
assert requirements.exists()
with requirements.open(encoding="utf-8") as f:
content = f.read()
assert poetry.locker.lock.exists()
expected = """\
foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
"""
assert content == expected
def test_export_exports_requirements_txt_file_locks_if_no_lock_file(
tester: CommandTester, poetry: Poetry
):
assert not poetry.locker.lock.exists()
_export_requirements(tester, poetry)
assert "The lock file does not exist. Locking." in tester.io.fetch_error()
def test_export_exports_requirements_txt_uses_lock_file(
tester: CommandTester, poetry: Poetry, do_lock: None
):
_export_requirements(tester, poetry)
assert "The lock file does not exist. Locking." not in tester.io.fetch_error()
def test_export_fails_on_invalid_format(tester: CommandTester, do_lock: None):
with pytest.raises(ValueError):
tester.execute("--format invalid")
def test_export_prints_to_stdout_by_default(tester: CommandTester, do_lock: None):
tester.execute("--format requirements.txt")
expected = """\
foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
"""
assert tester.io.fetch_output() == expected
def test_export_uses_requirements_txt_format_by_default(
tester: CommandTester, do_lock: None
):
tester.execute()
expected = """\
foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
"""
assert tester.io.fetch_output() == expected
def test_export_includes_extras_by_flag(tester: CommandTester, do_lock: None):
tester.execute("--format requirements.txt --extras feature_bar")
expected = """\
bar==1.1.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
"""
assert tester.io.fetch_output() == expected
def test_export_with_urls(
monkeypatch: MonkeyPatch, tester: CommandTester, poetry: Poetry
):
"""
We are just validating that the option gets passed. The option itself is tested in
the Exporter test.
"""
mock_export = Mock()
monkeypatch.setattr(Exporter, "with_urls", mock_export)
tester.execute("--without-urls")
mock_export.assert_called_once_with(False)
from __future__ import annotations
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import Iterator
import pytest
from poetry.core.packages.dependency import Dependency
from poetry.core.toml.file import TOMLFile
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
if TYPE_CHECKING:
from _pytest.capture import CaptureFixture
from pytest_mock import MockerFixture
from poetry.poetry import Poetry
from tests.conftest import Config
from tests.types import FixtureDirGetter
class Locker(BaseLocker):
def __init__(self) -> None:
self._lock = TOMLFile(Path.cwd().joinpath("poetry.lock"))
self._locked = True
self._content_hash = self._get_content_hash()
def locked(self, is_locked: bool = True) -> Locker:
self._locked = is_locked
return self
def mock_lock_data(self, data: dict[str, Any]):
self._lock_data = data
def is_locked(self) -> bool:
return self._locked
def is_fresh(self) -> bool:
return True
def _get_content_hash(self) -> str:
return "123456789"
@pytest.fixture
def working_directory() -> Path:
return Path(__file__).parent.parent.parent
@pytest.fixture(autouse=True)
def mock_path_cwd(
mocker: MockerFixture, working_directory: Path
) -> Iterator[MockerFixture]:
yield mocker.patch("pathlib.Path.cwd", return_value=working_directory)
@pytest.fixture()
def locker() -> Locker:
return Locker()
@pytest.fixture
def poetry(fixture_dir: FixtureDirGetter, locker: Locker) -> Poetry:
p = Factory().create_poetry(fixture_dir("sample_project"))
p._locker = locker
return p
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
package = poetry.package.with_dependency_groups([], only=True)
for pkg in packages:
if pkg.name not in skip:
package.add_dependency(pkg.to_dependency())
poetry._package = package
def test_exporter_can_export_requirements_txt_with_standard_packages(
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": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "extra =='foo'",
},
{
"name": "baz",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "sys_platform == 'win32'",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" and sys_platform == "win32" or\
python_version >= "3.6" and python_version < "4.0" and sys_platform == "win32"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_poetry(tmp_dir: str, poetry: Poetry):
"""Regression test for #3254"""
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "poetry",
"version": "1.1.4",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"keyring": "*"},
},
{
"name": "junit-xml",
"version": "1.9",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"six": "*"},
},
{
"name": "keyring",
"version": "21.8.0",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {
"SecretStorage": {
"version": "*",
"markers": "sys_platform == 'linux'",
}
},
},
{
"name": "secretstorage",
"version": "3.3.0",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"cryptography": "*"},
},
{
"name": "cryptography",
"version": "3.2",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"six": "*"},
},
{
"name": "six",
"version": "1.15.0",
"category": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {
"poetry": [],
"keyring": [],
"secretstorage": [],
"cryptography": [],
"six": [],
"junit-xml": [],
},
},
}
)
set_package_requires(
poetry, skip={"keyring", "secretstorage", "cryptography", "six"}
)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
# The dependency graph:
# junit-xml 1.9 Creates JUnit XML test result documents that can be read by tools
# └── six * such as Jenkins
# poetry 1.1.4 Python dependency management and packaging made easy.
# ├── keyring >=21.2.0,<22.0.0
# │ ├── importlib-metadata >=1
# │ │ └── zipp >=0.5
# │ ├── jeepney >=0.4.2
# │ ├── pywin32-ctypes <0.1.0 || >0.1.0,<0.1.1 || >0.1.1
# │ └── secretstorage >=3.2 -- On linux only
# │ ├── cryptography >=2.0
# │ │ └── six >=1.4.1
# │ └── jeepney >=0.6 (circular dependency aborted here)
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "4.0"'
linux = 'sys_platform=="linux"'
expected = {
"poetry": Dependency.create_from_pep_508(
f"poetry==1.1.4; {python27} or {python36}"
),
"junit-xml": Dependency.create_from_pep_508(
f"junit-xml==1.9 ; {python27} or {python36}"
),
"keyring": Dependency.create_from_pep_508(
f"keyring==21.8.0 ; {python27} or {python36}"
),
"secretstorage": Dependency.create_from_pep_508(
f"secretstorage==3.3.0 ; {python27} and {linux} or {python36} and {linux}"
),
"cryptography": Dependency.create_from_pep_508(
f"cryptography==3.2 ; {python27} and {linux} or {python36} and {linux}"
),
"six": Dependency.create_from_pep_508(
f"six==1.15.0 ; {python27} or {python36} or {python27} and {linux} or"
f" {python36} and {linux}"
),
}
for line in content.strip().split("\n"):
dependency = Dependency.create_from_pep_508(line)
assert dependency.name in expected
expected_dependency = expected.pop(dependency.name)
assert dependency == expected_dependency
assert dependency.marker == expected_dependency.marker
def test_exporter_can_export_requirements_txt_pyinstaller(tmp_dir: str, poetry: Poetry):
"""Regression test for #3254"""
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "pyinstaller",
"version": "4.0",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {
"altgraph": "*",
"macholib": {
"version": "*",
"markers": "sys_platform == 'darwin'",
},
},
},
{
"name": "altgraph",
"version": "0.17",
"category": "main",
"optional": False,
"python-versions": "*",
},
{
"name": "macholib",
"version": "1.8",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"altgraph": ">=0.15"},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"pyinstaller": [], "altgraph": [], "macholib": []},
},
}
)
set_package_requires(poetry, skip={"altgraph", "macholib"})
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
# Rationale for the results:
# * PyInstaller has an explicit dependency on altgraph, so it must always be
# installed.
# * PyInstaller requires macholib on Darwin, which in turn requires altgraph.
# The dependency graph:
# pyinstaller 4.0 PyInstaller bundles a Python application and all its
# ├── altgraph * dependencies into a single package.
# ├── macholib >=1.8 -- only on Darwin
# │ └── altgraph >=0.15
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "4.0"'
darwin = 'sys_platform=="darwin"'
expected = {
"pyinstaller": Dependency.create_from_pep_508(
f"pyinstaller==4.0 ; {python27} or {python36}"
),
"altgraph": Dependency.create_from_pep_508(
f"altgraph==0.17 ; {python27} or {python36} or {python27} and {darwin} or"
f" {python36} and {darwin}"
),
"macholib": Dependency.create_from_pep_508(
f"macholib==1.8 ; {python27} and {darwin} or {python36} and {darwin}"
),
}
for line in content.strip().split("\n"):
dependency = Dependency.create_from_pep_508(line)
assert dependency.name in expected
expected_dependency = expected.pop(dependency.name)
assert dependency == expected_dependency
assert dependency.marker == expected_dependency.marker
def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
"dependencies": {"b": ">=0.0.0", "c": ">=0.0.0"},
},
{
"name": "b",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "platform_system == 'Windows'",
"dependencies": {"d": ">=0.0.0"},
},
{
"name": "c",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "sys_platform == 'win32'",
"dependencies": {"d": ">=0.0.0"},
},
{
"name": "d",
"version": "0.0.1",
"category": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"a": [], "b": [], "c": [], "d": []},
},
}
)
set_package_requires(poetry, skip={"b", "c", "d"})
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "3.7"'
windows = 'platform_system == "Windows"'
win32 = 'sys_platform == "win32"'
expected = {
"a": Dependency.create_from_pep_508(f"a==1.2.3 ; {python27} or {python36}"),
"b": Dependency.create_from_pep_508(
f"b==4.5.6 ; {python27} and {windows} or {python36} and {windows}"
),
"c": Dependency.create_from_pep_508(
f"c==7.8.9 ; {python27} and {win32} or {python36} and {win32}"
),
"d": Dependency.create_from_pep_508(
f"d==0.0.1 ; {python27} and {windows} or {python36} and {windows} or"
f" {python27} and {win32} or {python36} and {win32}"
),
}
for line in content.strip().split("\n"):
dependency = Dependency.create_from_pep_508(line)
assert dependency.name in expected
expected_dependency = expected.pop(dependency.name)
assert dependency == expected_dependency
assert dependency.marker == expected_dependency.marker
assert expected == {}
@pytest.mark.parametrize(
["dev", "lines"],
[
(
False,
[
'a==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "3.8"'
],
),
(
True,
[
'a==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "3.8" or python_version'
' >= "3.6" and python_version < "4.0"',
'b==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
],
)
def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any(
tmp_dir: str, poetry: Poetry, dev: bool, lines: list[str]
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
},
{
"name": "b",
"version": "4.5.6",
"category": "dev",
"optional": False,
"python-versions": "*",
"dependencies": {"a": ">=1.2.3"},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"a": [], "b": []},
},
}
)
root = poetry.package.with_dependency_groups([], only=True)
root.add_dependency(
Factory.create_dependency(
name="a", constraint={"version": "^1.2.3", "python": "<3.8"}
)
)
root.add_dependency(
Factory.create_dependency(
name="b", constraint={"version": "^4.5.6"}, groups=["dev"]
)
)
poetry._package = root
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
assert content.strip() == "\n".join(lines)
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
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": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_disabled( # noqa: E501
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": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export(
"requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False
)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
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.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
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.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_without_optional_packages(
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": True,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
@pytest.mark.parametrize(
["extras", "lines"],
[
(
None,
[
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"'
],
),
(
False,
[
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"'
],
),
(
True,
[
'bar==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'spam==0.1.0 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
(
["feature_bar"],
[
'bar==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'spam==0.1.0 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
],
)
def test_exporter_exports_requirements_txt_with_optional_packages(
tmp_dir: str,
poetry: Poetry,
extras: bool | list[str] | None,
lines: list[str],
):
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": "main",
"optional": True,
"python-versions": "*",
"dependencies": {"spam": ">=0.1"},
},
{
"name": "spam",
"version": "0.1.0",
"category": "main",
"optional": True,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"], "spam": ["abcde"]},
},
"extras": {"feature_bar": ["bar"]},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
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:
content = f.read()
expected = "\n".join(lines)
assert content.strip() == expected
def test_exporter_can_export_requirements_txt_with_git_packages(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "git",
"url": "https://github.com/foo/foo.git",
"reference": "123456",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_nested_packages(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "git",
"url": "https://github.com/foo/foo.git",
"reference": "123456",
},
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"foo": "rev 123456"},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": []},
},
}
)
set_package_requires(poetry, skip={"foo"})
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"bar": {"version": "4.5.6"}},
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"baz": {"version": "7.8.9"}},
},
{
"name": "baz",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"foo": {"version": "1.2.3"}},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry, skip={"bar", "baz"})
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_markers(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"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",
"category": "main",
"optional": True,
"python-versions": "*",
"dependencies": {
"baz": {
"version": "!=10.11.12",
"markers": 'platform_system == "Windows"',
}
},
},
{
"name": "baz",
"version": "10.11.13",
"category": "main",
"optional": True,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export(
"requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False
)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" and platform_system != "Windows" or\
python_version >= "3.6" and python_version < "4.0" and platform_system != "Windows" or\
python_version >= "2.7" and python_version < "2.8" and platform_system == "Windows" or\
python_version >= "3.6" and python_version < "4.0" and platform_system == "Windows"
baz==10.11.13 ;\
python_version >= "2.7" and python_version < "2.8" and platform_system == "Windows" or\
python_version >= "3.6" and python_version < "4.0" and platform_system == "Windows"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_git_packages_and_markers(
tmp_dir: str, poetry: Poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
"source": {
"type": "git",
"url": "https://github.com/foo/foo.git",
"reference": "123456",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_directory_packages(
tmp_dir: str, poetry: Poetry, working_directory: Path
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_nested_directory_packages(
tmp_dir: str, poetry: Poetry, working_directory: Path
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project/../project_with_nested_local/bar", # noqa: E501
"reference": "",
},
},
{
"name": "baz",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project/../project_with_nested_local/bar/..", # noqa: E501
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = f"""\
bar @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local/bar ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
baz @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_directory_packages_and_markers(
tmp_dir: str, poetry: Poetry, working_directory: Path
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_file_packages(
tmp_dir: str, poetry: Poetry, working_directory: Path
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "file",
"url": "tests/fixtures/distributions/demo-0.1.0.tar.gz",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected
def test_exporter_can_export_requirements_txt_with_file_packages_and_markers(
tmp_dir: str, poetry: Poetry, working_directory: Path
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
"source": {
"type": "file",
"url": "tests/fixtures/distributions/demo-0.1.0.tar.gz",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
"""
assert content == expected
def test_exporter_exports_requirements_txt_with_legacy_packages(
tmp_dir: str, poetry: Poetry
):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
)
)
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": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple",
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
--extra-index-url https://example.com/simple
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_with_url_false(tmp_dir: str, poetry: Poetry):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
)
)
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": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple",
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export(
"requirements.txt", Path(tmp_dir), "requirements.txt", dev=True, with_urls=False
)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host(
tmp_dir: str, poetry: Poetry
):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"http://example.com/simple",
)
)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "bar",
"version": "4.5.6",
"category": "dev",
"optional": False,
"python-versions": "*",
"source": {
"type": "legacy",
"url": "http://example.com/simple",
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
--trusted-host example.com
--extra-index-url http://example.com/simple
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
"""
assert content == expected
@pytest.mark.parametrize(
["dev", "expected"],
[
(
True,
[
'bar==1.2.2 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'baz==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.1 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
(
False,
[
'bar==1.2.2 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.1 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
],
)
def test_exporter_exports_requirements_txt_with_dev_extras(
tmp_dir: str, poetry: Poetry, dev: bool, expected: list[str]
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.1",
"category": "main",
"optional": False,
"python-versions": "*",
},
{
"name": "bar",
"version": "1.2.2",
"category": "main",
"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",
"category": "dev",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=dev)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
assert content == "\n".join(expected) + "\n"
def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_sources(
tmp_dir: str, poetry: Poetry
):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
)
)
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://foobaz.com/simple",
)
)
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple",
"reference": "",
},
},
{
"name": "bar",
"version": "4.5.6",
"category": "dev",
"optional": False,
"python-versions": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple",
"reference": "",
},
},
{
"name": "baz",
"version": "7.8.9",
"category": "dev",
"optional": False,
"python-versions": "*",
"source": {
"type": "legacy",
"url": "https://foobaz.com/simple",
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"], "baz": ["24680"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
--extra-index-url https://example.com/simple
--extra-index-url https://foobaz.com/simple
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:24680
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
tmp_dir: str, poetry: Poetry, config: Config
):
poetry.config.merge(
{
"repositories": {"custom": {"url": "https://example.com/simple"}},
"http-basic": {"custom": {"username": "foo", "password": "bar"}},
}
)
poetry.pool.add_repository(
LegacyRepository("custom", "https://example.com/simple", config=poetry.config)
)
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": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple",
"reference": "",
},
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": ["12345"], "bar": ["67890"]},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
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:
content = f.read()
expected = """\
--extra-index-url https://foo:bar@example.com/simple
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345
"""
assert content == expected
def test_exporter_exports_requirements_txt_to_standard_output(
tmp_dir: str, poetry: Poetry, capsys: CaptureFixture
):
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": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": []},
},
}
)
set_package_requires(poetry)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert out == expected
def test_exporter_doesnt_confuse_repeated_packages(
tmp_dir: str, poetry: Poetry, capsys: CaptureFixture
):
# Testcase derived from <https://github.com/python-poetry/poetry/issues/5141>.
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "celery",
"version": "5.1.2",
"category": "main",
"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",
"category": "main",
"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",
"category": "main",
"optional": False,
"python-versions": (
">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
),
},
{
"name": "click",
"version": "8.0.3",
"category": "main",
"optional": False,
"python-versions": ">=3.6",
"dependencies": {},
},
{
"name": "click-didyoumean",
"version": "0.0.3",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"click": "*"},
},
{
"name": "click-didyoumean",
"version": "0.3.0",
"category": "main",
"optional": False,
"python-versions": ">=3.6.2,<4.0.0",
"dependencies": {"click": ">=7"},
},
{
"name": "click-plugins",
"version": "1.1.1",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"click": ">=4.0"},
},
],
"metadata": {
"lock-version": "1.1",
"python-versions": "^3.6",
"content-hash": (
"832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6"
),
"hashes": {
"celery": [],
"click-didyoumean": [],
"click-plugins": [],
"click": [],
},
},
}
)
root = poetry.package.with_dependency_groups([], only=True)
root.python_versions = "^3.6"
root.add_dependency(
Factory.create_dependency(
name="celery", constraint={"version": "5.1.2", "python": "<3.7"}
)
)
root.add_dependency(
Factory.create_dependency(
name="celery", constraint={"version": "5.2.3", "python": ">=3.7"}
)
)
poetry._package = root
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
celery==5.1.2 ; python_version >= "3.6" and python_version < "3.7"
celery==5.2.3 ; python_version >= "3.7" and python_version < "4.0"
click-didyoumean==0.0.3 ; python_version >= "3.6" and python_version < "3.7"
click-didyoumean==0.3.0 ; python_version >= "3.7" and python_full_version < "4.0.0"
click-plugins==1.1.1 ;\
python_version >= "3.6" and python_version < "3.7" or\
python_version >= "3.7" and python_version < "4.0"
click==7.1.2 ; python_version >= "3.6" and python_version < "3.7"
click==8.0.3 ;\
python_version >= "3.7" and python_version < "4.0" or\
python_version >= "3.7" and python_full_version < "4.0.0"
"""
assert out == expected
def test_exporter_handles_extras_next_to_non_extras(
tmp_dir: str, poetry: Poetry, capsys: CaptureFixture
):
# Testcase similar to the solver testcase added at #5305.
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "localstack",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {
"localstack-ext": [
{"version": ">=1.0.0"},
{
"version": ">=1.0.0",
"extras": ["bar"],
"markers": 'extra == "foo"',
},
]
},
"extras": {"foo": ["localstack-ext (>=1.0.0)"]},
},
{
"name": "localstack-ext",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {
"something": "*",
"something-else": {
"version": ">=1.0.0",
"markers": 'extra == "bar"',
},
"another-thing": {
"version": ">=1.0.0",
"markers": 'extra == "baz"',
},
},
"extras": {
"bar": ["something-else (>=1.0.0)"],
"baz": ["another-thing (>=1.0.0)"],
},
},
{
"name": "something",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
{
"name": "something-else",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
{
"name": "another-thing",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
],
"metadata": {
"lock-version": "1.1",
"python-versions": "^3.6",
"content-hash": (
"832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6"
),
"hashes": {
"localstack": [],
"localstack-ext": [],
"something": [],
"something-else": [],
"another-thing": [],
},
},
}
)
root = poetry.package.with_dependency_groups([], only=True)
root.python_versions = "^3.6"
root.add_dependency(
Factory.create_dependency(
name="localstack", constraint={"version": "^1.0.0", "extras": ["foo"]}
)
)
poetry._package = root
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
localstack-ext==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
localstack==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
something-else==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
something==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
"""
assert out == expected
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment