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/threads.py CHANGED
@@ -9,11 +9,11 @@
9
9
  # Use of this source code is governed by a 3-clause BSD-style
10
10
  # license that can be found in the LICENSE file.
11
11
  #
12
- from __future__ import annotations
13
-
14
12
  """Support for threading and multi-processing."""
15
13
 
16
- __all__ = ["set_thread_envvars", "disable_implicit_threading"]
14
+ from __future__ import annotations
15
+
16
+ __all__ = ["disable_implicit_threading", "set_thread_envvars"]
17
17
 
18
18
  import os
19
19
 
@@ -50,6 +50,12 @@ def set_thread_envvars(num_threads: int = 1, override: bool = False) -> None:
50
50
  if override or var not in os.environ:
51
51
  os.environ[var] = str(num_threads)
52
52
 
53
+ # Also specify an explicit value for OMP_PROC_BIND to tell OpenMP not to
54
+ # set CPU affinity.
55
+ var = "OMP_PROC_BIND"
56
+ if override or var not in os.environ:
57
+ os.environ[var] = "false"
58
+
53
59
 
54
60
  def disable_implicit_threading() -> None:
55
61
  """Do whatever is necessary to try to prevent implicit threading.
lsst/utils/timer.py CHANGED
@@ -10,40 +10,34 @@
10
10
  # license that can be found in the LICENSE file.
11
11
  #
12
12
 
13
- """Utilities for measuring execution time.
14
- """
13
+ """Utilities for measuring execution time."""
15
14
 
16
15
  from __future__ import annotations
17
16
 
18
- __all__ = ["profile", "logInfo", "timeMethod", "time_this"]
17
+ __all__ = ["duration_from_timeMethod", "logInfo", "profile", "timeMethod", "time_this"]
19
18
 
19
+ import dataclasses
20
20
  import datetime
21
21
  import functools
22
- import inspect
23
22
  import logging
23
+ import os
24
+ import platform
25
+ import sys
24
26
  import time
25
- from contextlib import contextmanager
26
- from typing import (
27
- TYPE_CHECKING,
28
- Any,
29
- Callable,
30
- Collection,
31
- Iterable,
32
- Iterator,
33
- MutableMapping,
34
- Optional,
35
- Tuple,
36
- )
27
+ import traceback
28
+ from collections.abc import Callable, Collection, Iterable, Iterator, MutableMapping
29
+ from contextlib import contextmanager, suppress
30
+ from typing import TYPE_CHECKING, Any, TypeVar, overload
37
31
 
38
32
  from astropy import units as u
39
33
 
34
+ from .introspection import find_outside_stacklevel
35
+ from .logging import LsstLoggers, _is_structlog_logger
40
36
  from .usage import _get_current_rusage, get_current_mem_usage, get_peak_mem_usage
41
37
 
42
38
  if TYPE_CHECKING:
43
39
  import cProfile
44
40
 
45
- from .logging import LsstLoggers
46
-
47
41
 
48
42
  _LOG = logging.getLogger(__name__)
49
43
 
@@ -80,54 +74,15 @@ def _add_to_metadata(metadata: MutableMapping, name: str, value: Any) -> None:
80
74
  metadata[name].append(value)
81
75
 
82
76
 
83
- def _find_outside_stacklevel() -> int:
84
- """Find the stack level corresponding to caller code outside of this
85
- module.
86
-
87
- This can be passed directly to `logging.Logger.log()` to ensure
88
- that log messages are issued as if they are coming from caller code.
89
-
90
- Returns
91
- -------
92
- stacklevel : `int`
93
- The stack level to use to refer to a caller outside of this module.
94
- A ``stacklevel`` of ``1`` corresponds to the caller of this internal
95
- function and that is the default expected by `logging.Logger.log()`.
96
-
97
- Notes
98
- -----
99
- Intended to be called from the function that is going to issue a log
100
- message. The result should be passed into `~logging.Logger.log` via the
101
- keyword parameter ``stacklevel``.
102
- """
103
- stacklevel = 1 # the default for `Logger.log`
104
- for i, s in enumerate(inspect.stack()):
105
- module = inspect.getmodule(s.frame)
106
- if module is None:
107
- # Stack frames sometimes hang around so explicilty delete.
108
- del s
109
- continue
110
- if not module.__name__.startswith("lsst.utils"):
111
- # 0 will be this function.
112
- # 1 will be the caller which will be the default for `Logger.log`
113
- # and so does not need adjustment.
114
- stacklevel = i
115
- break
116
- # Stack frames sometimes hang around so explicilty delete.
117
- del s
118
-
119
- return stacklevel
120
-
121
-
122
77
  def logPairs(
123
78
  obj: Any,
124
- pairs: Collection[Tuple[str, Any]],
79
+ pairs: Collection[tuple[str, Any]],
125
80
  logLevel: int = logging.DEBUG,
126
- metadata: Optional[MutableMapping] = None,
127
- logger: Optional[logging.Logger] = None,
128
- stacklevel: Optional[int] = None,
81
+ metadata: MutableMapping | None = None,
82
+ logger: LsstLoggers | None = None,
83
+ stacklevel: int | None = None,
129
84
  ) -> None:
130
- """Log ``(name, value)`` pairs to ``obj.metadata`` and ``obj.log``
85
+ """Log ``(name, value)`` pairs to ``obj.metadata`` and ``obj.log``.
131
86
 
132
87
  Parameters
133
88
  ----------
@@ -147,7 +102,7 @@ def logPairs(
147
102
  Log level (an `logging` level constant, such as `logging.DEBUG`).
148
103
  metadata : `collections.abc.MutableMapping`, optional
149
104
  Metadata object to write entries to. Ignored if `None`.
150
- logger : `logging.Logger`
105
+ logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`
151
106
  Log object to write entries to. Ignored if `None`.
152
107
  stacklevel : `int`, optional
153
108
  The stack level to pass to the logger to adjust which stack frame
@@ -158,15 +113,13 @@ def logPairs(
158
113
  """
159
114
  if obj is not None:
160
115
  if metadata is None:
161
- try:
116
+ with suppress(AttributeError):
162
117
  metadata = obj.metadata
163
- except AttributeError:
164
- pass
118
+
165
119
  if logger is None:
166
- try:
120
+ with suppress(AttributeError):
167
121
  logger = obj.log
168
- except AttributeError:
169
- pass
122
+
170
123
  strList = []
171
124
  for name, value in pairs:
172
125
  if metadata is not None:
@@ -179,7 +132,7 @@ def logPairs(
179
132
  timer_logger = logging.getLogger("timer." + logger.name)
180
133
  if timer_logger.isEnabledFor(logLevel):
181
134
  if stacklevel is None:
182
- stacklevel = _find_outside_stacklevel()
135
+ stacklevel = find_outside_stacklevel("lsst.utils")
183
136
  else:
184
137
  # Account for the caller stack.
185
138
  stacklevel += 1
@@ -190,9 +143,9 @@ def logInfo(
190
143
  obj: Any,
191
144
  prefix: str,
192
145
  logLevel: int = logging.DEBUG,
193
- metadata: Optional[MutableMapping] = None,
194
- logger: Optional[logging.Logger] = None,
195
- stacklevel: Optional[int] = None,
146
+ metadata: MutableMapping | None = None,
147
+ logger: LsstLoggers | None = None,
148
+ stacklevel: int | None = None,
196
149
  ) -> None:
197
150
  """Log timer information to ``obj.metadata`` and ``obj.log``.
198
151
 
@@ -216,7 +169,7 @@ def logInfo(
216
169
  Log level (a `logging` level constant, such as `logging.DEBUG`).
217
170
  metadata : `collections.abc.MutableMapping`, optional
218
171
  Metadata object to write entries to, overriding ``obj.metadata``.
219
- logger : `logging.Logger`
172
+ logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`
220
173
  Log object to write entries to, overriding ``obj.log``.
221
174
  stacklevel : `int`, optional
222
175
  The stack level to pass to the logger to adjust which stack frame
@@ -246,18 +199,24 @@ def logInfo(
246
199
  * Version 1: ``MaxResidentSetSize`` will be stored in bytes.
247
200
  """
248
201
  if metadata is None and obj is not None:
249
- try:
202
+ with suppress(AttributeError):
250
203
  metadata = obj.metadata
251
- except AttributeError:
252
- pass
204
+
253
205
  if metadata is not None:
254
206
  # Log messages already have timestamps.
255
- utcStr = datetime.datetime.utcnow().isoformat()
207
+ if sys.version_info < (3, 11, 0):
208
+ now = datetime.datetime.utcnow()
209
+ else:
210
+ now = datetime.datetime.now(datetime.UTC)
211
+ utcStr = now.isoformat()
256
212
  _add_to_metadata(metadata, name=prefix + "Utc", value=utcStr)
257
213
 
258
214
  # Force a version number into the metadata.
259
215
  # v1: Ensure that max_rss field is always bytes.
260
216
  metadata["__version__"] = 1
217
+
218
+ # Add node information.
219
+ metadata["nodeName"] = platform.node()
261
220
  if stacklevel is not None:
262
221
  # Account for the caller of this routine not knowing that we
263
222
  # are going one down in the stack.
@@ -274,23 +233,41 @@ def logInfo(
274
233
  )
275
234
 
276
235
 
236
+ _F = TypeVar("_F", bound=Callable)
237
+
238
+
239
+ @overload
240
+ def timeMethod(_func: _F) -> _F: ...
241
+
242
+
243
+ @overload
277
244
  def timeMethod(
278
- _func: Optional[Any] = None,
279
245
  *,
280
- metadata: Optional[MutableMapping] = None,
281
- logger: Optional[logging.Logger] = None,
246
+ metadata: MutableMapping | None = None,
247
+ logger: LsstLoggers | None = None,
248
+ logLevel: int = logging.DEBUG,
249
+ ) -> Callable[[_F], _F]: ...
250
+
251
+
252
+ def timeMethod(
253
+ _func: Any | None = None,
254
+ *,
255
+ metadata: MutableMapping | None = None,
256
+ logger: LsstLoggers | None = None,
282
257
  logLevel: int = logging.DEBUG,
283
258
  ) -> Callable:
284
259
  """Measure duration of a method.
285
260
 
261
+ Set LSST_UTILS_DISABLE_TIMER in your environment to disable this method.
262
+
286
263
  Parameters
287
264
  ----------
288
- func
265
+ _func : `~collections.abc.Callable` or `None`
289
266
  The method to wrap.
290
267
  metadata : `collections.abc.MutableMapping`, optional
291
268
  Metadata to use as override if the instance object attached
292
269
  to this timer does not support a ``metadata`` property.
293
- logger : `logging.Logger`, optional
270
+ logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`, optional
294
271
  Logger to use when the class containing the decorated method does not
295
272
  have a ``log`` property.
296
273
  logLevel : `int`, optional
@@ -358,37 +335,66 @@ def timeMethod(
358
335
 
359
336
  return timeMethod_wrapper
360
337
 
338
+ if "LSST_UTILS_DISABLE_TIMER" in os.environ:
339
+ if _func is not None:
340
+ return _func
341
+ else:
342
+
343
+ def pass_through(func: Callable) -> Callable:
344
+ """Pass through decorator.
345
+
346
+ This decorator will not appear in the call stack.
347
+ """
348
+ return func
349
+
350
+ return pass_through
351
+
361
352
  if _func is None:
362
353
  return decorator_timer
363
354
  else:
364
355
  return decorator_timer(_func)
365
356
 
366
357
 
358
+ @dataclasses.dataclass
359
+ class _TimerResult:
360
+ duration: float = 0.0
361
+ """Duration of context in seconds."""
362
+ mem_current_usage: float | None = None
363
+ """Memory usage at end of context in requested units."""
364
+ mem_peak_delta: float | None = None
365
+ """Peak differential between entering and leaving context."""
366
+ mem_current_delta: float | None = None
367
+ """Difference in usage between entering and leaving context."""
368
+
369
+
367
370
  @contextmanager
368
371
  def time_this(
369
- log: Optional[LsstLoggers] = None,
370
- msg: Optional[str] = None,
372
+ log: LsstLoggers | None = None,
373
+ msg: str | None = None,
371
374
  level: int = logging.DEBUG,
372
- prefix: Optional[str] = "timer",
375
+ prefix: str | None = "timer",
373
376
  args: Iterable[Any] = (),
377
+ kwargs: dict[str, Any] | None = None,
374
378
  mem_usage: bool = False,
375
379
  mem_child: bool = False,
376
- mem_unit: u.Quantity = u.byte,
380
+ mem_unit: u.Unit = u.byte,
377
381
  mem_fmt: str = ".0f",
378
- ) -> Iterator[None]:
382
+ force_mem_usage: bool = False,
383
+ ) -> Iterator[_TimerResult]:
379
384
  """Time the enclosed block and issue a log message.
380
385
 
381
386
  Parameters
382
387
  ----------
383
388
  log : `logging.Logger`, optional
384
389
  Logger to use to report the timer message. The root logger will
385
- be used if none is given.
390
+ be used if none is given. Is also allowed to be a `structlog` bound
391
+ logger.
386
392
  msg : `str`, optional
387
393
  Context to include in log message.
388
394
  level : `int`, optional
389
395
  Python logging level to use to issue the log message. If the
390
- code block raises an exception the log message will automatically
391
- switch to level ERROR.
396
+ code block raises an exception the log message will include some
397
+ information about the exception that occurred.
392
398
  prefix : `str`, optional
393
399
  Prefix to use to prepend to the supplied logger to
394
400
  create a new logger to use instead. No prefix is used if the value
@@ -396,9 +402,15 @@ def time_this(
396
402
  args : iterable of any
397
403
  Additional parameters passed to the log command that should be
398
404
  written to ``msg``.
405
+ kwargs : `dict`, optional
406
+ Additional keyword parameters passed to the log command. If a Structlog
407
+ is used then these will be added to the structured data. Otherwise
408
+ they will be converted to a single string for inclusion in the log
409
+ message.
399
410
  mem_usage : `bool`, optional
400
411
  Flag indicating whether to include the memory usage in the report.
401
- Defaults, to False.
412
+ Defaults, to False. Does nothing if the log message will not be
413
+ generated.
402
414
  mem_child : `bool`, optional
403
415
  Flag indication whether to include memory usage of the child processes.
404
416
  mem_unit : `astropy.units.Unit`, optional
@@ -406,22 +418,49 @@ def time_this(
406
418
  mem_fmt : `str`, optional
407
419
  Format specifier to use when displaying values related to memory usage.
408
420
  Defaults to '.0f'.
421
+ force_mem_usage : `bool`, optional
422
+ If `True` memory usage is always calculated even if the log message
423
+ will not be delivered. Use this if you want the information recorded
424
+ by the context manager.
425
+
426
+ Yields
427
+ ------
428
+ timer_result : `_TimerResult`
429
+ Simple data class containing the duration of the block in seconds via
430
+ the ``duration`` property.
409
431
  """
410
432
  if log is None:
411
433
  log = logging.getLogger()
412
- if prefix:
434
+ is_structlog = _is_structlog_logger(log)
435
+ if prefix and not is_structlog:
436
+ # Struct log loggers do not have a name property and so the prefix
437
+ # is not applied to them.
413
438
  log_name = f"{prefix}.{log.name}" if not isinstance(log, logging.RootLogger) else prefix
414
439
  log = logging.getLogger(log_name)
415
440
 
416
- success = False
441
+ # Some structured data that can be used if we have been given a
442
+ # structlog logger.
443
+ structured_args: dict[str, Any] = {}
444
+
417
445
  start = time.time()
446
+
447
+ if mem_usage and not log.isEnabledFor(level):
448
+ mem_usage = False
449
+ if force_mem_usage:
450
+ mem_usage = True
451
+
418
452
  if mem_usage:
419
453
  current_usages_start = get_current_mem_usage()
420
454
  peak_usages_start = get_peak_mem_usage()
421
455
 
456
+ timer_result = _TimerResult()
457
+ errmsg = ""
422
458
  try:
423
- yield
424
- success = True
459
+ yield timer_result
460
+ except BaseException as e:
461
+ frame, lineno = list(traceback.walk_tb(e.__traceback__))[-1]
462
+ errmsg = f"{e!r} @ {frame.f_code.co_filename}:{lineno}"
463
+ raise
425
464
  finally:
426
465
  end = time.time()
427
466
 
@@ -435,16 +474,18 @@ def time_this(
435
474
  # mypy stop complaining when additional parameters will be added below.
436
475
  params = list(args) if args else []
437
476
 
438
- if not success:
439
- # Something went wrong so change the log level to indicate
440
- # this.
441
- level = logging.ERROR
477
+ duration = end - start
478
+ timer_result.duration = duration
442
479
 
443
480
  # Specify stacklevel to ensure the message is reported from the
444
481
  # caller (1 is this file, 2 is contextlib, 3 is user)
445
- params += (": " if msg else "", end - start)
482
+ params += (": " if msg else "", duration)
446
483
  msg += "%sTook %.4f seconds"
447
- if mem_usage and log.isEnabledFor(level):
484
+ structured_args["duration"] = duration
485
+ if errmsg:
486
+ params += (f" (timed code triggered exception of {errmsg!r})",)
487
+ msg += "%s"
488
+ if mem_usage:
448
489
  current_usages_end = get_current_mem_usage()
449
490
  peak_usages_end = get_peak_mem_usage()
450
491
 
@@ -463,18 +504,37 @@ def time_this(
463
504
  _LOG.warning("Invalid memory unit '%s', using '%s' instead", mem_unit, u.byte)
464
505
  mem_unit = u.byte
465
506
 
507
+ current_usage = current_usage.to(mem_unit)
508
+ current_delta = current_delta.to(mem_unit)
509
+ peak_delta = peak_delta.to(mem_unit)
510
+
511
+ timer_result.mem_current_usage = float(current_usage.value)
512
+ timer_result.mem_current_delta = float(current_delta.value)
513
+ timer_result.mem_peak_delta = float(peak_delta.value)
514
+
466
515
  msg += (
467
- f"; current memory usage: {current_usage.to(mem_unit):{mem_fmt}}"
468
- f", delta: {current_delta.to(mem_unit):{mem_fmt}}"
469
- f", peak delta: {peak_delta.to(mem_unit):{mem_fmt}}"
516
+ f"; current memory usage: {current_usage:{mem_fmt}}"
517
+ f", delta: {current_delta:{mem_fmt}}"
518
+ f", peak delta: {peak_delta:{mem_fmt}}"
470
519
  )
471
- log.log(level, msg, *params, stacklevel=3)
520
+ structured_args["mem_current_usage"] = float(current_usage.value)
521
+ structured_args["mem_current_delta"] = float(current_delta.value)
522
+ structured_args["mem_peak_delta"] = float(peak_delta.value)
523
+ if not is_structlog:
524
+ # Can only use the structured content if we have structlog logger
525
+ # but stacklevel is only supported by standard loggers.
526
+ structured_args = {"stacklevel": 3}
527
+ if kwargs is not None:
528
+ msg += " %s"
529
+ params += ("; ".join(f"{k}={v!r}" for k, v in kwargs.items()),)
530
+ elif kwargs:
531
+ structured_args.update(kwargs)
532
+
533
+ log.log(level, msg, *params, **structured_args)
472
534
 
473
535
 
474
536
  @contextmanager
475
- def profile(
476
- filename: Optional[str], log: Optional[logging.Logger] = None
477
- ) -> Iterator[Optional[cProfile.Profile]]:
537
+ def profile(filename: str | None, log: LsstLoggers | None = None) -> Iterator[cProfile.Profile | None]:
478
538
  """Profile the enclosed code block and save the result to a file.
479
539
 
480
540
  Parameters
@@ -482,7 +542,7 @@ def profile(
482
542
  filename : `str` or `None`
483
543
  Filename to which to write profile (profiling disabled if `None` or
484
544
  empty string).
485
- log : `logging.Logger`, optional
545
+ log : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`, optional
486
546
  Log object for logging the profile operations.
487
547
 
488
548
  Yields
@@ -523,3 +583,40 @@ def profile(
523
583
  profile.dump_stats(filename)
524
584
  if log is not None:
525
585
  log.info("cProfile stats written to %s", filename)
586
+
587
+
588
+ def duration_from_timeMethod(
589
+ metadata: MutableMapping | None, method_name: str, clock: str = "Cpu"
590
+ ) -> float | None:
591
+ """Parse the metadata entries from ``timeMethod`` and return a duration.
592
+
593
+ Parameters
594
+ ----------
595
+ metadata : `collections.abc.MutableMapping`
596
+ The Task metadata that timing metrics were added to.
597
+ method_name : `str`
598
+ Name of the timed method to extract a duration for.
599
+ clock : `str`, optional
600
+ Options are "Cpu", "User", "System", or "Utc".
601
+
602
+ Returns
603
+ -------
604
+ duration : `float`
605
+ The time elapsed between the start and end of the timed method.
606
+ """
607
+ if metadata is None:
608
+ return None
609
+ if clock.lower() == "utc":
610
+ start = metadata[method_name + "StartUtc"]
611
+ end = metadata[method_name + "EndUtc"]
612
+ else:
613
+ start = metadata[method_name + "Start" + clock + "Time"]
614
+ end = metadata[method_name + "End" + clock + "Time"]
615
+ if isinstance(start, list):
616
+ start = start[-1]
617
+ if isinstance(end, list):
618
+ end = end[-1]
619
+ if isinstance(start, str) and isinstance(end, str):
620
+ return (datetime.datetime.fromisoformat(end) - datetime.datetime.fromisoformat(start)).total_seconds()
621
+ else:
622
+ return end - start
lsst/utils/usage.py CHANGED
@@ -9,8 +9,9 @@
9
9
  # Use of this source code is governed by a 3-clause BSD-style
10
10
  # license that can be found in the LICENSE file.
11
11
 
12
- """Utilities for measuring resource consumption.
13
- """
12
+ """Utilities for measuring resource consumption."""
13
+
14
+ from __future__ import annotations
14
15
 
15
16
  __all__ = ["get_current_mem_usage", "get_peak_mem_usage"]
16
17
 
@@ -18,7 +19,6 @@ import dataclasses
18
19
  import platform
19
20
  import resource
20
21
  import time
21
- from typing import Dict, Tuple, Union
22
22
 
23
23
  import astropy.units as u
24
24
  import psutil
@@ -48,7 +48,7 @@ def _get_rusage_multiplier() -> int:
48
48
  _RUSAGE_MEMORY_MULTIPLIER = _get_rusage_multiplier()
49
49
 
50
50
 
51
- def get_current_mem_usage() -> Tuple[u.Quantity, u.Quantity]:
51
+ def get_current_mem_usage() -> tuple[u.Quantity, u.Quantity]:
52
52
  """Report current memory usage.
53
53
 
54
54
  Returns
@@ -72,7 +72,7 @@ def get_current_mem_usage() -> Tuple[u.Quantity, u.Quantity]:
72
72
  return usage_main, usage_child
73
73
 
74
74
 
75
- def get_peak_mem_usage() -> Tuple[u.Quantity, u.Quantity]:
75
+ def get_peak_mem_usage() -> tuple[u.Quantity, u.Quantity]:
76
76
  """Report peak memory usage.
77
77
 
78
78
  Returns
@@ -112,7 +112,7 @@ class _UsageInfo:
112
112
  voluntaryContextSwitches: int
113
113
  involuntaryContextSwitches: int
114
114
 
115
- def dict(self) -> Dict[str, Union[float, int]]:
115
+ def dict(self) -> dict[str, float | int]:
116
116
  return dataclasses.asdict(self)
117
117
 
118
118
 
lsst/utils/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "25.2023.600"
2
+ __version__ = "29.2025.4800"