Coverage for python/lsst/utils/deprecated.py: 92%
71 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 08:50 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 08:50 +0000
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.
12from __future__ import annotations
14__all__ = ["DeprecatedDict", "deprecate_pybind11", "suppress_deprecations"]
16import copy
17import functools
18import unittest.mock
19import warnings
20from collections.abc import Iterator, Mapping
21from contextlib import contextmanager
22from typing import Any
24import deprecated.sphinx
27def deprecate_pybind11(obj: Any, reason: str, version: str, category: type[Warning] = FutureWarning) -> Any:
28 """Deprecate a pybind11-wrapped C++ interface function, method or class.
30 This needs to use a pass-through Python wrapper so that
31 `~deprecated.sphinx.deprecated` can update its docstring; pybind11
32 docstrings are native and cannot be modified.
34 Note that this is not a decorator; its output must be assigned to
35 replace the method being deprecated.
37 Parameters
38 ----------
39 obj : `~collections.abc.Callable`
40 The function, method, or class to deprecate.
41 reason : `str`
42 Reason for deprecation, passed to `~deprecated.sphinx.deprecated`.
43 version : `str`
44 Next major version in which the interface will be deprecated,
45 passed to `~deprecated.sphinx.deprecated`.
46 category : `Warning`
47 Warning category, passed to `~deprecated.sphinx.deprecated`.
49 Returns
50 -------
51 obj : `~collections.abc.Callable`
52 Wrapped function, method, or class.
54 Examples
55 --------
56 .. code-block:: python
58 ExposureF.getCalib = deprecate_pybind11(ExposureF.getCalib,
59 reason="Replaced by getPhotoCalib. (Will be removed in 18.0)",
60 version="17.0", category=FutureWarning))
61 """
63 @functools.wraps(obj)
64 def internal(*args: Any, **kwargs: Any) -> Any:
65 return obj(*args, **kwargs)
67 # Typeshed seems to have an incorrect definition for the deprecated
68 # function and believes the category should be
69 # Optional[Type[DeprecationWarning]] but this is wrong on two counts:
70 # 1. None is not actually allowed.
71 # 2. FutureWarning is explicitly allowed but that is not a subclass
72 # of DeprecationWarning.
73 # This has been fixed upstream at
74 # https://github.com/python/typeshed/pull/593
75 # and will pass once that is released.
76 return deprecated.sphinx.deprecated(reason=reason, version=version, category=category)(internal)
79@contextmanager
80def suppress_deprecations(category: type[Warning] = FutureWarning) -> Iterator[None]:
81 """Suppress warnings generated by `deprecated.sphinx.deprecated`.
83 Naively, one might attempt to suppress these warnings by using
84 `~warnings.catch_warnings`. However, `~deprecated.sphinx.deprecated`
85 attempts to install its own filter, overriding that. This convenience
86 method works around this and properly suppresses the warnings by providing
87 a mock `~warnings.simplefilter` for `~deprecated.sphinx.deprecated` to
88 call.
90 Parameters
91 ----------
92 category : `Warning`
93 The category of warning to suppress.
94 """
95 with warnings.catch_warnings():
96 warnings.simplefilter("ignore", category)
97 with unittest.mock.patch.object(warnings, "simplefilter"):
98 yield
101class DeprecatedDict(dict):
102 """A `dict` that warns when deprecated keys are first used.
104 DeprecatedDict behaves exactly like a built-in dict, except that each
105 deprecated key carries a reason. The first time a deprecated key is used
106 through a single-key operation (d[key], ~dict.get, ~dict.pop,
107 ~dict.setdefault, d[key] = value and del d[key]), it warns with that
108 reason. Bulk operations (iteration, copy, ``dict(d)``, ``update``) never
109 warn.
111 Parameters
112 ----------
113 *args : `~typing.Any`
114 Forwarded to `dict` to populate the initial contents.
115 deprecations : `~collections.abc.Mapping`
116 Maps each deprecated key to the reason shown in its warning. An
117 empty mapping warns that a plain `dict` should be used instead.
118 version : `str`, optional
119 Version in which the keys were deprecated, added to the message.
120 category : `type` [ `Warning` ], optional
121 Default warning category for deprecated keys.
122 deprecated_categories : `~collections.abc.Mapping`
123 Per-key overrides of ``category``.
124 stacklevel : `int`, optional
125 ``stacklevel`` for `warnings.warn`, so the warning points at the
126 caller rather than at ``DeprecatedDict`` internals.
127 **kwargs : `~typing.Any`
128 Forwarded to `dict` to populate the initial contents.
129 """
131 def __init__(
132 self,
133 *args: Any,
134 deprecations: Mapping[Any, str],
135 version: str = "",
136 category: type[Warning] = FutureWarning,
137 deprecated_categories: Mapping[Any, type[Warning]] | None = None,
138 stacklevel: int = 2,
139 **kwargs: Any,
140 ) -> None:
141 super().__init__(*args, **kwargs)
142 self._deprecations: dict[Any, str] = dict(deprecations)
143 self._version = version
144 overrides = deprecated_categories or {}
145 self._categories: dict[Any, type[Warning]] = {
146 key: overrides.get(key, category) for key in self._deprecations
147 }
148 self._stacklevel = stacklevel
149 # Fire each key at most once.
150 self._warned: set[Any] = set()
151 if not self._deprecations:
152 warnings.warn(
153 "DeprecatedDict was created with no deprecated keys; use a regular `dict` instead.",
154 UserWarning,
155 stacklevel=stacklevel,
156 )
158 def _warn(self, key: Any) -> None:
159 """Emit a one-time deprecation warning for ``key``.
161 Parameters
162 ----------
163 key : `~typing.Any`
164 The key being accessed.
165 """
166 if key not in self._deprecations or key in self._warned:
167 return
168 self._warned.add(key)
169 message = f"Key {key!r} is deprecated"
170 if self._version:
171 message += f" since {self._version}"
172 message += " and should no longer be used."
173 if reason := self._deprecations[key]: 173 ↛ 176line 173 didn't jump to line 176 because the condition on line 173 was always true
174 message += f" {reason}"
175 # +1 for the ``_warn`` frame between the public method and warn().
176 warnings.warn(message, self._categories[key], stacklevel=self._stacklevel + 1)
178 # Targeted single-key access: these warn.
179 def __getitem__(self, key: Any) -> Any:
180 self._warn(key)
181 return super().__getitem__(key)
183 def __setitem__(self, key: Any, value: Any) -> None:
184 self._warn(key)
185 super().__setitem__(key, value)
187 def __delitem__(self, key: Any) -> None:
188 self._warn(key)
189 super().__delitem__(key)
191 def get(self, key: Any, default: Any = None) -> Any:
192 # Docstring inherited.
193 self._warn(key)
194 return super().get(key, default)
196 def pop(self, key: Any, *args: Any) -> Any:
197 # Docstring inherited.
198 self._warn(key)
199 return super().pop(key, *args)
201 def setdefault(self, key: Any, default: Any = None) -> Any:
202 # Docstring inherited.
203 self._warn(key)
204 return super().setdefault(key, default)
206 # Copying: rebuild without routing items through __setitem__.
207 def _clone(self, data: dict) -> DeprecatedDict:
208 """Build a sibling that shares this dict's deprecation config.
210 Parameters
211 ----------
212 data : `dict`
213 The contents of the new mapping.
215 Returns
216 -------
217 new : `DeprecatedDict`
218 A new mapping with the same deprecation configuration and the
219 same set of already-warned keys.
220 """
221 new = DeprecatedDict(
222 data,
223 deprecations=self._deprecations,
224 version=self._version,
225 deprecated_categories=self._categories,
226 stacklevel=self._stacklevel,
227 )
228 new._warned = set(self._warned)
229 return new
231 def __copy__(self) -> DeprecatedDict:
232 return self._clone(dict(self))
234 def __deepcopy__(self, memo: dict) -> DeprecatedDict:
235 new = self._clone({k: copy.deepcopy(v, memo) for k, v in dict(self).items()})
236 memo[id(self)] = new
237 return new