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

Sharing my own noxfile.py - for anyone's usage, hope it's useful to you. #911

Open
jymchng opened this issue Jan 15, 2025 · 0 comments
Open

Comments

@jymchng
Copy link

jymchng commented Jan 15, 2025

How would this feature be useful?

This is the noxfile.py.

"""Nox configuration and automation tasks for Python projects.

This noxfile provides a comprehensive set of development, testing, and maintenance tasks.
All sessions can be run using: `uv run nox -s session_name`

Available Sessions:
-----------------

Testing & Quality:
    test                    Run pytest test suite
                           Usage: uv run nox -s test [-- tests/specific_test.py]
    
    coverage               Run tests with coverage reporting
                          Usage: uv run nox -s coverage
    
    integration_tests      Run integration test suite
                          Usage: uv run nox -s integration_tests

Code Quality:
    format                 Format code using ruff
                          Usage: uv run nox -s format [-- path/to/format]
    
    lint                   Run ruff linter
                          Usage: uv run nox -s lint [-- path/to/lint]
    
    type_check            Run mypy type checking
                          Usage: uv run nox -s type_check [-- path/to/check]
    
    security              Run bandit security checks
                          Usage: uv run nox -s security
    
    pre_commit            Run all pre-commit hooks
                          Usage: uv run nox -s pre_commit

Documentation:
    build_docs            Build documentation using MkDocs
                          Usage: uv run nox -s build_docs

Build & Release:
    build                 Build package distributions
                          Usage: uv run nox -s build [-- --wheel]
    
    clean                 Remove build artifacts
                          Usage: uv run nox -s clean
    
    publish              Check and upload package to PyPI
                         Usage: uv run nox -s publish

Development Tools:
    generate_requirements Generate requirements.txt files
                         Usage: uv run nox -s requirements [-- --format requirements-txt]
    
    benchmark            Run performance benchmarks
                         Usage: uv run nox -s benchmark
    
    dependency_check     Check dependencies for security issues
                        Usage: uv run nox -s dependency_check

Docker:
    docker_build         Build Docker image
                        Usage: uv run nox -s docker_build


Notes:
- Most sessions support additional arguments after `--`
- Sessions with dependency groups automatically install required packages
- Default Python version is determined by .python-version file or falls back to 3.13
- All sessions use UV as the virtual environment backend

Examples:
    # Run specific test file
    uv run nox -s test -- tests/test_specific.py
    
    # Format specific directory
    uv run nox -s format -- src/
    
    # Build wheel only
    uv run nox -s build -- --wheel
    
    # Generate requirements with specific format
    uv run nox -s requirements -- --format pip-compile
"""

from functools import wraps
import nox
from nox import Session
import logging
import sys
from pathlib import Path
from typing import (
    Any,
    Callable,
    Dict,
    Generator,
    Literal,
    Optional,
    Sequence,
    List,
    Tuple,
    TypeVar,
    Union,
)
from dataclasses import dataclass

if sys.version_info >= (3, 11):
    import tomllib as toml_lib
else:
    try:
        import tomli as toml_lib
    except ImportError:
        raise ImportError(
            "For Python versions < 3.11, please install 'tomli' using pip: pip install tomli"
        )

# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


T_co = TypeVar("T_co", covariant=True)
AnyCallable = Callable[..., Any]
SessionDecorator = Union[AnyCallable, Callable[[AnyCallable], AnyCallable]]


@dataclass(frozen=True)
class Constants:
    # Pure constants
    DOT = "."

    # Dependency-related constants
    DEPENDENCIES_GROUP_KEY: str = "dependency-groups"
    DEPENDENCIES_KEY: str = "dependencies"
    PROJECT_KEY: str = "project"
    DEV_KEY: str = "dev"
    TESTS_KEY: str = "tests"
    DOCS_KEY: str = "docs"
    RELEASE_KEY: str = "release"

    # Dependency keys paths
    DEV_DEPENDENCIES_KEYS: Tuple[str, ...] = (DEPENDENCIES_GROUP_KEY, DEV_KEY)
    RELEASE_DEPENDENCIES_KEYS: Tuple[str, ...] = (PROJECT_KEY, DEPENDENCIES_KEY)
    DOCS_DEPENDENCIES_KEYS: Tuple[str, ...] = (DEPENDENCIES_GROUP_KEY, DOCS_KEY)
    TESTS_DEPENDENCIES_KEYS: Tuple[str, ...] = (DEPENDENCIES_GROUP_KEY, TESTS_KEY)

    # Python version constants
    DEFAULT_PYTHON_VERSION: str = "3.13"
    MINIMALLY_SUPPORTED_PYTHON_VERSION: str = "3.8"

    @property
    def ALL_SUPPORTED_PYTHON_VERSIONS(self) -> Generator[str, None, None]:
        min_version = int(self.MINIMALLY_SUPPORTED_PYTHON_VERSION.split(".")[1])
        max_version = int(self.DEFAULT_PYTHON_VERSION.split(".")[1])
        return (f"3.{i}" for i in range(min_version, max_version + 1))


@dataclass(frozen=True)
class BinaryCommand:
    RUFF: str = "ruff"
    PYTEST: str = "pytest"
    PYTHON: str = "python"
    MYPY: str = "mypy"
    BANDIT: str = "bandit"
    MKDOCS: str = "mkdocs"
    SPHINX_BUILD: str = "sphinx-build"
    COVERAGE: str = "coverage"
    PRE_COMMIT: str = "pre-commit"
    RM: str = "rm"
    TWINE: str = "twine"
    PIP_COMPILE: str = "pip-compile"
    DOCKER: str = "docker"
    SAFETY: str = "safety"
    BUILD: str = "build"
    UV: str = "uv"


@dataclass(frozen=True)
class Directories:
    TESTS: str = Constants.TESTS_KEY
    DOCS: str = Constants.DOCS_KEY
    SRC: str = "src"
    BUILD: str = BinaryCommand.BUILD
    DIST: str = "dist"
    CWD: str = "."


NOX_OPTIONS_ACROSS_ALL_SESSIONS = {
    "reuse_venv": True,
    "venv_backend": "uv|virtualenv",
}

BASE_NOX_SESSION: Callable[[str], Callable[[Callable], SessionDecorator]] = (
    lambda name: nox.session(
        name=name,
        python=CURRENT_PYTHON_VERSION,
        **NOX_OPTIONS_ACROSS_ALL_SESSIONS,
    )
)

DEFAULT_UNNAMED_NOX_SESSION: Callable[
    [str, Optional[str], Optional[Tuple[str, ...]]],
    Callable[[Callable], SessionDecorator],
] = (
    lambda name,
    group_deps_name=None,
    default_posargs=(): lambda func: BASE_NOX_SESSION(name)(
        (
            alter_session_run(func, default_posargs),
            install_group_deps(group_deps_name)(
                alter_session_run(func, default_posargs)
            ),
        )[int(group_deps_name is not None)]
    )
)
"""Standard Nox Session decorator that creates a named session with configurable behavior.

This decorator wraps around the base Nox session decorator to provide three functionalities:
1. Creates a Nox session with a specified name
2. Optionally installs dependency groups before running the session
3. Allows specification of default positional arguments

Args:
    name (str): The name to use for the Nox session
    group_deps_name (str, optional): The name of the dependency group to install before running the session.
                                   If None, no additional dependencies will be installed.
    default_posargs (Tuple[str, ...], optional): Default positional arguments to be used when no
                                                command-line arguments are provided.

Examples:
    ```python
    # Basic usage with just a session name
    @DEFAULT_UNNAMED_NOX_SESSION('testing')
    def run_tests(session):
        session.run('pytest')

    # Usage with dependency group installation
    @DEFAULT_UNNAMED_NOX_SESSION('testing', group_deps_name='test-deps')
    def run_tests(session):
        session.run('pytest')

    # Usage with default positional arguments
    @DEFAULT_UNNAMED_NOX_SESSION('lint', default_posargs=('src/', 'tests/'))
    def run_lint(session):
        session.run('flake8', *session.posargs)

    # Combined usage
    @DEFAULT_UNNAMED_NOX_SESSION('pytest', group_deps_name='test-deps', default_posargs=('tests/',))
    def run_tests(session):
        session.run('pytest', *session.posargs)
    ```

Notes:
    - The decorated function becomes a Nox session that can be discovered and run by Nox
    - Session names must be unique across all sessions in the noxfile
    - Dependencies are installed via Poetry's group feature when group_deps_name is provided
    - Default positional arguments (default_posargs) are used only when no command-line arguments are given
    - Access runtime arguments within the session using session.posargs
    - Command-line arguments override default_posargs when provided

Returns:
    SessionDecorator: A decorated function that Nox recognizes as a session
"""

DEFAULT_NAMED_NOX_SESSION: Callable[
    [AnyCallable, Optional[Tuple[str, ...]]],
    Union[AnyCallable, Callable[[AnyCallable], AnyCallable]],
] = lambda group_deps_name, default_posargs=(): (
    # if `group_deps_name` is callable, means decorating a function, directly return the decorator `DEFAULT_UNNAMED_NOX_SESSION`
    DEFAULT_UNNAMED_NOX_SESSION(
        group_deps_name.__name__.replace("_", "-"), default_posargs=default_posargs
    )(group_deps_name)
    if callable(group_deps_name)
    else ...,
    # if `group_deps_name` is str, return lambda
    lambda func: DEFAULT_UNNAMED_NOX_SESSION(
        func.__name__.replace("_", "-"), default_posargs=default_posargs
    )(install_group_deps(group_deps_name)(func)),
)[int(isinstance(group_deps_name, str))]  # not callable => 0, callable => 1
"""A flexible Nox session decorator that automatically derives the session name from the function name.

This decorator serves as a higher-level wrapper around DEFAULT_UNNAMED_NOX_SESSION with two usage patterns:
1. As a simple decorator (@DEFAULT_NAMED_NOX_SESSION) that uses the function name as the session name
2. As a decorator factory (@DEFAULT_NAMED_NOX_SESSION("group")) that installs dependencies before running

The session name is automatically generated by replacing underscores with hyphens in the function name.

Args:
    group_deps_name (Union[str, Callable]): Either:
        - A function to decorate (when used as @DEFAULT_NAMED_NOX_SESSION)
        - A string specifying the dependency group name (when used as @DEFAULT_NAMED_NOX_SESSION("group"))
    default_posargs (Tuple[str, ...], optional): Default positional arguments to use when no
                                                command-line arguments are provided.

Examples:
    ```python
    # Simple usage - session name will be "run-tests"
    @DEFAULT_NAMED_NOX_SESSION
    def run_tests(session):
        session.run('pytest')

    # With dependency group - session name will be "integration-tests"
    @DEFAULT_NAMED_NOX_SESSION('test-deps')
    def integration_tests(session):
        session.run('pytest', 'tests/integration')

    # With default arguments - session name will be "run-lint"
    @DEFAULT_NAMED_NOX_SESSION
    def run_lint(session):
        session.run('flake8', *session.posargs)

    # Combined usage - session name will be "run-all-tests"
    @DEFAULT_NAMED_NOX_SESSION('test-deps', default_posargs=('tests/',))
    def run_all_tests(session):
        session.run('pytest', *session.posargs)
    ```

Notes:
    - Session names are automatically generated from the function name by replacing '_' with '-'
    - When used with a group name, dependencies are installed via Poetry's group feature
    - Default positional arguments are used only when no command-line arguments are provided
    - Access runtime arguments within the session using session.posargs
    - Command-line arguments take precedence over default_posargs

Returns:
    Union[AnyCallable, Callable[[AnyCallable], AnyCallable]]: Either:
        - The decorated session function (when used as simple decorator)
        - A decorator that will create the session (when used with arguments)
"""


TESTS_DEPS_NAMED_NOX_SESSION = lambda default_posargs=(): DEFAULT_NAMED_NOX_SESSION(
    "tests", default_posargs
)
"""A decorator that will set up a Nox session with all the group = 'tests' dependencies installed."""
DEV_DEPS_NAMED_NOX_SESSION = lambda default_posargs=(): DEFAULT_NAMED_NOX_SESSION(
    "dev", default_posargs
)
"""A decorator that will set up a Nox session with all the group = 'dev' dependencies installed."""
DOCS_DEPS_NAMED_NOX_SESSION = lambda default_posargs=(): DEFAULT_NAMED_NOX_SESSION(
    "docs", default_posargs
)
"""A decorator that will set up a Nox session with all the group = 'docs' dependencies installed."""
RELEASE_DEPS_NAMED_NOX_SESSION = lambda default_posargs=(): DEFAULT_NAMED_NOX_SESSION(
    "release", default_posargs
)
"""A decorator that will set up a Nox session with all the 'release' dependencies installed."""


def alter_session_run(func: Callable[..., Any], default_posargs: Tuple[str, ...] = ()):
    @wraps(func)
    def wrapper(session: Session, *args, **kwargs):
        if session.posargs or default_posargs:

            class SessionWithNoSlots(Session):
                __dict__ = {**session.__dict__}

                def run(self, *args, **kwargs):
                    args = [*args, *(session.posargs or default_posargs)]
                    return session.run(*args, **kwargs)

            new_session = object.__new__(SessionWithNoSlots)
            for k, v in session.__dict__.items():
                setattr(new_session, k, v)
            return func(new_session, *args, **kwargs)
        return func(session, *args, **kwargs)

    return wrapper


def install_group_deps(group_deps_name: str):
    def decorator(func: Callable[..., Any]):
        @wraps(func)
        def wrapper(session: Session, *args, **kwargs):
            logger.debug(f"Installing dependencies for group: {group_deps_name}")
            nox_session_install_deps_in_group(
                session, group_deps_name if group_deps_name else None
            )
            return func(session, *args, **kwargs)

        return wrapper

    return decorator


def find_any_file(file_name_with_extension: str) -> Callable[[], Optional[Path]]:
    try:
        from typing import cast
        from logging import Logger

        logger = cast(Logger, logger)
    except NameError:
        logger = type("nothing", (), {"debug": lambda *_args, **_kwargs: None})()

    try:
        file_name, extension = file_name_with_extension.split(".")
    except ValueError:
        return lambda: None

    def find_the_file() -> Optional[Path]:
        logger.debug(f"Entering `find_{file_name}_{extension}` function")

        curr_dir = Path(__file__).parent
        logger.debug(f"Starting search from directory: {curr_dir}")

        while curr_dir != curr_dir.parent:
            logger.debug(f"Checking directory: {curr_dir}")
            file_path = curr_dir / f"{file_name}.{extension}"
            if file_path.is_file():
                logger.debug(f"Found {file_name}.{extension} at: {file_path}")
                return file_path
            curr_dir = curr_dir.parent
            logger.debug(f"Moving up to parent directory: {curr_dir}")

        logger.debug(f"{file_name}.{extension} not found in any parent directory")
        return None

    find_any_file.__name__ = f"find_{file_name}_{extension}"

    return find_the_file


find_pyproject_toml = find_any_file("pyproject.toml")
find_dot_python_version = find_any_file(".python-version")


def get_min_python_version(requires_python: str):
    import re

    def parse_version(version_str: str):
        return tuple(map(int, version_str.split(".")))

    # Remove any whitespace
    requires_python = requires_python.replace(" ", "")

    # Initialize with the lowest possible Python 3 version
    min_version = (3, 0)

    # Split by commas to handle multiple conditions
    conditions = requires_python.split(",")

    for condition in conditions:
        match = re.match(r"(>=|>|<|<=|==|~=|\^)?(\d+(\.\d+)*)", condition)
        if match:
            operator, version = match.groups()[:2]
            version = parse_version(version)

            if operator in (">=", "==", "~=", "^") or operator is None:
                if version > min_version:
                    min_version = version
            elif operator == ">":
                next_version = version[:-1] + (version[-1] + 1,)
                if next_version > min_version:
                    min_version = next_version

    return ".".join(map(str, min_version))


def immediately_invoke(f: Callable[[], T_co]) -> T_co:
    return f()


@immediately_invoke
def CURRENT_PYTHON_VERSION() -> str:
    PYTHON_VERSION = Constants.DEFAULT_PYTHON_VERSION
    try:
        DOT_PYTHON_VERSION_FILE = find_dot_python_version()
        if DOT_PYTHON_VERSION_FILE:
            logger.debug("found `.python-version`")
            with open(str(DOT_PYTHON_VERSION_FILE), mode="r") as f:
                PYTHON_VERSION = f.readline().replace("\n", "")
    except (IOError, FileNotFoundError) as err:
        logger.error(
            f"error encountered when attempting to read `.python-version`; error: {err}"
        )
        return PYTHON_VERSION
    logger.debug(f"`CURRENT_PYTHON_VERSION` is set to '{PYTHON_VERSION}'")
    return PYTHON_VERSION


def get_pyproject_toml_data() -> Optional[Dict[str, str]]:
    logger.debug("Attempting to find `pyproject.toml` file")
    pyproject_toml_file = find_pyproject_toml()
    if pyproject_toml_file is None:
        logger.debug("`pyproject.toml` file not found")
        return None
    logger.debug(f"Reading `pyproject.toml` from {pyproject_toml_file}")
    pyproject_data = None
    try:
        with open(pyproject_toml_file, "rb") as f:
            pyproject_data = toml_lib.load(f)
        logger.debug(f"Successfully loaded `pyproject.toml` data: {pyproject_data}")
    except Exception as e:
        logger.debug(f"Error loading `pyproject.toml`: {str(e)}")
    if not pyproject_data:
        logger.debug("`pyproject.toml` data is empty")
        return None
    return pyproject_data


def get_value_through_keys_path(
    dictionary: Dict[str, str], keys_path: Sequence[str]
) -> Optional[Any]:
    logger.debug(f"Searching through dictionary using keys path: {keys_path}")
    value = None
    if type(dictionary) is not dict or not hasattr(dictionary, "get"):
        logger.debug("Input is not a valid dictionary")
        return None
    if type(keys_path) is str:
        logger.debug("Converting single string key to list")
        keys_path = [keys_path]
    for key in keys_path:
        logger.debug(f"Accessing key: {key}")
        value = dictionary.get(key)
        logger.debug(f"Value found for key {key}: {value}")
        if type(value) is dict:
            logger.debug("Value is a dictionary, continuing search")
            dictionary = value
        else:
            logger.debug("Value is not a dictionary, search complete")
            break
    return value


def get_dependencies(group: Literal["tests", "dev", "docs", None] = None) -> List[str]:
    logger.debug(f"Getting dependencies for group: {group}")
    group = group or "prod"
    logger.debug(f"Effective group: {group}")
    pyproject_toml_data = get_pyproject_toml_data()
    if not pyproject_toml_data:
        logger.debug("No pyproject.toml data available")
        return []
    group_keys_path_mapping: dict[
        Optional[Literal["tests", "dev", "docs", "prod"]], List[str]
    ] = {
        "dev": Constants.DEV_DEPENDENCIES_KEYS,
        "docs": Constants.DOCS_DEPENDENCIES_KEYS,
        "tests": Constants.TESTS_DEPENDENCIES_KEYS,
        "prod": Constants.RELEASE_DEPENDENCIES_KEYS,
    }
    keys_path = group_keys_path_mapping[group]
    logger.debug(f"Keys path for group {group}: {keys_path}")
    if not keys_path:
        logger.debug("No keys path found for group")
        return [""]
    list_deps = get_value_through_keys_path(pyproject_toml_data, keys_path)
    logger.debug(f"Dependencies found: {list_deps}")
    if isinstance(list_deps, str):
        logger.debug("Dependencies is a single string, converting to list")
        return [list_deps]
    if not all(isinstance(s, str) for s in list_deps):
        logger.debug("Not all dependencies are strings, returning empty list")
        return [""]
    return list_deps


def nox_session_install_deps(nox: Session, dependencies: List[str]):
    logger.debug(f"Installing dependencies: {dependencies}")
    if type(dependencies) is str:
        logger.debug("Converting single string dependency to tuple")
        dependencies = (dependencies,)
    for dep in dependencies:
        logger.debug(f"Installing dependency: {dep}")
        nox.install(dep)


def nox_session_install_deps_in_group(
    nox: Session, group: Literal["tests", "dev", "docs", None] = None
):
    logger.debug(f"Installing dependencies for group: {group}")
    dependencies = get_dependencies(group)
    logger.debug(f"Dependencies for group {group}: {dependencies}")
    return nox_session_install_deps(nox, dependencies)


# ========== NOX SESSIONS ==========


@DEV_DEPS_NAMED_NOX_SESSION(default_posargs=(Constants.DOT,))
def format(nox: Session) -> None:
    """Format Python code using Ruff formatter. Usage: `uv run nox -s format`.

    Automatically formats all Python files in the project using Ruff's opinionated formatter.
    By default, formats the entire project directory.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.RUFF, "format")


@DEV_DEPS_NAMED_NOX_SESSION(default_posargs=(Constants.DOT,))
def lint(nox: Session) -> None:
    """Run Ruff linter to check code quality. Usage: `uv run nox -s lint`.

    Performs static code analysis to identify potential issues, style violations,
    and other code quality concerns.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.RUFF, "check")


@DEV_DEPS_NAMED_NOX_SESSION(default_posargs=(Constants.DOT,))
def type_check(nox: Session) -> None:
    """Run MyPy static type checker. Usage: `uv run nox -s type-check`.

    Performs static type checking on Python code to catch type-related errors
    before runtime.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.MYPY)


@DEV_DEPS_NAMED_NOX_SESSION
def security(nox: Session) -> None:
    """Run Bandit security checker. Usage: `uv run nox -s security`.

    Scans Python code for common security issues and vulnerabilities.
    Focuses on the src directory by default.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.BANDIT, "-r", Directories.SRC)


@DOCS_DEPS_NAMED_NOX_SESSION
def build_docs(nox: Session) -> None:
    """Build project documentation using MkDocs. Usage: `uv run nox -s build-docs`.

    Generates static documentation site from markdown files.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.MKDOCS, "build")


@TESTS_DEPS_NAMED_NOX_SESSION
def coverage(nox: Session) -> None:
    """Run tests with coverage reporting. Usage: `uv run nox -s coverage`.

    Executes test suite and generates a coverage report showing which parts
    of the code were executed during testing.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.COVERAGE, "run", "-m", "pytest")
    nox.run(BinaryCommand.COVERAGE, "report", "-m")


@DEV_DEPS_NAMED_NOX_SESSION
def pre_commit(nox: Session) -> None:
    """Run all pre-commit hooks. Usage: `uv run nox -s pre-commit`.

    Executes all configured pre-commit hooks against all files in the repository.
    Useful for checking code before committing.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.PRE_COMMIT, "run", "--all-files")


@DEV_DEPS_NAMED_NOX_SESSION(default_posargs=("--wheel",))
def build(nox: Session) -> None:
    """Build package distributions using UV. Usage: `uv run nox -s build`.

    Creates distribution packages (wheel by default) for the project.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.UV, "build")


@DEFAULT_NAMED_NOX_SESSION
def clean(nox: Session) -> None:
    """Clean build artifacts. Usage: `uv run nox -s clean`.

    Removes all build artifacts including:
    - build/ directory
    - dist/ directory
    - *.egg-info directories

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.RM, "-rf", "build", "dist", "*.egg-info")


@DEV_DEPS_NAMED_NOX_SESSION
def publish(nox: Session) -> None:
    """Publish package to PyPI. Usage: `uv run nox -s publish`.

    Checks distribution files for issues and uploads them to PyPI.
    Requires proper PyPI credentials to be configured.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.TWINE, "check", "dist/*")
    nox.run(BinaryCommand.TWINE, "upload", "dist/*")


@DEFAULT_UNNAMED_NOX_SESSION("requirements", "dev", default_posargs=("--format", "requirements-txt"))
def generate_requirements(nox: Session) -> None:
    """Generate requirements files using UV. Usage: `uv run nox -s requirements`.

    Exports dependencies from pyproject.toml to requirements.txt format.
    By default, generates requirements-txt format.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.UV, "export")


@DEFAULT_NAMED_NOX_SESSION("dev")
def benchmark(nox: Session) -> None:
    """Run performance benchmarks. Usage: `uv run nox -s benchmark`.

    Executes benchmark tests and compares results with previous runs.
    Benchmarks should be located in the benchmarks/ directory.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.PYTHON, "-m", "pytest", "benchmarks/", "--benchmark-compare")


@DEFAULT_NAMED_NOX_SESSION
def docker_build(nox: Session) -> None:
    """Build Docker image for the project. Usage: `uv run nox -s docker-build`.

    Creates a Docker image tagged as 'myproject:latest' using the project's Dockerfile.

    Args:
        nox (Session): Nox session object
    """
    nox.run(BinaryCommand.DOCKER, "build", "-t", "myproject:latest", ".")


@DEFAULT_NAMED_NOX_SESSION
def integration_tests(nox: Session) -> None:
    """Run integration tests. Usage: `uv run nox -s integration_tests`.

    Executes integration tests located in the tests/integration directory.
    Installs test dependencies before running.

    Args:
        nox (Session): Nox session object
    """
    nox_session_install_deps_in_group(nox, "tests")
    nox.run(
        BinaryCommand.PYTHON,
        "-m",
        BinaryCommand.PYTEST,
        f"{Directories.TESTS}/integration/",
        "-s",
        "-vv",
    )


@DEFAULT_NAMED_NOX_SESSION("dev", default_posargs=("check", "--full-report", ))
def dependency_check(nox: Session) -> None:
    """Check dependencies for known security vulnerabilities. Usage: `uv run nox -s dependency-check`.

    Uses Safety to scan project dependencies against known security advisories.
    Generates a full report of findings.

    Args:
        nox (Session): Nox session object

    Example:
        
    """
    nox.run(BinaryCommand.SAFETY,)

Describe the solution you'd like

Basically, some of the benefits with this noxfile.py is that:

  1. it runs from Python 3.8.
  2. you can always pass in additional position arguments to Nox, e.g. nox -s test -- tests/test_stmt.py -s -vv, and presumably pytest will test a single file instead of the default testing of tests/ directory. (Session.run is modified by the decorator)
  3. The decorators will install the group dependencies using pyproject.toml and nox.Session.

Describe alternatives you've considered

Not that I know of.

Anything else?

No response

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants