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/__init__.py CHANGED
@@ -10,12 +10,9 @@
10
10
  # license that can be found in the LICENSE file.
11
11
  """General LSST Utilities."""
12
12
 
13
- from ._forwarded import *
14
13
  from ._packaging import *
15
- from .backtrace import *
16
14
  from .deprecated import *
17
15
  from .doImport import *
18
- from .get_caller_name import *
19
16
  from .inheritDoc import *
20
17
  from .version import *
21
18
  from .wrappers import *
lsst/utils/_packaging.py CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  """Functions to help find packages."""
13
13
 
14
+ from __future__ import annotations
15
+
14
16
  __all__ = ("getPackageDir",)
15
17
 
16
18
  import os
@@ -0,0 +1,79 @@
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
+ #
12
+
13
+ """Utilities to help with argument parsing in command line interfaces."""
14
+
15
+ from __future__ import annotations
16
+
17
+ __all__ = ["AppendDict"]
18
+
19
+ import argparse
20
+ import copy
21
+ from collections.abc import Mapping
22
+ from typing import Any
23
+
24
+
25
+ class AppendDict(argparse.Action):
26
+ """An action analogous to the built-in 'append' that appends to a `dict`
27
+ instead of a `list`.
28
+
29
+ Inputs are assumed to be strings in the form "key=value"; any input that
30
+ does not contain exactly one "=" character is invalid. If the default value
31
+ is non-empty, the default key-value pairs may be overwritten by values from
32
+ the command line.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ option_strings: str | list[str],
38
+ dest: str,
39
+ nargs: int | str | None = None,
40
+ const: Any | None = None,
41
+ default: Any | None = None,
42
+ type: type | None = None,
43
+ choices: Any | None = None,
44
+ required: bool = False,
45
+ help: str | None = None,
46
+ metavar: str | None = None,
47
+ ):
48
+ if default is None:
49
+ default = {}
50
+ if not isinstance(default, Mapping):
51
+ argname = option_strings if option_strings else metavar if metavar else dest
52
+ raise TypeError(f"Default for {argname} must be a mapping or None, got {default!r}.")
53
+ super().__init__(option_strings, dest, nargs, const, default, type, choices, required, help, metavar)
54
+
55
+ def __call__(
56
+ self, parser: argparse.ArgumentParser, namespace: Any, values: Any, option_string: str | None = None
57
+ ) -> None:
58
+ # argparse doesn't make defensive copies, so namespace.dest may be
59
+ # the same object as self.default. Do the copy ourselves and avoid
60
+ # modifying the object previously in namespace.dest.
61
+ mapping = copy.copy(getattr(namespace, self.dest))
62
+
63
+ # Sometimes values is a copy of default instead of an input???
64
+ if isinstance(values, Mapping):
65
+ mapping.update(values)
66
+ else:
67
+ # values may be either a string or list of strings, depending on
68
+ # nargs. Unsafe to test for Sequence, because a scalar string
69
+ # passes.
70
+ if not isinstance(values, list):
71
+ values = [values]
72
+ for value in values:
73
+ vars = value.split("=")
74
+ if len(vars) != 2:
75
+ raise ValueError(f"Argument {value!r} does not match format 'key=value'.")
76
+ mapping[vars[0]] = vars[1]
77
+
78
+ # Other half of the defensive copy.
79
+ setattr(namespace, self.dest, mapping)
lsst/utils/classes.py CHANGED
@@ -10,15 +10,16 @@
10
10
  # license that can be found in the LICENSE file.
11
11
  #
12
12
 
13
- """Utilities to help with class creation.
14
- """
13
+ """Utilities to help with class creation."""
15
14
 
16
15
  from __future__ import annotations
17
16
 
18
17
  __all__ = ["Singleton", "cached_getter", "immutable"]
19
18
 
20
19
  import functools
21
- from typing import Any, Callable, Dict, Type, TypeVar
20
+ from collections.abc import Callable
21
+ from threading import RLock
22
+ from typing import Any, ClassVar, TypeVar
22
23
 
23
24
 
24
25
  class Singleton(type):
@@ -32,18 +33,25 @@ class Singleton(type):
32
33
  adjust state of the singleton.
33
34
  """
34
35
 
35
- _instances: Dict[Type, Any] = {}
36
+ _instances: ClassVar[dict[type, Any]] = {}
37
+ # This lock isn't ideal because it is shared between all classes using this
38
+ # metaclass, but current use cases don't do long-running I/O during
39
+ # initialization so the performance impact should be low. It must be an
40
+ # RLock instead of a regular Lock because one singleton class might try to
41
+ # instantiate another as part of its initialization.
42
+ __lock: ClassVar[RLock] = RLock()
36
43
 
37
44
  # Signature is intentionally not substitutable for type.__call__ (no *args,
38
45
  # **kwargs) to require classes that use this metaclass to have no
39
46
  # constructor arguments.
40
47
  def __call__(cls) -> Any:
41
- if cls not in cls._instances:
42
- cls._instances[cls] = super(Singleton, cls).__call__()
43
- return cls._instances[cls]
48
+ with cls.__lock:
49
+ if cls not in cls._instances:
50
+ cls._instances[cls] = super().__call__()
51
+ return cls._instances[cls]
44
52
 
45
53
 
46
- _T = TypeVar("_T", bound="Type")
54
+ _T = TypeVar("_T", bound=Any)
47
55
 
48
56
 
49
57
  def immutable(cls: _T) -> _T:
@@ -80,7 +88,7 @@ def immutable(cls: _T) -> _T:
80
88
 
81
89
  # mypy says the variable here has signature (str, Any) i.e. no "self";
82
90
  # I think it's just confused by descriptor stuff.
83
- cls.__setattr__ = __setattr__ # type: ignore
91
+ cls.__setattr__ = __setattr__
84
92
 
85
93
  def __getstate__(self: _T) -> dict: # noqa: N807
86
94
  # Disable default state-setting when unpickled.
@@ -112,6 +120,16 @@ def cached_getter(func: Callable[[_S], _R]) -> Callable[[_S], _R]:
112
120
  Only works on methods that take only ``self``
113
121
  as an argument, and returns the cached result on subsequent calls.
114
122
 
123
+ Parameters
124
+ ----------
125
+ func : `~collections.abc.Callable`
126
+ Method from which the result should be cached.
127
+
128
+ Returns
129
+ -------
130
+ `~collections.abc.Callable`
131
+ Decorated method.
132
+
115
133
  Notes
116
134
  -----
117
135
  This is intended primarily as a stopgap for Python 3.8's more sophisticated
lsst/utils/db_auth.py ADDED
@@ -0,0 +1,339 @@
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
+ # (http://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This program is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # This program is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ from __future__ import annotations
23
+
24
+ import fnmatch
25
+ import json
26
+ import os
27
+ import stat
28
+ import urllib.parse
29
+
30
+ import yaml
31
+
32
+ __all__ = ["DbAuth", "DbAuthError", "DbAuthPermissionsError"]
33
+
34
+ _DEFAULT_PATH = "~/.lsst/db-auth.yaml"
35
+ _DEFAULT_ENVVAR = "LSST_DB_AUTH"
36
+ _DEFAULT_CREDS_ENVVAR = "LSST_DB_AUTH_CREDENTIALS"
37
+
38
+
39
+ class DbAuthError(RuntimeError):
40
+ """Exception raised when a problem has occurred retrieving database
41
+ authentication information.
42
+ """
43
+
44
+ pass
45
+
46
+
47
+ class DbAuthNotFoundError(DbAuthError):
48
+ """Credentials file does not exist or no match was found in it."""
49
+
50
+
51
+ class DbAuthPermissionsError(DbAuthError):
52
+ """Credentials file has incorrect permissions."""
53
+
54
+
55
+ class DbAuth:
56
+ """Retrieves authentication information for database connections.
57
+
58
+ The authorization configuration is taken from the ``authList`` parameter
59
+ or a (group- and world-inaccessible) YAML file located at a path specified
60
+ by the given environment variable or at a default path location.
61
+
62
+ Parameters
63
+ ----------
64
+ path : `str` or `None`, optional
65
+ Path to configuration file, default path is ``~/.lsst/db-auth.yaml``.
66
+ The default is used if `None`.
67
+ envVar : `str` or `None`, optional
68
+ Name of environment variable pointing to configuration file, default is
69
+ ``LSST_DB_AUTH``. This is used if the environment variable is available
70
+ even if the path is specified. The default is used if `None`.
71
+ authList : `list` [`dict`] or `None`, optional
72
+ Authentication configuration. Used directly if given without referring
73
+ to the environment.
74
+ credsEnvVar : `str` or `None`, optional
75
+ Name of environment variable containing the authentication
76
+ configuration in JSON format. Default is ``LSST_DB_AUTH_CREDENTIALS``.
77
+ If the environment variable is defined, this takes priority over
78
+ reading credentials from file. The default is used if `None`.
79
+
80
+ Notes
81
+ -----
82
+ Defaults will be used if no option is provided. If provided ``authList``
83
+ will be used directly. The JSON credentials environment variable will
84
+ be used if defined, in preference to reading from a file even if overrides
85
+ are given for ``path`` and ``envVar``.
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ path: str | None = _DEFAULT_PATH,
91
+ envVar: str | None = _DEFAULT_ENVVAR,
92
+ authList: list[dict[str, str]] | None = None,
93
+ credsEnvVar: str | None = _DEFAULT_CREDS_ENVVAR,
94
+ ):
95
+ if authList is not None:
96
+ self._db_auth_path = "<auth-list>"
97
+ self.authList = authList
98
+ return
99
+
100
+ # Credentials in JSON takes priority.
101
+ if jsonCreds := os.getenv(credsEnvVar or _DEFAULT_CREDS_ENVVAR):
102
+ try:
103
+ self.authList = json.loads(jsonCreds)
104
+ except json.JSONDecodeError as exc:
105
+ raise DbAuthError(
106
+ f"Unable to load DbAuth configuration using JSON environment variable {credsEnvVar}"
107
+ ) from exc
108
+ self._db_auth_path = "<json-env-var>"
109
+ return
110
+
111
+ secretPath = os.getenv(envVar or _DEFAULT_ENVVAR, path or _DEFAULT_PATH)
112
+ secretPath = os.path.expanduser(secretPath)
113
+ if not os.path.isfile(secretPath):
114
+ raise DbAuthNotFoundError(f"No DbAuth configuration file: {secretPath}")
115
+ mode = os.stat(secretPath).st_mode
116
+ if mode & (stat.S_IRWXG | stat.S_IRWXO) != 0:
117
+ raise DbAuthPermissionsError(
118
+ f"DbAuth configuration file {secretPath} has incorrect permissions: {mode:o}"
119
+ )
120
+
121
+ try:
122
+ with open(secretPath) as secretFile:
123
+ self.authList = yaml.safe_load(secretFile)
124
+ except Exception as exc:
125
+ raise DbAuthError(f"Unable to load DbAuth configuration file: {secretPath}.") from exc
126
+ self._db_auth_path = secretPath
127
+
128
+ @property
129
+ def db_auth_path(self) -> str:
130
+ """The path to the secrets file used to load credentials (`str`)."""
131
+ return self._db_auth_path
132
+
133
+ # dialectname, host, and database are tagged as Optional only because other
134
+ # routines delegate to this one in order to raise a consistent exception
135
+ # for that condition.
136
+ def getAuth(
137
+ self,
138
+ dialectname: str | None,
139
+ username: str | None,
140
+ host: str | None,
141
+ port: int | str | None,
142
+ database: str | None,
143
+ ) -> tuple[str | None, str]:
144
+ """Retrieve a username and password for a database connection.
145
+
146
+ This function matches elements from the database connection URL with
147
+ glob-like URL patterns in a list of configuration dictionaries.
148
+
149
+ Parameters
150
+ ----------
151
+ dialectname : `str`
152
+ Database dialect, for example sqlite, mysql, postgresql, oracle,
153
+ or mssql.
154
+ username : `str` or None
155
+ Username from connection URL if present.
156
+ host : `str`
157
+ Host name from connection URL if present.
158
+ port : `str` or `int` or None
159
+ Port from connection URL if present.
160
+ database : `str`
161
+ Database name from connection URL.
162
+
163
+ Returns
164
+ -------
165
+ username: `str`
166
+ Username to use for database connection; same as parameter if
167
+ present.
168
+ password: `str`
169
+ Password to use for database connection.
170
+
171
+ Raises
172
+ ------
173
+ DbAuthError
174
+ Raised if the input is missing elements, an authorization
175
+ dictionary is missing elements, the authorization file is
176
+ misconfigured, or no matching authorization is found.
177
+
178
+ Notes
179
+ -----
180
+ The list of authorization configuration dictionaries is tested in
181
+ order, with the first matching dictionary used. Each dictionary must
182
+ contain a ``url`` item with a pattern to match against the database
183
+ connection URL and a ``password`` item. If no username is provided in
184
+ the database connection URL, the dictionary must also contain a
185
+ ``username`` item.
186
+
187
+ The ``url`` item must begin with a dialect and is not allowed to
188
+ specify dialect+driver.
189
+
190
+ Glob-style patterns (using "``*``" and "``?``" as wildcards) can be
191
+ used to match the host and database name portions of the connection
192
+ URL. For the username, port, and database name portions, omitting them
193
+ from the pattern matches against any value in the connection URL.
194
+
195
+ Examples
196
+ --------
197
+ The connection URL
198
+ ``postgresql://user@host.example.com:5432/my_database`` matches against
199
+ the identical string as a pattern. Other patterns that would match
200
+ include:
201
+
202
+ * ``postgresql://*``
203
+ * ``postgresql://*.example.com``
204
+ * ``postgresql://*.example.com/my_*``
205
+ * ``postgresql://host.example.com/my_database``
206
+ * ``postgresql://host.example.com:5432/my_database``
207
+ * ``postgresql://user@host.example.com/my_database``
208
+
209
+ Note that the connection URL
210
+ ``postgresql://host.example.com/my_database`` would not match against
211
+ the pattern ``postgresql://host.example.com:5432``, even if the default
212
+ port for the connection is 5432.
213
+ """
214
+ # Check inputs, squashing MyPy warnings that they're unnecessary
215
+ # (since they're only unnecessary if everyone else runs MyPy).
216
+ if dialectname is None or dialectname == "":
217
+ raise DbAuthError("Missing dialectname parameter")
218
+ if host is None or host == "":
219
+ raise DbAuthError("Missing host parameter")
220
+ if database is None or database == "":
221
+ raise DbAuthError("Missing database parameter")
222
+
223
+ for authDict in self.authList:
224
+ # Check for mandatory entries
225
+ if "url" not in authDict:
226
+ raise DbAuthError("Missing URL in DbAuth configuration")
227
+
228
+ # Parse pseudo-URL from db-auth.yaml
229
+ components = urllib.parse.urlparse(authDict["url"])
230
+
231
+ # Check for same database backend type/dialect
232
+ if components.scheme == "":
233
+ raise DbAuthError("Missing database dialect in URL: " + authDict["url"])
234
+
235
+ if "+" in components.scheme:
236
+ raise DbAuthError(
237
+ "Authorization dictionary URLs should only specify "
238
+ f"dialects, got: {components.scheme}. instead."
239
+ )
240
+
241
+ # dialect and driver are allowed in db string, since functionality
242
+ # could change. Connecting to a DB using different driver does not
243
+ # change dbname/user/pass and other auth info so we ignore it.
244
+ # https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
245
+ dialect = dialectname.split("+")[0]
246
+ if dialect != components.scheme:
247
+ continue
248
+
249
+ # Check for same database name
250
+ if (
251
+ components.path != ""
252
+ and components.path != "/"
253
+ and not fnmatch.fnmatch(database, components.path.lstrip("/"))
254
+ ):
255
+ continue
256
+
257
+ # Check username
258
+ if components.username is not None:
259
+ if username is None or username == "":
260
+ continue
261
+ if username != components.username:
262
+ continue
263
+
264
+ # Check hostname
265
+ if components.hostname is None:
266
+ raise DbAuthError("Missing host in URL: " + authDict["url"])
267
+ if not fnmatch.fnmatch(host, components.hostname):
268
+ continue
269
+
270
+ # Check port
271
+ if components.port is not None and (port is None or str(port) != str(components.port)):
272
+ continue
273
+
274
+ # Don't override username from connection string
275
+ if username is not None and username != "":
276
+ return (username, authDict["password"])
277
+ else:
278
+ if "username" not in authDict:
279
+ return (None, authDict["password"])
280
+ return (authDict["username"], authDict["password"])
281
+
282
+ raise DbAuthNotFoundError(
283
+ f"No matching DbAuth configuration for: ({dialectname}, {username}, {host}, {port}, {database})"
284
+ )
285
+
286
+ def getUrl(self, url: str) -> str:
287
+ """Fill in a username and password in a database connection URL.
288
+
289
+ This function parses the URL and calls `getAuth`.
290
+
291
+ Parameters
292
+ ----------
293
+ url : `str`
294
+ Database connection URL.
295
+
296
+ Returns
297
+ -------
298
+ url : `str`
299
+ Database connection URL with username and password.
300
+
301
+ Raises
302
+ ------
303
+ DbAuthError
304
+ Raised if the input is missing elements, an authorization
305
+ dictionary is missing elements, the authorization file is
306
+ misconfigured, or no matching authorization is found.
307
+
308
+ See Also
309
+ --------
310
+ getAuth : Retrieve authentication credentials.
311
+ """
312
+ components = urllib.parse.urlparse(url)
313
+ username, password = self.getAuth(
314
+ components.scheme,
315
+ components.username,
316
+ components.hostname,
317
+ components.port,
318
+ components.path.lstrip("/"),
319
+ )
320
+ hostname = components.hostname
321
+ assert hostname is not None
322
+ if ":" in hostname: # ipv6
323
+ hostname = f"[{hostname}]"
324
+ assert username is not None
325
+ netloc = "{}:{}@{}".format(
326
+ urllib.parse.quote(username, safe=""), urllib.parse.quote(password, safe=""), hostname
327
+ )
328
+ if components.port is not None:
329
+ netloc += ":" + str(components.port)
330
+ return urllib.parse.urlunparse(
331
+ (
332
+ components.scheme,
333
+ netloc,
334
+ components.path,
335
+ components.params,
336
+ components.query,
337
+ components.fragment,
338
+ )
339
+ )
lsst/utils/deprecated.py CHANGED
@@ -9,18 +9,21 @@
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
+ from __future__ import annotations
13
+
12
14
  __all__ = ["deprecate_pybind11", "suppress_deprecations"]
13
15
 
14
16
  import functools
15
17
  import unittest.mock
16
18
  import warnings
19
+ from collections.abc import Iterator
17
20
  from contextlib import contextmanager
18
- from typing import Any, Iterator, Type
21
+ from typing import Any
19
22
 
20
23
  import deprecated.sphinx
21
24
 
22
25
 
23
- def deprecate_pybind11(obj: Any, reason: str, version: str, category: Type[Warning] = FutureWarning) -> Any:
26
+ def deprecate_pybind11(obj: Any, reason: str, version: str, category: type[Warning] = FutureWarning) -> Any:
24
27
  """Deprecate a pybind11-wrapped C++ interface function, method or class.
25
28
 
26
29
  This needs to use a pass-through Python wrapper so that
@@ -35,17 +38,17 @@ def deprecate_pybind11(obj: Any, reason: str, version: str, category: Type[Warni
35
38
  obj : function, method, or class
36
39
  The function, method, or class to deprecate.
37
40
  reason : `str`
38
- Reason for deprecation, passed to `~deprecated.sphinx.deprecated`
41
+ Reason for deprecation, passed to `~deprecated.sphinx.deprecated`.
39
42
  version : 'str'
40
43
  Next major version in which the interface will be deprecated,
41
- passed to `~deprecated.sphinx.deprecated`
44
+ passed to `~deprecated.sphinx.deprecated`.
42
45
  category : `Warning`
43
- Warning category, passed to `~deprecated.sphinx.deprecated`
46
+ Warning category, passed to `~deprecated.sphinx.deprecated`.
44
47
 
45
48
  Returns
46
49
  -------
47
50
  obj : function, method, or class
48
- Wrapped function, method, or class
51
+ Wrapped function, method, or class.
49
52
 
50
53
  Examples
51
54
  --------
@@ -73,7 +76,7 @@ def deprecate_pybind11(obj: Any, reason: str, version: str, category: Type[Warni
73
76
 
74
77
 
75
78
  @contextmanager
76
- def suppress_deprecations(category: Type[Warning] = FutureWarning) -> Iterator[None]:
79
+ def suppress_deprecations(category: type[Warning] = FutureWarning) -> Iterator[None]:
77
80
  """Suppress warnings generated by `deprecated.sphinx.deprecated`.
78
81
 
79
82
  Naively, one might attempt to suppress these warnings by using
lsst/utils/doImport.py CHANGED
@@ -9,14 +9,15 @@
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
+ from __future__ import annotations
13
+
12
14
  __all__ = ("doImport", "doImportType")
13
15
 
14
16
  import importlib
15
17
  import types
16
- from typing import List, Optional, Type, Union
17
18
 
18
19
 
19
- def doImport(importable: str) -> Union[types.ModuleType, Type]:
20
+ def doImport(importable: str) -> types.ModuleType | type:
20
21
  """Import a python object given an importable string and return it.
21
22
 
22
23
  Parameters
@@ -43,26 +44,24 @@ def doImport(importable: str) -> Union[types.ModuleType, Type]:
43
44
  if not isinstance(importable, str):
44
45
  raise TypeError(f"Unhandled type of importable, val: {importable}")
45
46
 
46
- def tryImport(
47
- module: str, fromlist: List[str], previousError: Optional[str]
48
- ) -> Union[types.ModuleType, Type]:
47
+ def tryImport(module: str, fromlist: list[str], previousError: str | None) -> types.ModuleType | type:
49
48
  pytype = importlib.import_module(module)
50
49
  # Can have functions inside classes inside modules
51
50
  for f in fromlist:
52
51
  try:
53
52
  pytype = getattr(pytype, f)
54
- except AttributeError:
53
+ except AttributeError as e:
55
54
  extra = f"({previousError})" if previousError is not None else ""
56
55
  raise ImportError(
57
56
  f"Could not get attribute '{f}' from '{module}' when importing '{importable}' {extra}"
58
- )
57
+ ) from e
59
58
  return pytype
60
59
 
61
60
  # Go through the import path attempting to load the module
62
61
  # and retrieve the class or function as an attribute. Shift components
63
62
  # from the module list to the attribute list until something works.
64
63
  moduleComponents = importable.split(".")
65
- infileComponents: List[str] = []
64
+ infileComponents: list[str] = []
66
65
  previousError = None
67
66
 
68
67
  while moduleComponents:
@@ -88,7 +87,7 @@ def doImport(importable: str) -> Union[types.ModuleType, Type]:
88
87
  raise ModuleNotFoundError(f"Unable to import {importable!r} {extra}")
89
88
 
90
89
 
91
- def doImportType(importable: str) -> Type:
90
+ def doImportType(importable: str) -> type:
92
91
  """Import a python type given an importable string and return it.
93
92
 
94
93
  Parameters