exceptions.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """
  2. General SymPy exceptions and warnings.
  3. """
  4. import warnings
  5. import contextlib
  6. from textwrap import dedent
  7. class SymPyDeprecationWarning(DeprecationWarning):
  8. r"""
  9. A warning for deprecated features of SymPy.
  10. See the :ref:`deprecation-policy` document for details on when and how
  11. things should be deprecated in SymPy.
  12. Note that simply constructing this class will not cause a warning to be
  13. issued. To do that, you must call the :func`sympy_deprecation_warning`
  14. function. For this reason, it is not recommended to ever construct this
  15. class directly.
  16. Explanation
  17. ===========
  18. The ``SymPyDeprecationWarning`` class is a subclass of
  19. ``DeprecationWarning`` that is used for all deprecations in SymPy. A
  20. special subclass is used so that we can automatically augment the warning
  21. message with additional metadata about the version the deprecation was
  22. introduced in and a link to the documentation. This also allows users to
  23. explicitly filter deprecation warnings from SymPy using ``warnings``
  24. filters (see :ref:`silencing-sympy-deprecation-warnings`).
  25. Additionally, ``SymPyDeprecationWarning`` is enabled to be shown by
  26. default, unlike normal ``DeprecationWarning``\s, which are only shown by
  27. default in interactive sessions. This ensures that deprecation warnings in
  28. SymPy will actually be seen by users.
  29. See the documentation of :func:`sympy_deprecation_warning` for a
  30. description of the parameters to this function.
  31. To mark a function as deprecated, you can use the :func:`@deprecated
  32. <sympy.utilities.decorator.deprecated>` decorator.
  33. See Also
  34. ========
  35. sympy.utilities.exceptions.sympy_deprecation_warning
  36. sympy.utilities.exceptions.ignore_warnings
  37. sympy.utilities.decorator.deprecated
  38. sympy.testing.pytest.warns_deprecated_sympy
  39. """
  40. def __init__(self, message, *, deprecated_since_version, active_deprecations_target):
  41. super().__init__(message, deprecated_since_version,
  42. active_deprecations_target)
  43. self.message = message
  44. if not isinstance(deprecated_since_version, str):
  45. raise TypeError(f"'deprecated_since_version' should be a string, got {deprecated_since_version!r}")
  46. self.deprecated_since_version = deprecated_since_version
  47. self.active_deprecations_target = active_deprecations_target
  48. if any(i in active_deprecations_target for i in '()='):
  49. raise ValueError("active_deprecations_target be the part inside of the '(...)='")
  50. self.full_message = f"""
  51. {dedent(message).strip()}
  52. See https://docs.sympy.org/latest/explanation/active-deprecations.html#{active_deprecations_target}
  53. for details.
  54. This has been deprecated since SymPy version {deprecated_since_version}. It
  55. will be removed in a future version of SymPy.
  56. """
  57. def __str__(self):
  58. return self.full_message
  59. def __repr__(self):
  60. return f"{self.__class__.__name__}({self.message!r}, deprecated_since_version={self.deprecated_since_version!r}, active_deprecations_target={self.active_deprecations_target!r})"
  61. def __eq__(self, other):
  62. return isinstance(other, SymPyDeprecationWarning) and self.args == other.args
  63. # Make pickling work. The by default, it tries to recreate the expression
  64. # from its args, but this doesn't work because of our keyword-only
  65. # arguments.
  66. @classmethod
  67. def _new(cls, message, deprecated_since_version,
  68. active_deprecations_target):
  69. return cls(message, deprecated_since_version=deprecated_since_version, active_deprecations_target=active_deprecations_target)
  70. def __reduce__(self):
  71. return (self._new, (self.message, self.deprecated_since_version, self.active_deprecations_target))
  72. # Python by default hides DeprecationWarnings, which we do not want.
  73. warnings.simplefilter("once", SymPyDeprecationWarning)
  74. def sympy_deprecation_warning(message, *, deprecated_since_version,
  75. active_deprecations_target, stacklevel=3):
  76. r'''
  77. Warn that a feature is deprecated in SymPy.
  78. See the :ref:`deprecation-policy` document for details on when and how
  79. things should be deprecated in SymPy.
  80. To mark an entire function or class as deprecated, you can use the
  81. :func:`@deprecated <sympy.utilities.decorator.deprecated>` decorator.
  82. Parameters
  83. ==========
  84. message: str
  85. The deprecation message. This may span multiple lines and contain
  86. code examples. Messages should be wrapped to 80 characters. The
  87. message is automatically dedented and leading and trailing whitespace
  88. stripped. Messages may include dynamic content based on the user
  89. input, but avoid using ``str(expression)`` if an expression can be
  90. arbitrary, as it might be huge and make the warning message
  91. unreadable.
  92. deprecated_since_version: str
  93. The version of SymPy the feature has been deprecated since. For new
  94. deprecations, this should be the version in `sympy/release.py
  95. <https://github.com/sympy/sympy/blob/master/sympy/release.py>`_
  96. without the ``.dev``. If the next SymPy version ends up being
  97. different from this, the release manager will need to update any
  98. ``SymPyDeprecationWarning``\s using the incorrect version. This
  99. argument is required and must be passed as a keyword argument.
  100. (example: ``deprecated_since_version="1.10"``).
  101. active_deprecations_target: str
  102. The Sphinx target corresponding to the section for the deprecation in
  103. the :ref:`active-deprecations` document (see
  104. ``doc/src/explanation/active-deprecations.md``). This is used to
  105. automatically generate a URL to the page in the warning message. This
  106. argument is required and must be passed as a keyword argument.
  107. (example: ``active_deprecations_target="deprecated-feature-abc"``)
  108. stacklevel: int (default: 3)
  109. The ``stacklevel`` parameter that is passed to ``warnings.warn``. If
  110. you create a wrapper that calls this function, this should be
  111. increased so that the warning message shows the user line of code that
  112. produced the warning. Note that in some cases there will be multiple
  113. possible different user code paths that could result in the warning.
  114. In that case, just choose the smallest common stacklevel.
  115. Examples
  116. ========
  117. >>> from sympy.utilities.exceptions import sympy_deprecation_warning
  118. >>> def is_this_zero(x, y=0):
  119. ... """
  120. ... Determine if x = 0.
  121. ...
  122. ... Parameters
  123. ... ==========
  124. ...
  125. ... x : Expr
  126. ... The expression to check.
  127. ...
  128. ... y : Expr, optional
  129. ... If provided, check if x = y.
  130. ...
  131. ... .. deprecated:: 1.1
  132. ...
  133. ... The ``y`` argument to ``is_this_zero`` is deprecated. Use
  134. ... ``is_this_zero(x - y)`` instead.
  135. ...
  136. ... """
  137. ... from sympy import simplify
  138. ...
  139. ... if y != 0:
  140. ... sympy_deprecation_warning("""
  141. ... The y argument to is_zero() is deprecated. Use is_zero(x - y) instead.""",
  142. ... deprecated_since_version="1.1",
  143. ... active_deprecations_target='is-this-zero-y-deprecation')
  144. ... return simplify(x - y) == 0
  145. >>> is_this_zero(0)
  146. True
  147. >>> is_this_zero(1, 1) # doctest: +SKIP
  148. <stdin>:1: SymPyDeprecationWarning:
  149. <BLANKLINE>
  150. The y argument to is_zero() is deprecated. Use is_zero(x - y) instead.
  151. <BLANKLINE>
  152. See https://docs.sympy.org/latest/explanation/active-deprecations.html#is-this-zero-y-deprecation
  153. for details.
  154. <BLANKLINE>
  155. This has been deprecated since SymPy version 1.1. It
  156. will be removed in a future version of SymPy.
  157. <BLANKLINE>
  158. is_this_zero(1, 1)
  159. True
  160. See Also
  161. ========
  162. sympy.utilities.exceptions.SymPyDeprecationWarning
  163. sympy.utilities.exceptions.ignore_warnings
  164. sympy.utilities.decorator.deprecated
  165. sympy.testing.pytest.warns_deprecated_sympy
  166. '''
  167. w = SymPyDeprecationWarning(message,
  168. deprecated_since_version=deprecated_since_version,
  169. active_deprecations_target=active_deprecations_target)
  170. warnings.warn(w, stacklevel=stacklevel)
  171. @contextlib.contextmanager
  172. def ignore_warnings(warningcls):
  173. '''
  174. Context manager to suppress warnings during tests.
  175. .. note::
  176. Do not use this with SymPyDeprecationWarning in the tests.
  177. warns_deprecated_sympy() should be used instead.
  178. This function is useful for suppressing warnings during tests. The warns
  179. function should be used to assert that a warning is raised. The
  180. ignore_warnings function is useful in situation when the warning is not
  181. guaranteed to be raised (e.g. on importing a module) or if the warning
  182. comes from third-party code.
  183. This function is also useful to prevent the same or similar warnings from
  184. being issue twice due to recursive calls.
  185. When the warning is coming (reliably) from SymPy the warns function should
  186. be preferred to ignore_warnings.
  187. >>> from sympy.utilities.exceptions import ignore_warnings
  188. >>> import warnings
  189. Here's a warning:
  190. >>> with warnings.catch_warnings(): # reset warnings in doctest
  191. ... warnings.simplefilter('error')
  192. ... warnings.warn('deprecated', UserWarning)
  193. Traceback (most recent call last):
  194. ...
  195. UserWarning: deprecated
  196. Let's suppress it with ignore_warnings:
  197. >>> with warnings.catch_warnings(): # reset warnings in doctest
  198. ... warnings.simplefilter('error')
  199. ... with ignore_warnings(UserWarning):
  200. ... warnings.warn('deprecated', UserWarning)
  201. (No warning emitted)
  202. See Also
  203. ========
  204. sympy.utilities.exceptions.SymPyDeprecationWarning
  205. sympy.utilities.exceptions.sympy_deprecation_warning
  206. sympy.utilities.decorator.deprecated
  207. sympy.testing.pytest.warns_deprecated_sympy
  208. '''
  209. # Absorbs all warnings in warnrec
  210. with warnings.catch_warnings(record=True) as warnrec:
  211. # Make sure our warning doesn't get filtered
  212. warnings.simplefilter("always", warningcls)
  213. # Now run the test
  214. yield
  215. # Reissue any warnings that we aren't testing for
  216. for w in warnrec:
  217. if not issubclass(w.category, warningcls):
  218. warnings.warn_explicit(w.message, w.category, w.filename, w.lineno)