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/logging.py CHANGED
@@ -14,10 +14,11 @@ 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
 
@@ -27,13 +28,24 @@ import time
27
28
  from collections.abc import Generator
28
29
  from contextlib import contextmanager
29
30
  from logging import LoggerAdapter
30
- from typing import Any, Union
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]
37
49
 
38
50
  # log level for trace (verbose debug).
39
51
  TRACE = 5
@@ -44,6 +56,23 @@ VERBOSE = (logging.INFO + logging.DEBUG) // 2
44
56
  logging.addLevelName(VERBOSE, "VERBOSE")
45
57
 
46
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
+
47
76
  def _calculate_base_stacklevel(default: int, offset: int) -> int:
48
77
  """Calculate the default logging stacklevel to use.
49
78
 
@@ -236,6 +265,15 @@ class LsstLogAdapter(LoggerAdapter):
236
265
 
237
266
  Arguments are as for `logging.info`.
238
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`.
239
277
  """
240
278
  # There is no other way to achieve this other than a special logger
241
279
  # method.
@@ -248,6 +286,15 @@ class LsstLogAdapter(LoggerAdapter):
248
286
 
249
287
  Arguments are as for `logging.info`.
250
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`.
251
298
  """
252
299
  # There is no other way to achieve this other than a special logger
253
300
  # method.
@@ -280,12 +327,21 @@ class LsstLogAdapter(LoggerAdapter):
280
327
  def addHandler(self, handler: logging.Handler) -> None:
281
328
  """Add a handler to this logger.
282
329
 
283
- 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.
284
334
  """
285
335
  self.logger.addHandler(handler)
286
336
 
287
337
  def removeHandler(self, handler: logging.Handler) -> None:
288
- """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
+ """
289
345
  self.logger.removeHandler(handler)
290
346
 
291
347
 
@@ -321,7 +377,7 @@ def getLogger(name: str | None = None, logger: logging.Logger | None = None) ->
321
377
  return LsstLogAdapter(logger, {})
322
378
 
323
379
 
324
- LsstLoggers = Union[logging.Logger, LsstLogAdapter]
380
+ LsstLoggers: TypeAlias = logging.Logger | LsstLogAdapter
325
381
 
326
382
 
327
383
  def getTraceLogger(logger: str | LsstLoggers, trace_level: int) -> LsstLogAdapter:
@@ -354,22 +410,28 @@ class PeriodicLogger:
354
410
  be useful to issue a log message periodically to show that the
355
411
  algorithm is progressing.
356
412
 
413
+ The first time threshold is counted from object construction, so in general
414
+ the first call to `log` does not log.
415
+
357
416
  Parameters
358
417
  ----------
359
418
  logger : `logging.Logger` or `LsstLogAdapter`
360
419
  Logger to use when issuing a message.
361
420
  interval : `float`
362
- The minimum interval between log messages. If `None` the class
363
- default will be used.
421
+ The minimum interval in seconds between log messages. If `None`,
422
+ `LOGGING_INTERVAL` will be used.
364
423
  level : `int`, optional
365
- Log level to use when issuing messages.
424
+ Log level to use when issuing messages, default is
425
+ `~logging.INFO`.
366
426
  """
367
427
 
368
428
  LOGGING_INTERVAL = 600.0
369
- """Default interval between log messages."""
429
+ """Default interval between log messages in seconds."""
370
430
 
371
- def __init__(self, logger: LsstLoggers, interval: float | None = None, level: int = VERBOSE):
431
+ def __init__(self, logger: LsstLoggers, interval: float | None = None, level: int = logging.INFO):
372
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.
373
435
  self.interval = interval if interval is not None else self.LOGGING_INTERVAL
374
436
  self.level = level
375
437
  self.next_log_time = time.time() + self.interval
@@ -385,12 +447,16 @@ class PeriodicLogger:
385
447
  def log(self, msg: str, *args: Any) -> bool:
386
448
  """Issue a log message if the interval has elapsed.
387
449
 
450
+ The interval is measured from the previous call to ``log``, or from the
451
+ creation of this object.
452
+
388
453
  Parameters
389
454
  ----------
390
455
  msg : `str`
391
456
  Message to issue if the time has been exceeded.
392
457
  *args : Any
393
- Parameters to be passed to the log system.
458
+ Arguments to be merged into the message string, as described under
459
+ `logging.Logger.debug`.
394
460
 
395
461
  Returns
396
462
  -------
lsst/utils/packages.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
- """Determine which packages are being used in the system and their versions.
13
- """
12
+ """Determine which packages are being used in the system and their versions."""
14
13
 
15
14
  from __future__ import annotations
16
15
 
16
+ import contextlib
17
17
  import hashlib
18
18
  import importlib
19
19
  import io
@@ -26,19 +26,21 @@ import subprocess
26
26
  import sys
27
27
  import types
28
28
  from collections.abc import Mapping
29
- from functools import lru_cache
30
- from typing import Any
29
+ from functools import cache, lru_cache
30
+ from importlib.metadata import packages_distributions
31
+ from typing import Any, ClassVar
31
32
 
32
33
  import yaml
33
34
 
34
35
  log = logging.getLogger(__name__)
35
36
 
36
37
  __all__ = [
37
- "getVersionFromPythonModule",
38
- "getPythonPackages",
39
- "getEnvironmentPackages",
40
- "getCondaPackages",
41
38
  "Packages",
39
+ "getAllPythonDistributions",
40
+ "getCondaPackages",
41
+ "getEnvironmentPackages",
42
+ "getPythonPackages",
43
+ "getVersionFromPythonModule",
42
44
  ]
43
45
 
44
46
 
@@ -48,7 +50,9 @@ BUILDTIME = {"boost", "eigen", "tmv"}
48
50
  # Python modules to attempt to load so we can try to get the version
49
51
  # We do this because the version only appears to be available from python,
50
52
  # but we use the library
51
- PYTHON = {"galsim"}
53
+ PYTHON: set[str] = set()
54
+
55
+ SPECIAL_NAMESPACES = {"lsst"}
52
56
 
53
57
  # Packages that don't seem to have a mechanism for reporting the runtime
54
58
  # version. We need to guess the version from the environment
@@ -73,6 +77,7 @@ def getVersionFromPythonModule(module: types.ModuleType) -> str:
73
77
  Returns
74
78
  -------
75
79
  version : `str`
80
+ The version of the python module.
76
81
 
77
82
  Raises
78
83
  ------
@@ -96,14 +101,37 @@ def getVersionFromPythonModule(module: types.ModuleType) -> str:
96
101
  return str(version)
97
102
 
98
103
 
104
+ @cache
105
+ def getAllPythonDistributions() -> dict[str, str]:
106
+ """Get the versions for all Python distributions that are installed.
107
+
108
+ Returns
109
+ -------
110
+ packages : `dict` [ `str`, `str` ]
111
+ Keys are distribution names; values are their versions.
112
+ Unlike `getPythonPackages` this function will not include
113
+ standard library packages defined in `sys.stdlib_module_names` but
114
+ will include a special ``python`` key reporting the Python version.
115
+
116
+ Notes
117
+ -----
118
+ If this function is called a second time an identical result will be
119
+ returned even if a new distribution has been installed.
120
+ """
121
+ packages = {"python": sys.version}
122
+
123
+ for dist in importlib.metadata.distributions():
124
+ packages[dist.name] = dist.version
125
+ return _mangle_lsst_package_names(packages)
126
+
127
+
99
128
  def getPythonPackages() -> dict[str, str]:
100
129
  """Get imported python packages and their versions.
101
130
 
102
131
  Returns
103
132
  -------
104
- packages : `dict`
105
- Keys (type `str`) are package names; values (type `str`) are their
106
- versions.
133
+ packages : `dict` [ `str`, `str` ]
134
+ Keys are package names; values are their versions.
107
135
 
108
136
  Notes
109
137
  -----
@@ -111,20 +139,29 @@ def getPythonPackages() -> dict[str, str]:
111
139
  module. Note, therefore, that we can only report on modules that have
112
140
  *already* been imported.
113
141
 
142
+ Python standard library packages are not included in the report. A
143
+ ``python`` key is inserted that records the Python version.
144
+
114
145
  We don't include any module for which we cannot determine a version.
146
+
147
+ Whilst distribution names are used to determine package versions, the
148
+ key returned for the package version is the package name that was imported.
149
+ This means that ``yaml`` will appear as the version key even though the
150
+ distribution would be called ``PyYAML``.
115
151
  """
116
152
  # Attempt to import libraries that only report their version in python
117
153
  for module_name in PYTHON:
118
- try:
154
+ # If it's not available we continue.
155
+ with contextlib.suppress(Exception):
119
156
  importlib.import_module(module_name)
120
- except Exception:
121
- pass # It's not available, so don't care
157
+
158
+ package_dist = packages_distributions()
122
159
 
123
160
  packages = {"python": sys.version}
124
161
 
125
162
  # Not iterating with sys.modules.iteritems() because it's not atomic and
126
163
  # subject to race conditions
127
- module_names = list(sys.modules.keys())
164
+ module_names = sorted(sys.modules.keys())
128
165
 
129
166
  # Use knowledge of package hierarchy to find the versions rather than
130
167
  # using each name independently. Group all the module names into the
@@ -144,7 +181,7 @@ def getPythonPackages() -> dict[str, str]:
144
181
  n_versions = 0
145
182
  n_checked = 0
146
183
  previous = ""
147
- for name in sorted(module_names):
184
+ for name in module_names:
148
185
  if name.startswith("_") or "._" in name:
149
186
  # Refers to a private module so we can ignore it and assume
150
187
  # version has been lifted into parent or, if top level, not
@@ -152,20 +189,31 @@ def getPythonPackages() -> dict[str, str]:
152
189
  # packages such as _abc and __future__.
153
190
  continue
154
191
 
155
- if name in _STDLIB:
156
- # Assign all standard library packages the python version
157
- # since they almost all lack explicit versions.
158
- packages[name] = sys.version
159
- previous = name
160
- continue
161
-
162
192
  if name.startswith(previous + ".") and previous in packages:
163
193
  # Already have this version. Use the same previous name
164
194
  # for the line after this.
165
195
  continue
166
196
 
167
- # Look for a version.
168
- ver = _get_python_package_version(name, packages)
197
+ # Find the namespace which we need to use package_dist.
198
+ namespace = name.split(".")[0]
199
+
200
+ if namespace in _STDLIB:
201
+ # If this is an import from the standard library, skip it.
202
+ # Standard library names only refer to top-level namespace
203
+ # so "importlib" appears but "importlib.metadata" does not.
204
+ previous = name
205
+ continue
206
+
207
+ # package_dist is a mapping from import namespace to distribution
208
+ # package names. This may be a one-to-many mapping due to namespace
209
+ # packages. Note that package_dist does not know about editable
210
+ # installs or eups installs via path manipulation.
211
+ if namespace in package_dist:
212
+ dist_names = package_dist[namespace]
213
+ else:
214
+ dist_names = [name]
215
+
216
+ ver = _get_python_package_version(name, namespace, dist_names, packages)
169
217
 
170
218
  n_checked += 1
171
219
  if ver is not None:
@@ -179,26 +227,41 @@ def getPythonPackages() -> dict[str, str]:
179
227
  n_versions,
180
228
  )
181
229
 
230
+ return _mangle_lsst_package_names(packages)
231
+
232
+
233
+ def _mangle_lsst_package_names(packages: dict[str, str]) -> dict[str, str]:
182
234
  for name in list(packages.keys()):
183
235
  # Use LSST package names instead of python module names
184
236
  # This matches the names we get from the environment (i.e., EUPS)
185
237
  # so we can clobber these build-time versions if the environment
186
238
  # reveals that we're not using the packages as-built.
187
239
  if name.startswith("lsst."):
188
- new_name = name.replace("lsst.", "").replace(".", "_")
189
- packages[new_name] = packages[name]
190
- del packages[name]
240
+ sep = "."
241
+ elif name.startswith("lsst-"):
242
+ sep = "-"
243
+ else:
244
+ continue
245
+ new_name = name.replace(f"lsst{sep}", "").replace(sep, "_")
246
+ packages[new_name] = packages[name]
247
+ del packages[name]
191
248
 
192
249
  return packages
193
250
 
194
251
 
195
- def _get_python_package_version(name: str, packages: dict[str, str]) -> str | None:
252
+ def _get_python_package_version(
253
+ name: str, namespace: str, dist_names: list[str], packages: dict[str, str]
254
+ ) -> str | None:
196
255
  """Given a package or module name, try to determine the version.
197
256
 
198
257
  Parameters
199
258
  ----------
200
259
  name : `str`
201
- The name of the package or module to try.
260
+ The imported name of the package or module to try.
261
+ namespace : `str`
262
+ The namespace of the package or module.
263
+ dist_names : `list` [ `str` ]
264
+ The distribution names of the package or module.
202
265
  packages : `dict` [ `str`, `str` ]
203
266
  A dictionary mapping a name to a version. Modified in place.
204
267
  The key used might not match exactly the given key.
@@ -209,10 +272,44 @@ def _get_python_package_version(name: str, packages: dict[str, str]) -> str | No
209
272
  The version string stored in ``packages``. Nothing is stored if the
210
273
  value here is `None`.
211
274
  """
275
+ # We have certain special namespaces that are used via eups that
276
+ # need to be enumerated here.
277
+ if len(dist_names) > 1 or namespace in SPECIAL_NAMESPACES:
278
+ # Split the name into parts.
279
+ name_parts = re.split("[._-]", name)
280
+
281
+ found = False
282
+ for dist_name in dist_names:
283
+ # It should be impossible for this to happen but it has happened
284
+ # so check for it.
285
+ if dist_name is None:
286
+ continue # type: ignore
287
+ dist_name_parts = re.split("[._-]", dist_name)
288
+
289
+ # Check if the components start with the namespace; this is
290
+ # needed because (at least) lsst.ts packages do not use
291
+ # ``lsst`` in the package name.
292
+ if dist_name_parts[0] != namespace:
293
+ dist_name_parts.insert(0, namespace)
294
+
295
+ if dist_name_parts == name_parts:
296
+ found = True
297
+ break
298
+
299
+ if not found:
300
+ # This fallback case occurs when (a) we are testing the overall
301
+ # namespace (e.g. "lsst" or "sphinxcontrib") and the code below
302
+ # will return None; or (b) for eups-installed and other
303
+ # "editable installations" that are not registered as part
304
+ # of importlib.packages_distributions().
305
+ dist_name = name
306
+ else:
307
+ dist_name = dist_names[0]
308
+
212
309
  try:
213
310
  # This is the Python standard way to find a package version.
214
311
  # It can be slow.
215
- ver = importlib.metadata.version(name)
312
+ ver = importlib.metadata.version(dist_name)
216
313
  except Exception:
217
314
  # Fall back to using the module itself. There is no guarantee
218
315
  # that "a" exists for module "a.b" so if hierarchy has been expanded
@@ -235,7 +332,7 @@ def _get_python_package_version(name: str, packages: dict[str, str]) -> str | No
235
332
  _eups: Any | None = None # Singleton Eups object
236
333
 
237
334
 
238
- @lru_cache(maxsize=1)
335
+ @lru_cache(maxsize=2)
239
336
  def getEnvironmentPackages(include_all: bool = False) -> dict[str, str]:
240
337
  """Get products and their versions from the environment.
241
338
 
@@ -258,6 +355,9 @@ def getEnvironmentPackages(include_all: bool = False) -> dict[str, str]:
258
355
  provide a means to determine the version any other way) and to check if
259
356
  uninstalled packages are being used. We only report the product/version
260
357
  for these packages unless ``include_all`` is `True`.
358
+
359
+ Assumes that no new EUPS packages are set up after this function is
360
+ called the first time.
261
361
  """
262
362
  try:
263
363
  from eups import Eups
@@ -287,7 +387,7 @@ def getEnvironmentPackages(include_all: bool = False) -> dict[str, str]:
287
387
  if not prod.version.startswith(Product.LocalVersionPrefix):
288
388
  if include_all:
289
389
  tags = {t for t in prod.tags if t != "current"}
290
- tag_msg = " (" + " ".join(tags) + ")" if tags else ""
390
+ tag_msg = " (" + " ".join(sorted(tags)) + ")" if tags else ""
291
391
  packages[prod.name] = prod.version + tag_msg
292
392
  continue
293
393
  ver = prod.version
@@ -336,21 +436,40 @@ def getCondaPackages() -> dict[str, str]:
336
436
  Returns empty result if a conda environment is not in use or can not
337
437
  be queried.
338
438
  """
439
+ if "CONDA_PREFIX" not in os.environ:
440
+ return {}
441
+
442
+ # conda list is very slow. Ten times faster to scan the directory
443
+ # directly. This will only find conda packages and not pip installed
444
+ # packages.
445
+ meta_path = os.path.join(os.environ["CONDA_PREFIX"], "conda-meta")
446
+
339
447
  try:
340
- from conda.cli.python_api import Commands, run_command
341
- except ImportError:
448
+ filenames = os.scandir(path=meta_path)
449
+ except FileNotFoundError:
342
450
  return {}
343
451
 
344
- # Get the installed package list
345
- versions_json = run_command(Commands.LIST, "--json")
346
- packages = {pkg["name"]: pkg["version"] for pkg in json.loads(versions_json[0])}
452
+ packages = {}
453
+
454
+ for filename in filenames:
455
+ if not filename.name.endswith(".json"):
456
+ continue
457
+ with open(filename) as f:
458
+ try:
459
+ data = json.load(f)
460
+ except ValueError:
461
+ continue
462
+ try:
463
+ packages[data["name"]] = data["version"]
464
+ except KeyError:
465
+ continue
466
+
467
+ packages = dict(sorted(packages.items()))
347
468
 
348
469
  # Try to work out the conda environment name and include it as a fake
349
470
  # package. The "obvious" way of running "conda info --json" does give
350
471
  # access to the active_prefix but takes about 2 seconds to run.
351
- # The equivalent to the code above would be:
352
- # info_json = run_command(Commands.INFO, "--json")
353
- # As a comporomise look for the env name in the path to the python
472
+ # As a compromise look for the env name in the path to the python
354
473
  # executable
355
474
  match = re.search(r"/envs/(.*?)/bin/", sys.executable)
356
475
  if match:
@@ -398,19 +517,19 @@ class Packages(dict):
398
517
  print("Extra packages compared to before:", pkgs.extra(old))
399
518
  print("Different packages: ", pkgs.difference(old))
400
519
  old.update(pkgs) # Include any new packages in the old
401
- old.write("/path/to/packages.pickle")
402
-
403
- Parameters
404
- ----------
405
- packages : `dict`
406
- A mapping {package: version} where both keys and values are type `str`.
520
+ old.write("/path/to/packages.pickle").
407
521
 
408
522
  Notes
409
523
  -----
410
524
  This is a wrapper around a dict with some convenience methods.
411
525
  """
412
526
 
413
- formats = {".pkl": "pickle", ".pickle": "pickle", ".yaml": "yaml", ".json": "json"}
527
+ formats: ClassVar[dict[str, str]] = {
528
+ ".pkl": "pickle",
529
+ ".pickle": "pickle",
530
+ ".yaml": "yaml",
531
+ ".json": "json",
532
+ }
414
533
 
415
534
  def __setstate__(self, state: dict[str, Any]) -> None:
416
535
  # This only seems to be called for old pickle files where
@@ -418,22 +537,43 @@ class Packages(dict):
418
537
  self.update(state["_packages"])
419
538
 
420
539
  @classmethod
421
- def fromSystem(cls) -> Packages:
540
+ def fromSystem(cls, include_all: bool = False) -> Packages:
422
541
  """Construct a `Packages` by examining the system.
423
542
 
424
- Determine packages by examining python's `sys.modules`, conda
543
+ Determine packages by examining python's installed packages
544
+ (by default filtered by `sys.modules`) or distributions, conda
425
545
  libraries and EUPS. EUPS packages take precedence over conda and
426
546
  general python packages.
427
547
 
548
+ Parameters
549
+ ----------
550
+ include_all : `bool`, optional
551
+ If `False`, will only include imported Python packages, installed
552
+ Conda packages and locally-setup EUPS packages. If `True` all
553
+ installed Python distributions and conda packages will be reported
554
+ as well as all EUPS packages that are set up.
555
+
428
556
  Returns
429
557
  -------
430
558
  packages : `Packages`
431
559
  All version package information that could be obtained.
560
+
561
+ Note
562
+ ----
563
+ The names of Python distributions can differ from the names of the
564
+ Python packages installed by those distributions. Since ``include_all``
565
+ set to `True` uses Python distributions and `False` uses Python
566
+ packages do not expect that the answers are directly comparable.
432
567
  """
433
568
  packages = {}
434
- packages.update(getPythonPackages())
569
+ if include_all:
570
+ packages.update(getAllPythonDistributions())
571
+ else:
572
+ packages.update(getPythonPackages())
573
+ # Conda list always reports all Conda packages.
435
574
  packages.update(getCondaPackages())
436
- packages.update(getEnvironmentPackages()) # Should be last, to override products with LOCAL versions
575
+ # Should be last, to override products with LOCAL versions
576
+ packages.update(getEnvironmentPackages(include_all=include_all))
437
577
  return cls(packages)
438
578
 
439
579
  @classmethod
@@ -523,7 +663,7 @@ class Packages(dict):
523
663
  filename : `str`
524
664
  Filename to which to write. The format of the data file
525
665
  is determined from the file extension. Currently supports
526
- ``.pickle``, ``.json``, and ``.yaml``
666
+ ``.pickle``, ``.json``, and ``.yaml``.
527
667
  """
528
668
  _, ext = os.path.splitext(filename)
529
669
  if ext not in self.formats:
@@ -608,6 +748,18 @@ class _BackwardsCompatibilityUnpickler(pickle.Unpickler):
608
748
  """Return the class that should be used for unpickling.
609
749
 
610
750
  This is always known to be the class in this package.
751
+
752
+ Parameters
753
+ ----------
754
+ module : `str`
755
+ Ignored.
756
+ name : `str`
757
+ Ignored.
758
+
759
+ Returns
760
+ -------
761
+ `type` [`Packages`]
762
+ The Python type to use. Always returns `Packages`.
611
763
  """
612
764
  return Packages
613
765
 
@@ -628,7 +780,11 @@ def _pkg_constructor(loader: yaml.constructor.SafeConstructor, node: yaml.Node)
628
780
  yield Packages(loader.construct_mapping(node, deep=True)) # type: ignore
629
781
 
630
782
 
631
- for loader in (yaml.Loader, yaml.CLoader, yaml.UnsafeLoader, yaml.SafeLoader, yaml.FullLoader):
783
+ for loader_str in ("Loader", "CLoader", "UnsafeLoader", "SafeLoader", "FullLoader"):
784
+ loader = getattr(yaml, loader_str, None)
785
+ if loader is None:
786
+ continue
787
+
632
788
  yaml.add_constructor("lsst.utils.packages.Packages", _pkg_constructor, Loader=loader)
633
789
 
634
790
  # Register the old name with YAML.
@@ -0,0 +1,15 @@
1
+ # This file is part of utils.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (https://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # Use of this source code is governed by a 3-clause BSD-style
10
+ # license that can be found in the LICENSE file.
11
+ """General LSST Plotting Utilities."""
12
+
13
+ from .figures import *
14
+ from .limits import *
15
+ from .publication_plots import *