lsst-utils 25.2023.600__py3-none-any.whl → 29.2025.4800__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsst/utils/__init__.py +0 -3
- lsst/utils/_packaging.py +2 -0
- lsst/utils/argparsing.py +79 -0
- lsst/utils/classes.py +27 -9
- lsst/utils/db_auth.py +339 -0
- lsst/utils/deprecated.py +10 -7
- lsst/utils/doImport.py +8 -9
- lsst/utils/inheritDoc.py +34 -6
- lsst/utils/introspection.py +285 -19
- lsst/utils/iteration.py +193 -7
- lsst/utils/logging.py +155 -105
- lsst/utils/packages.py +324 -82
- lsst/utils/plotting/__init__.py +15 -0
- lsst/utils/plotting/figures.py +159 -0
- lsst/utils/plotting/limits.py +155 -0
- lsst/utils/plotting/publication_plots.py +184 -0
- lsst/utils/plotting/rubin.mplstyle +46 -0
- lsst/utils/tests.py +231 -102
- lsst/utils/threads.py +9 -3
- lsst/utils/timer.py +207 -110
- lsst/utils/usage.py +6 -6
- lsst/utils/version.py +1 -1
- lsst/utils/wrappers.py +74 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +19 -15
- lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
- lsst/utils/_forwarded.py +0 -28
- lsst/utils/backtrace/__init__.py +0 -33
- lsst/utils/ellipsis.py +0 -54
- lsst/utils/get_caller_name.py +0 -45
- lsst_utils-25.2023.600.dist-info/RECORD +0 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/zip-safe +0 -0
lsst/utils/logging.py
CHANGED
|
@@ -14,26 +14,39 @@ from __future__ import annotations
|
|
|
14
14
|
__all__ = (
|
|
15
15
|
"TRACE",
|
|
16
16
|
"VERBOSE",
|
|
17
|
-
"getLogger",
|
|
18
|
-
"getTraceLogger",
|
|
19
17
|
"LsstLogAdapter",
|
|
18
|
+
"LsstLoggers",
|
|
20
19
|
"PeriodicLogger",
|
|
20
|
+
"getLogger",
|
|
21
|
+
"getTraceLogger",
|
|
21
22
|
"trace_set_at",
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
import logging
|
|
26
|
+
import sys
|
|
25
27
|
import time
|
|
28
|
+
from collections.abc import Generator
|
|
26
29
|
from contextlib import contextmanager
|
|
27
30
|
from logging import LoggerAdapter
|
|
28
|
-
from typing import
|
|
29
|
-
|
|
30
|
-
from deprecated.sphinx import deprecated
|
|
31
|
+
from typing import TYPE_CHECKING, Any, TypeAlias, TypeGuard
|
|
31
32
|
|
|
32
33
|
try:
|
|
33
34
|
import lsst.log.utils as logUtils
|
|
34
35
|
except ImportError:
|
|
35
36
|
logUtils = None
|
|
36
37
|
|
|
38
|
+
try:
|
|
39
|
+
from structlog import get_context as get_structlog_context
|
|
40
|
+
except ImportError:
|
|
41
|
+
get_structlog_context = None # type: ignore[assignment]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
try:
|
|
46
|
+
from structlog.typing import BindableLogger
|
|
47
|
+
except ImportError:
|
|
48
|
+
BindableLogger: TypeAlias = Any # type: ignore[no-redef]
|
|
49
|
+
|
|
37
50
|
# log level for trace (verbose debug).
|
|
38
51
|
TRACE = 5
|
|
39
52
|
logging.addLevelName(TRACE, "TRACE")
|
|
@@ -43,6 +56,56 @@ VERBOSE = (logging.INFO + logging.DEBUG) // 2
|
|
|
43
56
|
logging.addLevelName(VERBOSE, "VERBOSE")
|
|
44
57
|
|
|
45
58
|
|
|
59
|
+
def _is_structlog_logger(
|
|
60
|
+
logger: logging.Logger | LsstLogAdapter | BindableLogger,
|
|
61
|
+
) -> TypeGuard[BindableLogger]:
|
|
62
|
+
"""Check if the given logger is a structlog logger."""
|
|
63
|
+
if get_structlog_context is None:
|
|
64
|
+
return False # type: ignore[unreachable]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Returns a dict for structlog loggers; raises for stdlib logger
|
|
68
|
+
# objects.
|
|
69
|
+
get_structlog_context(logger) # type: ignore[arg-type]
|
|
70
|
+
return True
|
|
71
|
+
except Exception:
|
|
72
|
+
# In practice this is usually ValueError or AttributeError.
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _calculate_base_stacklevel(default: int, offset: int) -> int:
|
|
77
|
+
"""Calculate the default logging stacklevel to use.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
default : `int`
|
|
82
|
+
The stacklevel to use in Python 3.11 and newer where the only
|
|
83
|
+
thing to take into account is the number of levels above the core
|
|
84
|
+
Python logging infrastructure.
|
|
85
|
+
offset : `int`
|
|
86
|
+
The offset to apply for older Python implementations that need to
|
|
87
|
+
take into account internal call stacks.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
stacklevel : `int`
|
|
92
|
+
The stack level to pass to internal logging APIs that should result
|
|
93
|
+
in the log messages being reported in caller lines.
|
|
94
|
+
|
|
95
|
+
Notes
|
|
96
|
+
-----
|
|
97
|
+
In Python 3.11 the logging infrastructure was fixed such that we no
|
|
98
|
+
longer need to understand that a LoggerAdapter log messages need to
|
|
99
|
+
have a stack level one higher than a Logger would need. ``stacklevel=1``
|
|
100
|
+
now always means "log from the caller's line" without the caller having
|
|
101
|
+
to understand internal implementation details.
|
|
102
|
+
"""
|
|
103
|
+
stacklevel = default
|
|
104
|
+
if sys.version_info < (3, 11, 0):
|
|
105
|
+
stacklevel += offset
|
|
106
|
+
return stacklevel
|
|
107
|
+
|
|
108
|
+
|
|
46
109
|
def trace_set_at(name: str, number: int) -> None:
|
|
47
110
|
"""Adjust logging level to display messages with the trace number being
|
|
48
111
|
less than or equal to the provided value.
|
|
@@ -132,8 +195,14 @@ class LsstLogAdapter(LoggerAdapter):
|
|
|
132
195
|
TRACE = TRACE
|
|
133
196
|
VERBOSE = VERBOSE
|
|
134
197
|
|
|
198
|
+
# The stack level to use when issuing log messages. For Python 3.11
|
|
199
|
+
# this is generally 2 (this method and the internal infrastructure).
|
|
200
|
+
# For older python we need one higher because of the extra indirection
|
|
201
|
+
# via LoggingAdapter internals.
|
|
202
|
+
_stacklevel = _calculate_base_stacklevel(2, 1)
|
|
203
|
+
|
|
135
204
|
@contextmanager
|
|
136
|
-
def temporary_log_level(self, level:
|
|
205
|
+
def temporary_log_level(self, level: int | str) -> Generator:
|
|
137
206
|
"""Temporarily set the level of this logger.
|
|
138
207
|
|
|
139
208
|
Parameters
|
|
@@ -168,111 +237,71 @@ class LsstLogAdapter(LoggerAdapter):
|
|
|
168
237
|
"""
|
|
169
238
|
return getLogger(name=name, logger=self.logger)
|
|
170
239
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def getName(self) -> str:
|
|
185
|
-
return self.name
|
|
186
|
-
|
|
187
|
-
@deprecated(
|
|
188
|
-
reason="Use Python Logger compatible .level property. Will be removed after v23.",
|
|
189
|
-
version="v23",
|
|
190
|
-
category=FutureWarning,
|
|
191
|
-
)
|
|
192
|
-
def getLevel(self) -> int:
|
|
193
|
-
return self.logger.level
|
|
240
|
+
def _process_stacklevel(self, kwargs: dict[str, Any], offset: int = 0) -> int:
|
|
241
|
+
# Return default stacklevel, taking into account kwargs[stacklevel].
|
|
242
|
+
stacklevel = self._stacklevel
|
|
243
|
+
if "stacklevel" in kwargs:
|
|
244
|
+
# External user expects stacklevel=1 to mean "report from their
|
|
245
|
+
# line" but the code here is already trying to achieve that by
|
|
246
|
+
# default. Therefore if an external stacklevel is specified we
|
|
247
|
+
# adjust their stacklevel request by 1.
|
|
248
|
+
stacklevel = stacklevel + kwargs.pop("stacklevel") - 1
|
|
249
|
+
|
|
250
|
+
# The offset can be used to indicate that we have to take into account
|
|
251
|
+
# additional internal layers before calling python logging.
|
|
252
|
+
return _calculate_base_stacklevel(stacklevel, offset)
|
|
194
253
|
|
|
195
254
|
def fatal(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
|
196
255
|
# Python does not provide this method in LoggerAdapter but does
|
|
197
|
-
# not formally
|
|
256
|
+
# not formally deprecate it in favor of critical() either.
|
|
198
257
|
# Provide it without deprecation message for consistency with Python.
|
|
199
|
-
# stacklevel
|
|
200
|
-
self.critical
|
|
258
|
+
# Have to adjust stacklevel on Python 3.10 and older to account
|
|
259
|
+
# for call through self.critical.
|
|
260
|
+
stacklevel = self._process_stacklevel(kwargs, offset=1)
|
|
261
|
+
self.critical(msg, *args, **kwargs, stacklevel=stacklevel)
|
|
201
262
|
|
|
202
263
|
def verbose(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
203
264
|
"""Issue a VERBOSE level log message.
|
|
204
265
|
|
|
205
266
|
Arguments are as for `logging.info`.
|
|
206
267
|
``VERBOSE`` is between ``DEBUG`` and ``INFO``.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
fmt : `str`
|
|
272
|
+
Log message.
|
|
273
|
+
*args : `~typing.Any`
|
|
274
|
+
Parameters references by log message.
|
|
275
|
+
**kwargs : `~typing.Any`
|
|
276
|
+
Parameters forwarded to `log`.
|
|
207
277
|
"""
|
|
208
278
|
# There is no other way to achieve this other than a special logger
|
|
209
279
|
# method.
|
|
210
280
|
# Stacklevel is passed in so that the correct line is reported
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# itself.
|
|
214
|
-
self.log(VERBOSE, fmt, *args, stacklevel=3, **kwargs)
|
|
281
|
+
stacklevel = self._process_stacklevel(kwargs)
|
|
282
|
+
self.log(VERBOSE, fmt, *args, **kwargs, stacklevel=stacklevel)
|
|
215
283
|
|
|
216
|
-
def trace(self, fmt: str, *args: Any) -> None:
|
|
284
|
+
def trace(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
217
285
|
"""Issue a TRACE level log message.
|
|
218
286
|
|
|
219
287
|
Arguments are as for `logging.info`.
|
|
220
288
|
``TRACE`` is lower than ``DEBUG``.
|
|
289
|
+
|
|
290
|
+
Parameters
|
|
291
|
+
----------
|
|
292
|
+
fmt : `str`
|
|
293
|
+
Log message.
|
|
294
|
+
*args : `~typing.Any`
|
|
295
|
+
Parameters references by log message.
|
|
296
|
+
**kwargs : `~typing.Any`
|
|
297
|
+
Parameters forwarded to `log`.
|
|
221
298
|
"""
|
|
222
299
|
# There is no other way to achieve this other than a special logger
|
|
223
|
-
# method.
|
|
224
|
-
self.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
version="v23",
|
|
229
|
-
category=FutureWarning,
|
|
230
|
-
)
|
|
231
|
-
def tracef(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
232
|
-
# Stacklevel is 4 to account for the deprecation wrapper
|
|
233
|
-
self.log(TRACE, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
234
|
-
|
|
235
|
-
@deprecated(
|
|
236
|
-
reason="Use Python Logger compatible method. Will be removed after v23.",
|
|
237
|
-
version="v23",
|
|
238
|
-
category=FutureWarning,
|
|
239
|
-
)
|
|
240
|
-
def debugf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
241
|
-
self.log(logging.DEBUG, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
242
|
-
|
|
243
|
-
@deprecated(
|
|
244
|
-
reason="Use Python Logger compatible method. Will be removed after v23.",
|
|
245
|
-
version="v23",
|
|
246
|
-
category=FutureWarning,
|
|
247
|
-
)
|
|
248
|
-
def infof(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
249
|
-
self.log(logging.INFO, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
250
|
-
|
|
251
|
-
@deprecated(
|
|
252
|
-
reason="Use Python Logger compatible method. Will be removed after v23.",
|
|
253
|
-
version="v23",
|
|
254
|
-
category=FutureWarning,
|
|
255
|
-
)
|
|
256
|
-
def warnf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
257
|
-
self.log(logging.WARNING, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
258
|
-
|
|
259
|
-
@deprecated(
|
|
260
|
-
reason="Use Python Logger compatible method. Will be removed after v23.",
|
|
261
|
-
version="v23",
|
|
262
|
-
category=FutureWarning,
|
|
263
|
-
)
|
|
264
|
-
def errorf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
265
|
-
self.log(logging.ERROR, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
266
|
-
|
|
267
|
-
@deprecated(
|
|
268
|
-
reason="Use Python Logger compatible method. Will be removed after v23.",
|
|
269
|
-
version="v23",
|
|
270
|
-
category=FutureWarning,
|
|
271
|
-
)
|
|
272
|
-
def fatalf(self, fmt: str, *args: Any, **kwargs: Any) -> None:
|
|
273
|
-
self.log(logging.CRITICAL, _F(fmt, *args, **kwargs), stacklevel=4)
|
|
274
|
-
|
|
275
|
-
def setLevel(self, level: Union[int, str]) -> None:
|
|
300
|
+
# method.
|
|
301
|
+
stacklevel = self._process_stacklevel(kwargs)
|
|
302
|
+
self.log(TRACE, fmt, *args, **kwargs, stacklevel=stacklevel)
|
|
303
|
+
|
|
304
|
+
def setLevel(self, level: int | str) -> None:
|
|
276
305
|
"""Set the level for the logger, trapping lsst.log values.
|
|
277
306
|
|
|
278
307
|
Parameters
|
|
@@ -291,23 +320,32 @@ class LsstLogAdapter(LoggerAdapter):
|
|
|
291
320
|
self.logger.setLevel(level)
|
|
292
321
|
|
|
293
322
|
@property
|
|
294
|
-
def handlers(self) ->
|
|
323
|
+
def handlers(self) -> list[logging.Handler]:
|
|
295
324
|
"""Log handlers associated with this logger."""
|
|
296
325
|
return self.logger.handlers
|
|
297
326
|
|
|
298
327
|
def addHandler(self, handler: logging.Handler) -> None:
|
|
299
328
|
"""Add a handler to this logger.
|
|
300
329
|
|
|
301
|
-
|
|
330
|
+
Parameters
|
|
331
|
+
----------
|
|
332
|
+
handler : `logging.Handler`
|
|
333
|
+
Handler to add. The handler is forwarded to the underlying logger.
|
|
302
334
|
"""
|
|
303
335
|
self.logger.addHandler(handler)
|
|
304
336
|
|
|
305
337
|
def removeHandler(self, handler: logging.Handler) -> None:
|
|
306
|
-
"""Remove the given handler from the underlying logger.
|
|
338
|
+
"""Remove the given handler from the underlying logger.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
handler : `logging.Handler`
|
|
343
|
+
Handler to remove.
|
|
344
|
+
"""
|
|
307
345
|
self.logger.removeHandler(handler)
|
|
308
346
|
|
|
309
347
|
|
|
310
|
-
def getLogger(name:
|
|
348
|
+
def getLogger(name: str | None = None, logger: logging.Logger | None = None) -> LsstLogAdapter:
|
|
311
349
|
"""Get a logger compatible with LSST usage.
|
|
312
350
|
|
|
313
351
|
Parameters
|
|
@@ -330,7 +368,7 @@ def getLogger(name: Optional[str] = None, logger: Optional[logging.Logger] = Non
|
|
|
330
368
|
uniform interface than when using `logging.setLoggerClass`. An adapter
|
|
331
369
|
can be wrapped around the root logger and the `~logging.setLoggerClass`
|
|
332
370
|
will return the logger first given that name even if the name was
|
|
333
|
-
used before the
|
|
371
|
+
used before the `~lsst.pipe.base.Task` was created.
|
|
334
372
|
"""
|
|
335
373
|
if not logger:
|
|
336
374
|
logger = logging.getLogger(name)
|
|
@@ -339,10 +377,10 @@ def getLogger(name: Optional[str] = None, logger: Optional[logging.Logger] = Non
|
|
|
339
377
|
return LsstLogAdapter(logger, {})
|
|
340
378
|
|
|
341
379
|
|
|
342
|
-
LsstLoggers =
|
|
380
|
+
LsstLoggers: TypeAlias = logging.Logger | LsstLogAdapter
|
|
343
381
|
|
|
344
382
|
|
|
345
|
-
def getTraceLogger(logger:
|
|
383
|
+
def getTraceLogger(logger: str | LsstLoggers, trace_level: int) -> LsstLogAdapter:
|
|
346
384
|
"""Get a logger with the appropriate TRACE name.
|
|
347
385
|
|
|
348
386
|
Parameters
|
|
@@ -372,22 +410,28 @@ class PeriodicLogger:
|
|
|
372
410
|
be useful to issue a log message periodically to show that the
|
|
373
411
|
algorithm is progressing.
|
|
374
412
|
|
|
413
|
+
The first time threshold is counted from object construction, so in general
|
|
414
|
+
the first call to `log` does not log.
|
|
415
|
+
|
|
375
416
|
Parameters
|
|
376
417
|
----------
|
|
377
418
|
logger : `logging.Logger` or `LsstLogAdapter`
|
|
378
419
|
Logger to use when issuing a message.
|
|
379
420
|
interval : `float`
|
|
380
|
-
The minimum interval between log messages. If `None
|
|
381
|
-
|
|
421
|
+
The minimum interval in seconds between log messages. If `None`,
|
|
422
|
+
`LOGGING_INTERVAL` will be used.
|
|
382
423
|
level : `int`, optional
|
|
383
|
-
Log level to use when issuing messages
|
|
424
|
+
Log level to use when issuing messages, default is
|
|
425
|
+
`~logging.INFO`.
|
|
384
426
|
"""
|
|
385
427
|
|
|
386
428
|
LOGGING_INTERVAL = 600.0
|
|
387
|
-
"""Default interval between log messages."""
|
|
429
|
+
"""Default interval between log messages in seconds."""
|
|
388
430
|
|
|
389
|
-
def __init__(self, logger: LsstLoggers, interval:
|
|
431
|
+
def __init__(self, logger: LsstLoggers, interval: float | None = None, level: int = logging.INFO):
|
|
390
432
|
self.logger = logger
|
|
433
|
+
# None -> LOGGING_INTERVAL conversion done so that unit tests (e.g., in
|
|
434
|
+
# pipe_base) can tweak log interval without access to the constructor.
|
|
391
435
|
self.interval = interval if interval is not None else self.LOGGING_INTERVAL
|
|
392
436
|
self.level = level
|
|
393
437
|
self.next_log_time = time.time() + self.interval
|
|
@@ -395,18 +439,24 @@ class PeriodicLogger:
|
|
|
395
439
|
|
|
396
440
|
# The stacklevel we need to issue logs is determined by the type
|
|
397
441
|
# of logger we have been given. A LoggerAdapter has an extra
|
|
398
|
-
# level of indirection.
|
|
399
|
-
|
|
442
|
+
# level of indirection. In Python 3.11 the logging infrastructure
|
|
443
|
+
# takes care to check for internal logging stack frames so there
|
|
444
|
+
# is no need for a difference.
|
|
445
|
+
self._stacklevel = _calculate_base_stacklevel(2, 1 if isinstance(self.logger, LoggerAdapter) else 0)
|
|
400
446
|
|
|
401
447
|
def log(self, msg: str, *args: Any) -> bool:
|
|
402
448
|
"""Issue a log message if the interval has elapsed.
|
|
403
449
|
|
|
450
|
+
The interval is measured from the previous call to ``log``, or from the
|
|
451
|
+
creation of this object.
|
|
452
|
+
|
|
404
453
|
Parameters
|
|
405
454
|
----------
|
|
406
455
|
msg : `str`
|
|
407
456
|
Message to issue if the time has been exceeded.
|
|
408
457
|
*args : Any
|
|
409
|
-
|
|
458
|
+
Arguments to be merged into the message string, as described under
|
|
459
|
+
`logging.Logger.debug`.
|
|
410
460
|
|
|
411
461
|
Returns
|
|
412
462
|
-------
|