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

Add @nox.session drop-in replacement #259

Merged
merged 14 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ max-line-length = 80
max-complexity = 10
docstring-convention = google
per-file-ignores = tests/*:S101
rst-roles = const,class,func,mod
rst-roles = const,class,func,meth,mod
85 changes: 31 additions & 54 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@ nox-poetry

Use Poetry_ inside Nox_ sessions

This package provides a drop-in replacement for ``session.install`` in Nox sessions.
It modifies its behavior in two ways:
This package provides a drop-in replacement for the ``nox.session`` decorator,
and for the ``nox.Session`` object passed to user-defined session functions.
This enables ``session.install`` to install packages at the versions specified in the Poetry lock file.

- Packages are pinned to the versions specified in Poetry's lock file.
- The argument ``"."`` is replaced by a wheel built from the package.
.. code:: python

from nox_poetry import session

@session(python=["3.8", "3.9"])
def tests(session):
session.install("pytest", ".")
session.run("pytest")


Installation
Expand All @@ -68,64 +75,38 @@ use the following command to install this package into the same environment:
Usage
-----

Import ``nox_poetry.patch`` at the top of your ``noxfile.py``.
Import the ``@session`` decorator from ``nox_poetry`` instead of ``nox``.
There is nothing else you need to do.
The ``session.install`` method automatically honors the Poetry lock file when installing dependencies.
This allows you to manage packages used in Nox sessions as development dependencies in Poetry.

``nox-poetry`` intercepts calls to ``session.install``
and uses Poetry to export a `constraints file`_ and build the package behind the scenes.
All packages installed in Nox sessions must be managed as dependencies in Poetry.
This works because session functions are passed instances of ``nox_poetry.Session``,
a proxy for ``nox.Session`` adding Poetry-related functionality.
Behind the scenes, nox-poetry uses Poetry to export a `constraints file`_ and build the package.

For example, the following Nox session runs your test suite:
For more fine-grained control, additional utilities are available under the ``session.poetry`` attribute:

.. code:: python

# noxfile.py
import nox
import nox_poetry.patch
from nox.sessions import Session

@nox.session
def tests(session: Session) -> None:
"""Run the test suite."""
session.install(".")
session.install("pytest")
session.run("pytest")

More precisely, the session builds a wheel from the local package,
installs the wheel as well as the ``pytest`` package, and
invokes ``pytest`` to run the test suite against the installation.

If you prefer a more explicit approach,
invoke ``nox_poetry.install`` and ``nox_poetry.installroot`` instead of ``session.install``.
Use the ``nox_poetry.WHEEL`` or ``nox_poetry.SDIST`` constants to specify the distribution format for the local package.

Here is the example above using the more explicit approach:

.. code:: python

# noxfile.py
import nox
import nox_poetry
from nox.sessions import Session

@nox.session
def tests(session: Session) -> None:
"""Run the test suite."""
nox_poetry.installroot(session, distribution_format=nox_poetry.WHEEL)
#nox_poetry.install(session, ".") # this is equivalent to the statement above
nox_poetry.install(session, "pytest")
session.run("pytest")
- ``session.poetry.installroot(distribution_format=[WHEEL|SDIST])``
- ``session.poetry.build_package(distribution_format=[WHEEL|SDIST])``
- ``session.poetry.export_requirements()``


Why?
----

Consider what would happen in the first version without the line importing ``nox-poetry.patch``:
The example session above performs the following steps:

- Build a wheel from the local package.
- Install the wheel as well as the ``pytest`` package.
- Invoke ``pytest`` to run the test suite against the installation.

Consider what would happen in this session
if we had imported ``@session`` from ``nox`` instead of ``nox_poetry``:

- Package dependencies would only be constrained by the wheel metadata, not by the lock file.
In other words, their versions would not be *pinned*.
- The ``pytest`` dependency would not be constrained at all.
- Poetry would be installed as a build backend every time
(although this could be avoided by passing the option ``--no-build-isolation``).
- Poetry would be installed as a build backend every time.

Unpinned dependencies mean that your checks are not reproducible and deterministic,
which can lead to surprises in Continuous Integration and when collaborating with others.
Expand Down Expand Up @@ -168,10 +149,6 @@ In summary, this approach brings the following advantages:
- Every tool can run in an isolated environment with minimal dependencies.
- No need to install your package with all its dependencies if all you need is some linter.

For more details, take a look at `this article`__.

__ https://cjolowicz.github.io/posts/hypermodern-python-03-linting/#managing-dependencies-in-nox-sessions-with-poetry


Contributing
------------
Expand Down
26 changes: 12 additions & 14 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ API Reference

.. automodule:: nox_poetry

Constants
.........

.. autodata:: WHEEL
.. autodata:: SDIST

Functions
.........

.. autofunction:: install
.. autofunction:: installroot
.. autofunction:: build_package
.. autofunction:: export_requirements
.. autofunction:: nox_poetry.core.patch
.. autofunction:: session

Modules
Classes
.......

**nox_poetry.patch**
.. autoclass:: Session
.. automethod:: nox_poetry.sessions._PoetrySession.install
.. automethod:: nox_poetry.sessions._PoetrySession.installroot
.. automethod:: nox_poetry.sessions._PoetrySession.export_requirements
.. automethod:: nox_poetry.sessions._PoetrySession.build_package

.. automodule:: nox_poetry.patch
Constants
.........

.. autodata:: WHEEL
.. autodata:: SDIST
27 changes: 14 additions & 13 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from textwrap import dedent

import nox
from nox.sessions import Session

import nox_poetry.patch
from nox_poetry import Session
from nox_poetry import session


package = "nox_poetry"
Expand Down Expand Up @@ -73,7 +73,7 @@ def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
hook.write_text("\n".join(lines))


@nox.session(name="pre-commit", python="3.9")
@session(name="pre-commit", python="3.9")
def precommit(session: Session) -> None:
"""Lint using pre-commit."""
args = session.posargs or ["run", "--all-files", "--show-diff-on-failure"]
Expand All @@ -95,17 +95,17 @@ def precommit(session: Session) -> None:
activate_virtualenv_in_precommit_hooks(session)


@nox.session(python="3.9")
@session(python="3.9")
def safety(session: Session) -> None:
"""Scan dependencies for insecure packages."""
requirements = nox_poetry.export_requirements(session)
requirements = session.poetry.export_requirements()
session.install("safety")
# Ignore CVE-2020-28476 affecting all versions of tornado
# https://github.com/tornadoweb/tornado/issues/2981
session.run("safety", "check", f"--file={requirements}", "--bare", "--ignore=39462")


@nox.session(python=python_versions)
@session(python=python_versions)
def mypy(session: Session) -> None:
"""Type-check using mypy."""
args = session.posargs or ["src", "tests", "docs/conf.py"]
Expand All @@ -116,7 +116,7 @@ def mypy(session: Session) -> None:
session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py")


@nox.session(python=python_versions)
@session(python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
session.install(".")
Expand All @@ -138,11 +138,12 @@ def tests(session: Session) -> None:
session.notify("coverage")


@nox.session
@session
def coverage(session: Session) -> None:
"""Produce the coverage report."""
# Do not use session.posargs unless this is the only session.
has_args = session.posargs and len(session._runner.manifest) == 1
nsessions = len(session._runner.manifest) # type: ignore[attr-defined]
has_args = session.posargs and nsessions == 1
args = session.posargs if has_args else ["report"]

session.install("coverage[toml]")
Expand All @@ -153,15 +154,15 @@ def coverage(session: Session) -> None:
session.run("coverage", *args)


@nox.session(python=python_versions)
@session(python=python_versions)
def typeguard(session: Session) -> None:
"""Runtime type checking using Typeguard."""
session.install(".")
session.install("pytest", "typeguard", "pygments")
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)


@nox.session(python=python_versions)
@session(python=python_versions)
def xdoctest(session: Session) -> None:
"""Run examples with xdoctest."""
args = session.posargs or ["all"]
Expand All @@ -170,7 +171,7 @@ def xdoctest(session: Session) -> None:
session.run("python", "-m", "xdoctest", package, *args)


@nox.session(name="docs-build", python="3.8")
@session(name="docs-build", python="3.8")
def docs_build(session: Session) -> None:
"""Build the documentation."""
args = session.posargs or ["docs", "docs/_build"]
Expand All @@ -184,7 +185,7 @@ def docs_build(session: Session) -> None:
session.run("sphinx-build", *args)


@nox.session(python="3.8")
@session(python="3.8")
def docs(session: Session) -> None:
"""Build and serve the documentation with live reloading on file changes."""
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
Expand Down
28 changes: 21 additions & 7 deletions src/nox_poetry/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
"""Using Poetry in Nox sessions.

This package provides a facility to monkey-patch Nox so ``session.install``
installs packages at the versions specified in the Poetry lock file, and
``"."`` is replaced by a wheel built from the local package.
See :mod:`nox_poetry.patch`.
This package provides a drop-in replacement for the :func:`session` decorator,
and for the :class:`Session` object passed to user-defined session functions.
This enables :meth:`session.install
<nox_poetry.sessions._PoetrySession.install>` to install packages at the
versions specified in the Poetry lock file.

Example:
>>> @session(python=["3.8", "3.9"])
... def tests(session: Session) -> None:
... session.install("pytest", ".")
... session.run("pytest")

It also provides helper functions that allow more fine-grained control:

- :func:`install`
- :func:`build_package`
- :func:`export_requirements`
- :meth:`session.poetry.installroot
<nox_poetry.sessions._PoetrySession.installroot>`
- :meth:`session.poetry.build_package
<nox_poetry.sessions._PoetrySession.build_package>`
- :meth:`session.poetry.export_requirements
<nox_poetry.sessions._PoetrySession.export_requirements>`

Two constants are defined to specify the format for distribution archives:

Expand All @@ -21,6 +31,8 @@
from nox_poetry.core import install
from nox_poetry.core import installroot
from nox_poetry.poetry import DistributionFormat
from nox_poetry.sessions import Session
from nox_poetry.sessions import session


#: A wheel archive.
Expand All @@ -34,6 +46,8 @@
"export_requirements",
"install",
"installroot",
"Session",
"session",
"SDIST",
"WHEEL",
]
Loading