Commit 11c2b9a5 by Clemens Kaposi Committed by Sébastien Eustace

Add `-o` flag for `export` command (#1035)

* Add `-o` flag for `export` command

The export command now prints to standard output by default.  To store the
output in a file, the new `-o <filename>` argument can be used.

* Fix code style

* Improve usage examples for `export` command

* Add missing import statement to exporter module

* Make Exporter class work w/ IO objects for output

* Fix test suite for `Exporter` utility class
parent 01fddda5
...@@ -546,6 +546,21 @@ This command locks (without installing) the dependencies specified in `pyproject ...@@ -546,6 +546,21 @@ This command locks (without installing) the dependencies specified in `pyproject
poetry lock poetry lock
``` ```
### export
This command exports the lock file to other formats.
```bash
poetry export -f requirements.txt > requirements.txt
```
#### Options
* `--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.
## The `pyproject.toml` file ## The `pyproject.toml` file
......
...@@ -427,16 +427,21 @@ The new version should ideally be a valid semver string or a valid bump rule: ...@@ -427,16 +427,21 @@ The new version should ideally be a valid semver string or a valid bump rule:
This command exports the lock file to other formats. This command exports the lock file to other formats.
If the lock file does not exist, it will be created automatically.
```bash ```bash
poetry export -f requirements.txt poetry export -f requirements.txt > requirements.txt
``` ```
!!!note !!!note
Only the `requirements.txt` format is currently supported. Only the `requirements.txt` format is currently supported.
### Options
* `--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.
## env ## env
The `env` command regroups sub commands to interact with the virtualenvs The `env` command regroups sub commands to interact with the virtualenvs
......
...@@ -11,6 +11,7 @@ class ExportCommand(Command): ...@@ -11,6 +11,7 @@ class ExportCommand(Command):
options = [ options = [
option("format", "f", "Format to export to.", flag=False), option("format", "f", "Format to export to.", 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."),
] ]
...@@ -21,6 +22,8 @@ class ExportCommand(Command): ...@@ -21,6 +22,8 @@ class ExportCommand(Command):
if fmt not in Exporter.ACCEPTED_FORMATS: if fmt not in Exporter.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt)) raise ValueError("Invalid export format: {}".format(fmt))
output = self.option("output")
locker = self.poetry.locker locker = self.poetry.locker
if not locker.is_locked(): if not locker.is_locked():
self.line("<comment>The lock file does not exist. Locking.</comment>") self.line("<comment>The lock file does not exist. Locking.</comment>")
...@@ -48,6 +51,7 @@ class ExportCommand(Command): ...@@ -48,6 +51,7 @@ class ExportCommand(Command):
exporter.export( exporter.export(
fmt, fmt,
self.poetry.file.parent, self.poetry.file.parent,
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"),
) )
from typing import Optional
from clikit.api.io import IO
from poetry.packages.locker import Locker from poetry.packages.locker import Locker
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
...@@ -14,19 +18,18 @@ class Exporter(object): ...@@ -14,19 +18,18 @@ class Exporter(object):
self._lock = lock self._lock = lock
def export( def export(
self, fmt, cwd, with_hashes=True, dev=False self, fmt, cwd, output, with_hashes=True, dev=False
): # type: (str, Path, bool, bool) -> None ): # type: (str, Path, Union[IO, str], 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))
getattr(self, "_export_{}".format(fmt.replace(".", "_")))( getattr(self, "_export_{}".format(fmt.replace(".", "_")))(
cwd, with_hashes=with_hashes, dev=dev cwd, output, with_hashes=with_hashes, dev=dev
) )
def _export_requirements_txt( def _export_requirements_txt(
self, cwd, with_hashes=True, dev=False self, cwd, output, with_hashes=True, dev=False
): # type: (Path, bool, bool) -> None ): # type: (Path, Union[IO, str], bool, bool) -> None
filepath = cwd / "requirements.txt"
content = "" content = ""
for package in sorted( for package in sorted(
...@@ -59,5 +62,15 @@ class Exporter(object): ...@@ -59,5 +62,15 @@ class Exporter(object):
line += "\n" line += "\n"
content += line content += line
with filepath.open("w", encoding="utf-8") as f: self._output(content, cwd, output)
f.write(decode(content))
def _output(
self, content, cwd, output
): # type: (str, Path, Union[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)
...@@ -69,7 +69,7 @@ def test_export_exports_requirements_txt_file_locks_if_no_lock_file(app, repo): ...@@ -69,7 +69,7 @@ def test_export_exports_requirements_txt_file_locks_if_no_lock_file(app, repo):
repo.add_package(get_package("foo", "1.0.0")) repo.add_package(get_package("foo", "1.0.0"))
tester.execute("--format requirements.txt") tester.execute("--format requirements.txt --output requirements.txt")
requirements = app.poetry.file.parent / "requirements.txt" requirements = app.poetry.file.parent / "requirements.txt"
assert requirements.exists() assert requirements.exists()
...@@ -99,7 +99,7 @@ def test_export_exports_requirements_txt_uses_lock_file(app, repo): ...@@ -99,7 +99,7 @@ def test_export_exports_requirements_txt_uses_lock_file(app, repo):
command = app.find("export") command = app.find("export")
tester = CommandTester(command) tester = CommandTester(command)
tester.execute("--format requirements.txt") tester.execute("--format requirements.txt --output requirements.txt")
requirements = app.poetry.file.parent / "requirements.txt" requirements = app.poetry.file.parent / "requirements.txt"
assert requirements.exists() assert requirements.exists()
...@@ -131,3 +131,24 @@ def test_export_fails_on_invalid_format(app, repo): ...@@ -131,3 +131,24 @@ def test_export_fails_on_invalid_format(app, repo):
with pytest.raises(ValueError): with pytest.raises(ValueError):
tester.execute("--format invalid") tester.execute("--format invalid")
def test_export_prints_to_stdout_by_default(app, repo):
repo.add_package(get_package("foo", "1.0.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")
expected = """\
foo==1.0.0
"""
assert expected == tester.io.fetch_output()
import sys
import pytest import pytest
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
...@@ -61,7 +63,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, lo ...@@ -61,7 +63,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, lo
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -104,7 +106,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( ...@@ -104,7 +106,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -149,7 +151,9 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_ ...@@ -149,7 +151,9 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir), with_hashes=False) exporter.export(
"requirements.txt", Path(tmp_dir), "requirements.txt", with_hashes=False
)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -192,7 +196,7 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default( ...@@ -192,7 +196,7 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -235,7 +239,7 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( ...@@ -235,7 +239,7 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir), dev=True) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -276,7 +280,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, locker) ...@@ -276,7 +280,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, locker)
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -310,7 +314,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l ...@@ -310,7 +314,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -344,7 +348,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker ...@@ -344,7 +348,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, locker
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir)) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -389,7 +393,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker) ...@@ -389,7 +393,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, locker)
) )
exporter = Exporter(locker) exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir), dev=True) exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
...@@ -403,3 +407,42 @@ foo==1.2.3 \\ ...@@ -403,3 +407,42 @@ foo==1.2.3 \\
""" """
assert expected == content assert expected == content
def test_exporter_exports_requirements_txt_to_standard_output(tmp_dir, locker, capsys):
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": []},
},
}
)
exporter = Exporter(locker)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
bar==4.5.6
foo==1.2.3
"""
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