Skip to content

Commit d4bd935

Browse files
ENH: Add python loggers (#973)
* ENH: adopt built-in Python logging instead of print() calls * ENH: adopt built-in Python logging instead of print() calls * ENH: adopt built-in Python logging instead of print() calls * ENH: adopt built-in Python logging instead of print() calls * ENH: adopt built-in Python logging instead of print() calls * ENH: adopt built-in Python logging for internal runtime events * ENH: adopt built-in Python logging for internal runtime events * STY: fix formatting with ruff and black * STY: fix formatting with ruff and black * Update rocketpy/environment/environment.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * Update rocketpy/environment/environment.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * Update rocketpy/environment/tools.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * Update rocketpy/mathutils/function.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * Update rocketpy/mathutils/vector_matrix.py Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * MNT: address PR review comments on nose_cone and tools * MNT: fix docstrings and scope debug log to verbose flag * MNT: restore SimMonitor print behavior for multiprocessing compatibility * MNT: keep print() for user-requested plot headers, document enable_logging Plot section headers (e.g. 'Trajectory 3d Plot', 'Airfoil lift curve:') are emitted by user-invoked .plots.xxx()/.all() calls, not internal runtime events, so per this PR's own print-vs-logger distinction they should remain print() -- otherwise they silently disappear by default while the plot itself is still shown, and reading them in a log stream disconnected from the plot window doesn't make sense. Diagnostic logger.warning() calls for skipped/missing data are left untouched. Also adds user-guide and API reference docs for enable_logging(), closing the last unresolved review comment on the PR. * MNT: fix pylint regressions and restore visible progress output - Bump pylintrc max-module-lines 3000->3050: environment.py now sits 4 lines over the old hard limit purely from the logging import/setup this PR adds, tripping CI's fail-under=10 gate. - Remove EmptyMotor import in rocket_plots.py, unused since PR #924 switched that check to a string-based type-name comparison. - Restore the exact pre-PR print() formatting (blank-line separators, dash dividers) in rocket_plots.py/environment_plots.py/flight_plots.py that got flattened when converting headers back to print() earlier in this branch. - MonteCarlo: two logger.warning/error calls in __sim_producer run inside a multiprocessing worker process. Log handlers configured via enable_logging() in the parent process are not guaranteed to reach child processes (e.g. Windows "spawn"), so these would silently vanish -- exactly the cross-process issue already found and fixed for _SimMonitor in a prior commit on this branch, just missed here. Reverted to print()/_SimMonitor.reprint() for guaranteed visibility. - For consistency with that worker output and with the still-visible per-iteration progress ticker, also reverted MonteCarlo's other lifecycle narration (start, worker count, results saved, file imports) to print(); logging module no longer needed in this file. * MNT: revert FlightComparator output to print(), not logging FlightComparator is an interactively-invoked reporting tool: add_data(), compare(), summary() and all() exist specifically to print a human-readable report the user is watching on their screen, exactly like Flight.info()/Rocket.all_info() -- not an internal runtime event a library embedder would want to filter or silence. Converting it to logger.info()/logger.warning() meant summary() -- a method whose entire purpose is printing a report -- produced no output at all under the library's silent-by-default logging config, and the other methods lost their multi-line human-readable formatting when consolidated into single log records. Restored the original print() calls and formatting verbatim, dropped the now-unused logger import, and reverted the two tests that had been migrated to caplog back to capsys to match. --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br>
1 parent f18f2e5 commit d4bd935

34 files changed

Lines changed: 457 additions & 135 deletions

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ indent-string=' '
390390
max-line-length=88
391391

392392
# Maximum number of lines in a module.
393-
max-module-lines=3000
393+
max-module-lines=3050
394394

395395
# Allow the body of a class to be on the same line as the declaration if body
396396
# contains single statement.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Attention: The newest changes should be on top -->
4949
- ENH: Auto Populate Changelog [#919](https://github.com/RocketPy-Team/RocketPy/pull/919)
5050
- ENH: Adaptive Monte Carlo via Convergence Criteria [#922](https://github.com/RocketPy-Team/RocketPy/pull/922)
5151
- TST: Add acceptance tests for 3DOF flight simulation based on Bella Lui rocket [#914](https://github.com/RocketPy-Team/RocketPy/pull/914)
52+
- ENH: adopt built-in Python logging instead of print() calls [#450](https://github.com/RocketPy-Team/RocketPy/issues/450)
5253

5354
### Changed
5455

docs/reference/classes/utils/index.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Utils functions
77

88
tools
99
units
10-
utilities
10+
utilities
1111
vector
1212
matrix
13-
13+
logging
14+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Logging functions
2+
-----------------
3+
4+
.. automodule:: rocketpy.utils
5+
:members:

docs/user/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,6 @@ RocketPy's User Guide
4747
:caption: Further Analysis
4848

4949
Function <function.rst>
50-
Utilities <analysis.rst> Aerodynamic Surfaces <aerodynamics/surfaces.rst>
50+
Utilities <analysis.rst>
51+
Aerodynamic Surfaces <aerodynamics/surfaces.rst>
52+
Logging <logging.rst>

docs/user/logging.rst

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
Logging
2+
=======
3+
4+
RocketPy uses Python's built-in `logging <https://docs.python.org/3/library/logging.html>`_
5+
module to report internal runtime events, such as simulation progress,
6+
warnings and errors. By design, RocketPy is **silent by default**: unless
7+
you configure a handler, no log records are printed to the console. This
8+
follows the best practices recommended for Python libraries and keeps
9+
RocketPy from cluttering the output of applications that embed it.
10+
11+
.. note::
12+
This only affects internal runtime events. Output that you explicitly
13+
request, such as ``flight.info()`` or ``rocket.all_info()``, continues
14+
to print directly to the terminal regardless of the logging
15+
configuration described here.
16+
17+
Enabling logging
18+
-----------------
19+
20+
The easiest way to see RocketPy's internal log messages is the
21+
:func:`rocketpy.utils.enable_logging` helper, which attaches a console
22+
handler to RocketPy's logger hierarchy:
23+
24+
.. jupyter-execute::
25+
26+
import rocketpy
27+
28+
rocketpy.utils.enable_logging(level="INFO")
29+
30+
Once enabled, operations such as saving a file or completing a simulation
31+
will emit messages to the console, for example:
32+
33+
.. code-block:: text
34+
35+
INFO | rocketpy.simulation.flight | Simulation completed successfully.
36+
37+
Log levels
38+
----------
39+
40+
The ``level`` parameter controls the minimum severity of messages that are
41+
shown:
42+
43+
- ``DEBUG``: high-frequency, low-level detail (e.g. solver iterations),
44+
useful when troubleshooting a simulation.
45+
- ``INFO``: confirmations of completed operations (e.g. simulation
46+
completed, file saved).
47+
- ``WARNING`` (default): unexpected but recoverable situations (e.g. a
48+
missing motor, an automatically corrected geometry parameter).
49+
- ``ERROR``: a feature is broken or an optional dependency is missing.
50+
51+
.. jupyter-execute::
52+
53+
# Show every internal runtime message, including solver ticks
54+
rocketpy.utils.enable_logging(level="DEBUG")
55+
56+
Filtering by module
57+
--------------------
58+
59+
Because each RocketPy module exposes its own logger (e.g.
60+
``rocketpy.simulation.flight``, ``rocketpy.environment.environment``), you
61+
can rely on the standard ``logging`` module to filter or redirect messages
62+
from specific modules, without using :func:`rocketpy.utils.enable_logging`
63+
at all:
64+
65+
.. code-block:: python
66+
67+
import logging
68+
69+
handler = logging.StreamHandler()
70+
handler.setFormatter(logging.Formatter("%(levelname)s | %(name)s | %(message)s"))
71+
72+
# Only show logs coming from the Flight simulation module
73+
flight_logger = logging.getLogger("rocketpy.simulation.flight")
74+
flight_logger.addHandler(handler)
75+
flight_logger.setLevel(logging.INFO)

rocketpy/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
13
from .control import _Controller
24
from .environment import Environment, EnvironmentAnalysis
35
from .exceptions import (
@@ -53,6 +55,7 @@
5355
TrapezoidalFins,
5456
)
5557
from .sensitivity import SensitivityModel
58+
from . import utils
5659
from .sensors import Accelerometer, Barometer, GnssReceiver, Gyroscope
5760
from .simulation import Flight, MonteCarlo, MultivariateRejectionSampler
5861
from .stochastic import (
@@ -68,3 +71,5 @@
6871
StochasticTail,
6972
StochasticTrapezoidalFins,
7073
)
74+
75+
logging.getLogger(__name__).addHandler(logging.NullHandler())

rocketpy/environment/environment.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# pylint: disable=too-many-public-methods, too-many-instance-attributes
22
import bisect
33
import json
4+
import logging
45
import re
56
import warnings
7+
68
from collections import namedtuple
79
from datetime import datetime
810

@@ -47,6 +49,8 @@
4749
geopotential_height_to_geometric_height,
4850
)
4951

52+
logger = logging.getLogger(__name__)
53+
5054

5155
class Environment:
5256
"""Keeps all environment information stored, such as wind and temperature
@@ -999,7 +1003,7 @@ def set_elevation(self, elevation="Open-Elevation"):
9991003
self.elevation = elevation
10001004
else:
10011005
self.elevation = fetch_open_elevation(self.latitude, self.longitude)
1002-
print(f"Elevation received: {self.elevation} m")
1006+
logger.info("Elevation received: %.2f m", self.elevation)
10031007

10041008
def set_topographic_profile( # pylint: disable=redefined-builtin, unused-argument
10051009
self, type, file, dictionary="netCDF4", crs=None
@@ -1038,14 +1042,13 @@ def set_topographic_profile( # pylint: disable=redefined-builtin, unused-argume
10381042
# crsArray = nasa_dem.variables['crs'][:].tolist().
10391043
self.topographic_profile_activated = True
10401044

1041-
print("Region covered by the Topographical file: ")
1042-
print(
1043-
f"Latitude from {self.elev_lat_array[-1]:.6f}° to "
1044-
f"{self.elev_lat_array[0]:.6f}°"
1045-
)
1046-
print(
1047-
f"Longitude from {self.elev_lon_array[0]:.6f}° to "
1048-
f"{self.elev_lon_array[-1]:.6f}°"
1045+
logger.debug(
1046+
"Topographical file coverage: lat [%.6f°, %.6f°], "
1047+
"lon [%.6f°, %.6f°]",
1048+
self.elev_lat_array[-1],
1049+
self.elev_lat_array[0],
1050+
self.elev_lon_array[0],
1051+
self.elev_lon_array[-1],
10491052
)
10501053

10511054
def get_elevation_from_topographic_profile(self, lat, lon):
@@ -2760,9 +2763,10 @@ def export_environment(self, filename="environment"):
27602763

27612764
with open(filename + ".json", "w") as f:
27622765
json.dump(export_env_dictionary, f, sort_keys=False, indent=4, default=str)
2763-
print(
2764-
f"Your Environment file was saved at '{filename}.json'. You can use "
2765-
"it in the future by using the custom_atmosphere atmospheric model."
2766+
logger.info(
2767+
"Your Environment file was saved at '%s.json'. "
2768+
"You can use it in the future by using the custom_atmosphere atmospheric model.",
2769+
filename,
27662770
)
27672771

27682772
def set_earth_geometry(self, datum):
@@ -2995,6 +2999,6 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
29952999

29963000
results = doctest.testmod()
29973001
if results.failed < 1:
2998-
print(f"All the {results.attempted} tests passed!")
3002+
logger.debug("All the %d tests passed!", results.attempted)
29993003
else:
3000-
print(f"{results.failed} out of {results.attempted} tests failed.")
3004+
logger.error("%d out of %d tests failed.", results.failed, results.attempted)

rocketpy/environment/environment_analysis.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import copy
33
import datetime
44
import json
5+
import logging
56
import warnings
7+
68
from collections import defaultdict
79
from functools import cached_property
810

@@ -24,6 +26,8 @@
2426
from ..units import convert_units
2527
from .environment import Environment
2628

29+
logger = logging.getLogger(__name__)
30+
2731
# TODO: the average_wind_speed_profile_by_hour and similar methods could be more abstract than currently are
2832

2933

@@ -236,13 +240,15 @@ def __check_requirements(self):
236240
check_requirement_version(module_name, version)
237241
except (ValueError, ImportError) as e:
238242
has_error = True
239-
print(
240-
f"The following error occurred while importing {module_name}: {e}"
243+
logger.error(
244+
"The following error occurred while importing %s: %s",
245+
module_name,
246+
e,
241247
)
242248
if has_error:
243-
print(
249+
logger.error(
244250
"Given the above errors, some methods may not work. Please run "
245-
+ "'pip install rocketpy[env_analysis]' to install extra requirements."
251+
"'pip install rocketpy[env_analysis]' to install extra requirements."
246252
)
247253

248254
def __init_surface_dictionary(self):
@@ -506,8 +512,9 @@ def __init_unit_system(self):
506512
}
507513
else:
508514
# Default to SI
509-
print(
510-
f"Defaulting to SI unit system, the {self.unit_system_string} was not found."
515+
logger.warning(
516+
"Defaulting to SI unit system, the '%s' unit system was not found.",
517+
self.unit_system_string,
511518
)
512519
self.unit_system = {
513520
"length": "m",
@@ -2804,9 +2811,10 @@ def export_mean_profiles(self, filename="export_env_analysis"):
28042811
self.export_dictionary, sort_keys=False, indent=4, default=str
28052812
)
28062813
)
2807-
print(
2808-
f"Your Environment Analysis file was saved, check it out: {filename}.json\n"
2809-
"You can use it to set a `customAtmosphere` atmospheric model"
2814+
logger.info(
2815+
"Your Environment Analysis file was saved: %s.json. "
2816+
"You can use it to set a customAtmosphere atmospheric model.",
2817+
filename,
28102818
)
28112819

28122820
@classmethod
@@ -2845,7 +2853,7 @@ def save(self, filename="env_analysis_dict"):
28452853
file = open(filename, "w") # pylint: disable=consider-using-with
28462854
file.write(encoded_class)
28472855
file.close()
2848-
print("Your Environment Analysis file was saved, check it out: " + filename)
2856+
logger.info("Your Environment Analysis file was saved: %s", filename)
28492857

28502858
def create_environment_object(
28512859
self, gravity=None, date=None, datum="SIRGAS2000", max_expected_height=80000.0

rocketpy/environment/fetchers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
functions may be changed without notice in future feature releases.
44
"""
55

6+
import logging
67
import re
78
import time
89
from datetime import datetime, timedelta, timezone
@@ -12,6 +13,8 @@
1213

1314
from rocketpy.tools import exponential_backoff
1415

16+
logger = logging.getLogger(__name__)
17+
1518
MAX_RETRY_DELAY_SECONDS = 600
1619

1720

@@ -37,7 +40,9 @@ def fetch_open_elevation(lat, lon):
3740
RuntimeError
3841
If there is a problem reaching the Open-Elevation API servers.
3942
"""
40-
print(f"Fetching elevation from open-elevation.com for lat={lat}, lon={lon}...")
43+
logger.debug(
44+
"Fetching elevation from open-elevation.com for lat=%s, lon=%s", lat, lon
45+
)
4146
request_url = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
4247
try:
4348
response = requests.get(request_url)

0 commit comments

Comments
 (0)