Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
33a8005
intial pseudocode
jauy123 Apr 23, 2026
1bf0ee1
inital thought code (hadn't run it yet)
jauy123 Apr 23, 2026
24a6c31
more comments
jauy123 Apr 23, 2026
7a5c0b3
general cleanup
jauy123 Apr 23, 2026
f1be0df
Post black
jauy123 Apr 23, 2026
dbcfb1c
removed loop based control
jauy123 Apr 23, 2026
93966fb
Added formatter to handler
jauy123 Apr 23, 2026
c5f6133
Added format to start_logging() to replicate old behavior
jauy123 Apr 23, 2026
e30d0ab
Applied flake8
jauy123 Apr 23, 2026
5a5906d
notes for test later
jauy123 Apr 23, 2026
f679251
fix linter
jauy123 Apr 23, 2026
2ba1c2e
put logger back to start_logging()
jauy123 Apr 24, 2026
cca9258
cleaned up create()
jauy123 Apr 24, 2026
94f056f
removed MDAnalysis NullHandler
jauy123 Apr 24, 2026
7f5812f
readded top level import -- it broke some tests
jauy123 Apr 24, 2026
ba9315e
ported over tests
jauy123 Apr 24, 2026
0e36231
Used black
jauy123 Apr 24, 2026
e63f5ef
Added Tests for start_logging() and stop_logging()
jauy123 Apr 24, 2026
a81e4ad
Added comment
jauy123 Apr 24, 2026
f25aec7
Added comment
jauy123 Apr 24, 2026
bdaf241
post black
jauy123 Apr 24, 2026
ab00172
Merge branch 'logging' of github.com:jauy123/mdanalysis into logging
jauy123 Apr 24, 2026
d0b732f
Added comments about deprecations
jauy123 Apr 24, 2026
ec0ed25
Replaced logging.getLogger(MDAnalysis) with logging.getLogger(__name__)
jauy123 Apr 24, 2026
6d1a675
refined comments
jauy123 Apr 24, 2026
74bcc03
Updated tests
jauy123 Apr 24, 2026
0fdd02e
Change __name__ to MDAnalysis fix tests
jauy123 Apr 24, 2026
d952902
Updated Tests
jauy123 Apr 24, 2026
c34eca5
Added future tests
jauy123 Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package/MDAnalysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
from .lib import log
from .lib.log import start_logging, stop_logging

logging.getLogger("MDAnalysis").addHandler(log.NullHandler())
logging.getLogger("MDAnalysis").addHandler(logging.NullHandler())
del logging

# only MDAnalysis DeprecationWarnings are loud by default
Expand Down
105 changes: 64 additions & 41 deletions package/MDAnalysis/lib/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,41 @@
"""
import sys
import logging
import re
import os

from tqdm.auto import tqdm

# from MDAnalysis.lib.util import deprecate

from .. import version

# Things to deprecated:
# logfile -> stream (bc it could be any stream like object)
#
# version? Why would any user want to modify version in the first place?
# Just have the message logged it directly


def start_logging(logfile="MDAnalysis.log", version=version.__version__):
def start_logging(stream="MDAnalysis.log", version=version.__version__):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Under semantic versioning, you need to maintain backwards compatibility. So something like

def start_logging(stream="MDAnalysis.log", logfile=None, version=version.__version__):
  if logfile is not None:
     warnings.warn(DeprecationWarning, "logfile kwarg will be removed in MDAnalysis 3.0.0, use stream instead"
     # we probably have a dedicated function for the deprecation warning...
     stream = logfile
  ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See my comment above in line 101. Why would a user want to manually change the version in this function? The version keyword should be deprecated and removed.

"""Start logging of messages to file and console.

The default logfile is named `MDAnalysis.log` and messages are
logged with the tag *MDAnalysis*.
"""
create("MDAnalysis", logfile=logfile)
logging.getLogger("MDAnalysis").info(
Comment thread
jauy123 marked this conversation as resolved.
"MDAnalysis %s STARTED logging to %r", version, logfile
create(
"MDAnalysis",
stream=stream,
level="DEBUG",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
create(
"MDAnalysis",
stream=sys.stdout,
level="INFO",
fmt="%(name)-12s: %(levelname)-8s %(message)s",
)
Comment on lines +110 to +121
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All of this is intend to replicate old behavior from the original.

logging.getLogger(__name__).info(
f"MDAnalysis {version} STARTED logging to {stream!r}"
)


Expand All @@ -112,7 +131,30 @@ def stop_logging():
clear_handlers(logger) # this _should_ do the job...


def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"):
# Things to deprecated overall:
# log.NullHandler -> replace with logging.NullHandler() warning
# create() -> add_handler() (potentially confusing bc standard library
# has the same method but in camelcase weirdly?)
# or create_handler()?
#
# For create():
# logfile -> stream (bc it could be any stream like object, not just logfiles)
# logger_name should be deprecated.
#
# Standard library recommends constructing through
# logging.getLogger(__name__) because all loggers share the same namespace
# and this is a systemmatic way of defining loggers
Comment on lines +144 to +146
Copy link
Copy Markdown
Contributor Author

@jauy123 jauy123 Apr 24, 2026

Choose a reason for hiding this comment

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

Note that changing from logging.getLogger("MDAnalysis") to logging.getLogger(__name__) blindly breaks things for some reason (See commit 74bcc03 and commit 0fdd02e). It's something with the fancy printer.

[3] --- Logging error ---
Traceback (most recent call last):
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/io.py", line 326, in format_bibtex
    bib_source = cpBibTeX(fname)
                 ^^^^^^^^^^^^^^^
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibtex.py", line 69, in __init__
    bibtex_database = BibTeXParser(filename, encoding)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibparse.py", line 44, in __init__
    self._parse(self.file)
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibparse.py", line 50, in _parse
    entry = self._parse_entry(file)
            ^^^^^^^^^^^^^^^^^^^^^^^
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibparse.py", line 98, in _parse_entry
    value = self._parse_value(file)
            ^^^^^^^^^^^^^^^^^^^^^^^
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibparse.py", line 138, in _parse_value
    value = self._parse_variable(file, char)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/citeproc/source/bibtex/bibparse.py", line 178, in _parse_variable
    value = self.standard_variables[key.lower()]
            ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
KeyError: 'june'

During handling of the above exception, another exception occurred:
"""

Traceback (most recent call last):
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/logging/__init__.py", line 1163, in emit
    stream.write(msg + self.terminator)
ValueError: I/O operation on closed file.
Call stack:
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/utils.py", line 209, in wrapped_func
    return f(*args, **kwargs)
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/dueswitch.py", line 102, in dump
    due_summary.dump()
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/collector.py", line 499, in dump
    output.dump()
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/io.py", line 247, in dump
    self.fd.write(get_text_rendering(cit.entry, style=self.style))
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/io.py", line 254, in get_text_rendering
    return format_bibtex(get_bibtex_rendering(entry), style=style)
  File "/nfs/homes3/jauy1/opt/miniforge3/envs/mdanalysis-dev/lib/python3.12/site-packages/duecredit/io.py", line 343, in format_bibtex
    lgr.error(msg)
Message: "Failed to process BibTeX file /tmp/tmpj15zx5r6.bib: 'june'."
Arguments: ()

#
# __name__ is MDAnalysis
# See 2nd paragraph: https://docs.python.org/3/library/logging.html#logger-objects
#
def create(
logger_name="MDAnalysis",
stream="MDAnalysis.log",
level="DEBUG",
fmt=None,
mode="a",
):
"""Create a top level logger.

- The file logger logs everything (including DEBUG).
Expand All @@ -130,25 +172,23 @@ def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"):
http://docs.python.org/library/logging.html?#logging-to-multiple-destinations
"""

# replaced with logging.getLogger(__name__)
logger = logging.getLogger(logger_name)

logger.setLevel(logging.DEBUG)

# handler that writes to logfile
logfile_handler = logging.FileHandler(logfile)
logfile_formatter = logging.Formatter(
"%(asctime)s %(name)-12s %(levelname)-8s %(message)s"
)
logfile_handler.setFormatter(logfile_formatter)
logger.addHandler(logfile_handler)

# define a Handler which writes INFO messages or higher to the sys.stderr
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# set a format which is simpler for console use
formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s")
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# This checks for file-like object per duck typing
# https://docs.python.org/3/library/logging.handlers.html#streamhandler
if hasattr(stream, "write") and hasattr(stream, "flush"):
handler = logging.StreamHandler(stream)
elif isinstance(stream, (str, os.PathLike)):
handler = logging.FileHandler(stream, mode=mode)
else:
raise TypeError(
"Input Stream is neither a string, PathLike object or a stream"
)

handler.setLevel(level.upper())
handler.setFormatter(logging.Formatter(fmt))
logger.addHandler(handler)

return logger

Expand All @@ -159,27 +199,10 @@ def clear_handlers(logger):
(only important for reload/debug cycles...)

"""
for h in logger.handlers:
for h in list(logger.handlers):
logger.removeHandler(h)
Comment on lines +202 to 203
Copy link
Copy Markdown
Contributor Author

@jauy123 jauy123 Apr 24, 2026

Choose a reason for hiding this comment

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

There's a really subtle bug with the original version. logger.removeHandler(h) mutates the original logger.handlers so it "skips" handlers in the list. Putting in a list creates a copy, so the entire elements get deleted

See test_stop_logging under TestConvenienceFunctions. The original version without list would have failed that test.



class NullHandler(logging.Handler):
"""Silent Handler.

Useful as a default::

h = NullHandler()
logging.getLogger("MDAnalysis").addHandler(h)
del h

see the advice on logging and libraries in
http://docs.python.org/library/logging.html?#configuring-logging-for-a-library
"""

def emit(self, record):
pass


Comment on lines -166 to -182
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See #5368

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should deprecated instead of remove huh

class ProgressBar(tqdm):
r"""Display a visual progress bar and time estimate.

Expand Down
78 changes: 77 additions & 1 deletion testsuite/MDAnalysisTests/lib/test_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,88 @@
# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations.
# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787
#
import warnings

import pytest
import sys
import logging
import MDAnalysis as mda

from os.path import basename
from MDAnalysis.lib.log import ProgressBar


class TestConvenienceFunctions:
def test_start_logging(self, tmp_path):
mda.start_logging(tmp_path / "MDAnalysis.log")
logger = logging.getLogger("MDAnalysis")

# Test expected handlers' presence and behavior
assert any(isinstance(h, logging.NullHandler) for h in logger.handlers)
any(
isinstance(h, logging.FileHandler)
and basename(h.stream.name) == "MDAnalysis.log"
and h.level == logging.DEBUG
for h in logger.handlers
)
assert any(
isinstance(h, logging.StreamHandler)
and h.stream is sys.stdout
and h.level == logging.INFO
for h in logger.handlers
)

def test_stop_logging(self, tmp_path):
mda.lib.log.start_logging(tmp_path / "MDAnalysis.log")
logger = logging.getLogger("MDAnalysis")
mda.lib.log.stop_logging()

assert len(logger.handlers) == 0

def test_message_console(tmp_path):
pass

def test_message_file(tmp_path):
pass

# TODO need to make a fixture that can clear all handlers per test
class TestCreateBehaviors:

def test_input_path(self, tmp_path):
mda.lib.log.create(stream=tmp_path / "foo.log")

assert (tmp_path / "foo.log").exists()

def test_input_string(self, tmp_path):
mda.lib.log.create(stream=str(tmp_path / "foo.log"))

assert (tmp_path / "foo.log").exists()

# NOTE Assert state could be cleaned up after clear_handlers() fixture is implemented
def test_input_stream(self):
mda.lib.log.create(stream=sys.stdout)

logger = logging.getLogger("MDAnalysis")
assert any(
isinstance(h, logging.StreamHandler) and h.stream is sys.stdout
for h in logger.handlers
)

def test_exception(tmp_path):
with pytest.raises(
TypeError,
match="Input Stream is neither a string, PathLike object or a stream",
):
mda.lib.log.create(stream=2)

def test_level_parameter():
pass

def test_fmt_parameter():
pass

def test_mode_parameter():
pass

class TestProgressBar(object):

def test_output(self, capsys):
Expand Down
75 changes: 0 additions & 75 deletions testsuite/MDAnalysisTests/utils/test_log.py

This file was deleted.

Loading