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/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__ = ["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
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: logging.Logger | None = None,
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
- try:
116
+ with suppress(AttributeError):
113
117
  metadata = obj.metadata
114
- except AttributeError:
115
- pass
118
+
116
119
  if logger is None:
117
- try:
120
+ with suppress(AttributeError):
118
121
  logger = obj.log
119
- except AttributeError:
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: logging.Logger | None = None,
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
- try:
202
+ with suppress(AttributeError):
201
203
  metadata = obj.metadata
202
- except AttributeError:
203
- pass
204
+
204
205
  if metadata is not None:
205
206
  # Log messages already have timestamps.
206
- 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()
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: logging.Logger | None = None,
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
- func
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.Quantity = u.byte,
380
+ mem_unit: u.Unit = u.byte,
328
381
  mem_fmt: str = ".0f",
329
- ) -> Iterator[None]:
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 automatically
342
- switch to level ERROR.
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
- 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.
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
- 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
+
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
- 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
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
- if not success:
394
- # Something went wrong so change the log level to indicate
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 "", end - start)
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.to(mem_unit):{mem_fmt}}"
423
- f", delta: {current_delta.to(mem_unit):{mem_fmt}}"
424
- 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}}"
425
519
  )
426
- 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)
427
534
 
428
535
 
429
536
  @contextmanager
430
- def profile(filename: str | None, log: logging.Logger | None = None) -> Iterator[cProfile.Profile | None]:
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__ = "25.2023.2800"
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__ = ("continueClass", "inClass", "TemplateMeta")
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 {})".format(
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 (including
454
- aliases), or ``default`` if the key is not recognized.
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)