lsst-utils 25.2023.2800__py3-none-any.whl → 29.2025.4800__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsst/utils/__init__.py +0 -1
- lsst/utils/argparsing.py +79 -0
- lsst/utils/classes.py +26 -9
- lsst/utils/db_auth.py +339 -0
- lsst/utils/deprecated.py +4 -4
- lsst/utils/inheritDoc.py +30 -4
- lsst/utils/introspection.py +242 -23
- lsst/utils/iteration.py +188 -2
- lsst/utils/logging.py +78 -12
- lsst/utils/packages.py +211 -55
- lsst/utils/plotting/__init__.py +15 -0
- lsst/utils/plotting/figures.py +159 -0
- lsst/utils/plotting/limits.py +12 -1
- lsst/utils/plotting/publication_plots.py +184 -0
- lsst/utils/plotting/rubin.mplstyle +46 -0
- lsst/utils/tests.py +112 -57
- lsst/utils/threads.py +1 -1
- lsst/utils/timer.py +189 -45
- lsst/utils/usage.py +2 -2
- lsst/utils/version.py +1 -1
- lsst/utils/wrappers.py +65 -22
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +18 -12
- lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
- lsst/utils/ellipsis.py +0 -66
- lsst/utils/get_caller_name.py +0 -47
- lsst_utils-25.2023.2800.dist-info/RECORD +0 -28
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
- {lsst_utils-25.2023.2800.dist-info → lsst_utils-29.2025.4800.dist-info}/zip-safe +0 -0
lsst/utils/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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
363
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
154
|
+
# If it's not available we continue.
|
|
155
|
+
with contextlib.suppress(Exception):
|
|
119
156
|
importlib.import_module(module_name)
|
|
120
|
-
|
|
121
|
-
|
|
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 =
|
|
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
|
|
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
|
-
#
|
|
168
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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(
|
|
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=
|
|
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
|
-
|
|
341
|
-
except
|
|
448
|
+
filenames = os.scandir(path=meta_path)
|
|
449
|
+
except FileNotFoundError:
|
|
342
450
|
return {}
|
|
343
451
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 *
|