Commit e95a05c9 by Sébastien Eustace Committed by GitHub

Improve the export command (#1277)

* Improve the export command

* Update documentation
parent 93e562ec
......@@ -437,10 +437,13 @@ poetry export -f requirements.txt > requirements.txt
### Options
* `--format (-f)`: the format to export to. Currently, only
* `--format (-f)`: The format to export to. Currently, only
`requirements.txt` is supported.
* `--output (-o)`: the name of the output file. If omitted, print to standard
* `--output (-o)`: The name of the output file. If omitted, print to standard
output.
* `--dev`: Include development dependencies.
* `--without-hashes`: Exclude hashes from the exported file.
* `--with-credentials`: Include credentials for extra indices.
## env
......
......@@ -14,6 +14,7 @@ class ExportCommand(Command):
option("output", "o", "The name of the output file.", flag=False),
option("without-hashes", None, "Exclude hashes from the exported file."),
option("dev", None, "Include development dependencies."),
option("with-credentials", None, "Include credentials for extra indices."),
]
def handle(self):
......@@ -47,11 +48,12 @@ class ExportCommand(Command):
"</warning>"
)
exporter = Exporter(self.poetry.locker)
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"),
with_credentials=self.option("with-credentials"),
)
......@@ -15,6 +15,7 @@ from .constraints import parse_constraint as parse_generic_constraint
from .constraints.constraint import Constraint
from .constraints.multi_constraint import MultiConstraint
from .constraints.union_constraint import UnionConstraint
from .utils.utils import convert_markers
class Dependency(object):
......@@ -188,6 +189,7 @@ class Dependency(object):
requirement = self.base_pep_508_name
markers = []
has_extras = False
if not self.marker.is_any():
marker = self.marker
if not with_extras:
......@@ -195,6 +197,8 @@ class Dependency(object):
if not marker.is_empty():
markers.append(str(marker))
has_extras = "extra" in convert_markers(marker)
else:
# Python marker
if self.python_versions != "*":
......@@ -205,7 +209,7 @@ class Dependency(object):
)
in_extras = " || ".join(self._in_extras)
if in_extras and with_extras:
if in_extras and with_extras and not has_extras:
markers.append(
self._create_nested_marker("extra", parse_generic_constraint(in_extras))
)
......
from typing import Optional
from typing import Union
from clikit.api.io import IO
from poetry.packages.locker import Locker
from poetry.packages.directory_dependency import DirectoryDependency
from poetry.packages.file_dependency import FileDependency
from poetry.packages.url_dependency import URLDependency
from poetry.packages.vcs_dependency import VCSDependency
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils._compat import decode
......@@ -13,55 +17,122 @@ class Exporter(object):
"""
ACCEPTED_FORMATS = ("requirements.txt",)
ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512")
def __init__(self, lock): # type: (Locker) -> None
self._lock = lock
def __init__(self, poetry): # type: (Poetry) -> None
self._poetry = poetry
def export(
self, fmt, cwd, output, with_hashes=True, dev=False
): # type: (str, Path, Union[IO, str], bool, bool) -> None
self, fmt, cwd, output, with_hashes=True, dev=False, with_credentials=False
): # type: (str, Path, Union[IO, str], bool, bool, bool) -> None
if fmt not in self.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt))
getattr(self, "_export_{}".format(fmt.replace(".", "_")))(
cwd, output, with_hashes=with_hashes, dev=dev
cwd,
output,
with_hashes=with_hashes,
dev=dev,
with_credentials=with_credentials,
)
def _export_requirements_txt(
self, cwd, output, with_hashes=True, dev=False
): # type: (Path, Union[IO, str], bool, bool) -> None
self, cwd, output, with_hashes=True, dev=False, with_credentials=False
): # type: (Path, Union[IO, str], bool, bool, bool) -> None
indexes = []
content = ""
for package in sorted(
self._lock.locked_repository(dev).packages, key=lambda p: p.name
self._poetry.locker.locked_repository(dev).packages, key=lambda p: p.name
):
if package.source_type == "git":
dependency = VCSDependency(
package.name,
package.source_type,
package.source_url,
package.source_reference,
)
dependency.marker = package.marker
line = "-e git+{}@{}#egg={}".format(
package.source_url, package.source_reference, package.name
)
elif package.source_type in ["directory", "file"]:
line = ""
elif package.source_type in ["directory", "file", "url"]:
if package.source_type == "file":
dependency = FileDependency(package.name, Path(package.source_url))
elif package.source_type == "directory":
dependency = DirectoryDependency(
package.name, Path(package.source_url)
)
else:
dependency = URLDependency(package.name, package.source_url)
dependency.marker = package.marker
line = "{}".format(package.source_url)
if package.develop:
line += "-e "
line += package.source_url
line = "-e " + line
else:
line = "{}=={}".format(package.name, package.version.text)
dependency = package.to_dependency()
line = "{}=={}".format(package.name, package.version)
if package.source_type == "legacy" and package.source_url:
line += " \\\n"
line += " --index-url {}".format(package.source_url)
requirement = dependency.to_pep_508()
if ";" in requirement:
line += "; {}".format(requirement.split(";")[1].strip())
if package.source_type == "legacy" and package.source_url:
indexes.append(package.source_url)
if package.hashes and with_hashes:
hashes = []
for h in package.hashes:
algorithm = "sha256"
if ":" in h:
algorithm, h = h.split(":")
if package.hashes and with_hashes:
if algorithm not in self.ALLOWED_HASH_ALGORITHMS:
continue
hashes.append("{}:{}".format(algorithm, h))
if hashes:
line += " \\\n"
for i, h in enumerate(package.hashes):
line += " --hash=sha256:{}{}".format(
for i, h in enumerate(hashes):
line += " --hash={}{}".format(
h, " \\\n" if i < len(package.hashes) - 1 else ""
)
line += "\n"
content += line
if indexes:
# If we have extra indexes, we add them to the begin
# of the output
indexes_header = ""
for index in indexes:
repository = [
r
for r in self._poetry.pool.repositories
if r.url == index.rstrip("/")
][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 = "--index-url {}\n".format(url)
continue
url = (
repository.authenticated_url if with_credentials else repository.url
)
indexes_header += "--extra-index-url {}\n".format(url)
content = indexes_header + "\n" + content
self._output(content, cwd, output)
def _output(
......
......@@ -3,6 +3,9 @@ import sys
import pytest
from poetry.packages import Locker as BaseLocker
from poetry.poetry import Poetry
from poetry.repositories.auth import Auth
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.utils._compat import Path
from poetry.utils.exporter import Exporter
......@@ -35,8 +38,16 @@ def locker():
return Locker()
def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, locker):
locker.mock_lock_data(
@pytest.fixture
def poetry(fixture_dir, locker):
p = Poetry.create(fixture_dir("sample_project"))
p._locker = locker
return p
def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, poetry):
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -61,7 +72,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, lo
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -76,10 +87,55 @@ foo==1.2.3
assert expected == content
def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers(
tmp_dir, 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'",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"foo": [], "bar": []},
},
}
)
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; extra == "foo"
foo==1.2.3; python_version < "3.7"
"""
assert expected == content
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
tmp_dir, locker
tmp_dir, poetry
):
locker.mock_lock_data(
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -104,7 +160,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -122,9 +178,9 @@ foo==1.2.3 \\
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_disabled(
tmp_dir, locker
tmp_dir, poetry
):
locker.mock_lock_data(
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -149,7 +205,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export(
"requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False
......@@ -167,9 +223,9 @@ foo==1.2.3
def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
tmp_dir, locker
tmp_dir, poetry
):
locker.mock_lock_data(
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -194,7 +250,7 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -210,9 +266,9 @@ foo==1.2.3 \\
def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
tmp_dir, locker
tmp_dir, poetry
):
locker.mock_lock_data(
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -237,7 +293,7 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
......@@ -254,8 +310,8 @@ foo==1.2.3 \\
assert expected == content
def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, locker):
locker.mock_lock_data(
def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry):
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -278,7 +334,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, locker)
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -292,8 +348,10 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, locker)
assert expected == content
def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, locker):
locker.mock_lock_data(
def test_exporter_can_export_requirements_txt_with_git_packages_and_markers(
tmp_dir, poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -302,7 +360,12 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l
"category": "main",
"optional": False,
"python-versions": "*",
"source": {"type": "directory", "url": "../foo", "reference": ""},
"marker": "python_version < '3.7'",
"source": {
"type": "git",
"url": "https://github.com/foo/foo.git",
"reference": "123456",
},
}
],
"metadata": {
......@@ -312,7 +375,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -320,14 +383,14 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l
content = f.read()
expected = """\
-e ../foo
-e git+https://github.com/foo/foo.git@123456#egg=foo; python_version < "3.7"
"""
assert expected == content
def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker):
locker.mock_lock_data(
def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, poetry):
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -336,7 +399,11 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker
"category": "main",
"optional": False,
"python-versions": "*",
"source": {"type": "file", "url": "../foo.tar.gz", "reference": ""},
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
}
],
"metadata": {
......@@ -346,7 +413,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
......@@ -354,14 +421,141 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker
content = f.read()
expected = """\
-e ../foo.tar.gz
-e tests/fixtures/sample_project
"""
assert expected == content
def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker):
locker.mock_lock_data(
def test_exporter_can_export_requirements_txt_with_directory_packages_and_markers(
tmp_dir, 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": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"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 = """\
-e tests/fixtures/sample_project; python_version < "3.7"
"""
assert expected == content
def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, poetry):
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": []},
},
}
)
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 = """\
-e tests/fixtures/distributions/demo-0.1.0.tar.gz
"""
assert expected == content
def test_exporter_can_export_requirements_txt_with_file_packages_and_markers(
tmp_dir, 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": "file",
"url": "tests/fixtures/distributions/demo-0.1.0.tar.gz",
"reference": "",
},
}
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"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 = """\
-e tests/fixtures/distributions/demo-0.1.0.tar.gz; python_version < "3.7"
"""
assert expected == content
def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
auth=Auth("https://example.com/simple", "foo", "bar"),
)
)
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -379,7 +573,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker)
"python-versions": "*",
"source": {
"type": "legacy",
"url": "https://example.com/simple/",
"url": "https://example.com/simple",
"reference": "",
},
},
......@@ -391,7 +585,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker)
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
......@@ -399,8 +593,74 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker)
content = f.read()
expected = """\
--extra-index-url https://example.com/simple
bar==4.5.6 \\
--hash=sha256:67890
foo==1.2.3 \\
--hash=sha256:12345
"""
assert expected == content
def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
tmp_dir, poetry, config
):
poetry.pool.add_repository(
LegacyRepository(
"custom",
"https://example.com/simple",
auth=Auth("https://example.com/simple", "foo", "bar"),
)
)
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"]},
},
}
)
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 \\
--index-url https://example.com/simple/ \\
--hash=sha256:67890
foo==1.2.3 \\
--hash=sha256:12345
......@@ -409,8 +669,8 @@ foo==1.2.3 \\
assert expected == content
def test_exporter_exports_requirements_txt_to_standard_output(tmp_dir, locker, capsys):
locker.mock_lock_data(
def test_exporter_exports_requirements_txt_to_standard_output(tmp_dir, poetry, capsys):
poetry.locker.mock_lock_data(
{
"package": [
{
......@@ -435,7 +695,7 @@ def test_exporter_exports_requirements_txt_to_standard_output(tmp_dir, locker, c
},
}
)
exporter = Exporter(locker)
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
......
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