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

export: remove in-tree export logic

parent d52780c5
...@@ -53,7 +53,6 @@ COMMANDS = [ ...@@ -53,7 +53,6 @@ COMMANDS = [
"build", "build",
"check", "check",
"config", "config",
"export",
"init", "init",
"install", "install",
"lock", "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)
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