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
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
......
......@@ -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.
If the lock file does not exist, it will be created automatically.
```bash
poetry export -f requirements.txt
poetry export -f requirements.txt > requirements.txt
```
!!!note
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
The `env` command regroups sub commands to interact with the virtualenvs
......
......@@ -11,6 +11,7 @@ class ExportCommand(Command):
options = [
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("dev", None, "Include development dependencies."),
]
......@@ -21,6 +22,8 @@ class ExportCommand(Command):
if fmt not in Exporter.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt))
output = self.option("output")
locker = self.poetry.locker
if not locker.is_locked():
self.line("<comment>The lock file does not exist. Locking.</comment>")
......@@ -48,6 +51,7 @@ class ExportCommand(Command):
exporter.export(
fmt,
self.poetry.file.parent,
output or self.io,
with_hashes=not self.option("without-hashes"),
dev=self.option("dev"),
)
from typing import Optional
from clikit.api.io import IO
from poetry.packages.locker import Locker
from poetry.utils._compat import Path
from poetry.utils._compat import decode
......@@ -14,19 +18,18 @@ class Exporter(object):
self._lock = lock
def export(
self, fmt, cwd, with_hashes=True, dev=False
): # type: (str, Path, bool, bool) -> None
self, fmt, cwd, output, with_hashes=True, dev=False
): # type: (str, Path, Union[IO, str], bool, bool) -> None
if fmt not in self.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt))
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(
self, cwd, with_hashes=True, dev=False
): # type: (Path, bool, bool) -> None
filepath = cwd / "requirements.txt"
self, cwd, output, with_hashes=True, dev=False
): # type: (Path, Union[IO, str], bool, bool) -> None
content = ""
for package in sorted(
......@@ -59,5 +62,15 @@ class Exporter(object):
line += "\n"
content += line
with filepath.open("w", encoding="utf-8") as f:
f.write(decode(content))
self._output(content, cwd, output)
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):
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"
assert requirements.exists()
......@@ -99,7 +99,7 @@ def test_export_exports_requirements_txt_uses_lock_file(app, repo):
command = app.find("export")
tester = CommandTester(command)
tester.execute("--format requirements.txt")
tester.execute("--format requirements.txt --output requirements.txt")
requirements = app.poetry.file.parent / "requirements.txt"
assert requirements.exists()
......@@ -131,3 +131,24 @@ def test_export_fails_on_invalid_format(app, repo):
with pytest.raises(ValueError):
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
from poetry.packages import Locker as BaseLocker
......@@ -61,7 +63,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(tmp_dir, lo
)
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:
content = f.read()
......@@ -104,7 +106,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
)
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:
content = f.read()
......@@ -149,7 +151,9 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_
)
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:
content = f.read()
......@@ -192,7 +196,7 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
)
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:
content = f.read()
......@@ -235,7 +239,7 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
)
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:
content = f.read()
......@@ -276,7 +280,7 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, 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:
content = f.read()
......@@ -310,7 +314,7 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(tmp_dir, l
)
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:
content = f.read()
......@@ -344,7 +348,7 @@ def test_exporter_can_export_requirements_txt_with_file_packages(tmp_dir, 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:
content = f.read()
......@@ -389,7 +393,7 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, 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:
content = f.read()
......@@ -403,3 +407,42 @@ foo==1.2.3 \\
"""
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