Skip to content

Commit ded9c9a

Browse files
MNT: move enable_logging from utils.py into utilities.py
Removes the third helper module flagged in review. enable_logging is a user-facing convenience, so it belongs in the public utilities module alongside the other analysis helpers rather than in a standalone utils.py. - rocketpy.utils.enable_logging -> rocketpy.utilities.enable_logging - delete rocketpy/utils.py; expose rocketpy.utilities via __init__ (imported last, since utilities pulls in Environment/Rocket/encoders) - move the logging unit tests into test_utilities.py; delete test_utils.py - update the logging user guide and reference pages Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6f248ba commit ded9c9a

8 files changed

Lines changed: 141 additions & 140 deletions

File tree

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
Logging functions
22
-----------------
33

4-
.. automodule:: rocketpy.utils
5-
:members:
4+
.. autofunction:: rocketpy.utilities.enable_logging

docs/reference/classes/utils/utilities.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ At RocketPy projects, utilities are functions that:
1111
Below you have a list of all utilities functions available in rocketpy.
1212

1313
.. automodule:: rocketpy.utilities
14-
:members:
14+
:members:
15+
:exclude-members: enable_logging

docs/user/logging.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ Enabling logging
1818
-----------------
1919

2020
The easiest way to see RocketPy's internal log messages is the
21-
:func:`rocketpy.utils.enable_logging` helper, which attaches a console
21+
:func:`rocketpy.utilities.enable_logging` helper, which attaches a console
2222
handler to RocketPy's logger hierarchy:
2323

2424
.. jupyter-execute::
2525

2626
import rocketpy
2727

28-
rocketpy.utils.enable_logging(level="INFO")
28+
rocketpy.utilities.enable_logging(level="INFO")
2929

3030
Once enabled, operations such as saving a file or completing a simulation
3131
will emit messages to the console, for example:
@@ -51,15 +51,15 @@ shown:
5151
.. jupyter-execute::
5252

5353
# Show every internal runtime message, including solver ticks
54-
rocketpy.utils.enable_logging(level="DEBUG")
54+
rocketpy.utilities.enable_logging(level="DEBUG")
5555

5656
Filtering by module
5757
--------------------
5858

5959
Because each RocketPy module exposes its own logger (e.g.
6060
``rocketpy.simulation.flight``, ``rocketpy.environment.environment``), you
6161
can rely on the standard ``logging`` module to filter or redirect messages
62-
from specific modules, without using :func:`rocketpy.utils.enable_logging`
62+
from specific modules, without using :func:`rocketpy.utilities.enable_logging`
6363
at all:
6464

6565
.. code-block:: python

rocketpy/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from . import utils
21
from .control import _Controller
32
from .environment import Environment, EnvironmentAnalysis
43
from .exceptions import (
@@ -70,3 +69,8 @@
7069
StochasticTail,
7170
StochasticTrapezoidalFins,
7271
)
72+
73+
# Imported last: utilities pulls in Environment/Rocket/encoders, which are only
74+
# fully available once the imports above have run. Exposes
75+
# ``rocketpy.utilities`` (including ``enable_logging``) on ``import rocketpy``.
76+
from . import utilities

rocketpy/utilities.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import inspect
22
import json
3+
import logging
34
import os
45
import warnings
56
from datetime import date
@@ -22,6 +23,57 @@
2223
from .simulation.flight import Flight
2324

2425

26+
def enable_logging(level="WARNING"):
27+
"""Enable RocketPy logging output to the console.
28+
29+
Attaches a StreamHandler to the ``rocketpy`` logger so that internal
30+
runtime events (simulation progress, warnings, errors) are printed to
31+
the terminal. Only RocketPy logs are affected — global/root logging
32+
is not modified. By default, only WARNING and above are shown.
33+
34+
Parameters
35+
----------
36+
level : str, optional
37+
The minimum logging level to display. Options are "DEBUG", "INFO",
38+
"WARNING", "ERROR", and "CRITICAL". Default is "WARNING".
39+
40+
Examples
41+
--------
42+
Show only warnings and errors (default):
43+
44+
>>> import rocketpy
45+
>>> rocketpy.utilities.enable_logging()
46+
47+
Show all internal runtime messages, including simulation progress:
48+
49+
>>> import rocketpy
50+
>>> rocketpy.utilities.enable_logging(level="DEBUG")
51+
52+
Show confirmations like "Simulation completed" and "File saved":
53+
54+
>>> import rocketpy
55+
>>> rocketpy.utilities.enable_logging(level="INFO")
56+
"""
57+
numeric_level = getattr(logging, level.upper(), None)
58+
if not isinstance(numeric_level, int):
59+
raise ValueError(f"Invalid logging level: '{level}'")
60+
61+
logger = logging.getLogger("rocketpy")
62+
63+
# Remove any existing StreamHandlers to avoid duplicate messages
64+
logger.handlers = [
65+
h for h in logger.handlers if not isinstance(h, logging.StreamHandler)
66+
]
67+
68+
logger.setLevel(numeric_level)
69+
70+
handler = logging.StreamHandler()
71+
handler.setLevel(numeric_level)
72+
handler.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s"))
73+
74+
logger.addHandler(handler)
75+
76+
2577
def compute_cd_s_from_drop_test(
2678
terminal_velocity, rocket_mass, air_density=1.225, g=9.80665
2779
):

rocketpy/utils.py

Lines changed: 0 additions & 52 deletions
This file was deleted.

tests/unit/test_utilities.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23
from unittest.mock import patch
34

@@ -345,3 +346,79 @@ def test_load_from_rpy(mock_show): # pylint: disable=unused-argument
345346
)
346347
assert loaded_flight.info() is None
347348
assert loaded_flight.all_info() is None
349+
350+
351+
# --- Logging (rocketpy.utilities.enable_logging) ------------------------------
352+
353+
354+
@pytest.fixture
355+
def reset_rocketpy_logger():
356+
"""Reset the rocketpy logger to its original state after each test."""
357+
logger = logging.getLogger("rocketpy")
358+
original_level = logger.level
359+
original_handlers = logger.handlers[:]
360+
yield
361+
logger.handlers = original_handlers
362+
logger.setLevel(original_level)
363+
364+
365+
def test_enable_logging_adds_stream_handler(reset_rocketpy_logger):
366+
"""enable_logging() must attach a StreamHandler to the rocketpy logger."""
367+
utilities.enable_logging(level="INFO")
368+
369+
logger = logging.getLogger("rocketpy")
370+
stream_handlers = [
371+
h for h in logger.handlers if isinstance(h, logging.StreamHandler)
372+
]
373+
assert len(stream_handlers) >= 1
374+
375+
376+
def test_enable_logging_sets_correct_level(reset_rocketpy_logger):
377+
"""enable_logging() must set the requested level on the rocketpy logger."""
378+
utilities.enable_logging(level="DEBUG")
379+
assert logging.getLogger("rocketpy").level == logging.DEBUG
380+
381+
utilities.enable_logging(level="WARNING")
382+
assert logging.getLogger("rocketpy").level == logging.WARNING
383+
384+
385+
def test_enable_logging_no_duplicate_handlers(reset_rocketpy_logger):
386+
"""Calling enable_logging() twice must not duplicate StreamHandlers."""
387+
utilities.enable_logging(level="INFO")
388+
utilities.enable_logging(level="INFO")
389+
390+
logger = logging.getLogger("rocketpy")
391+
stream_handlers = [
392+
h for h in logger.handlers if isinstance(h, logging.StreamHandler)
393+
]
394+
assert len(stream_handlers) == 1
395+
396+
397+
def test_enable_logging_replaces_handler_on_level_change(reset_rocketpy_logger):
398+
"""Calling enable_logging() with a new level must replace the old handler."""
399+
utilities.enable_logging(level="WARNING")
400+
utilities.enable_logging(level="DEBUG")
401+
402+
logger = logging.getLogger("rocketpy")
403+
stream_handlers = [
404+
h for h in logger.handlers if isinstance(h, logging.StreamHandler)
405+
]
406+
assert len(stream_handlers) == 1
407+
assert logger.level == logging.DEBUG
408+
409+
410+
def test_enable_logging_invalid_level_raises(reset_rocketpy_logger):
411+
"""enable_logging() must raise ValueError for an unrecognised level string."""
412+
with pytest.raises(ValueError, match="Invalid logging level"):
413+
utilities.enable_logging(level="INVALID")
414+
415+
416+
def test_enable_logging_messages_are_captured(reset_rocketpy_logger, caplog):
417+
"""After enable_logging(), internal rocketpy log messages must be visible."""
418+
utilities.enable_logging(level="DEBUG")
419+
420+
with caplog.at_level(logging.DEBUG, logger="rocketpy"):
421+
logger = logging.getLogger("rocketpy.simulation.flight")
422+
logger.info("test message from flight")
423+
424+
assert "test message from flight" in caplog.text

tests/unit/test_utils.py

Lines changed: 0 additions & 80 deletions
This file was deleted.

0 commit comments

Comments
 (0)