Commit 72c96a81 by Tom Milligan Committed by Sébastien Eustace

export: add extras flag and exclude optional packages by default (#1285)

parent b637e9a1
...@@ -442,6 +442,7 @@ poetry export -f requirements.txt > requirements.txt ...@@ -442,6 +442,7 @@ poetry export -f requirements.txt > requirements.txt
* `--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. output.
* `--dev`: Include development dependencies. * `--dev`: Include development dependencies.
* `--extras (-E)`: Extra sets of dependencies to include.
* `--without-hashes`: Exclude hashes from the exported file. * `--without-hashes`: Exclude hashes from the exported file.
* `--with-credentials`: Include credentials for extra indices. * `--with-credentials`: Include credentials for extra indices.
......
from cleo import option
from poetry.utils.exporter import Exporter from poetry.utils.exporter import Exporter
from cleo import option
from .command import Command from .command import Command
...@@ -14,6 +15,13 @@ class ExportCommand(Command): ...@@ -14,6 +15,13 @@ class ExportCommand(Command):
option("output", "o", "The name of the output file.", flag=False), option("output", "o", "The name of the output file.", flag=False),
option("without-hashes", None, "Exclude hashes from the exported file."), option("without-hashes", None, "Exclude hashes from the exported file."),
option("dev", None, "Include development dependencies."), 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."), option("with-credentials", None, "Include credentials for extra indices."),
] ]
...@@ -55,5 +63,6 @@ class ExportCommand(Command): ...@@ -55,5 +63,6 @@ class ExportCommand(Command):
output or self.io, output or self.io,
with_hashes=not self.option("without-hashes"), with_hashes=not self.option("without-hashes"),
dev=self.option("dev"), dev=self.option("dev"),
extras=self.option("extras"),
with_credentials=self.option("with-credentials"), with_credentials=self.option("with-credentials"),
) )
...@@ -23,7 +23,14 @@ class Exporter(object): ...@@ -23,7 +23,14 @@ class Exporter(object):
self._poetry = poetry self._poetry = poetry
def export( def export(
self, fmt, cwd, output, with_hashes=True, dev=False, with_credentials=False self,
fmt,
cwd,
output,
with_hashes=True,
dev=False,
extras=None,
with_credentials=False,
): # type: (str, Path, Union[IO, str], bool, bool, bool) -> None ): # type: (str, Path, Union[IO, str], bool, bool, bool) -> None
if fmt not in self.ACCEPTED_FORMATS: if fmt not in self.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt)) raise ValueError("Invalid export format: {}".format(fmt))
...@@ -33,18 +40,39 @@ class Exporter(object): ...@@ -33,18 +40,39 @@ class Exporter(object):
output, output,
with_hashes=with_hashes, with_hashes=with_hashes,
dev=dev, dev=dev,
extras=extras,
with_credentials=with_credentials, with_credentials=with_credentials,
) )
def _export_requirements_txt( def _export_requirements_txt(
self, cwd, output, with_hashes=True, dev=False, with_credentials=False self,
cwd,
output,
with_hashes=True,
dev=False,
extras=None,
with_credentials=False,
): # type: (Path, Union[IO, str], bool, bool, bool) -> None ): # type: (Path, Union[IO, str], bool, bool, bool) -> None
indexes = [] indexes = []
content = "" content = ""
# Generate a list of package names we have opted into via `extras`
extras_set = frozenset(extras or ())
extra_package_names = set()
if extras:
for extra_name, extra_packages in self._poetry.locker.lock_data.get(
"extras", {}
).items():
if extra_name in extras_set:
extra_package_names.update(extra_packages)
for package in sorted( for package in sorted(
self._poetry.locker.locked_repository(dev).packages, key=lambda p: p.name self._poetry.locker.locked_repository(dev).packages, key=lambda p: p.name
): ):
# If a package is optional and we haven't opted in to it, continue
if package.optional and package.name not in extra_package_names:
continue
if package.source_type == "git": if package.source_type == "git":
dependency = VCSDependency( dependency = VCSDependency(
package.name, package.name,
......
...@@ -39,6 +39,10 @@ classifiers = [ ...@@ -39,6 +39,10 @@ classifiers = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "~2.7 || ^3.4" python = "~2.7 || ^3.4"
foo = "^1.0" foo = "^1.0"
bar = { version = "^1.1", optional = true }
[tool.poetry.extras]
feature_bar = ["bar"]
""" """
...@@ -68,6 +72,7 @@ def test_export_exports_requirements_txt_file_locks_if_no_lock_file(app, repo): ...@@ -68,6 +72,7 @@ def test_export_exports_requirements_txt_file_locks_if_no_lock_file(app, repo):
assert not app.poetry.locker.lock.exists() assert not app.poetry.locker.lock.exists()
repo.add_package(get_package("foo", "1.0.0")) repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
tester.execute("--format requirements.txt --output requirements.txt") tester.execute("--format requirements.txt --output requirements.txt")
...@@ -89,6 +94,7 @@ foo==1.0.0 ...@@ -89,6 +94,7 @@ foo==1.0.0
def test_export_exports_requirements_txt_uses_lock_file(app, repo): def test_export_exports_requirements_txt_uses_lock_file(app, repo):
repo.add_package(get_package("foo", "1.0.0")) repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
command = app.find("lock") command = app.find("lock")
tester = CommandTester(command) tester = CommandTester(command)
...@@ -119,6 +125,7 @@ foo==1.0.0 ...@@ -119,6 +125,7 @@ foo==1.0.0
def test_export_fails_on_invalid_format(app, repo): def test_export_fails_on_invalid_format(app, repo):
repo.add_package(get_package("foo", "1.0.0")) repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
command = app.find("lock") command = app.find("lock")
tester = CommandTester(command) tester = CommandTester(command)
...@@ -135,6 +142,7 @@ def test_export_fails_on_invalid_format(app, repo): ...@@ -135,6 +142,7 @@ def test_export_fails_on_invalid_format(app, repo):
def test_export_prints_to_stdout_by_default(app, repo): def test_export_prints_to_stdout_by_default(app, repo):
repo.add_package(get_package("foo", "1.0.0")) repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
command = app.find("lock") command = app.find("lock")
tester = CommandTester(command) tester = CommandTester(command)
...@@ -152,3 +160,26 @@ foo==1.0.0 ...@@ -152,3 +160,26 @@ foo==1.0.0
""" """
assert expected == tester.io.fetch_output() assert expected == tester.io.fetch_output()
def test_export_includes_extras_by_flag(app, repo):
repo.add_package(get_package("foo", "1.0.0"))
repo.add_package(get_package("bar", "1.1.0"))
command = app.find("lock")
tester = CommandTester(command)
tester.execute()
assert app.poetry.locker.lock.exists()
command = app.find("export")
tester = CommandTester(command)
tester.execute("--format requirements.txt --extras feature_bar")
expected = """\
bar==1.1.0
foo==1.0.0
"""
assert expected == tester.io.fetch_output()
...@@ -310,6 +310,99 @@ foo==1.2.3 \\ ...@@ -310,6 +310,99 @@ foo==1.2.3 \\
assert expected == content assert expected == content
def test_exporter_exports_requirements_txt_without_optional_packages(tmp_dir, 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"]},
},
}
)
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 \\
--hash=sha256:12345
"""
assert expected == content
def test_exporter_exports_requirements_txt_with_optional_packages_if_opted_in(
tmp_dir, 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"]},
},
"extras": {"feature_bar": ["bar"]},
}
)
exporter = Exporter(poetry)
exporter.export(
"requirements.txt",
Path(tmp_dir),
"requirements.txt",
dev=True,
extras=["feature_bar"],
)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()
expected = """\
bar==4.5.6 \\
--hash=sha256:67890
foo==1.2.3 \\
--hash=sha256:12345
"""
assert expected == content
def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry): def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry):
poetry.locker.mock_lock_data( poetry.locker.mock_lock_data(
{ {
......
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