🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@jbdyn
Copy link
Contributor

@jbdyn jbdyn commented May 15, 2025

Hi there 👋,

I started using nox for matrix testing in elva and got a working noxfile.py in no time.
Very nice ❤️

Since there are many sessions, I want to write session-specific logs.

However, there was no way to capture the output of a failed pytest run to see what went wrong.

So, here is my patch to overcome this.

Basically, the output of a failed process is no longer written to sys.stderr solely but instead by the session logger via logger.error.
Before, any output logging was prevented by a raised CommandFailed exception.

This whole story is somewhat related to issue #409.
The difference is that I didn't need to use asyncio in popen.

That is the current state of my noxfile.py
from itertools import product
from typing import Generator, Iterable
import logging
import sys
from pathlib import Path
from datetime import datetime

import nox

def sanitize_path(path):
    for char in ": =().":
        path = path.replace(char, "-")
    return path

BACKEND = "uv|virtualenv"
EDITABLE = ("-e", ".[dev,logo]")
TIMESTAMP = sanitize_path(datetime.now().isoformat(timespec="minutes"))
LOG_PATH = Path(__file__).parent / "logs" / "nox" / TIMESTAMP
LOG_PATH.mkdir(parents=True, exist_ok=True)

PROJECT = nox.project.load_toml("pyproject.toml")

# from classifiers and not `requires-python` entry;
# see https://nox.thea.codes/en/stable/config.html#nox.project.python_versions
PYTHON = nox.project.python_versions(PROJECT)

# versions to test by compatible release;
# check for every version adding new functionality or breaking the API
WEBSOCKETS = ("13.0.0", "13.1.0", "14.0.0", "14.1.0", "14.2.0", "15.0.0")
TEXTUAL = ("1.0.0", "2.0.0", "2.1.0", "3.0.0", "3.1.0", "3.2.0")


def parameters_excluding_last(*params: Iterable[Iterable[str]]) -> Generator[nox.param, None, None]:
    """Genererate the products of parameters except for the last one."""
    latest = tuple(param[-1] for param in params)

    for prod in product(*params):
        if prod != latest:
            yield nox.param(*prod)



def set_log_file(path):
    SESSION_HANDLER = "nox-session"
    logger = logging.getLogger()

    for handler in logger.handlers:
        if handler.name == SESSION_HANDLER:
            logger.removeHandler(handler)

    handler = logging.FileHandler(path)
    handler.name = SESSION_HANDLER
    logger.addHandler(handler)


@nox.session(
    venv_backend=BACKEND,
)
@nox.parametrize(
    # exclude newest since this environment configuration is covered by the `coverage` session below
    ("python", "websockets", "textual"), parameters_excluding_last(PYTHON, WEBSOCKETS, TEXTUAL)
)
def tests(session, websockets, textual):
    LOG_FILE = LOG_PATH / f"{sanitize_path(session.name)}.log"
    set_log_file(LOG_FILE)

    # idempotent
    session.notify("coverage")

    # install from `pyproject.toml`
    session.install(*EDITABLE)

    # overwrite with specific versions;
    # for compatible release specifier spec,
    # see https://packaging.python.org/en/latest/specifications/version-specifiers/#compatible-release
    session.install(
        f"websockets~={websockets}",
        f"textual~={textual}",
    )

    # TODO: run across all tests
    session.run(
        "pytest",
        "--log-file",
        LOG_FILE,
        "--log-file-mode=a",
        "tests/test_component.py",
        silent=True,
    )


@nox.session(
    venv_backend=BACKEND,
)
def coverage(session):
    LOG_FILE = LOG_PATH / f"{sanitize_path(session.name)}.log"
    set_log_file(LOG_FILE)

    # install from `pyproject.toml`;
    # make sure to install the latest possible versions since `uv` won't update otherwise
    if session.venv_backend == "uv":
        session.install("--exact", "--reinstall", *EDITABLE)
    else:
        session.install(*EDITABLE)

    # TODO: run across all tests
    session.run(
        "coverage",
        "run",
        "-m",
        "pytest",
        "--log-file",
        LOG_FILE,
        "--log-file-mode=a",
        "tests/test_component.py",
        silent=False
    )

    # generate reports
    session.run("coverage", "combine", silent=True)
    session.run("coverage", "report", silent=True)
    session.run("coverage", "html", silent=True)

What do you think?

@jbdyn
Copy link
Contributor Author

jbdyn commented May 15, 2025

Consequently, I would like to name the log file the same as its corresponding environment directory.

Since neither _runner in Session nor _normalize_path are part of the public API, I felt that exposing _runner.envdir in Session as a property would be the least invasive change.

I don't know whether that needs testing, though. 🤔

Copy link
Collaborator

@theacodes theacodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm supportive of the feature added in this PR, but there seems to be a lot of unrelated changes that stringify annotations.

…derr`

Previously, there was no way to capture the output of a failed `pytest` run from within `noxfile.py`
@jbdyn jbdyn force-pushed the log-output-of-failed-process branch from 8ab668f to 3b74bff Compare October 14, 2025 08:49
@jbdyn jbdyn force-pushed the log-output-of-failed-process branch from 3b74bff to c8b2dd3 Compare October 14, 2025 08:57
@jbdyn
Copy link
Contributor Author

jbdyn commented Oct 14, 2025

Hey @theacodes, thanks for your time! I applied the needed code changes by rebasing on main. Commit 0cfa507 - implementing output logging - remained untouched.

@jbdyn jbdyn requested a review from theacodes October 14, 2025 09:06
@theacodes
Copy link
Collaborator

Looks good! I'd love for @henryiii to take a look if he's up for it. :)

@henryiii
Copy link
Collaborator

Much better looking diff now. :) I think it looks good, I'd be even up to getting this into #1010, I think.

@henryiii
Copy link
Collaborator

Though the linter/coverage issue needs fixing.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii henryiii dismissed theacodes’s stale review October 14, 2025 19:29

Changed as requested.

@henryiii henryiii changed the title Log Output of Failed Process fix: log Output of Failed Process Oct 14, 2025
@henryiii henryiii merged commit 1a4b11a into wntrblm:main Oct 14, 2025
21 checks passed
@henryiii
Copy link
Collaborator

Since neither _runner in Session nor _normalize_path are part of the public API

Is session.env_dir the same as Path(session.virtualenv.location)?

@jbdyn
Copy link
Contributor Author

jbdyn commented Oct 15, 2025

Is session.env_dir the same as Path(session.virtualenv.location)?

Yes, it is:

  • Session.virtualenv is equivalent to SessionRunner.venv
  • SessionRunner.venv is created via get_virtualenv passing SessionRunner.envdir as location argument for VirtualEnv, so SessionRunner.envdir is equivalent to SessionRunner.venv.location
  • Session.env_dir is equivalent to SessionRunner.envdir
  • By logic, that means that Session.env_dir is equivalent to Session.virtualenv.location

My definition of Session.env_dir has the advantage of linking directly to SessionRunner.envdir, which seems to be always available.
Session.virtualenv.location is not, as Session.virtualenv aka SessionRunner.venv needs to exist, else a ValueError is raised in Session.virtualenv.

But, I don't know whether this is relevant in practice.

@jbdyn
Copy link
Contributor Author

jbdyn commented Oct 15, 2025

@henryiii Thank you, too, for reviewing and adding the last test as well!

@jbdyn
Copy link
Contributor Author

jbdyn commented Oct 15, 2025

While we are at it: We now have Session.env_dir, SessionRunner.envdir, global_config.envdir and ProcessEnv.location.

@theacodes @henryiii Shall I rename Session.env_dir back to envdir again?
Or drop it entirely?

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants