Coverage for nilearn/_utils/logger.py: 35%
65 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Logging facility for nilearn."""
3import inspect
4import traceback
5from pathlib import Path
7from sklearn.base import BaseEstimator
10def _has_rich():
11 """Check if rich is installed."""
12 try:
13 import rich # noqa: F401
15 return True
17 except ImportError:
18 return False
21if _has_rich(): 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true
22 from rich import print
23 from rich.markup import escape
26# The technique used in the log() function only applies to CPython, because
27# it uses the inspect module to walk the call stack.
28def log(
29 msg,
30 verbose=1,
31 object_classes=(BaseEstimator,),
32 stack_level=None,
33 msg_level=1,
34 with_traceback=False,
35):
36 """Display a message to the user, depending on the verbosity level.
38 This function allows to display some information that references an object
39 that is significant to the user, instead of a internal function. The goal
40 is to make user's code as simple to debug as possible.
42 Parameters
43 ----------
44 msg : str
45 Message to display.
47 verbose : int, default=1
48 Current verbosity level. Message is displayed if this value is greater
49 or equal to msg_level.
51 object_classes : tuple of type, default=(BaseEstimator, )
52 Classes that should appear to emit the message.
54 stack_level : int or None, default=None
55 If no object in the call stack matches object_classes, go back that
56 amount in the call stack and display class/function name thereof.
57 If None is passed this should show
58 the first nilearn public function in the stack.
60 msg_level : int, default=1
61 Verbosity level at and above which message should be displayed to the
62 user. Most of the time this parameter can be left unchanged.
64 Notes
65 -----
66 This function does tricky things to ensure that the proper object is
67 referenced in the message. If it is called e.g. inside a function that is
68 called by a method of an object inheriting from any class in
69 object_classes, then the name of the object (and the method) will be
70 displayed to the user. If several matching objects exist in the call
71 stack, the highest one is used (first call chronologically), because this
72 is the one which is most likely to have been written in the user's script.
74 """
75 if verbose < msg_level:
76 return
77 if stack_level is None:
78 stack_level = find_stack_level() - 2
79 stack = inspect.stack()
80 object_frame = None
81 object_self = None
82 for f in reversed(stack):
83 frame = f[0]
84 current_self = frame.f_locals.get("self", None)
85 if isinstance(current_self, object_classes):
86 object_frame = frame
87 func_name = f[3]
88 object_self = current_self
89 break
91 if object_frame is None: # no object found: use stack_level
92 if stack_level >= len(stack):
93 func_name = "<top_level>"
94 else:
95 object_frame, _, _, func_name = stack[stack_level][:4]
96 object_self = object_frame.f_locals.get("self", None)
98 if object_self is not None:
99 func_name = f"{object_self.__class__.__name__}.{func_name}"
101 if _has_rich():
102 print(f"[blue]\\[{func_name}][/blue] {escape(msg)}")
103 else:
104 print(f"[{func_name}] {msg}")
106 if with_traceback:
107 traceback.print_exc()
110def compose_err_msg(msg, **kwargs):
111 """Append key-value pairs to msg, for display. # noqa: D301.
113 Parameters
114 ----------
115 msg : string
116 Arbitrary message.
118 kwargs : dict, optional
119 Arbitrary dictionary.
121 Returns
122 -------
123 updated_msg : string
124 msg, with "key: value" appended. Only string values are appended.
126 Example
127 -------
128 >>> compose_err_msg('Error message with arguments...', arg_num=123, \
129 arg_str='filename.nii', arg_bool=True)
130 'Error message with arguments...\\narg_str: filename.nii'
131 >>>
133 """
134 updated_msg = msg
135 for k, v in sorted(kwargs.items()):
136 if isinstance(v, str): # print only str-like arguments
137 updated_msg += f"\n{k}: {v}"
139 return updated_msg
142def find_stack_level() -> int:
143 """
144 Find the first place in the stack that is not inside nilearn
145 (tests notwithstanding).
147 Taken from the pandas codebase.
148 https://github.com/pandas-dev/pandas/tree/main/pandas/util/_exceptions.py#L37
149 """
150 import nilearn as nil
152 pkg_dir = Path(nil.__file__).parent
154 # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow
155 frame = inspect.currentframe()
156 try:
157 n = 0
158 while frame: 158 ↛ 169line 158 didn't jump to line 169 because the condition on line 158 was always true
159 filename = inspect.getfile(frame)
160 is_test_file = Path(filename).name.startswith("test_")
161 in_nilearn_code = filename.startswith(str(pkg_dir))
162 if not in_nilearn_code or is_test_file:
163 break
164 frame = frame.f_back
165 n += 1
166 finally:
167 # See note in
168 # https://docs.python.org/3/library/inspect.html#inspect.Traceback
169 del frame
170 return n
173def one_level_deeper():
174 """Use for testing find_stack_level.
176 Needs to be in a module that does not start with 'test'
177 """
178 return find_stack_level()