Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include dependency groups in constraints file #1139

Merged
merged 26 commits into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f0b8a5b
test: add Project.locked_packages to conftest
cjolowicz Jul 8, 2023
44ff14a
test: add functional test for dependency groups in constraints
cjolowicz Jul 8, 2023
3dde6bd
test: add unit test for Config.dependency_groups
cjolowicz Jul 8, 2023
5b7b212
feat: add Config.dependency_groups
cjolowicz Jul 8, 2023
8246eac
refactor: extract fixture create_config
cjolowicz Jul 8, 2023
a91a7cc
test: add unit test for no groups
cjolowicz Jul 8, 2023
f4ba5e5
test: add unit test for dev group
cjolowicz Jul 8, 2023
1d4eac2
feat: support the legacy dev dependency group
cjolowicz Jul 8, 2023
2e1658f
feat: include all dependency groups in constraints
cjolowicz Jul 8, 2023
58885e5
test: support python <3.11 in test project
cjolowicz Jul 8, 2023
dcb08f4
test: add unit test for Poetry.version
cjolowicz Jul 8, 2023
a516e79
feat: add Poetry.version
cjolowicz Jul 8, 2023
825a103
test: add test for invalid Poetry version
cjolowicz Jul 8, 2023
544f754
feat: raise exception if poetry version cannot be parsed
cjolowicz Jul 8, 2023
d34da9c
perf: cache Poetry.version
cjolowicz Jul 8, 2023
a6b613b
test: add test for cached version
cjolowicz Jul 8, 2023
c27927c
test: add unit test for Poetry.has_dependency_groups
cjolowicz Jul 8, 2023
bfb9115
feat: add Poetry.has_dependency_groups
cjolowicz Jul 8, 2023
4f5556a
feat: support Poetry <1.2
cjolowicz Jul 8, 2023
866677f
test: adapt FakeSession.run_always for `poetry --version`
cjolowicz Jul 8, 2023
713d9d7
test: adapt coverage exclusion to changed code path
cjolowicz Jul 8, 2023
fe108e2
refactor: extract variable dependency_groups
cjolowicz Jul 8, 2023
b497c81
test: skip functional test on Poetry < 1.2
cjolowicz Jul 8, 2023
76e1bcc
build: add importlib-metadata to dev dependencies
cjolowicz Jul 8, 2023
609e384
fix: avoid walrus for Python 3.7
cjolowicz Jul 8, 2023
077fcb8
chore: avoid type error on conditional import
cjolowicz Jul 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <you@example.com>"]

[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