Skip to content

Commit

Permalink
Add @nox.session drop-in replacement (#259)
Browse files Browse the repository at this point in the history
* chore: Configure flake8-rst-docstrings to recognize :meth: roles

* feat: Add sessions module with @session and Session

* docs: Remove obsolete advice from docstring for `install`

* feat: Export @session and Session from package

* refactor: Add type stubs for @session and Session

* tests: Add functional tests for @session

* tests: Add unit tests for @session

* docs: Update API reference for @session and Session

* docs: Update README for @session and Session

* refactor(sessions): Remove dependency on core module

* refactor(sessions): Eliminate redundant instantiations of Poetry

* refactor(core): Depend on sessions module

* test: Restore coverage for core.build_package

* chore: Use nox_poetry.session in our own noxfile
  • Loading branch information
cjolowicz committed Feb 5, 2021
1 parent 66df6f8 commit 01d21aa
Show file tree
Hide file tree
Showing 11 changed files with 594 additions and 256 deletions.
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

0 comments on commit 01d21aa

Please sign in to comment.