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.
- lsst/utils/__init__.py +0 -3
- lsst/utils/_packaging.py +2 -0
- lsst/utils/argparsing.py +79 -0
- lsst/utils/classes.py +27 -9
- lsst/utils/db_auth.py +339 -0
- lsst/utils/deprecated.py +10 -7
- lsst/utils/doImport.py +8 -9
- lsst/utils/inheritDoc.py +34 -6
- lsst/utils/introspection.py +285 -19
- lsst/utils/iteration.py +193 -7
- lsst/utils/logging.py +155 -105
- lsst/utils/packages.py +324 -82
- lsst/utils/plotting/__init__.py +15 -0
- lsst/utils/plotting/figures.py +159 -0
- lsst/utils/plotting/limits.py +155 -0
- lsst/utils/plotting/publication_plots.py +184 -0
- lsst/utils/plotting/rubin.mplstyle +46 -0
- lsst/utils/tests.py +231 -102
- lsst/utils/threads.py +9 -3
- lsst/utils/timer.py +207 -110
- lsst/utils/usage.py +6 -6
- lsst/utils/version.py +1 -1
- lsst/utils/wrappers.py +74 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +19 -15
- lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
- lsst/utils/_forwarded.py +0 -28
- lsst/utils/backtrace/__init__.py +0 -33
- lsst/utils/ellipsis.py +0 -54
- lsst/utils/get_caller_name.py +0 -45
- lsst_utils-25.2023.600.dist-info/RECORD +0 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
- {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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 : `
|
|
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("
|
|
100
|
+
version += " with " + " ".join(f"{pkg}={deps[pkg]}" for pkg in sorted(buildtime))
|
|
89
101
|
return str(version)
|
|
90
102
|
|
|
91
103
|
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
154
|
+
# If it's not available we continue.
|
|
155
|
+
with contextlib.suppress(Exception):
|
|
112
156
|
importlib.import_module(module_name)
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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"
|
|
143
|
-
|
|
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
|
|
329
|
+
return ver
|
|
148
330
|
|
|
149
331
|
|
|
150
|
-
_eups:
|
|
332
|
+
_eups: Any | None = None # Singleton Eups object
|
|
151
333
|
|
|
152
334
|
|
|
153
|
-
@lru_cache(maxsize=
|
|
154
|
-
def getEnvironmentPackages(include_all: bool = False) ->
|
|
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() ->
|
|
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
|
-
|
|
256
|
-
except
|
|
448
|
+
filenames = os.scandir(path=meta_path)
|
|
449
|
+
except FileNotFoundError:
|
|
257
450
|
return {}
|
|
258
451
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
#
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
|
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,
|
|
775
|
+
yaml.add_representer(Packages, _pkg_representer)
|
|
539
776
|
|
|
540
777
|
|
|
541
|
-
def
|
|
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
|
|
546
|
-
yaml
|
|
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",
|
|
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 *
|