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.
Files changed (35) hide show
  1. lsst/utils/__init__.py +0 -3
  2. lsst/utils/_packaging.py +2 -0
  3. lsst/utils/argparsing.py +79 -0
  4. lsst/utils/classes.py +27 -9
  5. lsst/utils/db_auth.py +339 -0
  6. lsst/utils/deprecated.py +10 -7
  7. lsst/utils/doImport.py +8 -9
  8. lsst/utils/inheritDoc.py +34 -6
  9. lsst/utils/introspection.py +285 -19
  10. lsst/utils/iteration.py +193 -7
  11. lsst/utils/logging.py +155 -105
  12. lsst/utils/packages.py +324 -82
  13. lsst/utils/plotting/__init__.py +15 -0
  14. lsst/utils/plotting/figures.py +159 -0
  15. lsst/utils/plotting/limits.py +155 -0
  16. lsst/utils/plotting/publication_plots.py +184 -0
  17. lsst/utils/plotting/rubin.mplstyle +46 -0
  18. lsst/utils/tests.py +231 -102
  19. lsst/utils/threads.py +9 -3
  20. lsst/utils/timer.py +207 -110
  21. lsst/utils/usage.py +6 -6
  22. lsst/utils/version.py +1 -1
  23. lsst/utils/wrappers.py +74 -29
  24. {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +19 -15
  25. lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
  26. {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
  27. lsst/utils/_forwarded.py +0 -28
  28. lsst/utils/backtrace/__init__.py +0 -33
  29. lsst/utils/ellipsis.py +0 -54
  30. lsst/utils/get_caller_name.py +0 -45
  31. lsst_utils-25.2023.600.dist-info/RECORD +0 -29
  32. {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
  33. {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
  34. {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
  35. {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 Any, Generator, List, Optional, Union
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: Union[int, str]) -> Generator:
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
- @deprecated(
172
- reason="Use Python Logger compatible isEnabledFor Will be removed after v23.",
173
- version="v23",
174
- category=FutureWarning,
175
- )
176
- def isDebugEnabled(self) -> bool:
177
- return self.isEnabledFor(self.DEBUG)
178
-
179
- @deprecated(
180
- reason="Use Python Logger compatible 'name' attribute. Will be removed after v23.",
181
- version="v23",
182
- category=FutureWarning,
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 deprecated it in favor of critical() either.
256
+ # not formally deprecate it in favor of critical() either.
198
257
  # Provide it without deprecation message for consistency with Python.
199
- # stacklevel=5 accounts for the forwarding of LoggerAdapter.
200
- self.critical(msg, *args, **kwargs, stacklevel=4)
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
- # in the log record and not this line. 3 is this method,
212
- # 2 is the level from `self.log` and 1 is the log infrastructure
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. For stacklevel discussion see `verbose()`.
224
- self.log(TRACE, fmt, *args, stacklevel=3)
225
-
226
- @deprecated(
227
- reason="Use Python Logger compatible method. Will be removed after v23.",
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) -> List[logging.Handler]:
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
- The handler is forwarded to the underlying logger.
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: Optional[str] = None, logger: Optional[logging.Logger] = None) -> LsstLogAdapter:
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 `Task` was created.
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 = Union[logging.Logger, LsstLogAdapter]
380
+ LsstLoggers: TypeAlias = logging.Logger | LsstLogAdapter
343
381
 
344
382
 
345
- def getTraceLogger(logger: Union[str, LsstLoggers], trace_level: int) -> LsstLogAdapter:
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` the class
381
- default will be used.
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: Optional[float] = None, level: int = VERBOSE):
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
- self._stacklevel = 3 if isinstance(self.logger, LoggerAdapter) else 2
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
- Parameters to be passed to the log system.
458
+ Arguments to be merged into the message string, as described under
459
+ `logging.Logger.debug`.
410
460
 
411
461
  Returns
412
462
  -------