Skip to content

Enable reading dependency specifiers from PEP 621 pyproject.toml #845

@paduszyk

Description

@paduszyk

How would this feature be useful?

Let us assume I develop a Python package. In my PEP 621 pyproject.toml I have:

[project]
# ...
dependencies = [
   "django >= 3.2, < 5.2",
   "typing-extensions >= 4.12.2, < 5",
]

# ...

[project.optional-dependencies]
# dev = [ ... ]
lint = [
  "ruff >= 0.6.3, < 1",
  "django-stubs[compatible-mypy] >= 5.0.4, < 6",
]
# test = [ ... ]

Then, I would like to have separate sessions for checking and formatting my codebase with ruff and type-checking it with mypy. It would go like this:

@nox.session(tags=["lint"])
@nox.parametrize(
    "command",
    [
        "check",
        "format",
    ],
)
def ruff(session: nox.Session, command: str) -> None:
    session.install("-e", ".[lint]")

    session.run("ruff", command)


@nox.session(tags=["lint"])
def mypy(session: nox.Session) -> None:
    session.install("-e", ".[lint]")
 
    session.run("mypy", ".")

It works, but:

  • actually, ruff session does not need the package as well as any other lint dependencies to be installed — we need ruff ONLY;
  • mypy does not need ruff — nevertheless, it's installed because it's in the same dependency group.

A nice feature would be to tell Nox which dependencies should be installed, with version specifiers automatically read from pyproject.toml.

Describe the solution you'd like

This is a draft for my noxfile.py implementing a potential solution:

from __future__ import annotations

__all__ = [
    "mypy",
    "ruff",
]

import re
from functools import partial
from pathlib import Path
from typing import cast

import nox

PYPROJECT_TOML_PATH = Path(__file__).resolve().parent / "pyproject.toml"

def get_dependency_specifiers(pattern: str, *, group: str | None = None) -> list[str]:
    project = nox.project.load_toml(PYPROJECT_TOML_PATH).get("project")

    if not project:
        msg = (
            f"{PYPROJECT_TOML_PATH} is not a valid PEP 621 metadata file as "
            f"it does not contain a [project] table"
        )

        raise LookupError(msg)

    if group is None:
        dependencies = project.get("dependencies", [])
    else:
        try:
            dependencies = project.get("optional-dependencies", {})[group]
        except KeyError as e:
            msg = f"{group!r} dependencies are not defined in {PYPROJECT_TOML_PATH}"

            raise LookupError(msg) from e

    dependencies = cast(list[str], dependencies)

    if not (dependencies := list(filter(partial(re.match, pattern), dependencies))):
        msg = (
            f"{PYPROJECT_TOML_PATH} does not define any dependencies "
            f"that match {pattern!r}"
        )

        raise LookupError(msg)

    return dependencies


@nox.session(tags=["lint"])
@nox.parametrize(
    "command",
    [
        "check",
        "format",
    ],
)
def ruff(session: nox.Session, command: str) -> None:
    session.install(
        *get_dependency_specifiers(r"^ruff(\[.*\])?", group="lint"),
    )

    session.run("ruff", command)


@nox.session(tags=["lint"])
def mypy(session: nox.Session) -> None:
    session.install("-e", ".")
    session.install(
        *get_dependency_specifiers(r"^django\-stubs\[compatible-mypy\]", group="lint"),
    )

    session.run("mypy", ".")

Now, each environment contains only the relevant dependencies with the versions retrieved directly from the project's metadata.

Describe alternatives you've considered

No response

Anything else?

The function get_dependency_specifiers could be a part of the nox.project module.

Of course, I am open to any changes in the design. If you're interested, I can open a PR assuming you will assist in developing tests.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions