deprecation.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. from inspect import Parameter, signature
  2. import functools
  3. import warnings
  4. from importlib import import_module
  5. from scipy._lib._docscrape import FunctionDoc
  6. __all__ = ["_deprecated"]
  7. # Object to use as default value for arguments to be deprecated. This should
  8. # be used over 'None' as the user could parse 'None' as a positional argument
  9. _NoValue = object()
  10. def _sub_module_deprecation(*, sub_package, module, private_modules, all,
  11. attribute, correct_module=None, dep_version="1.16.0"):
  12. """Helper function for deprecating modules that are public but were
  13. intended to be private.
  14. Parameters
  15. ----------
  16. sub_package : str
  17. Subpackage the module belongs to eg. stats
  18. module : str
  19. Public but intended private module to deprecate
  20. private_modules : list
  21. Private replacement(s) for `module`; should contain the
  22. content of ``all``, possibly spread over several modules.
  23. all : list
  24. ``__all__`` belonging to `module`
  25. attribute : str
  26. The attribute in `module` being accessed
  27. correct_module : str, optional
  28. Module in `sub_package` that `attribute` should be imported from.
  29. Default is that `attribute` should be imported from ``scipy.sub_package``.
  30. dep_version : str, optional
  31. Version in which deprecated attributes will be removed.
  32. """
  33. if correct_module is not None:
  34. correct_import = f"scipy.{sub_package}.{correct_module}"
  35. else:
  36. correct_import = f"scipy.{sub_package}"
  37. if attribute not in all:
  38. raise AttributeError(
  39. f"`scipy.{sub_package}.{module}` has no attribute `{attribute}`; "
  40. f"furthermore, `scipy.{sub_package}.{module}` is deprecated "
  41. f"and will be removed in SciPy 2.0.0."
  42. )
  43. attr = getattr(import_module(correct_import), attribute, None)
  44. if attr is not None:
  45. message = (
  46. f"Please import `{attribute}` from the `{correct_import}` namespace; "
  47. f"the `scipy.{sub_package}.{module}` namespace is deprecated "
  48. f"and will be removed in SciPy 2.0.0."
  49. )
  50. else:
  51. message = (
  52. f"`scipy.{sub_package}.{module}.{attribute}` is deprecated along with "
  53. f"the `scipy.{sub_package}.{module}` namespace. "
  54. f"`scipy.{sub_package}.{module}.{attribute}` will be removed "
  55. f"in SciPy {dep_version}, and the `scipy.{sub_package}.{module}` namespace "
  56. f"will be removed in SciPy 2.0.0."
  57. )
  58. warnings.warn(message, category=DeprecationWarning, stacklevel=3)
  59. for module in private_modules:
  60. try:
  61. return getattr(import_module(f"scipy.{sub_package}.{module}"), attribute)
  62. except AttributeError as e:
  63. # still raise an error if the attribute isn't in any of the expected
  64. # private modules
  65. if module == private_modules[-1]:
  66. raise e
  67. continue
  68. def _deprecated(msg, stacklevel=2):
  69. """Deprecate a function by emitting a warning on use."""
  70. def wrap(fun):
  71. if isinstance(fun, type):
  72. warnings.warn(
  73. f"Trying to deprecate class {fun!r}",
  74. category=RuntimeWarning, stacklevel=2)
  75. return fun
  76. @functools.wraps(fun)
  77. def call(*args, **kwargs):
  78. warnings.warn(msg, category=DeprecationWarning,
  79. stacklevel=stacklevel)
  80. return fun(*args, **kwargs)
  81. call.__doc__ = fun.__doc__
  82. return call
  83. return wrap
  84. class _DeprecationHelperStr:
  85. """
  86. Helper class used by deprecate_cython_api
  87. """
  88. def __init__(self, content, message):
  89. self._content = content
  90. self._message = message
  91. def __hash__(self):
  92. return hash(self._content)
  93. def __eq__(self, other):
  94. res = (self._content == other)
  95. if res:
  96. warnings.warn(self._message, category=DeprecationWarning,
  97. stacklevel=2)
  98. return res
  99. def deprecate_cython_api(module, routine_name, new_name=None, message=None):
  100. """
  101. Deprecate an exported cdef function in a public Cython API module.
  102. Only functions can be deprecated; typedefs etc. cannot.
  103. Parameters
  104. ----------
  105. module : module
  106. Public Cython API module (e.g. scipy.linalg.cython_blas).
  107. routine_name : str
  108. Name of the routine to deprecate. May also be a fused-type
  109. routine (in which case its all specializations are deprecated).
  110. new_name : str
  111. New name to include in the deprecation warning message
  112. message : str
  113. Additional text in the deprecation warning message
  114. Examples
  115. --------
  116. Usually, this function would be used in the top-level of the
  117. module ``.pyx`` file:
  118. >>> from scipy._lib.deprecation import deprecate_cython_api
  119. >>> import scipy.linalg.cython_blas as mod
  120. >>> deprecate_cython_api(mod, "dgemm", "dgemm_new",
  121. ... message="Deprecated in Scipy 1.5.0")
  122. >>> del deprecate_cython_api, mod
  123. After this, Cython modules that use the deprecated function emit a
  124. deprecation warning when they are imported.
  125. """
  126. old_name = f"{module.__name__}.{routine_name}"
  127. if new_name is None:
  128. depdoc = f"`{old_name}` is deprecated!"
  129. else:
  130. depdoc = f"`{old_name}` is deprecated, use `{new_name}` instead!"
  131. if message is not None:
  132. depdoc += "\n" + message
  133. d = module.__pyx_capi__
  134. # Check if the function is a fused-type function with a mangled name
  135. j = 0
  136. has_fused = False
  137. while True:
  138. fused_name = f"__pyx_fuse_{j}{routine_name}"
  139. if fused_name in d:
  140. has_fused = True
  141. d[_DeprecationHelperStr(fused_name, depdoc)] = d.pop(fused_name)
  142. j += 1
  143. else:
  144. break
  145. # If not, apply deprecation to the named routine
  146. if not has_fused:
  147. d[_DeprecationHelperStr(routine_name, depdoc)] = d.pop(routine_name)
  148. # taken from scikit-learn, see
  149. # https://github.com/scikit-learn/scikit-learn/blob/1.3.0/sklearn/utils/validation.py#L38
  150. def _deprecate_positional_args(func=None, *, version=None,
  151. deprecated_args=None, custom_message=""):
  152. """Decorator for methods that issues warnings for positional arguments.
  153. Using the keyword-only argument syntax in pep 3102, arguments after the
  154. * will issue a warning when passed as a positional argument.
  155. Parameters
  156. ----------
  157. func : callable, default=None
  158. Function to check arguments on.
  159. version : callable, default=None
  160. The version when positional arguments will result in error.
  161. deprecated_args : set of str, optional
  162. Arguments to deprecate - whether passed by position or keyword.
  163. custom_message : str, optional
  164. Custom message to add to deprecation warning and documentation.
  165. """
  166. if version is None:
  167. msg = "Need to specify a version where signature will be changed"
  168. raise ValueError(msg)
  169. deprecated_args = set() if deprecated_args is None else set(deprecated_args)
  170. def _inner_deprecate_positional_args(f):
  171. sig = signature(f)
  172. kwonly_args = []
  173. all_args = []
  174. for name, param in sig.parameters.items():
  175. if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
  176. all_args.append(name)
  177. elif param.kind == Parameter.KEYWORD_ONLY:
  178. kwonly_args.append(name)
  179. def warn_deprecated_args(kwargs):
  180. intersection = deprecated_args.intersection(kwargs)
  181. if intersection:
  182. message = (f"Arguments {intersection} are deprecated, whether passed "
  183. "by position or keyword. They will be removed in SciPy "
  184. f"{version}. ")
  185. message += custom_message
  186. warnings.warn(message, category=DeprecationWarning, stacklevel=3)
  187. @functools.wraps(f)
  188. def inner_f(*args, **kwargs):
  189. extra_args = len(args) - len(all_args)
  190. if extra_args <= 0:
  191. warn_deprecated_args(kwargs)
  192. return f(*args, **kwargs)
  193. # extra_args > 0
  194. kwonly_extra_args = set(kwonly_args[:extra_args]) - deprecated_args
  195. args_msg = ", ".join(kwonly_extra_args)
  196. warnings.warn(
  197. (
  198. f"You are passing as positional arguments: {args_msg}. "
  199. "Please change your invocation to use keyword arguments. "
  200. f"From SciPy {version}, passing these as positional "
  201. "arguments will result in an error."
  202. ),
  203. DeprecationWarning,
  204. stacklevel=2,
  205. )
  206. kwargs.update(zip(sig.parameters, args))
  207. warn_deprecated_args(kwargs)
  208. return f(**kwargs)
  209. doc = FunctionDoc(inner_f)
  210. kwonly_extra_args = set(kwonly_args) - deprecated_args
  211. admonition = f"""
  212. .. deprecated:: {version}
  213. Use of argument(s) ``{kwonly_extra_args}`` by position is deprecated; beginning in
  214. SciPy {version}, these will be keyword-only. """
  215. if deprecated_args:
  216. admonition += (f"Argument(s) ``{deprecated_args}`` are deprecated, whether "
  217. "passed by position or keyword; they will be removed in "
  218. f"SciPy {version}. ")
  219. admonition += custom_message
  220. doc['Extended Summary'] += [admonition]
  221. doc = str(doc).split("\n", 1)[1].lstrip(" \n") # remove signature
  222. inner_f.__doc__ = str(doc)
  223. return inner_f
  224. if func is not None:
  225. return _inner_deprecate_positional_args(func)
  226. return _inner_deprecate_positional_args