Skip to content

Do not configure logging when pyprep is imported - #211

Merged
sappelhoff merged 7 commits into
mainfrom
quiet-logging-by-default
Aug 21, 2026
Merged

Do not configure logging when pyprep is imported#211
sappelhoff merged 7 commits into
mainfrom
quiet-logging-by-default

Conversation

@sappelhoff

@sappelhoff sappelhoff commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Reverts pyprep's loud-by-default logging, which shipped in 0.8.0, and aligns the
_logging module with the design used by a few sibling projects.

This is a breaking change. Anyone who got "INFO" output for free by
importing pyprep 0.8.0 will now see only warnings and errors until they ask for
more.

Why

0.8.0 called setup_logging() at import time, attaching a handler on
sys.stdout to the pyprep logger. That is a decision a library cannot make on
the application's behalf: it does not know whether the application wants stdout,
a file, JSON to an aggregator, or a Rich console. MNE does this, but MNE is the
exception people copy rather than the model.

No NullHandler goes in its place, deliberately, and this is not the old
"libraries should add a NullHandler" advice. A NullHandler satisfies the
handler search in logging.Logger.callHandlers, which stops
logging.lastResort from firing and would silently drop warnings and errors
instead of merely hiding "INFO". So pyprep is now quiet, but not silent:
with no configuration at all, warnings and errors still reach stderr.
tests/test_logging.py enforces both properties from a clean interpreter.

What changed

  • Importing pyprep configures nothing. The pyprep logger sits at NOTSET
    with no handlers and propagate = True.
  • New pyprep.set_log_level(level, return_old_level=False), mirroring
    mne.set_log_level. It changes only the level, so an application that already
    routes pyprep's records through its own handlers can turn the package up
    without losing the stream, format and propagation it chose.
  • setup_logging(level="info", *, stream=None, fmt=DEFAULT_FORMAT): level names
    are accepted in any case, stream and the new fmt are keyword-only, the
    handler is always installed fresh on the given stream rather than reusing a
    previously installed one, and the configured logger is returned.
  • The propagate argument is removed. It existed to undo the import-time
    configuration and hand the records to the application's handlers; with nothing
    configured on import, propagation is simply what happens when setup_logging
    is never called.
  • The autouse test fixture that flipped propagate for the session is gone with
    the import-time call it worked around. caplog now works out of the box.

Examples and RANSAC output

Examples execute on every docs build here, so their log output is published. All
three now call pyprep.setup_logging("info") themselves — run_ransac.py
configured nothing at all before — and keep mne.set_log_level("warning").

That made two print-era artifacts in pyprep/ransac.py visible. Channel-wise
RANSAC emitted logger.info(current) once per chunk: a bare integer on its own
timestamped line, 94 of them in run_ransac.py alone, under a "Current chunk:" header that only read as print output. It now reports the chunk size
and the number of chunks once, after the first chunk has proved the size fits in
memory. Two other messages carried embedded newlines that split each of them
across two log lines with an empty prefix.

In run_ransac.py the second RANSAC call alone published 94 bare-integer log
lines; the whole example now publishes 6 log lines in total.

Also in here

  • removeTrend logged an error for a 'local detrend' step size larger than
    the window or smaller than one sample, then detrended with it anyway. It now
    raises.
  • get_bads' docstring said the summary is "printed" and typed verbose as
    bool | None when only its truthiness is used.
  • Folds in Accept level names in any case #210, whose level.upper() fix is a strict subset of the new
    _as_level helper. That PR is superseded and closed; its changelog entry is
    reworked here.

Docs

The Logging section of docs/api.rst is the one place this is explained:
quiet-by-default, the lastResort fallback, the two ways to ask for "INFO",
and the fact that the progress bar drawn during window-wise RANSAC comes from
MNE and writes to its own stream, so neither function silences it — otherwise
that looks like a bug.

README.rst orients the reader in three sentences and links there.
CONTRIBUTING.md carries only what library code has to do to keep that
behavior true — one module logger, no print, no configuring anyone else's
logger, do not add a NullHandler, keep _logging.py in sync with the sibling
copies — which is not user documentation and is not written down anywhere else.

Verification

  • pytest: 61 passed (the new ValueError gets a test of its own)
  • pre-commit run --all-files: clean
  • cd docs && make html: clean, all three examples executed
  • Fresh-interpreter checks: importing pyprep leaves both the pyprep and the
    root logger with no handlers; a warning from pyprep.x reaches stderr with
    no configuration; INFO is hidden until either set_log_level("info") with
    an application root handler or setup_logging("info"), and appears exactly
    once in both cases.
  • Python 3.10 and mne 1.3.0 compatibility holds by inspection: the new code uses
    only long-stable logging API and adds no MNE call.

Open questions

  • No release is cut here and no pyproject pin is touched, even though
    docs/changelog.rst describes 0.9.0 as unreleased. Cutting it is yours to
    decide.
  • The changelog entries cite :gh:211``, assuming this PR takes that number. If
    it does not, they need a one-line correction.

0.8.0 attached a handler on sys.stdout to the pyprep logger at import time, so
everyone got INFO output for free. That is a decision a library cannot make: it
does not know whether the application wants stdout, a file, JSON to an
aggregator, or a Rich console. Hand it back to the caller.

No NullHandler goes in its place, deliberately. A NullHandler satisfies the
handler search in logging.Logger.callHandlers, which stops logging.lastResort
from firing and would drop warnings and errors instead of merely hiding INFO.
pyprep is now quiet, but not silent: without any configuration warnings and
errors still reach stderr, and INFO appears once the caller asks for it.

setup_logging gains that shape too. It always installs a fresh handler on the
stream it was given rather than reusing a previously installed one, takes the
stream and a new format string as keyword-only arguments, accepts level names in
any case like mne.set_log_level, and returns the logger it configured. The
propagate argument is gone: it existed to undo the import-time configuration,
and with nothing configured on import, propagation is simply what happens when
setup_logging is never called.

Changing the level is the job of the new set_log_level, which mirrors
mne.set_log_level and touches nothing but the level, so an application that
routes pyprep's records through its own handlers can turn the package up without
losing the stream, the format and the propagation it chose.

The autouse fixture that flipped propagate for the test session goes away with
the import-time call it was working around; caplog now works out of the box.
The README and the API page describe quiet-by-default, the lastResort fallback
that keeps warnings and errors visible with no configuration at all, and the two
ways to ask for INFO: setup_logging for a script or a notebook, set_log_level
for an application that already routes logging somewhere of its own.

Both note that the progress bar drawn during window-wise RANSAC comes from MNE
and writes to its own stream, so neither function silences it; without that it
looks like a bug.

CONTRIBUTING gains the conventions the library code follows, including why the
NullHandler is absent and that _logging.py is kept in sync with copies in
sibling projects.
The examples now configure pyprep themselves, at INFO, since importing it no
longer does. run_ransac.py configured nothing at all before and printed only its
own timings.

They run on every docs build, so their output is published, and channel-wise
RANSAC emitted logger.info(current) once per chunk: a bare integer on its own
timestamped line, 94 of them in run_ransac.py alone, preceded by a "Current
chunk:" header that only made sense as print output. Report the chunk size and
the number of chunks once, after the first chunk has proved the size fits in
memory. Two messages also carried embedded newlines left over from the same
print era, which split each of them across two lines with an empty prefix.
removeTrend logged an error for a 'local detrend' step size larger than the
window or smaller than one sample, and then detrended with it anyway. Raise
instead, so the caller finds out before reading the output.

Also correct get_bads' docstring: the summary is logged, not printed, and
verbose is a plain bool, only its truthiness being used.
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.03%. Comparing base (e252bf4) to head (1771bbd).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #211      +/-   ##
==========================================
+ Coverage   97.92%   98.03%   +0.10%     
==========================================
  Files           8        8              
  Lines         869      864       -5     
==========================================
- Hits          851      847       -4     
+ Misses         18       17       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The same explanation had been written out three times: in the README, in the
Logging section of the API docs, and in CONTRIBUTING. Keep the API docs as the
one narrative and point the others at it.

The README now orients the reader in three sentences and links to that section.
CONTRIBUTING keeps only what library code has to do to keep the documented
behavior true, which is not user documentation and is not written down anywhere
else.

setup_logging's Notes had also grown into a second copy of the same narrative,
which renders on the very same page as the section it repeated.
@sappelhoff
sappelhoff merged commit 64f1fef into main Aug 21, 2026
10 checks passed
@sappelhoff
sappelhoff deleted the quiet-logging-by-default branch August 21, 2026 09:37
@sappelhoff
sappelhoff restored the quiet-logging-by-default branch August 21, 2026 09:37
@sappelhoff
sappelhoff deleted the quiet-logging-by-default branch August 21, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant