lsst-utils 25.2023.2800__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 -1
- lsst/utils/argparsing.py +79 -0
- lsst/utils/classes.py +26 -9
- lsst/utils/db_auth.py +339 -0
- lsst/utils/deprecated.py +4 -4
- lsst/utils/inheritDoc.py +30 -4
- lsst/utils/introspection.py +242 -23
- lsst/utils/iteration.py +188 -2
- lsst/utils/logging.py +78 -12
- lsst/utils/packages.py +211 -55
- lsst/utils/plotting/__init__.py +15 -0
- lsst/utils/plotting/figures.py +159 -0
- lsst/utils/plotting/limits.py +12 -1
- lsst/utils/plotting/publication_plots.py +184 -0
- lsst/utils/plotting/rubin.mplstyle +46 -0
- lsst/utils/tests.py +112 -57
- lsst/utils/threads.py +1 -1
- lsst/utils/timer.py +189 -45
- lsst/utils/usage.py +2 -2
- lsst/utils/version.py +1 -1
- lsst/utils/wrappers.py +65 -22
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +18 -12
- lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
- lsst/utils/ellipsis.py +0 -66
- lsst/utils/get_caller_name.py +0 -47
- lsst_utils-25.2023.2800.dist-info/RECORD +0 -28
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/zip-safe +0 -0
lsst/utils/timer.py
CHANGED
|
@@ -10,25 +10,29 @@
|
|
|
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__ = ["
|
|
17
|
+
__all__ = ["duration_from_timeMethod", "logInfo", "profile", "timeMethod", "time_this"]
|
|
19
18
|
|
|
19
|
+
import dataclasses
|
|
20
20
|
import datetime
|
|
21
21
|
import functools
|
|
22
22
|
import logging
|
|
23
|
+
import os
|
|
24
|
+
import platform
|
|
25
|
+
import sys
|
|
23
26
|
import time
|
|
27
|
+
import traceback
|
|
24
28
|
from collections.abc import Callable, Collection, Iterable, Iterator, MutableMapping
|
|
25
|
-
from contextlib import contextmanager
|
|
26
|
-
from typing import TYPE_CHECKING, Any
|
|
29
|
+
from contextlib import contextmanager, suppress
|
|
30
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
27
31
|
|
|
28
32
|
from astropy import units as u
|
|
29
33
|
|
|
30
34
|
from .introspection import find_outside_stacklevel
|
|
31
|
-
from .logging import LsstLoggers
|
|
35
|
+
from .logging import LsstLoggers, _is_structlog_logger
|
|
32
36
|
from .usage import _get_current_rusage, get_current_mem_usage, get_peak_mem_usage
|
|
33
37
|
|
|
34
38
|
if TYPE_CHECKING:
|
|
@@ -75,10 +79,10 @@ def logPairs(
|
|
|
75
79
|
pairs: Collection[tuple[str, Any]],
|
|
76
80
|
logLevel: int = logging.DEBUG,
|
|
77
81
|
metadata: MutableMapping | None = None,
|
|
78
|
-
logger:
|
|
82
|
+
logger: LsstLoggers | None = None,
|
|
79
83
|
stacklevel: int | None = None,
|
|
80
84
|
) -> None:
|
|
81
|
-
"""Log ``(name, value)`` pairs to ``obj.metadata`` and ``obj.log
|
|
85
|
+
"""Log ``(name, value)`` pairs to ``obj.metadata`` and ``obj.log``.
|
|
82
86
|
|
|
83
87
|
Parameters
|
|
84
88
|
----------
|
|
@@ -98,7 +102,7 @@ def logPairs(
|
|
|
98
102
|
Log level (an `logging` level constant, such as `logging.DEBUG`).
|
|
99
103
|
metadata : `collections.abc.MutableMapping`, optional
|
|
100
104
|
Metadata object to write entries to. Ignored if `None`.
|
|
101
|
-
logger : `logging.Logger`
|
|
105
|
+
logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`
|
|
102
106
|
Log object to write entries to. Ignored if `None`.
|
|
103
107
|
stacklevel : `int`, optional
|
|
104
108
|
The stack level to pass to the logger to adjust which stack frame
|
|
@@ -109,15 +113,13 @@ def logPairs(
|
|
|
109
113
|
"""
|
|
110
114
|
if obj is not None:
|
|
111
115
|
if metadata is None:
|
|
112
|
-
|
|
116
|
+
with suppress(AttributeError):
|
|
113
117
|
metadata = obj.metadata
|
|
114
|
-
|
|
115
|
-
pass
|
|
118
|
+
|
|
116
119
|
if logger is None:
|
|
117
|
-
|
|
120
|
+
with suppress(AttributeError):
|
|
118
121
|
logger = obj.log
|
|
119
|
-
|
|
120
|
-
pass
|
|
122
|
+
|
|
121
123
|
strList = []
|
|
122
124
|
for name, value in pairs:
|
|
123
125
|
if metadata is not None:
|
|
@@ -142,7 +144,7 @@ def logInfo(
|
|
|
142
144
|
prefix: str,
|
|
143
145
|
logLevel: int = logging.DEBUG,
|
|
144
146
|
metadata: MutableMapping | None = None,
|
|
145
|
-
logger:
|
|
147
|
+
logger: LsstLoggers | None = None,
|
|
146
148
|
stacklevel: int | None = None,
|
|
147
149
|
) -> None:
|
|
148
150
|
"""Log timer information to ``obj.metadata`` and ``obj.log``.
|
|
@@ -167,7 +169,7 @@ def logInfo(
|
|
|
167
169
|
Log level (a `logging` level constant, such as `logging.DEBUG`).
|
|
168
170
|
metadata : `collections.abc.MutableMapping`, optional
|
|
169
171
|
Metadata object to write entries to, overriding ``obj.metadata``.
|
|
170
|
-
logger : `logging.Logger`
|
|
172
|
+
logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`
|
|
171
173
|
Log object to write entries to, overriding ``obj.log``.
|
|
172
174
|
stacklevel : `int`, optional
|
|
173
175
|
The stack level to pass to the logger to adjust which stack frame
|
|
@@ -197,18 +199,24 @@ def logInfo(
|
|
|
197
199
|
* Version 1: ``MaxResidentSetSize`` will be stored in bytes.
|
|
198
200
|
"""
|
|
199
201
|
if metadata is None and obj is not None:
|
|
200
|
-
|
|
202
|
+
with suppress(AttributeError):
|
|
201
203
|
metadata = obj.metadata
|
|
202
|
-
|
|
203
|
-
pass
|
|
204
|
+
|
|
204
205
|
if metadata is not None:
|
|
205
206
|
# Log messages already have timestamps.
|
|
206
|
-
|
|
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()
|
|
207
212
|
_add_to_metadata(metadata, name=prefix + "Utc", value=utcStr)
|
|
208
213
|
|
|
209
214
|
# Force a version number into the metadata.
|
|
210
215
|
# v1: Ensure that max_rss field is always bytes.
|
|
211
216
|
metadata["__version__"] = 1
|
|
217
|
+
|
|
218
|
+
# Add node information.
|
|
219
|
+
metadata["nodeName"] = platform.node()
|
|
212
220
|
if stacklevel is not None:
|
|
213
221
|
# Account for the caller of this routine not knowing that we
|
|
214
222
|
# are going one down in the stack.
|
|
@@ -225,23 +233,41 @@ def logInfo(
|
|
|
225
233
|
)
|
|
226
234
|
|
|
227
235
|
|
|
236
|
+
_F = TypeVar("_F", bound=Callable)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@overload
|
|
240
|
+
def timeMethod(_func: _F) -> _F: ...
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@overload
|
|
244
|
+
def timeMethod(
|
|
245
|
+
*,
|
|
246
|
+
metadata: MutableMapping | None = None,
|
|
247
|
+
logger: LsstLoggers | None = None,
|
|
248
|
+
logLevel: int = logging.DEBUG,
|
|
249
|
+
) -> Callable[[_F], _F]: ...
|
|
250
|
+
|
|
251
|
+
|
|
228
252
|
def timeMethod(
|
|
229
253
|
_func: Any | None = None,
|
|
230
254
|
*,
|
|
231
255
|
metadata: MutableMapping | None = None,
|
|
232
|
-
logger:
|
|
256
|
+
logger: LsstLoggers | None = None,
|
|
233
257
|
logLevel: int = logging.DEBUG,
|
|
234
258
|
) -> Callable:
|
|
235
259
|
"""Measure duration of a method.
|
|
236
260
|
|
|
261
|
+
Set LSST_UTILS_DISABLE_TIMER in your environment to disable this method.
|
|
262
|
+
|
|
237
263
|
Parameters
|
|
238
264
|
----------
|
|
239
|
-
|
|
265
|
+
_func : `~collections.abc.Callable` or `None`
|
|
240
266
|
The method to wrap.
|
|
241
267
|
metadata : `collections.abc.MutableMapping`, optional
|
|
242
268
|
Metadata to use as override if the instance object attached
|
|
243
269
|
to this timer does not support a ``metadata`` property.
|
|
244
|
-
logger : `logging.Logger`, optional
|
|
270
|
+
logger : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`, optional
|
|
245
271
|
Logger to use when the class containing the decorated method does not
|
|
246
272
|
have a ``log`` property.
|
|
247
273
|
logLevel : `int`, optional
|
|
@@ -309,12 +335,38 @@ def timeMethod(
|
|
|
309
335
|
|
|
310
336
|
return timeMethod_wrapper
|
|
311
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
|
+
|
|
312
352
|
if _func is None:
|
|
313
353
|
return decorator_timer
|
|
314
354
|
else:
|
|
315
355
|
return decorator_timer(_func)
|
|
316
356
|
|
|
317
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
|
+
|
|
318
370
|
@contextmanager
|
|
319
371
|
def time_this(
|
|
320
372
|
log: LsstLoggers | None = None,
|
|
@@ -322,24 +374,27 @@ def time_this(
|
|
|
322
374
|
level: int = logging.DEBUG,
|
|
323
375
|
prefix: str | None = "timer",
|
|
324
376
|
args: Iterable[Any] = (),
|
|
377
|
+
kwargs: dict[str, Any] | None = None,
|
|
325
378
|
mem_usage: bool = False,
|
|
326
379
|
mem_child: bool = False,
|
|
327
|
-
mem_unit: u.
|
|
380
|
+
mem_unit: u.Unit = u.byte,
|
|
328
381
|
mem_fmt: str = ".0f",
|
|
329
|
-
|
|
382
|
+
force_mem_usage: bool = False,
|
|
383
|
+
) -> Iterator[_TimerResult]:
|
|
330
384
|
"""Time the enclosed block and issue a log message.
|
|
331
385
|
|
|
332
386
|
Parameters
|
|
333
387
|
----------
|
|
334
388
|
log : `logging.Logger`, optional
|
|
335
389
|
Logger to use to report the timer message. The root logger will
|
|
336
|
-
be used if none is given.
|
|
390
|
+
be used if none is given. Is also allowed to be a `structlog` bound
|
|
391
|
+
logger.
|
|
337
392
|
msg : `str`, optional
|
|
338
393
|
Context to include in log message.
|
|
339
394
|
level : `int`, optional
|
|
340
395
|
Python logging level to use to issue the log message. If the
|
|
341
|
-
code block raises an exception the log message will
|
|
342
|
-
|
|
396
|
+
code block raises an exception the log message will include some
|
|
397
|
+
information about the exception that occurred.
|
|
343
398
|
prefix : `str`, optional
|
|
344
399
|
Prefix to use to prepend to the supplied logger to
|
|
345
400
|
create a new logger to use instead. No prefix is used if the value
|
|
@@ -347,9 +402,15 @@ def time_this(
|
|
|
347
402
|
args : iterable of any
|
|
348
403
|
Additional parameters passed to the log command that should be
|
|
349
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.
|
|
350
410
|
mem_usage : `bool`, optional
|
|
351
411
|
Flag indicating whether to include the memory usage in the report.
|
|
352
|
-
Defaults, to False.
|
|
412
|
+
Defaults, to False. Does nothing if the log message will not be
|
|
413
|
+
generated.
|
|
353
414
|
mem_child : `bool`, optional
|
|
354
415
|
Flag indication whether to include memory usage of the child processes.
|
|
355
416
|
mem_unit : `astropy.units.Unit`, optional
|
|
@@ -357,26 +418,49 @@ def time_this(
|
|
|
357
418
|
mem_fmt : `str`, optional
|
|
358
419
|
Format specifier to use when displaying values related to memory usage.
|
|
359
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.
|
|
360
431
|
"""
|
|
361
432
|
if log is None:
|
|
362
433
|
log = logging.getLogger()
|
|
363
|
-
|
|
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.
|
|
364
438
|
log_name = f"{prefix}.{log.name}" if not isinstance(log, logging.RootLogger) else prefix
|
|
365
439
|
log = logging.getLogger(log_name)
|
|
366
440
|
|
|
367
|
-
|
|
441
|
+
# Some structured data that can be used if we have been given a
|
|
442
|
+
# structlog logger.
|
|
443
|
+
structured_args: dict[str, Any] = {}
|
|
444
|
+
|
|
368
445
|
start = time.time()
|
|
369
446
|
|
|
370
447
|
if mem_usage and not log.isEnabledFor(level):
|
|
371
448
|
mem_usage = False
|
|
449
|
+
if force_mem_usage:
|
|
450
|
+
mem_usage = True
|
|
372
451
|
|
|
373
452
|
if mem_usage:
|
|
374
453
|
current_usages_start = get_current_mem_usage()
|
|
375
454
|
peak_usages_start = get_peak_mem_usage()
|
|
376
455
|
|
|
456
|
+
timer_result = _TimerResult()
|
|
457
|
+
errmsg = ""
|
|
377
458
|
try:
|
|
378
|
-
yield
|
|
379
|
-
|
|
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
|
|
380
464
|
finally:
|
|
381
465
|
end = time.time()
|
|
382
466
|
|
|
@@ -390,15 +474,17 @@ def time_this(
|
|
|
390
474
|
# mypy stop complaining when additional parameters will be added below.
|
|
391
475
|
params = list(args) if args else []
|
|
392
476
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# this.
|
|
396
|
-
level = logging.ERROR
|
|
477
|
+
duration = end - start
|
|
478
|
+
timer_result.duration = duration
|
|
397
479
|
|
|
398
480
|
# Specify stacklevel to ensure the message is reported from the
|
|
399
481
|
# caller (1 is this file, 2 is contextlib, 3 is user)
|
|
400
|
-
params += (": " if msg else "",
|
|
482
|
+
params += (": " if msg else "", duration)
|
|
401
483
|
msg += "%sTook %.4f seconds"
|
|
484
|
+
structured_args["duration"] = duration
|
|
485
|
+
if errmsg:
|
|
486
|
+
params += (f" (timed code triggered exception of {errmsg!r})",)
|
|
487
|
+
msg += "%s"
|
|
402
488
|
if mem_usage:
|
|
403
489
|
current_usages_end = get_current_mem_usage()
|
|
404
490
|
peak_usages_end = get_peak_mem_usage()
|
|
@@ -418,16 +504,37 @@ def time_this(
|
|
|
418
504
|
_LOG.warning("Invalid memory unit '%s', using '%s' instead", mem_unit, u.byte)
|
|
419
505
|
mem_unit = u.byte
|
|
420
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
|
+
|
|
421
515
|
msg += (
|
|
422
|
-
f"; current memory usage: {current_usage
|
|
423
|
-
f", delta: {current_delta
|
|
424
|
-
f", peak delta: {peak_delta
|
|
516
|
+
f"; current memory usage: {current_usage:{mem_fmt}}"
|
|
517
|
+
f", delta: {current_delta:{mem_fmt}}"
|
|
518
|
+
f", peak delta: {peak_delta:{mem_fmt}}"
|
|
425
519
|
)
|
|
426
|
-
|
|
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)
|
|
427
534
|
|
|
428
535
|
|
|
429
536
|
@contextmanager
|
|
430
|
-
def profile(filename: str | None, log:
|
|
537
|
+
def profile(filename: str | None, log: LsstLoggers | None = None) -> Iterator[cProfile.Profile | None]:
|
|
431
538
|
"""Profile the enclosed code block and save the result to a file.
|
|
432
539
|
|
|
433
540
|
Parameters
|
|
@@ -435,7 +542,7 @@ def profile(filename: str | None, log: logging.Logger | None = None) -> Iterator
|
|
|
435
542
|
filename : `str` or `None`
|
|
436
543
|
Filename to which to write profile (profiling disabled if `None` or
|
|
437
544
|
empty string).
|
|
438
|
-
log : `logging.Logger`, optional
|
|
545
|
+
log : `logging.Logger` or `lsst.utils.logging.LsstLogAdapter`, optional
|
|
439
546
|
Log object for logging the profile operations.
|
|
440
547
|
|
|
441
548
|
Yields
|
|
@@ -476,3 +583,40 @@ def profile(filename: str | None, log: logging.Logger | None = None) -> Iterator
|
|
|
476
583
|
profile.dump_stats(filename)
|
|
477
584
|
if log is not None:
|
|
478
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,8 @@
|
|
|
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
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
__all__ = ["get_current_mem_usage", "get_peak_mem_usage"]
|
lsst/utils/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "
|
|
2
|
+
__version__ = "29.2025.4800"
|
lsst/utils/wrappers.py
CHANGED
|
@@ -13,10 +13,11 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import sys
|
|
15
15
|
import types
|
|
16
|
+
from typing import Any
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
|
|
19
|
-
__all__ = ("
|
|
20
|
+
__all__ = ("TemplateMeta", "continueClass", "inClass")
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
INTRINSIC_SPECIAL_ATTRIBUTES = frozenset(
|
|
@@ -34,13 +35,25 @@ INTRINSIC_SPECIAL_ATTRIBUTES = frozenset(
|
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
def isAttributeSafeToTransfer(name, value):
|
|
38
|
+
def isAttributeSafeToTransfer(name: str, value: Any) -> bool:
|
|
38
39
|
"""Return True if an attribute is safe to monkeypatch-transfer to another
|
|
39
40
|
class.
|
|
40
41
|
|
|
41
42
|
This rejects special methods that are defined automatically for all
|
|
42
43
|
classes, leaving only those explicitly defined in a class decorated by
|
|
43
44
|
`continueClass` or registered with an instance of `TemplateMeta`.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
name : `str`
|
|
49
|
+
The name of the attribute to check.
|
|
50
|
+
value : `~typing.Any`
|
|
51
|
+
The value of the attribute.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
`bool`
|
|
56
|
+
Whether the attribute is safe to monkeypatch-transfer.
|
|
44
57
|
"""
|
|
45
58
|
if name.startswith("__") and (
|
|
46
59
|
value is getattr(object, name, None) or name in INTRINSIC_SPECIAL_ATTRIBUTES
|
|
@@ -60,6 +73,7 @@ def continueClass(cls):
|
|
|
60
73
|
class Foo:
|
|
61
74
|
pass
|
|
62
75
|
|
|
76
|
+
|
|
63
77
|
@continueClass
|
|
64
78
|
class Foo:
|
|
65
79
|
def run(self):
|
|
@@ -78,7 +92,6 @@ def continueClass(cls):
|
|
|
78
92
|
Python's built-in `super` function does not behave properly in classes
|
|
79
93
|
decorated with `continueClass`. Base class methods must be invoked
|
|
80
94
|
directly using their explicit types instead.
|
|
81
|
-
|
|
82
95
|
"""
|
|
83
96
|
orig = getattr(sys.modules[cls.__module__], cls.__name__)
|
|
84
97
|
for name in dir(cls):
|
|
@@ -93,9 +106,17 @@ def continueClass(cls):
|
|
|
93
106
|
return orig
|
|
94
107
|
|
|
95
108
|
|
|
96
|
-
def inClass(cls, name=None):
|
|
109
|
+
def inClass(cls, name: str | None = None):
|
|
97
110
|
"""Add the decorated function to the given class as a method.
|
|
98
111
|
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
name : `str` or `None`, optional
|
|
115
|
+
Name to be associated with the decorated function if the default
|
|
116
|
+
can not be determined.
|
|
117
|
+
|
|
118
|
+
Examples
|
|
119
|
+
--------
|
|
99
120
|
For example:
|
|
100
121
|
|
|
101
122
|
.. code-block:: python
|
|
@@ -103,6 +124,7 @@ def inClass(cls, name=None):
|
|
|
103
124
|
class Foo:
|
|
104
125
|
pass
|
|
105
126
|
|
|
127
|
+
|
|
106
128
|
@inClass(Foo)
|
|
107
129
|
def run(self):
|
|
108
130
|
return None
|
|
@@ -115,6 +137,8 @@ def inClass(cls, name=None):
|
|
|
115
137
|
def run(self):
|
|
116
138
|
return None
|
|
117
139
|
|
|
140
|
+
Notes
|
|
141
|
+
-----
|
|
118
142
|
Standard decorators like ``classmethod``, ``staticmethod``, and
|
|
119
143
|
``property`` may be used *after* this decorator. Custom decorators
|
|
120
144
|
may only be used if they return an object with a ``__name__`` attribute
|
|
@@ -169,9 +193,11 @@ class TemplateMeta(type):
|
|
|
169
193
|
import numpy as np
|
|
170
194
|
from ._image import ImageF, ImageD
|
|
171
195
|
|
|
196
|
+
|
|
172
197
|
class Image(metaclass=TemplateMeta):
|
|
173
198
|
pass
|
|
174
199
|
|
|
200
|
+
|
|
175
201
|
Image.register(np.float32, ImageF)
|
|
176
202
|
Image.register(np.float64, ImageD)
|
|
177
203
|
Image.alias("F", ImageF)
|
|
@@ -212,10 +238,10 @@ class TemplateMeta(type):
|
|
|
212
238
|
.. code-block:: python
|
|
213
239
|
|
|
214
240
|
class Image(metaclass=TemplateMeta):
|
|
215
|
-
|
|
216
241
|
def sum(self):
|
|
217
242
|
return np.sum(self.getArray())
|
|
218
243
|
|
|
244
|
+
|
|
219
245
|
Image.register(np.float32, ImageF)
|
|
220
246
|
Image.register(np.float64, ImageD)
|
|
221
247
|
|
|
@@ -250,7 +276,6 @@ class TemplateMeta(type):
|
|
|
250
276
|
Python's built-in `super` function does not behave properly in classes
|
|
251
277
|
that have `TemplateMeta` as their metaclass (which should be rare, as
|
|
252
278
|
TemplateMeta ABCs will have base classes of their own)..
|
|
253
|
-
|
|
254
279
|
"""
|
|
255
280
|
|
|
256
281
|
def __new__(cls, name, bases, attrs):
|
|
@@ -307,20 +332,14 @@ class TemplateMeta(type):
|
|
|
307
332
|
# any registered type or true subclass thereof.
|
|
308
333
|
if subclass in cls._registry.values():
|
|
309
334
|
return True
|
|
310
|
-
for v in cls._registry.values()
|
|
311
|
-
if issubclass(subclass, v):
|
|
312
|
-
return True
|
|
313
|
-
return False
|
|
335
|
+
return any(issubclass(subclass, v) for v in cls._registry.values())
|
|
314
336
|
|
|
315
337
|
def __instancecheck__(cls, instance):
|
|
316
338
|
# Special method hook for the isinstance built-in: we return true for
|
|
317
339
|
# an instance of any registered type or true subclass thereof.
|
|
318
340
|
if type(instance) in cls._registry.values():
|
|
319
341
|
return True
|
|
320
|
-
for v in cls._registry.values()
|
|
321
|
-
if isinstance(instance, v):
|
|
322
|
-
return True
|
|
323
|
-
return False
|
|
342
|
+
return any(isinstance(instance, v) for v in cls._registry.values())
|
|
324
343
|
|
|
325
344
|
def __subclasses__(cls):
|
|
326
345
|
"""Return a tuple of all classes that inherit from this class."""
|
|
@@ -329,11 +348,18 @@ class TemplateMeta(type):
|
|
|
329
348
|
# functionality.
|
|
330
349
|
return tuple(set(cls._registry.values()))
|
|
331
350
|
|
|
332
|
-
def register(cls, key, subclass):
|
|
351
|
+
def register(cls, key, subclass) -> None:
|
|
333
352
|
"""Register a subclass of this ABC with the given key (a string,
|
|
334
353
|
number, type, or other hashable).
|
|
335
354
|
|
|
336
355
|
Register may only be called once for a given key or a given subclass.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
key : `str` or `numbers.Number` or `None` or `collections.abc.Hashable`
|
|
360
|
+
Key to use for registration.
|
|
361
|
+
subclass : `type`
|
|
362
|
+
Subclass to register.
|
|
337
363
|
"""
|
|
338
364
|
if key is None:
|
|
339
365
|
raise ValueError("None may not be used as a key.")
|
|
@@ -396,17 +422,22 @@ class TemplateMeta(type):
|
|
|
396
422
|
setattrSafe(p, k)
|
|
397
423
|
else:
|
|
398
424
|
raise ValueError(
|
|
399
|
-
"key must have {} elements (one for each of {})"
|
|
400
|
-
len(cls.TEMPLATE_PARAMS), cls.TEMPLATE_PARAMS
|
|
401
|
-
)
|
|
425
|
+
f"key must have {len(cls.TEMPLATE_PARAMS)} elements (one for each of {cls.TEMPLATE_PARAMS})"
|
|
402
426
|
)
|
|
403
427
|
|
|
404
428
|
for name, attr in cls._inherited.items():
|
|
405
429
|
setattr(subclass, name, attr)
|
|
406
430
|
|
|
407
|
-
def alias(cls, key, subclass):
|
|
431
|
+
def alias(cls, key, subclass) -> None:
|
|
408
432
|
"""Add an alias that allows an existing subclass to be accessed with a
|
|
409
433
|
different key.
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
key : `str` or `numbers.Number` or `None` or `collections.abc.Hashable`
|
|
438
|
+
Key to use for aliasing.
|
|
439
|
+
subclass : `type`
|
|
440
|
+
Subclass to alias.
|
|
410
441
|
"""
|
|
411
442
|
if key is None:
|
|
412
443
|
raise ValueError("None may not be used as a key.")
|
|
@@ -449,8 +480,20 @@ class TemplateMeta(type):
|
|
|
449
480
|
"""Return an iterable of (key, subclass) pairs."""
|
|
450
481
|
return cls._registry.items()
|
|
451
482
|
|
|
452
|
-
def get(cls, key, default=None):
|
|
453
|
-
"""Return the subclass associated with the given key
|
|
454
|
-
|
|
483
|
+
def get(cls, key, default=None) -> type:
|
|
484
|
+
"""Return the subclass associated with the given key.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
key : `~collections.abc.Hashable`
|
|
489
|
+
Key to query.
|
|
490
|
+
default : `~typing.Any` or `None`, optional
|
|
491
|
+
Default value to return if ``key`` is not found.
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
`type`
|
|
496
|
+
Subclass with the given key. Includes aliases. Returns ``default``
|
|
497
|
+
if the key is not recognized.
|
|
455
498
|
"""
|
|
456
499
|
return cls._registry.get(key, default)
|