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/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
14
  from __future__ import annotations
13
15
 
14
- """
15
- Determine which packages are being used in the system and their versions
16
- """
16
+ import contextlib
17
17
  import hashlib
18
18
  import importlib
19
19
  import io
@@ -26,33 +26,44 @@ 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, Dict, Optional, Tuple, Type
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
 
45
47
  # Packages used at build-time (e.g., header-only)
46
- BUILDTIME = set(["boost", "eigen", "tmv"])
48
+ BUILDTIME = {"boost", "eigen", "tmv"}
47
49
 
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 = set(["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
55
- ENVIRONMENT = set(["astrometry_net", "astrometry_net_data", "minuit2", "xpa"])
59
+ ENVIRONMENT = {"astrometry_net", "astrometry_net_data", "minuit2", "xpa"}
60
+
61
+ try:
62
+ # Python 3.10 includes a list of standard library modules.
63
+ # These will all have the same version number as Python itself.
64
+ _STDLIB = sys.stdlib_module_names
65
+ except AttributeError:
66
+ _STDLIB = frozenset()
56
67
 
57
68
 
58
69
  def getVersionFromPythonModule(module: types.ModuleType) -> str:
@@ -60,17 +71,18 @@ def getVersionFromPythonModule(module: types.ModuleType) -> str:
60
71
 
61
72
  Parameters
62
73
  ----------
63
- module : `module`
74
+ module : `~types.ModuleType`
64
75
  Module for which to get version.
65
76
 
66
77
  Returns
67
78
  -------
68
79
  version : `str`
80
+ The version of the python module.
69
81
 
70
82
  Raises
71
83
  ------
72
84
  AttributeError
73
- Raised if __version__ attribute is not set.
85
+ Raised if ``__version__`` attribute is not set.
74
86
 
75
87
  Notes
76
88
  -----
@@ -85,18 +97,41 @@ def getVersionFromPythonModule(module: types.ModuleType) -> str:
85
97
  deps = module.__dependency_versions__
86
98
  buildtime = BUILDTIME & set(deps.keys())
87
99
  if buildtime:
88
- version += " with " + " ".join("%s=%s" % (pkg, deps[pkg]) for pkg in sorted(buildtime))
100
+ version += " with " + " ".join(f"{pkg}={deps[pkg]}" for pkg in sorted(buildtime))
89
101
  return str(version)
90
102
 
91
103
 
92
- def getPythonPackages() -> Dict[str, str]:
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
+
128
+ def getPythonPackages() -> dict[str, str]:
93
129
  """Get imported python packages and their versions.
94
130
 
95
131
  Returns
96
132
  -------
97
- packages : `dict`
98
- Keys (type `str`) are package names; values (type `str`) are their
99
- versions.
133
+ packages : `dict` [ `str`, `str` ]
134
+ Keys are package names; values are their versions.
100
135
 
101
136
  Notes
102
137
  -----
@@ -104,54 +139,201 @@ def getPythonPackages() -> Dict[str, str]:
104
139
  module. Note, therefore, that we can only report on modules that have
105
140
  *already* been imported.
106
141
 
142
+ Python standard library packages are not included in the report. A
143
+ ``python`` key is inserted that records the Python version.
144
+
107
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``.
108
151
  """
109
152
  # Attempt to import libraries that only report their version in python
110
153
  for module_name in PYTHON:
111
- try:
154
+ # If it's not available we continue.
155
+ with contextlib.suppress(Exception):
112
156
  importlib.import_module(module_name)
113
- except Exception:
114
- pass # It's not available, so don't care
157
+
158
+ package_dist = packages_distributions()
115
159
 
116
160
  packages = {"python": sys.version}
161
+
117
162
  # Not iterating with sys.modules.iteritems() because it's not atomic and
118
163
  # subject to race conditions
119
- moduleNames = list(sys.modules.keys())
120
- for name in moduleNames:
121
- module = sys.modules[name]
122
- try:
123
- ver = getVersionFromPythonModule(module)
124
- except Exception:
125
- continue # Can't get a version from it, don't care
126
-
127
- # Remove "foo.bar.version" in favor of "foo.bar"
128
- # This prevents duplication when the __init__.py includes
129
- # "from .version import *"
130
- for ending in (".version", "._version"):
131
- if name.endswith(ending):
132
- name = name[: -len(ending)]
133
- if name in packages:
134
- assert ver == packages[name]
135
- elif name in packages:
136
- assert ver == packages[name]
164
+ module_names = sorted(sys.modules.keys())
165
+
166
+ # Use knowledge of package hierarchy to find the versions rather than
167
+ # using each name independently. Group all the module names into the
168
+ # hierarchy, splitting on dot, and skipping any component that starts
169
+ # with an underscore.
170
+
171
+ # Sorting the module names gives us:
172
+ # lsst
173
+ # lsst.afw
174
+ # lsst.afw.cameraGeom
175
+ # ...
176
+ # lsst.daf
177
+ # lsst.daf.butler
178
+ #
179
+ # and so we can use knowledge of the previous version to inform whether
180
+ # we need to look at the subsequent line.
181
+ n_versions = 0
182
+ n_checked = 0
183
+ previous = ""
184
+ for name in module_names:
185
+ if name.startswith("_") or "._" in name:
186
+ # Refers to a private module so we can ignore it and assume
187
+ # version has been lifted into parent or, if top level, not
188
+ # relevant for versioning. This applies also to standard library
189
+ # packages such as _abc and __future__.
190
+ continue
191
+
192
+ if name.startswith(previous + ".") and previous in packages:
193
+ # Already have this version. Use the same previous name
194
+ # for the line after this.
195
+ continue
196
+
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)
217
+
218
+ n_checked += 1
219
+ if ver is not None:
220
+ n_versions += 1
221
+ previous = name
222
+
223
+ log.debug(
224
+ "Given %d modules but checked %d in hierarchy and found versions for %d",
225
+ len(module_names),
226
+ n_checked,
227
+ n_versions,
228
+ )
229
+
230
+ return _mangle_lsst_package_names(packages)
231
+
137
232
 
233
+ def _mangle_lsst_package_names(packages: dict[str, str]) -> dict[str, str]:
234
+ for name in list(packages.keys()):
138
235
  # Use LSST package names instead of python module names
139
236
  # This matches the names we get from the environment (i.e., EUPS)
140
237
  # so we can clobber these build-time versions if the environment
141
238
  # reveals that we're not using the packages as-built.
142
- if "lsst" in name:
143
- name = name.replace("lsst.", "").replace(".", "_")
239
+ if name.startswith("lsst."):
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]
248
+
249
+ return packages
250
+
251
+
252
+ def _get_python_package_version(
253
+ name: str, namespace: str, dist_names: list[str], packages: dict[str, str]
254
+ ) -> str | None:
255
+ """Given a package or module name, try to determine the version.
256
+
257
+ Parameters
258
+ ----------
259
+ name : `str`
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.
265
+ packages : `dict` [ `str`, `str` ]
266
+ A dictionary mapping a name to a version. Modified in place.
267
+ The key used might not match exactly the given key.
268
+
269
+ Returns
270
+ -------
271
+ ver : `str` or `None`
272
+ The version string stored in ``packages``. Nothing is stored if the
273
+ value here is `None`.
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
+
309
+ try:
310
+ # This is the Python standard way to find a package version.
311
+ # It can be slow.
312
+ ver = importlib.metadata.version(dist_name)
313
+ except Exception:
314
+ # Fall back to using the module itself. There is no guarantee
315
+ # that "a" exists for module "a.b" so if hierarchy has been expanded
316
+ # this might fail. Check first.
317
+ if name not in sys.modules:
318
+ return None
319
+ module = sys.modules[name]
320
+ try:
321
+ ver = getVersionFromPythonModule(module)
322
+ except Exception:
323
+ return None # Can't get a version from it, don't care
144
324
 
325
+ # Update the package information.
326
+ if ver is not None:
145
327
  packages[name] = ver
146
328
 
147
- return packages
329
+ return ver
148
330
 
149
331
 
150
- _eups: Optional[Any] = None # Singleton Eups object
332
+ _eups: Any | None = None # Singleton Eups object
151
333
 
152
334
 
153
- @lru_cache(maxsize=1)
154
- def getEnvironmentPackages(include_all: bool = False) -> Dict[str, str]:
335
+ @lru_cache(maxsize=2)
336
+ def getEnvironmentPackages(include_all: bool = False) -> dict[str, str]:
155
337
  """Get products and their versions from the environment.
156
338
 
157
339
  Parameters
@@ -173,6 +355,9 @@ def getEnvironmentPackages(include_all: bool = False) -> Dict[str, str]:
173
355
  provide a means to determine the version any other way) and to check if
174
356
  uninstalled packages are being used. We only report the product/version
175
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.
176
361
  """
177
362
  try:
178
363
  from eups import Eups
@@ -202,7 +387,7 @@ def getEnvironmentPackages(include_all: bool = False) -> Dict[str, str]:
202
387
  if not prod.version.startswith(Product.LocalVersionPrefix):
203
388
  if include_all:
204
389
  tags = {t for t in prod.tags if t != "current"}
205
- tag_msg = " (" + " ".join(tags) + ")" if tags else ""
390
+ tag_msg = " (" + " ".join(sorted(tags)) + ")" if tags else ""
206
391
  packages[prod.name] = prod.version + tag_msg
207
392
  continue
208
393
  ver = prod.version
@@ -237,7 +422,7 @@ def getEnvironmentPackages(include_all: bool = False) -> Dict[str, str]:
237
422
 
238
423
 
239
424
  @lru_cache(maxsize=1)
240
- def getCondaPackages() -> Dict[str, str]:
425
+ def getCondaPackages() -> dict[str, str]:
241
426
  """Get products and their versions from the conda environment.
242
427
 
243
428
  Returns
@@ -251,21 +436,40 @@ def getCondaPackages() -> Dict[str, str]:
251
436
  Returns empty result if a conda environment is not in use or can not
252
437
  be queried.
253
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
+
254
447
  try:
255
- from conda.cli.python_api import Commands, run_command
256
- except ImportError:
448
+ filenames = os.scandir(path=meta_path)
449
+ except FileNotFoundError:
257
450
  return {}
258
451
 
259
- # Get the installed package list
260
- versions_json = run_command(Commands.LIST, "--json")
261
- 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()))
262
468
 
263
469
  # Try to work out the conda environment name and include it as a fake
264
470
  # package. The "obvious" way of running "conda info --json" does give
265
471
  # access to the active_prefix but takes about 2 seconds to run.
266
- # The equivalent to the code above would be:
267
- # info_json = run_command(Commands.INFO, "--json")
268
- # 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
269
473
  # executable
270
474
  match = re.search(r"/envs/(.*?)/bin/", sys.executable)
271
475
  if match:
@@ -313,42 +517,63 @@ class Packages(dict):
313
517
  print("Extra packages compared to before:", pkgs.extra(old))
314
518
  print("Different packages: ", pkgs.difference(old))
315
519
  old.update(pkgs) # Include any new packages in the old
316
- old.write("/path/to/packages.pickle")
317
-
318
- Parameters
319
- ----------
320
- packages : `dict`
321
- A mapping {package: version} where both keys and values are type `str`.
520
+ old.write("/path/to/packages.pickle").
322
521
 
323
522
  Notes
324
523
  -----
325
524
  This is a wrapper around a dict with some convenience methods.
326
525
  """
327
526
 
328
- 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
+ }
329
533
 
330
- def __setstate__(self, state: Dict[str, Any]) -> None:
534
+ def __setstate__(self, state: dict[str, Any]) -> None:
331
535
  # This only seems to be called for old pickle files where
332
536
  # the data was stored in _packages.
333
537
  self.update(state["_packages"])
334
538
 
335
539
  @classmethod
336
- def fromSystem(cls) -> Packages:
540
+ def fromSystem(cls, include_all: bool = False) -> Packages:
337
541
  """Construct a `Packages` by examining the system.
338
542
 
339
- 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
340
545
  libraries and EUPS. EUPS packages take precedence over conda and
341
546
  general python packages.
342
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
+
343
556
  Returns
344
557
  -------
345
558
  packages : `Packages`
346
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.
347
567
  """
348
568
  packages = {}
349
- 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.
350
574
  packages.update(getCondaPackages())
351
- 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))
352
577
  return cls(packages)
353
578
 
354
579
  @classmethod
@@ -438,7 +663,7 @@ class Packages(dict):
438
663
  filename : `str`
439
664
  Filename to which to write. The format of the data file
440
665
  is determined from the file extension. Currently supports
441
- ``.pickle``, ``.json``, and ``.yaml``
666
+ ``.pickle``, ``.json``, and ``.yaml``.
442
667
  """
443
668
  _, ext = os.path.splitext(filename)
444
669
  if ext not in self.formats:
@@ -449,7 +674,7 @@ class Packages(dict):
449
674
  ff.write(self.toBytes(self.formats[ext]))
450
675
 
451
676
  def __str__(self) -> str:
452
- ss = "%s({\n" % self.__class__.__name__
677
+ ss = self.__class__.__name__ + "({\n"
453
678
  # Sort alphabetically by module name, for convenience in reading
454
679
  ss += ",\n".join(f"{prod!r}:{self[prod]!r}" for prod in sorted(self))
455
680
  ss += ",\n})"
@@ -459,7 +684,7 @@ class Packages(dict):
459
684
  # Default repr() does not report the class name.
460
685
  return f"{self.__class__.__name__}({super().__repr__()})"
461
686
 
462
- def extra(self, other: Mapping) -> Dict[str, str]:
687
+ def extra(self, other: Mapping) -> dict[str, str]:
463
688
  """Get packages in self but not in another `Packages` object.
464
689
 
465
690
  Parameters
@@ -475,7 +700,7 @@ class Packages(dict):
475
700
  """
476
701
  return {pkg: self[pkg] for pkg in self.keys() - other.keys()}
477
702
 
478
- def missing(self, other: Mapping) -> Dict[str, str]:
703
+ def missing(self, other: Mapping) -> dict[str, str]:
479
704
  """Get packages in another `Packages` object but missing from self.
480
705
 
481
706
  Parameters
@@ -491,7 +716,7 @@ class Packages(dict):
491
716
  """
492
717
  return {pkg: other[pkg] for pkg in other.keys() - self.keys()}
493
718
 
494
- def difference(self, other: Mapping) -> Dict[str, Tuple[str, str]]:
719
+ def difference(self, other: Mapping) -> dict[str, tuple[str, str]]:
495
720
  """Get packages in symmetric difference of self and another `Packages`
496
721
  object.
497
722
 
@@ -502,9 +727,9 @@ class Packages(dict):
502
727
 
503
728
  Returns
504
729
  -------
505
- difference : `dict` [`str`, `tuple` [`str`, `str`]]
730
+ difference : `dict` [`str`, `tuple` [ `str`, `str` ]]
506
731
  Packages in symmetric difference. Keys (type `str`) are package
507
- names; values (type `tuple`[`str`, `str`]) are their versions.
732
+ names; values (type `tuple` [ `str`, `str` ]) are their versions.
508
733
  """
509
734
  return {pkg: (self[pkg], other[pkg]) for pkg in self.keys() & other.keys() if self[pkg] != other[pkg]}
510
735
 
@@ -519,10 +744,22 @@ class _BackwardsCompatibilityUnpickler(pickle.Unpickler):
519
744
  `~lsst.utils.packages.Packages` instance.
520
745
  """
521
746
 
522
- def find_class(self, module: str, name: str) -> Type:
747
+ def find_class(self, module: str, name: str) -> type:
523
748
  """Return the class that should be used for unpickling.
524
749
 
525
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`.
526
763
  """
527
764
  return Packages
528
765
 
@@ -530,20 +767,25 @@ class _BackwardsCompatibilityUnpickler(pickle.Unpickler):
530
767
  # Register YAML representers
531
768
 
532
769
 
533
- def pkg_representer(dumper: yaml.Dumper, data: Any) -> yaml.MappingNode:
770
+ def _pkg_representer(dumper: yaml.Dumper, data: Any) -> yaml.MappingNode:
534
771
  """Represent Packages as a simple dict"""
535
772
  return dumper.represent_mapping("lsst.utils.packages.Packages", data, flow_style=None)
536
773
 
537
774
 
538
- yaml.add_representer(Packages, pkg_representer)
775
+ yaml.add_representer(Packages, _pkg_representer)
539
776
 
540
777
 
541
- def pkg_constructor(loader: yaml.constructor.SafeConstructor, node: yaml.Node) -> Any:
778
+ def _pkg_constructor(loader: yaml.constructor.SafeConstructor, node: yaml.Node) -> Any:
779
+ """Convert YAML representation back to Python class."""
542
780
  yield Packages(loader.construct_mapping(node, deep=True)) # type: ignore
543
781
 
544
782
 
545
- for loader in (yaml.Loader, yaml.CLoader, yaml.UnsafeLoader, yaml.SafeLoader, yaml.FullLoader):
546
- yaml.add_constructor("lsst.utils.packages.Packages", pkg_constructor, Loader=loader)
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
+
788
+ yaml.add_constructor("lsst.utils.packages.Packages", _pkg_constructor, Loader=loader)
547
789
 
548
790
  # Register the old name with YAML.
549
- yaml.add_constructor("lsst.base.Packages", pkg_constructor, Loader=loader)
791
+ yaml.add_constructor("lsst.base.Packages", _pkg_constructor, Loader=loader)
@@ -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 *