Skip to content

Commit

Permalink
feat: constraints.txt support
Browse files Browse the repository at this point in the history
`constraints.txt` does not support develop (editable/`-e`) dependencies, or extras, but is otherwise symmetrical with `requirements.txt`

Fixes #125

Co-authored-by: Bjorn Neergaard <bjorn@neersighted.com>
  • Loading branch information
bmw and neersighted authored Oct 1, 2022
1 parent db9647e commit 371c334
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 15 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This package is a plugin that allows the export of locked packages to various formats.

**Note**: For now, only the `requirements.txt` format is available.
**Note**: For now, only the `constraints.txt` and `requirements.txt` formats are available.

This plugin provides the same features as the existing `export` command of Poetry which it will eventually replace.

Expand Down Expand Up @@ -36,11 +36,11 @@ The plugin provides an `export` command to export to the desired format.
poetry export -f requirements.txt --output requirements.txt
```

**Note**: Only the `requirements.txt` format is currently supported.
**Note**: Only the `constraints.txt` and `requirements.txt` formats are currently supported.

### Available options

* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `requirements.txt` is supported.
* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `constraints.txt` and `requirements.txt` are supported.
* `--output (-o)`: The name of the output file. If omitted, print to standard output.
* `--without`: The dependency groups to ignore when exporting.
* `--with`: The optional dependency groups to include when exporting.
Expand Down
4 changes: 2 additions & 2 deletions docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ menu:
The export plugin allows the export of locked packages to various formats.

{{% note %}}
Only the `requirements.txt` format is currently supported.
Only the `constraints.txt` and `requirements.txt` formats are currently supported.
{{% /note %}}

## Exporting packages
Expand Down Expand Up @@ -65,7 +65,7 @@ poetry export --only test,docs

### Available options

* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `requirements.txt` is supported.
* `--format (-f)`: The format to export to (default: `requirements.txt`). Currently, only `constraints.txt` and `requirements.txt` are supported.
* `--output (-o)`: The name of the output file. If omitted, print to standard output.
* `--without`: The dependency groups to ignore when exporting.
* `--with`: The optional dependency groups to include when exporting.
Expand Down
3 changes: 2 additions & 1 deletion src/poetry_plugin_export/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class ExportCommand(GroupCommand):
option(
"format",
"f",
"Format to export to. Currently, only requirements.txt is supported.",
"Format to export to. Currently, only constraints.txt and requirements.txt"
" are supported.",
flag=False,
default=Exporter.FORMAT_REQUIREMENTS_TXT,
),
Expand Down
35 changes: 27 additions & 8 deletions src/poetry_plugin_export/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import urllib.parse

from functools import partialmethod
from typing import TYPE_CHECKING
from typing import Iterable

Expand All @@ -23,10 +24,14 @@ class Exporter:
Exporter class to export a lock file to alternative formats.
"""

FORMAT_CONSTRAINTS_TXT = "constraints.txt"
FORMAT_REQUIREMENTS_TXT = "requirements.txt"
ALLOWED_HASH_ALGORITHMS = ("sha256", "sha384", "sha512")

EXPORT_METHODS = {FORMAT_REQUIREMENTS_TXT: "_export_requirements_txt"}
EXPORT_METHODS = {
FORMAT_CONSTRAINTS_TXT: "_export_constraints_txt",
FORMAT_REQUIREMENTS_TXT: "_export_requirements_txt",
}

def __init__(self, poetry: Poetry) -> None:
self._poetry = poetry
Expand Down Expand Up @@ -71,7 +76,9 @@ def export(self, fmt: str, cwd: Path, output: IO | str) -> None:

getattr(self, self.EXPORT_METHODS[fmt])(cwd, output)

def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None:
def _export_generic_txt(
self, cwd: Path, output: IO | str, with_extras: bool, allow_editable: bool
) -> None:
from poetry.core.packages.utils.utils import path_to_url

indexes = set()
Expand All @@ -90,10 +97,18 @@ def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None:
):
line = ""

if not with_extras:
dependency_package = dependency_package.without_features()

dependency = dependency_package.dependency
package = dependency_package.package

if package.develop:
if not allow_editable:
raise RuntimeError(
f"{package.pretty_name} is locked in develop (editable) mode,"
" which is incompatible with the constraints.txt format."
)
line += "-e "

requirement = dependency.to_pep_508(with_extras=False)
Expand Down Expand Up @@ -182,12 +197,16 @@ def _export_requirements_txt(self, cwd: Path, output: IO | str) -> None:

content = indexes_header + "\n" + content

self._output(content, cwd, output)

def _output(self, content: str, cwd: Path, output: IO | str) -> None:
if isinstance(output, IO):
output.write(content)
else:
filepath = cwd / output
with filepath.open("w", encoding="utf-8") as f:
f.write(content)
with (cwd / output).open("w", encoding="utf-8") as txt:
txt.write(content)

_export_constraints_txt = partialmethod(
_export_generic_txt, with_extras=False, allow_editable=False
)

_export_requirements_txt = partialmethod(
_export_generic_txt, with_extras=True, allow_editable=True
)
133 changes: 132 additions & 1 deletion tests/test_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_sorted_
assert content == expected


def test_exporter_requirements_txt_with_standard_packages_and_hashes_disabled(
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_disabled( # noqa: E501
tmp_dir: str, poetry: Poetry
) -> None:
poetry.locker.mock_lock_data( # type: ignore[attr-defined]
Expand Down Expand Up @@ -2479,6 +2479,137 @@ def test_exporter_omits_unwanted_extras(
assert io.fetch_output() == "\n".join(expected) + "\n"


@pytest.mark.parametrize(
["fmt", "expected"],
[
(
"constraints.txt",
[
f"bar==4.5.6 ; {MARKER_PY}",
f"baz==7.8.9 ; {MARKER_PY}",
f"foo==1.2.3 ; {MARKER_PY}",
],
),
(
"requirements.txt",
[
f"bar==4.5.6 ; {MARKER_PY}",
f"bar[baz]==4.5.6 ; {MARKER_PY}",
f"baz==7.8.9 ; {MARKER_PY}",
f"foo==1.2.3 ; {MARKER_PY}",
],
),
],
)
def test_exporter_omits_and_includes_extras_for_txt_formats(
tmp_dir: str, poetry: Poetry, fmt: str, expected: list[str]
) -> None:
poetry.locker.mock_lock_data( # type: ignore[attr-defined]
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {
"bar": {
"extras": ["baz"],
"version": ">=0.1.0",
}
},
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {
"baz": {
"version": ">=0.1.0",
"optional": True,
"markers": "extra == 'baz'",
}
},
"extras": {"baz": ["baz (>=0.1.0)"]},
},
{
"name": "baz",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"files": {"foo": [], "bar": [], "baz": []},
},
}
)
set_package_requires(poetry)

exporter = Exporter(poetry)
exporter.export(fmt, Path(tmp_dir), "exported.txt")

with (Path(tmp_dir) / "exported.txt").open(encoding="utf-8") as f:
content = f.read()

assert content == "\n".join(expected) + "\n"


def test_exporter_raises_exception_for_constraints_txt_with_editable_packages(
tmp_dir: str, poetry: Poetry
) -> None:
poetry.locker.mock_lock_data( # type: ignore[attr-defined]
{
"package": [
{
"name": "foo",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "git",
"url": "https://github.com/foo/foo.git",
"reference": "123456",
},
"develop": True,
},
{
"name": "bar",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"source": {
"type": "directory",
"url": "tests/fixtures/sample_project",
"reference": "",
},
"develop": True,
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"files": {"foo": [], "bar": []},
},
}
)
set_package_requires(poetry)

with pytest.raises(RuntimeError):
exporter = Exporter(poetry)
exporter.export("constraints.txt", Path(tmp_dir), "constraints.txt")

assert not (Path(tmp_dir) / "constraints.txt").exists()


def test_exporter_respects_package_sources(tmp_dir: str, poetry: Poetry) -> None:
poetry.locker.mock_lock_data( # type: ignore[attr-defined]
{
Expand Down

0 comments on commit 371c334

Please sign in to comment.