From 2a8bed7d3bddc20b8d84608468dab9705da34e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Rami=CC=81rez=20Mondrago=CC=81n?= Date: Sun, 18 Sep 2022 13:36:12 -0500 Subject: [PATCH] Initial support for installing Poetry dependency groups --- noxfile.py | 6 +++- src/nox_poetry/poetry.py | 52 +++++++++++++++++++++++++----- src/nox_poetry/sessions.py | 63 ++++++++++++++++++++++++++++++++----- src/nox_poetry/sessions.pyi | 12 +++++-- 4 files changed, 116 insertions(+), 17 deletions(-) diff --git a/noxfile.py b/noxfile.py index 82105439..9882a9fa 100644 --- a/noxfile.py +++ b/noxfile.py @@ -152,7 +152,11 @@ def mypy(session: Session) -> None: @session @nox.parametrize( "python,poetry", - [(python_versions[0], "1.0.10"), *((python, None) for python in python_versions)], + [ + (python_versions[0], "1.2.0"), + (python_versions[0], "1.0.10"), + *((python, None) for python in python_versions), + ], ) def tests(session: Session, poetry: Optional[str]) -> None: """Run the test suite.""" diff --git a/src/nox_poetry/poetry.py b/src/nox_poetry/poetry.py index e56299fe..97ccf391 100644 --- a/src/nox_poetry/poetry.py +++ b/src/nox_poetry/poetry.py @@ -1,15 +1,22 @@ """Poetry interface.""" import sys from enum import Enum +from importlib import metadata from pathlib import Path from typing import Any from typing import Iterable from typing import Iterator from typing import List from typing import Optional +from typing import Tuple import tomlkit from nox.sessions import Session +from packaging.version import Version + + +POETRY_VERSION = Version(metadata.version("poetry")) +POETRY_VERSION_1_2_0 = Version("1.2.0") class CommandSkippedError(Exception): @@ -69,32 +76,62 @@ def config(self) -> Config: self._config = Config(Path.cwd()) return self._config - def export(self) -> str: + def export( + self, + *, + extras: bool = True, + with_hashes: bool = False, + include_groups: Tuple[str] = ("dev",), + exclude_groups: Tuple[str] = (), + ) -> str: """Export the lock file to requirements format. + Args: + extras: Whether to include package extras. + with_hashes: Whether to include hashes in the output. + include_groups: The groups to include. + exclude_groups: The groups to exclude. + Returns: The generated requirements as text. Raises: CommandSkippedError: The command `poetry export` was not executed. """ - output = self.session.run_always( + args = [ "poetry", "export", "--format=requirements.txt", - "--dev", - *[f"--extras={extra}" for extra in self.config.extras], - "--without-hashes", + ] + + if not with_hashes: + args.append("--without-hashes") + + if extras: + args.extend(f"--extras={extra}" for extra in self.config.extras) + + if POETRY_VERSION >= POETRY_VERSION_1_2_0: + if include_groups: + args.append(f"--with={','.join(include_groups)}") + + if exclude_groups: + args.append(f"--without={','.join(exclude_groups)}") + else: + args.append("--dev") + + output = self.session.run_always( + *args, external=True, silent=True, stderr=None, ) if output is None: - raise CommandSkippedError( + errmsg = ( "The command `poetry export` was not executed" " (a possible cause is specifying `--no-install`)" ) + raise CommandSkippedError(errmsg) assert isinstance(output, str) # noqa: S101 @@ -145,10 +182,11 @@ def build(self, *, format: str) -> str: ) if output is None: - raise CommandSkippedError( + errmsg = ( "The command `poetry build` was not executed" " (a possible cause is specifying `--no-install`)" ) + raise CommandSkippedError(errmsg) assert isinstance(output, str) # noqa: S101 return output.split()[-1] diff --git a/src/nox_poetry/sessions.py b/src/nox_poetry/sessions.py index 9d451147..fb90ba52 100644 --- a/src/nox_poetry/sessions.py +++ b/src/nox_poetry/sessions.py @@ -67,7 +67,8 @@ def to_constraint(requirement_string: str, line: int) -> Optional[str]: try: requirement = Requirement(requirement_string) except InvalidRequirement as error: - raise RuntimeError(f"line {line}: {requirement_string!r}: {error}") from error + errmsg = f"line {line}: {requirement_string!r}: {error}" + raise RuntimeError(errmsg) from error if not (requirement.name and requirement.specifier): return None @@ -144,7 +145,7 @@ def rewrite(arg: str, extras: Optional[str]) -> str: self.session.run_always("pip", "uninstall", "--yes", package, silent=True) try: - requirements = self.export_requirements() + requirements = self.export_requirements(filename="constraints.txt") except CommandSkippedError: return @@ -173,7 +174,7 @@ def installroot( """ try: package = self.build_package(distribution_format=distribution_format) - requirements = self.export_requirements() + requirements = self.export_requirements(filename="constraints.txt") except CommandSkippedError: return @@ -196,7 +197,37 @@ def installroot( self.session.install(f"--constraint={requirements}", package) - def export_requirements(self) -> Path: + def install_groups(self, groups: Tuple[str], *args, **kwargs) -> None: + """Install all packages in the given Poetry dependency group into a Nox session + using Poetry. + + Args: + group: The name of the dependency group to install. + args: Command-line arguments for ``pip install``. + kwargs: Keyword-arguments for ``session.install``. These are the same + as those for :meth:`nox.sessions.Session.run`. + """ + + try: + requirements = self.export_requirements( + as_constraints=False, + extras=False, + groups=groups, + ) + except CommandSkippedError: + return + + self.install("-r", str(requirements), *args, **kwargs) + + def export_requirements( + self, + *, + filename: str = "requirements.txt", + as_constraints: bool = True, + extras: bool = True, + with_hashes: bool = False, + groups: Tuple[str] = ("dev",), + ) -> Path: """Export a requirements file from Poetry. This function uses `poetry export `_ @@ -217,15 +248,21 @@ def export_requirements(self) -> Path: tmpdir = Path(self.session._runner.envdir) / "tmp" tmpdir.mkdir(exist_ok=True, parents=True) - path = tmpdir / "requirements.txt" + path = tmpdir / filename hashfile = tmpdir / f"{path.name}.hash" lockdata = Path("poetry.lock").read_bytes() digest = hashlib.blake2b(lockdata).hexdigest() if not hashfile.is_file() or hashfile.read_text() != digest: - constraints = to_constraints(self.poetry.export()) - path.write_text(constraints) + contents = self.poetry.export( + include_groups=groups, + extras=extras, + with_hashes=with_hashes, + ) + if as_constraints: + contents = to_constraints(contents) + path.write_text(contents) hashfile.write_text(digest) return path @@ -290,3 +327,15 @@ def __init__(self, session: nox.Session) -> None: def install(self, *args: str, **kwargs: Any) -> None: """Install packages into a Nox session using Poetry.""" return self.poetry.install(*args, **kwargs) + + def install_groups(self, groups: Tuple[str], *args, **kwargs) -> None: + """Install all packages in the given Poetry dependency group into a Nox session + using Poetry. + + Args: + group: The name of the dependency group to install. + args: Command-line arguments for ``pip install``. + kwargs: Keyword-arguments for ``session.install``. These are the same + as those for :meth:`nox.sessions.Session.run`. + """ + return self.poetry.install_groups(groups, *args, **kwargs) diff --git a/src/nox_poetry/sessions.pyi b/src/nox_poetry/sessions.pyi index 299e95e3..ac3df038 100644 --- a/src/nox_poetry/sessions.pyi +++ b/src/nox_poetry/sessions.pyi @@ -9,6 +9,7 @@ from typing import Mapping from typing import NoReturn from typing import Optional from typing import Sequence +from typing import Tuple from typing import TypeVar from typing import Union from typing import overload @@ -16,7 +17,6 @@ from typing import overload import nox.sessions import nox.virtualenv - Python = Optional[Union[str, Sequence[str], bool]] class _PoetrySession: @@ -24,7 +24,15 @@ class _PoetrySession: def installroot( self, *, distribution_format: str = ..., extras: Iterable[str] = ... ) -> None: ... - def export_requirements(self) -> Path: ... + def export_requirements( + self, + *, + filename: str = ..., + as_constraints: bool = ..., + extras: bool = ..., + with_hashes: bool = ..., + groups: Tuple[str] = ..., + ) -> Path: ... def build_package(self, *, distribution_format: str = ...) -> str: ... class Session(nox.Session):