Skip to content

Commit

Permalink
Include dependency groups in constraints file (#1139)
Browse files Browse the repository at this point in the history
* test: add Project.locked_packages to conftest

* test: add functional test for dependency groups in constraints

* test: add unit test for Config.dependency_groups

* feat: add Config.dependency_groups

* refactor: extract fixture create_config

* test: add unit test for no groups

* test: add unit test for dev group

* feat: support the legacy dev dependency group

* feat: include all dependency groups in constraints

* test: support python <3.11 in test project

* test: add unit test for Poetry.version

* feat: add Poetry.version

* test: add test for invalid Poetry version

* feat: raise exception if poetry version cannot be parsed

* perf: cache Poetry.version

* test: add test for cached version

* test: add unit test for Poetry.has_dependency_groups

* feat: add Poetry.has_dependency_groups

* feat: support Poetry <1.2

* test: adapt FakeSession.run_always for `poetry --version`

* test: adapt coverage exclusion to changed code path

* refactor: extract variable dependency_groups

* test: skip functional test on Poetry < 1.2

* build: add importlib-metadata to dev dependencies

* fix: avoid walrus for Python 3.7

* chore: avoid type error on conditional import
  • Loading branch information
cjolowicz authored Jul 8, 2023
1 parent ac16126 commit 66ddf00
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 15 deletions.
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def safety(session: Session) -> None:
def mypy(session: Session) -> None:
"""Type-check using mypy."""
args = session.posargs or ["src", "tests", "docs/conf.py"]
session.install(".", "mypy", "pytest")
session.install(".", "mypy", "pytest", "importlib-metadata")
session.run("mypy", *args)
if not session.posargs and session.python == python_versions[0]:
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pyupgrade = ">=2.31.0"
isort = ">=5.10.1"
myst-parser = ">=0.16.1"

[tool.poetry.group.dev.dependencies]
importlib-metadata = {version = "<6.8", python = "<3.8"}

[tool.coverage.paths]
source = ["src", "*/site-packages"]

Expand Down
58 changes: 56 additions & 2 deletions src/nox_poetry/poetry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Poetry interface."""
import re
import sys
from enum import Enum
from pathlib import Path
Expand Down Expand Up @@ -49,6 +50,17 @@ def extras(self) -> List[str]:
)
return list(extras)

@property
def dependency_groups(self) -> List[str]:
"""Return the dependency groups."""
groups = self._config.get("group", {})
if not groups and "dev-dependencies" in self._config:
return ["dev"]
return list(groups)


VERSION_PATTERN = re.compile(r"[0-9]+(\.[0-9+])+[-+.0-9a-zA-Z]+")


class Poetry:
"""Helper class for invoking Poetry inside a Nox session.
Expand All @@ -61,6 +73,42 @@ def __init__(self, session: Session) -> None:
"""Initialize."""
self.session = session
self._config: Optional[Config] = None
self._version: Optional[str] = None

@property
def version(self) -> str:
"""Return the Poetry version."""
if self._version is not None:
return self._version

output = self.session.run_always(
"poetry",
"--version",
"--no-ansi",
external=True,
silent=True,
stderr=None,
)
if output is None:
raise CommandSkippedError(
"The command `poetry --version` was not executed"
" (a possible cause is specifying `--no-install`)"
)

assert isinstance(output, str) # noqa: S101

match = VERSION_PATTERN.search(output)
if match:
self._version = match.group()
return self._version

raise RuntimeError("Cannot parse output of `poetry --version`")

@property
def has_dependency_groups(self) -> bool:
"""Return True if Poetry version supports dependency groups."""
version = tuple(int(part) for part in self.version.split(".")[:2])
return version >= (1, 2)

@property
def config(self) -> Config:
Expand All @@ -78,11 +126,17 @@ def export(self) -> str:
Raises:
CommandSkippedError: The command `poetry export` was not executed.
"""
dependency_groups = (
[f"--with={group}" for group in self.config.dependency_groups]
if self.has_dependency_groups
else ["--dev"]
)

output = self.session.run_always(
"poetry",
"export",
"--format=requirements.txt",
"--dev",
*dependency_groups,
*[f"--extras={extra}" for extra in self.config.extras],
"--without-hashes",
external=True,
Expand All @@ -91,7 +145,7 @@ def export(self) -> str:
)

if output is None:
raise CommandSkippedError(
raise CommandSkippedError( # pragma: no cover
"The command `poetry export` was not executed"
" (a possible cause is specifying `--no-install`)"
)
Expand Down
14 changes: 12 additions & 2 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ def _get_config(self, key: str) -> Any:
data: Any = self._read_toml("pyproject.toml")
return data["tool"]["poetry"][key]

def get_dependency(self, name: str) -> Package:
def get_dependency(self, name: str, data: Any = None) -> Package:
"""Return the package with the given name."""
data = self._read_toml("poetry.lock")
if data is None:
data = self._read_toml("poetry.lock")

for package in data["package"]:
if package["name"] == name:
url = package.get("source", {}).get("url")
Expand Down Expand Up @@ -83,6 +85,14 @@ def development_dependencies(self) -> List[Package]:
dependencies: List[str] = list(self._get_config("dev-dependencies"))
return [self.get_dependency(package) for package in dependencies]

@property
def locked_packages(self) -> List[Package]:
"""Return all packages from the lockfile."""
data = self._read_toml("poetry.lock")
return [
self.get_dependency(package["name"], data) for package in data["package"]
]


@pytest.fixture
def project(shared_datadir: Path) -> Project:
Expand Down
1 change: 1 addition & 0 deletions tests/functional/data/dependency-group/dependency_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Empty module."""
17 changes: 17 additions & 0 deletions tests/functional/data/dependency-group/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/functional/data/dependency-group/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool.poetry]
name = "dependency-group"
version = "0.1.0"
description = ""
authors = ["Your Name <[email protected]>"]

[tool.poetry.dependencies]
python = "^3.7"

[tool.poetry.group.lint.dependencies]
pyflakes = "^2.1.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
29 changes: 29 additions & 0 deletions tests/functional/test_session.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
"""Functional tests for the `@session` decorator."""
import sys
from pathlib import Path

import nox.sessions
import pytest
from packaging.version import Version

import nox_poetry
from tests.functional.conftest import Project
from tests.functional.conftest import list_packages
from tests.functional.conftest import run_nox_with_noxfile


if sys.version_info >= (3, 8):
from importlib.metadata import version
else:
from importlib_metadata import version


def test_local(project: Project) -> None:
"""It installs the local package."""

Expand Down Expand Up @@ -158,3 +166,24 @@ def test(session: nox_poetry.Session) -> None:
process = run_nox_with_noxfile(project, [test], [nox_poetry])

assert "Warning:" in process.stderr


@pytest.mark.skipif(
Version(version("poetry")) < Version("1.2"),
reason="Poetry < 1.2 does not support dependency groups",
)
def test_dependency_group(shared_datadir: Path) -> None:
"""It pins packages in dependency groups."""
project = Project(shared_datadir / "dependency-group")

@nox_poetry.session
def test(session: nox_poetry.Session) -> None:
"""Install the dependencies."""
session.install(".", "pyflakes")

run_nox_with_noxfile(project, [test], [nox_poetry])

expected = [project.package, *project.locked_packages]
packages = list_packages(project, test)

assert set(expected) == set(packages)
3 changes: 3 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def run_always(self, *args: str, **kargs: Any) -> Optional[str]:
if self.no_install:
return None

if args[:2] == ("poetry", "--version"):
return "1.1.15"

path = Path("dist") / "example.whl"
path.touch()
return path.name
Expand Down
119 changes: 110 additions & 9 deletions tests/unit/test_poetry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Unit tests for the poetry module."""
from pathlib import Path
from textwrap import dedent
from typing import Any
from typing import Callable
from typing import Dict

import nox._options
Expand All @@ -12,20 +14,68 @@
from nox_poetry import poetry


def test_config_non_ascii(tmp_path: Path) -> None:
"""It decodes non-ASCII characters in pyproject.toml."""
text = """\
[tool.poetry]
name = "África"
"""
CreateConfig = Callable[[str], poetry.Config]


@pytest.fixture
def create_config(tmp_path: Path) -> CreateConfig:
"""Factory fixture for a Poetry config."""

def _(text: str) -> poetry.Config:
path = tmp_path / "pyproject.toml"
path.write_text(dedent(text), encoding="utf-8")

path = tmp_path / "pyproject.toml"
path.write_text(text, encoding="utf-8")
return poetry.Config(path.parent)

config = poetry.Config(path.parent)
return _


def test_config_non_ascii(create_config: CreateConfig) -> None:
"""It decodes non-ASCII characters in pyproject.toml."""
config = create_config(
"""
[tool.poetry]
name = "África"
"""
)
assert config.name == "África"


def test_config_dependency_groups(create_config: CreateConfig) -> None:
"""It returns the dependency groups from pyproject.toml."""
config = create_config(
"""
[tool.poetry.group.tests.dependencies]
pytest = "^1.0.0"
[tool.poetry.group.docs.dependencies]
sphinx = "^1.0.0"
"""
)
assert config.dependency_groups == ["tests", "docs"]


def test_config_no_dependency_groups(create_config: CreateConfig) -> None:
"""It returns an empty list."""
config = create_config(
"""
[tool.poetry]
"""
)
assert config.dependency_groups == []


def test_config_dev_dependency_group(create_config: CreateConfig) -> None:
"""It returns the dev dependency group."""
config = create_config(
"""
[tool.poetry.dev-dependencies]
pytest = "^1.0.0"
"""
)
assert config.dependency_groups == ["dev"]


@pytest.fixture
def session(monkeypatch: pytest.MonkeyPatch) -> nox.Session:
"""Fixture for a Nox session."""
Expand All @@ -43,6 +93,57 @@ def test(session: nox.Session) -> None:
return nox.Session(runner)


def test_poetry_version(session: nox.Session) -> None:
"""It returns the Poetry version."""
version = poetry.Poetry(session).version
assert all(part.isnumeric() for part in version.split(".")[:3])


def test_poetry_cached_version(session: nox.Session) -> None:
"""It caches the Poetry version."""
p = poetry.Poetry(session)
assert p.version == p.version


def test_poetry_invalid_version(
session: nox.Session, monkeypatch: pytest.MonkeyPatch
) -> None:
"""It raises an exception if the Poetry version cannot be parsed."""

def _run(*args: Any, **kwargs: Any) -> str:
return "bogus"

monkeypatch.setattr("nox.command.run", _run)

with pytest.raises(RuntimeError):
poetry.Poetry(session).version


@pytest.mark.parametrize(
("version", "expected"),
[
("1.0.10", False),
("1.1.15", False),
("1.2.2", True),
("1.6.0.dev0", True),
],
)
def test_poetry_has_dependency_groups(
session: nox.Session,
monkeypatch: pytest.MonkeyPatch,
version: str,
expected: bool,
) -> None:
"""It returns True if Poetry supports dependency groups."""

def _run(*args: Any, **kwargs: Any) -> str:
return version

monkeypatch.setattr("nox.command.run", _run)

assert poetry.Poetry(session).has_dependency_groups is expected


def test_export_with_warnings(
session: nox.Session, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down

0 comments on commit 66ddf00

Please sign in to comment.