Skip to content

Commit

Permalink
Fix crash when users run Nox with the new --no-install option (#382)
Browse files Browse the repository at this point in the history
* build: Add typing_extensions ^3.10.0 to dev dependencies

* ci: Install typing_extensions in tests session

* test: Allow functional tests to pass command-line arguments to Nox

* test: Add functional tests for --no-install

* test: Add fixture sessionfactory for simulating --no-install

* test: Add unit tests for --no-install

* feat: Raise CommandSkippedError if Nox skipped `poetry` command

* feat: Return early if `poetry` command raises CommandSkippedError
  • Loading branch information
cjolowicz authored Jun 11, 2021
1 parent 7691314 commit 8e0262b
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 64 deletions.
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def tests(session: Session, poetry: Optional[str]) -> None:
"pytest",
"pytest-datadir",
"pygments",
"typing_extensions",
)
if session.python == "3.6":
session.install("dataclasses")
Expand Down
56 changes: 5 additions & 51 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Pygments = "^2.9.0"
poetry = "^1.1.6"
pytest-datadir = "^1.3.1"
dataclasses = {version = "^0.8", python = "<3.7"}
typing-extensions = "^3.10.0"

[tool.coverage.paths]
source = ["src", "*/site-packages"]
Expand Down
24 changes: 24 additions & 0 deletions src/nox_poetry/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
from nox.sessions import Session


class CommandSkippedError(Exception):
"""The command was not executed by Nox."""


class DistributionFormat(str, Enum):
"""Type of distribution archive for a Python package."""

Expand Down Expand Up @@ -67,6 +71,9 @@ def export(self) -> str:
Returns:
The generated requirements as text.
Raises:
CommandSkippedError: The command `poetry export` was not executed.
"""
output = self.session.run_always(
"poetry",
Expand All @@ -79,6 +86,13 @@ def export(self) -> str:
silent=True,
stderr=None,
)

if output is None:
raise CommandSkippedError(
"The command `poetry export` was not executed"
" (a possible cause is specifying `--no-install`)"
)

assert isinstance(output, str) # noqa: S101
return output

Expand All @@ -102,6 +116,9 @@ def build(self, *, format: str) -> str:
Returns:
The basename of the wheel built by Poetry.
Raises:
CommandSkippedError: The command `poetry build` was not executed.
"""
if not isinstance(format, DistributionFormat):
format = DistributionFormat(format)
Expand All @@ -114,5 +131,12 @@ def build(self, *, format: str) -> str:
silent=True,
stderr=None,
)

if output is None:
raise CommandSkippedError(
"The command `poetry build` was not executed"
" (a possible cause is specifying `--no-install`)"
)

assert isinstance(output, str) # noqa: S101
return output.split()[-1]
19 changes: 15 additions & 4 deletions src/nox_poetry/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement

from nox_poetry.poetry import CommandSkippedError
from nox_poetry.poetry import DistributionFormat
from nox_poetry.poetry import Poetry

Expand Down Expand Up @@ -125,7 +126,10 @@ def install(self, *args: str, **kwargs: Any) -> None:
args_extras = [_split_extras(arg) for arg in args]

if "." in [arg for arg, _ in args_extras]:
package = self.build_package()
try:
package = self.build_package()
except CommandSkippedError:
return

def rewrite(arg: str, extras: Optional[str]) -> str:
if arg != ".":
Expand All @@ -141,7 +145,11 @@ def rewrite(arg: str, extras: Optional[str]) -> str:

self.session.run_always("pip", "uninstall", "--yes", package, silent=True)

requirements = self.export_requirements()
try:
requirements = self.export_requirements()
except CommandSkippedError:
return

Session_install(self.session, f"--constraint={requirements}", *args, **kwargs)

def installroot(
Expand All @@ -167,8 +175,11 @@ def installroot(
"""
from nox_poetry.core import Session_install

package = self.build_package(distribution_format=distribution_format)
requirements = self.export_requirements()
try:
package = self.build_package(distribution_format=distribution_format)
requirements = self.export_requirements()
except CommandSkippedError:
return

self.session.run_always("pip", "uninstall", "--yes", package, silent=True)

Expand Down
7 changes: 4 additions & 3 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ def project(datadir: Path) -> Project:
return Project(datadir / "example")


def _run_nox(project: Project) -> CompletedProcess:
def _run_nox(project: Project, *nox_args: str) -> CompletedProcess:
env = os.environ.copy()
env.pop("NOXSESSION", None)

try:
return subprocess.run( # noqa: S603, S607
["nox"],
["nox", *nox_args],
check=True,
universal_newlines=True,
stdout=subprocess.PIPE,
Expand Down Expand Up @@ -128,10 +128,11 @@ def run_nox_with_noxfile(
project: Project,
sessions: Iterable[SessionFunction],
imports: Iterable[ModuleType],
*nox_args: str,
) -> None:
"""Write a noxfile and run Nox in the project."""
_write_noxfile(project, sessions, imports)
_run_nox(project)
_run_nox(project, *nox_args)


def list_packages(project: Project, session: SessionFunction) -> List[Package]:
Expand Down
34 changes: 34 additions & 0 deletions tests/functional/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,37 @@ def test(session: nox_poetry.Session) -> None:
session.poetry.export_requirements()

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


def test_install_no_install(project: Project) -> None:
"""It skips installation when --no-install is passed."""

@nox_poetry.session
def test(session: nox_poetry.Session) -> None:
"""Install the local package."""
session.install(".")

run_nox_with_noxfile(project, [test], [nox_poetry])
run_nox_with_noxfile(project, [test], [nox_poetry], "-R")

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

assert set(expected) == set(packages)


def test_installroot_no_install(project: Project) -> None:
"""It skips installation when --no-install is passed."""

@nox_poetry.session
def test(session: nox_poetry.Session) -> None:
"""Install the local package."""
session.poetry.installroot()

run_nox_with_noxfile(project, [test], [nox_poetry])
run_nox_with_noxfile(project, [test], [nox_poetry], "-R")

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

assert set(expected) == set(packages)
42 changes: 36 additions & 6 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""Fixtures."""
import sys
from pathlib import Path
from typing import Any
from typing import cast
from typing import Optional

if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol

import pytest
from _pytest.monkeypatch import MonkeyPatch
Expand All @@ -19,23 +26,46 @@ def __init__(self, path: Path) -> None:
class FakeSession:
"""Fake session."""

def __init__(self, path: Path) -> None:
def __init__(self, path: Path, no_install: bool) -> None:
"""Initialize."""
self._runner = FakeSessionRunner(path)
self.no_install = no_install
self.install_called = False

def run_always(self, *args: str, **kargs: Any) -> str:
def run_always(self, *args: str, **kargs: Any) -> Optional[str]:
"""Run."""
if self.no_install:
return None

path = Path("dist") / "example.whl"
path.touch()
return path.name

def install(self, *args: str, **kargs: Any) -> None:
"""Install."""
self.install_called = True


class FakeSessionFactory(Protocol):
"""Factory for fake sessions."""

def __call__(self, *, no_install: bool) -> Session:
"""Create a fake session."""


@pytest.fixture
def sessionfactory(tmp_path: Path, monkeypatch: MonkeyPatch) -> FakeSessionFactory:
"""Return a factory for a fake Nox session."""

def _sessionfactory(*, no_install: bool) -> Session:
monkeypatch.setattr("nox_poetry.core.Session_install", FakeSession.install)
session = FakeSession(tmp_path, no_install=no_install)
return cast(Session, session)

return _sessionfactory


@pytest.fixture
def session(tmp_path: Path, monkeypatch: MonkeyPatch) -> Session:
def session(sessionfactory: FakeSessionFactory) -> Session:
"""Return a fake Nox session."""
monkeypatch.setattr("nox_poetry.core.Session_install", FakeSession.install)
session = FakeSession(tmp_path)
return cast(Session, session)
return sessionfactory(no_install=False)
42 changes: 42 additions & 0 deletions tests/unit/test_sessions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Unit tests for the sessions module."""
from textwrap import dedent
from typing import Callable
from typing import cast
from typing import Iterator

import nox._options
import nox.manifest
import nox.registry
import pytest
from tests.unit.conftest import FakeSession
from tests.unit.conftest import FakeSessionFactory

import nox_poetry
from nox_poetry.sessions import to_constraints # type: ignore[attr-defined]
Expand Down Expand Up @@ -175,3 +178,42 @@ def test_invalid_constraint() -> None:
"""It raises an exception."""
with pytest.raises(Exception):
to_constraints("example @ /tmp/example")


@pytest.mark.parametrize("no_install", [False, True])
def test_install_local_no_install(
sessionfactory: FakeSessionFactory, no_install: bool
) -> None:
"""It returns early with --no-install if the environment is being reused."""
session = sessionfactory(no_install=no_install)
proxy = nox_poetry.Session(session)

proxy.install(".")

assert cast(FakeSession, session).install_called is not no_install


@pytest.mark.parametrize("no_install", [False, True])
def test_install_dependency_no_install(
sessionfactory: FakeSessionFactory, no_install: bool
) -> None:
"""It returns early with --no-install if the environment is being reused."""
session = sessionfactory(no_install=no_install)
proxy = nox_poetry.Session(session)

proxy.install("first")

assert cast(FakeSession, session).install_called is not no_install


@pytest.mark.parametrize("no_install", [False, True])
def test_installroot_no_install(
sessionfactory: FakeSessionFactory, no_install: bool
) -> None:
"""It returns early with --no-install if the environment is being reused."""
session = sessionfactory(no_install=no_install)
proxy = nox_poetry.Session(session)

proxy.poetry.installroot()

assert cast(FakeSession, session).install_called is not no_install

0 comments on commit 8e0262b

Please sign in to comment.