File size: 9,840 Bytes
7885a28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
from inspect import Parameter, signature
import functools
import warnings
from importlib import import_module
from scipy._lib._docscrape import FunctionDoc


__all__ = ["_deprecated"]


# Object to use as default value for arguments to be deprecated. This should
# be used over 'None' as the user could parse 'None' as a positional argument
_NoValue = object()

def _sub_module_deprecation(*, sub_package, module, private_modules, all,
                            attribute, correct_module=None, dep_version="1.16.0"):
    """Helper function for deprecating modules that are public but were
    intended to be private.

    Parameters
    ----------
    sub_package : str
        Subpackage the module belongs to eg. stats
    module : str
        Public but intended private module to deprecate
    private_modules : list
        Private replacement(s) for `module`; should contain the
        content of ``all``, possibly spread over several modules.
    all : list
        ``__all__`` belonging to `module`
    attribute : str
        The attribute in `module` being accessed
    correct_module : str, optional
        Module in `sub_package` that `attribute` should be imported from.
        Default is that `attribute` should be imported from ``scipy.sub_package``.
    dep_version : str, optional
        Version in which deprecated attributes will be removed.
    """
    if correct_module is not None:
        correct_import = f"scipy.{sub_package}.{correct_module}"
    else:
        correct_import = f"scipy.{sub_package}"

    if attribute not in all:
        raise AttributeError(
            f"`scipy.{sub_package}.{module}` has no attribute `{attribute}`; "
            f"furthermore, `scipy.{sub_package}.{module}` is deprecated "
            f"and will be removed in SciPy 2.0.0."
        )

    attr = getattr(import_module(correct_import), attribute, None)

    if attr is not None:
        message = (
            f"Please import `{attribute}` from the `{correct_import}` namespace; "
            f"the `scipy.{sub_package}.{module}` namespace is deprecated "
            f"and will be removed in SciPy 2.0.0."
        )
    else:
        message = (
            f"`scipy.{sub_package}.{module}.{attribute}` is deprecated along with "
            f"the `scipy.{sub_package}.{module}` namespace. "
            f"`scipy.{sub_package}.{module}.{attribute}` will be removed "
            f"in SciPy {dep_version}, and the `scipy.{sub_package}.{module}` namespace "
            f"will be removed in SciPy 2.0.0."
        )

    warnings.warn(message, category=DeprecationWarning, stacklevel=3)

    for module in private_modules:
        try:
            return getattr(import_module(f"scipy.{sub_package}.{module}"), attribute)
        except AttributeError as e:
            # still raise an error if the attribute isn't in any of the expected
            # private modules
            if module == private_modules[-1]:
                raise e
            continue
    

def _deprecated(msg, stacklevel=2):
    """Deprecate a function by emitting a warning on use."""
    def wrap(fun):
        if isinstance(fun, type):
            warnings.warn(
                f"Trying to deprecate class {fun!r}",
                category=RuntimeWarning, stacklevel=2)
            return fun

        @functools.wraps(fun)
        def call(*args, **kwargs):
            warnings.warn(msg, category=DeprecationWarning,
                          stacklevel=stacklevel)
            return fun(*args, **kwargs)
        call.__doc__ = fun.__doc__
        return call

    return wrap


class _DeprecationHelperStr:
    """
    Helper class used by deprecate_cython_api
    """
    def __init__(self, content, message):
        self._content = content
        self._message = message

    def __hash__(self):
        return hash(self._content)

    def __eq__(self, other):
        res = (self._content == other)
        if res:
            warnings.warn(self._message, category=DeprecationWarning,
                          stacklevel=2)
        return res


def deprecate_cython_api(module, routine_name, new_name=None, message=None):
    """
    Deprecate an exported cdef function in a public Cython API module.

    Only functions can be deprecated; typedefs etc. cannot.

    Parameters
    ----------
    module : module
        Public Cython API module (e.g. scipy.linalg.cython_blas).
    routine_name : str
        Name of the routine to deprecate. May also be a fused-type
        routine (in which case its all specializations are deprecated).
    new_name : str
        New name to include in the deprecation warning message
    message : str
        Additional text in the deprecation warning message

    Examples
    --------
    Usually, this function would be used in the top-level of the
    module ``.pyx`` file:

    >>> from scipy._lib.deprecation import deprecate_cython_api
    >>> import scipy.linalg.cython_blas as mod
    >>> deprecate_cython_api(mod, "dgemm", "dgemm_new",
    ...                      message="Deprecated in Scipy 1.5.0")
    >>> del deprecate_cython_api, mod

    After this, Cython modules that use the deprecated function emit a
    deprecation warning when they are imported.

    """
    old_name = f"{module.__name__}.{routine_name}"

    if new_name is None:
        depdoc = f"`{old_name}` is deprecated!"
    else:
        depdoc = f"`{old_name}` is deprecated, use `{new_name}` instead!"

    if message is not None:
        depdoc += "\n" + message

    d = module.__pyx_capi__

    # Check if the function is a fused-type function with a mangled name
    j = 0
    has_fused = False
    while True:
        fused_name = f"__pyx_fuse_{j}{routine_name}"
        if fused_name in d:
            has_fused = True
            d[_DeprecationHelperStr(fused_name, depdoc)] = d.pop(fused_name)
            j += 1
        else:
            break

    # If not, apply deprecation to the named routine
    if not has_fused:
        d[_DeprecationHelperStr(routine_name, depdoc)] = d.pop(routine_name)


# taken from scikit-learn, see
# https://github.com/scikit-learn/scikit-learn/blob/1.3.0/sklearn/utils/validation.py#L38
def _deprecate_positional_args(func=None, *, version=None,
                               deprecated_args=None, custom_message=""):
    """Decorator for methods that issues warnings for positional arguments.

    Using the keyword-only argument syntax in pep 3102, arguments after the
    * will issue a warning when passed as a positional argument.

    Parameters
    ----------
    func : callable, default=None
        Function to check arguments on.
    version : callable, default=None
        The version when positional arguments will result in error.
    deprecated_args : set of str, optional
        Arguments to deprecate - whether passed by position or keyword.
    custom_message : str, optional
        Custom message to add to deprecation warning and documentation.
    """
    if version is None:
        msg = "Need to specify a version where signature will be changed"
        raise ValueError(msg)

    deprecated_args = set() if deprecated_args is None else set(deprecated_args)

    def _inner_deprecate_positional_args(f):
        sig = signature(f)
        kwonly_args = []
        all_args = []

        for name, param in sig.parameters.items():
            if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
                all_args.append(name)
            elif param.kind == Parameter.KEYWORD_ONLY:
                kwonly_args.append(name)

        def warn_deprecated_args(kwargs):
            intersection = deprecated_args.intersection(kwargs)
            if intersection:
                message = (f"Arguments {intersection} are deprecated, whether passed "
                           "by position or keyword. They will be removed in SciPy "
                           f"{version}. ")
                message += custom_message
                warnings.warn(message, category=DeprecationWarning, stacklevel=3)

        @functools.wraps(f)
        def inner_f(*args, **kwargs):

            extra_args = len(args) - len(all_args)
            if extra_args <= 0:
                warn_deprecated_args(kwargs)
                return f(*args, **kwargs)

            # extra_args > 0
            kwonly_extra_args = set(kwonly_args[:extra_args]) - deprecated_args
            args_msg = ", ".join(kwonly_extra_args)
            warnings.warn(
                (
                    f"You are passing as positional arguments: {args_msg}. "
                    "Please change your invocation to use keyword arguments. "
                    f"From SciPy {version}, passing these as positional "
                    "arguments will result in an error."
                ),
                DeprecationWarning,
                stacklevel=2,
            )
            kwargs.update(zip(sig.parameters, args))
            warn_deprecated_args(kwargs)
            return f(**kwargs)

        doc = FunctionDoc(inner_f)
        kwonly_extra_args = set(kwonly_args) - deprecated_args
        admonition = f"""
.. deprecated:: {version}
    Use of argument(s) ``{kwonly_extra_args}`` by position is deprecated; beginning in 
    SciPy {version}, these will be keyword-only. """
        if deprecated_args:
            admonition += (f"Argument(s) ``{deprecated_args}`` are deprecated, whether "
                           "passed by position or keyword; they will be removed in "
                           f"SciPy {version}. ")
        admonition += custom_message
        doc['Extended Summary'] += [admonition]

        doc = str(doc).split("\n", 1)[1]  # remove signature
        inner_f.__doc__ = str(doc)

        return inner_f

    if func is not None:
        return _inner_deprecate_positional_args(func)

    return _inner_deprecate_positional_args