Skip to content

Commit

Permalink
Merge pull request python-poetry#6 from python-poetry/groups-support
Browse files Browse the repository at this point in the history
Add support for dependency groups
  • Loading branch information
sdispater authored Sep 12, 2021
2 parents 84c0663 + 7e72fff commit 97b576e
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 85 deletions.
84 changes: 72 additions & 12 deletions src/poetry_export_plugin/console/commands/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,34 @@ class ExportCommand(Command):
default=Exporter.FORMAT_REQUIREMENTS_TXT,
),
option("output", "o", "The name of the output file.", flag=False),
option(
"without",
None,
"The dependency groups to ignore when exporting.",
flag=False,
multiple=True,
),
option(
"with",
None,
"The optional dependency groups to include when exporting.",
flag=False,
multiple=True,
),
option("default", None, "Only export the default dependencies."),
option(
"only",
None,
"The only dependency groups to include when exporting.",
flag=False,
multiple=True,
),
option(
"dev",
None,
"Include development dependencies. (<warning>Deprecated</warning>)",
),
option("without-hashes", None, "Exclude hashes from the exported file."),
option("dev", None, "Include development dependencies."),
option(
"extras",
"E",
Expand All @@ -36,11 +62,47 @@ def handle(self) -> None:
if fmt not in Exporter.ACCEPTED_FORMATS:
raise ValueError("Invalid export format: {}".format(fmt))

excluded_groups = []
included_groups = []
only_groups = []
if self.option("dev"):
self.line_error(
"<warning>The --dev option is deprecated, "
"use the `--with dev` notation instead.</warning>"
)
self.line_error("")
included_groups.append("dev")

excluded_groups.extend(
[
group.strip()
for groups in self.option("without")
for group in groups.split(",")
]
)
included_groups.extend(
[
group.strip()
for groups in self.option("with")
for group in groups.split(",")
]
)
only_groups.extend(
[
group.strip()
for groups in self.option("only")
for group in groups.split(",")
]
)

if self.option("default"):
only_groups.append("default")

output = self.option("output")

locker = self.poetry.locker
if not locker.is_locked():
self.line("<comment>The lock file does not exist. Locking.</comment>")
self.line_error("<comment>The lock file does not exist. Locking.</comment>")
options = []
if self.io.is_debug():
options.append(("-vvv", None))
Expand All @@ -52,7 +114,7 @@ def handle(self) -> None:
self.call("lock", " ".join(options))

if not locker.is_fresh():
self.line(
self.line_error(
"<warning>"
"Warning: The lock file is not up to date with "
"the latest changes in pyproject.toml. "
Expand All @@ -62,12 +124,10 @@ def handle(self) -> None:
)

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"),
extras=self.option("extras"),
with_credentials=self.option("with-credentials"),
)
exporter.with_groups(included_groups)
exporter.without_groups(excluded_groups)
exporter.only_groups(only_groups)
exporter.with_extras(self.option("extras"))
exporter.with_hashes(not self.option("without-hashes"))
exporter.with_credentials(self.option("with-credentials"))
exporter.export(fmt, self.poetry.file.parent, output or self.io)
125 changes: 89 additions & 36 deletions src/poetry_export_plugin/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from pathlib import Path
from typing import TYPE_CHECKING
from typing import List
from typing import Optional
from typing import Sequence
from typing import Union


Expand All @@ -24,52 +24,103 @@ class Exporter:

def __init__(self, poetry: "Poetry") -> None:
self._poetry = poetry
self._without_groups: Optional[List[str]] = None
self._with_groups: Optional[List[str]] = None
self._only_groups: Optional[List[str]] = None
self._extras: Optional[List[str]] = None
self._with_hashes: bool = True
self._with_credentials: bool = False

def export(
self,
fmt: str,
cwd: Path,
output: Union["IO", str],
with_hashes: bool = True,
dev: bool = False,
extras: Optional[Union[bool, Sequence[str]]] = None,
with_credentials: bool = False,
) -> None:
def without_groups(self, groups: List[str]) -> "Exporter":
self._without_groups = groups

return self

def with_groups(self, groups: List[str]) -> "Exporter":
self._with_groups = groups

return self

def only_groups(self, groups: List[str]) -> "Exporter":
self._only_groups = groups

return self

def with_extras(self, extras: List[str]) -> "Exporter":
self._extras = extras

return self

def with_hashes(self, with_hashes: bool = True) -> "Exporter":
self._with_hashes = with_hashes

return self

def with_credentials(self, with_credentials: bool = True) -> "Exporter":
self._with_credentials = with_credentials

return self

def export(self, fmt: str, cwd: Path, output: Union["IO", str]) -> None:
if fmt not in self.ACCEPTED_FORMATS:
raise ValueError(f"Invalid export format: {fmt}")

getattr(self, "_export_{}".format(fmt.replace(".", "_")))(
cwd,
output,
with_hashes=with_hashes,
dev=dev,
extras=extras,
with_credentials=with_credentials,
)

def _export_requirements_txt(
self,
cwd: Path,
output: Union["IO", str],
with_hashes: bool = True,
dev: bool = False,
extras: Optional[Union[bool, Sequence[str]]] = None,
with_credentials: bool = False,
) -> None:
getattr(self, "_export_{}".format(fmt.replace(".", "_")))(cwd, output)

def _export_requirements_txt(self, cwd: Path, output: Union["IO", str]) -> None:
from cleo.io.null_io import NullIO
from poetry.core.packages.utils.utils import path_to_url
from poetry.puzzle.solver import Solver
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository

indexes = set()
content = ""
dependency_lines = set()

if self._without_groups or self._with_groups or self._only_groups:
if self._with_groups:
# Default dependencies and opted-in optional dependencies
root = self._poetry.package.with_dependency_groups(self._with_groups)
elif self._without_groups:
# Default dependencies without selected groups
root = self._poetry.package.without_dependency_groups(
self._without_groups
)
else:
# Only selected groups
root = self._poetry.package.with_dependency_groups(
self._only_groups, only=True
)
else:
root = self._poetry.package.with_dependency_groups(["default"], only=True)

locked_repository = self._poetry.locker.locked_repository(True)

pool = Pool(ignore_repository_names=True)
pool.add_repository(locked_repository)

solver = Solver(root, pool, Repository(), locked_repository, NullIO())
# Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies)
solver.provider.load_deferred(False)

ops = solver.solve().calculate_operations()
packages = sorted([op.package for op in ops], key=lambda package: package.name)

for dependency_package in self._poetry.locker.get_project_dependency_packages(
project_requires=self._poetry.package.all_requires, dev=dev, extras=extras
project_requires=root.all_requires,
dev=True,
extras=self._extras,
):
line = ""

dependency = dependency_package.dependency
package = dependency_package.package

if package not in packages:
continue

if package.develop:
line += "-e "

Expand All @@ -82,8 +133,8 @@ def _export_requirements_txt(
if is_direct_remote_reference:
line = requirement
elif is_direct_local_reference:
dependency_uri = path_to_url(dependency.source_url)
line = f"{dependency.name} @ {dependency_uri}"
dependency_uri = path_to_url(package.source_url)
line = f"{package.name} @ {dependency_uri}"
else:
line = f"{package.name}=={package.version}"

Expand All @@ -100,7 +151,7 @@ def _export_requirements_txt(
):
indexes.add(package.source_url)

if package.files and with_hashes:
if package.files and self._with_hashes:
hashes = []
for f in package.files:
h = f["hash"]
Expand Down Expand Up @@ -142,14 +193,16 @@ def _export_requirements_txt(
):
url = (
repository.authenticated_url
if with_credentials
if self._with_credentials
else repository.url
)
indexes_header = f"--index-url {url}\n"
continue

url = (
repository.authenticated_url if with_credentials else repository.url
repository.authenticated_url
if self._with_credentials
else repository.url
)
parsed_url = urllib.parse.urlsplit(url)
if parsed_url.scheme == "http":
Expand All @@ -161,7 +214,7 @@ def _export_requirements_txt(
self._output(content, cwd, output)

def _output(self, content: str, cwd: Path, output: Union["IO", str]) -> None:
decoded = content.decode()
decoded = content
try:
output.write(decoded)
except AttributeError:
Expand Down
3 changes: 3 additions & 0 deletions tests/console/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from cleo.io.null_io import NullIO
from cleo.testers.command_tester import CommandTester
from poetry.installation import Installer
from poetry.utils.env import MockEnv
Expand All @@ -27,6 +28,8 @@ def env(tmp_dir):
@pytest.fixture
def command_tester_factory(app, env):
def _tester(command, poetry=None, installer=None, executor=None, environment=None):
app._load_plugins(NullIO())

command = app.find(command)
tester = CommandTester(command)

Expand Down
4 changes: 2 additions & 2 deletions tests/console/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ def _export_requirements(tester, poetry):
def test_export_exports_requirements_txt_file_locks_if_no_lock_file(tester, poetry):
assert not poetry.locker.lock.exists()
_export_requirements(tester, poetry)
assert "The lock file does not exist. Locking." in tester.io.fetch_output()
assert "The lock file does not exist. Locking." in tester.io.fetch_error()


def test_export_exports_requirements_txt_uses_lock_file(tester, poetry, do_lock):
_export_requirements(tester, poetry)
assert "The lock file does not exist. Locking." not in tester.io.fetch_output()
assert "The lock file does not exist. Locking." not in tester.io.fetch_error()


def test_export_fails_on_invalid_format(tester, do_lock):
Expand Down
Loading

0 comments on commit 97b576e

Please sign in to comment.