decorator.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. """Useful utility decorators. """
  2. import sys
  3. import types
  4. import inspect
  5. from functools import wraps, update_wrapper
  6. from sympy.utilities.exceptions import sympy_deprecation_warning
  7. def threaded_factory(func, use_add):
  8. """A factory for ``threaded`` decorators. """
  9. from sympy.core import sympify
  10. from sympy.matrices import MatrixBase
  11. from sympy.utilities.iterables import iterable
  12. @wraps(func)
  13. def threaded_func(expr, *args, **kwargs):
  14. if isinstance(expr, MatrixBase):
  15. return expr.applyfunc(lambda f: func(f, *args, **kwargs))
  16. elif iterable(expr):
  17. try:
  18. return expr.__class__([func(f, *args, **kwargs) for f in expr])
  19. except TypeError:
  20. return expr
  21. else:
  22. expr = sympify(expr)
  23. if use_add and expr.is_Add:
  24. return expr.__class__(*[ func(f, *args, **kwargs) for f in expr.args ])
  25. elif expr.is_Relational:
  26. return expr.__class__(func(expr.lhs, *args, **kwargs),
  27. func(expr.rhs, *args, **kwargs))
  28. else:
  29. return func(expr, *args, **kwargs)
  30. return threaded_func
  31. def threaded(func):
  32. """Apply ``func`` to sub--elements of an object, including :class:`~.Add`.
  33. This decorator is intended to make it uniformly possible to apply a
  34. function to all elements of composite objects, e.g. matrices, lists, tuples
  35. and other iterable containers, or just expressions.
  36. This version of :func:`threaded` decorator allows threading over
  37. elements of :class:`~.Add` class. If this behavior is not desirable
  38. use :func:`xthreaded` decorator.
  39. Functions using this decorator must have the following signature::
  40. @threaded
  41. def function(expr, *args, **kwargs):
  42. """
  43. return threaded_factory(func, True)
  44. def xthreaded(func):
  45. """Apply ``func`` to sub--elements of an object, excluding :class:`~.Add`.
  46. This decorator is intended to make it uniformly possible to apply a
  47. function to all elements of composite objects, e.g. matrices, lists, tuples
  48. and other iterable containers, or just expressions.
  49. This version of :func:`threaded` decorator disallows threading over
  50. elements of :class:`~.Add` class. If this behavior is not desirable
  51. use :func:`threaded` decorator.
  52. Functions using this decorator must have the following signature::
  53. @xthreaded
  54. def function(expr, *args, **kwargs):
  55. """
  56. return threaded_factory(func, False)
  57. def conserve_mpmath_dps(func):
  58. """After the function finishes, resets the value of mpmath.mp.dps to
  59. the value it had before the function was run."""
  60. import mpmath
  61. def func_wrapper(*args, **kwargs):
  62. dps = mpmath.mp.dps
  63. try:
  64. return func(*args, **kwargs)
  65. finally:
  66. mpmath.mp.dps = dps
  67. func_wrapper = update_wrapper(func_wrapper, func)
  68. return func_wrapper
  69. class no_attrs_in_subclass:
  70. """Don't 'inherit' certain attributes from a base class
  71. >>> from sympy.utilities.decorator import no_attrs_in_subclass
  72. >>> class A(object):
  73. ... x = 'test'
  74. >>> A.x = no_attrs_in_subclass(A, A.x)
  75. >>> class B(A):
  76. ... pass
  77. >>> hasattr(A, 'x')
  78. True
  79. >>> hasattr(B, 'x')
  80. False
  81. """
  82. def __init__(self, cls, f):
  83. self.cls = cls
  84. self.f = f
  85. def __get__(self, instance, owner=None):
  86. if owner == self.cls:
  87. if hasattr(self.f, '__get__'):
  88. return self.f.__get__(instance, owner)
  89. return self.f
  90. raise AttributeError
  91. def doctest_depends_on(exe=None, modules=None, disable_viewers=None, python_version=None):
  92. """
  93. Adds metadata about the dependencies which need to be met for doctesting
  94. the docstrings of the decorated objects.
  95. exe should be a list of executables
  96. modules should be a list of modules
  97. disable_viewers should be a list of viewers for preview() to disable
  98. python_version should be the minimum Python version required, as a tuple
  99. (like (3, 0))
  100. """
  101. dependencies = {}
  102. if exe is not None:
  103. dependencies['executables'] = exe
  104. if modules is not None:
  105. dependencies['modules'] = modules
  106. if disable_viewers is not None:
  107. dependencies['disable_viewers'] = disable_viewers
  108. if python_version is not None:
  109. dependencies['python_version'] = python_version
  110. def skiptests():
  111. from sympy.testing.runtests import DependencyError, SymPyDocTests, PyTestReporter # lazy import
  112. r = PyTestReporter()
  113. t = SymPyDocTests(r, None)
  114. try:
  115. t._check_dependencies(**dependencies)
  116. except DependencyError:
  117. return True # Skip doctests
  118. else:
  119. return False # Run doctests
  120. def depends_on_deco(fn):
  121. fn._doctest_depends_on = dependencies
  122. fn.__doctest_skip__ = skiptests
  123. if inspect.isclass(fn):
  124. fn._doctest_depdends_on = no_attrs_in_subclass(
  125. fn, fn._doctest_depends_on)
  126. fn.__doctest_skip__ = no_attrs_in_subclass(
  127. fn, fn.__doctest_skip__)
  128. return fn
  129. return depends_on_deco
  130. def public(obj):
  131. """
  132. Append ``obj``'s name to global ``__all__`` variable (call site).
  133. By using this decorator on functions or classes you achieve the same goal
  134. as by filling ``__all__`` variables manually, you just do not have to repeat
  135. yourself (object's name). You also know if object is public at definition
  136. site, not at some random location (where ``__all__`` was set).
  137. Note that in multiple decorator setup (in almost all cases) ``@public``
  138. decorator must be applied before any other decorators, because it relies
  139. on the pointer to object's global namespace. If you apply other decorators
  140. first, ``@public`` may end up modifying the wrong namespace.
  141. Examples
  142. ========
  143. >>> from sympy.utilities.decorator import public
  144. >>> __all__ # noqa: F821
  145. Traceback (most recent call last):
  146. ...
  147. NameError: name '__all__' is not defined
  148. >>> @public
  149. ... def some_function():
  150. ... pass
  151. >>> __all__ # noqa: F821
  152. ['some_function']
  153. """
  154. if isinstance(obj, types.FunctionType):
  155. ns = obj.__globals__
  156. name = obj.__name__
  157. elif isinstance(obj, (type(type), type)):
  158. ns = sys.modules[obj.__module__].__dict__
  159. name = obj.__name__
  160. else:
  161. raise TypeError("expected a function or a class, got %s" % obj)
  162. if "__all__" not in ns:
  163. ns["__all__"] = [name]
  164. else:
  165. ns["__all__"].append(name)
  166. return obj
  167. def memoize_property(propfunc):
  168. """Property decorator that caches the value of potentially expensive
  169. `propfunc` after the first evaluation. The cached value is stored in
  170. the corresponding property name with an attached underscore."""
  171. attrname = '_' + propfunc.__name__
  172. sentinel = object()
  173. @wraps(propfunc)
  174. def accessor(self):
  175. val = getattr(self, attrname, sentinel)
  176. if val is sentinel:
  177. val = propfunc(self)
  178. setattr(self, attrname, val)
  179. return val
  180. return property(accessor)
  181. def deprecated(message, *, deprecated_since_version,
  182. active_deprecations_target, stacklevel=3):
  183. '''
  184. Mark a function as deprecated.
  185. This decorator should be used if an entire function or class is
  186. deprecated. If only a certain functionality is deprecated, you should use
  187. :func:`~.warns_deprecated_sympy` directly. This decorator is just a
  188. convenience. There is no functional difference between using this
  189. decorator and calling ``warns_deprecated_sympy()`` at the top of the
  190. function.
  191. The decorator takes the same arguments as
  192. :func:`~.warns_deprecated_sympy`. See its
  193. documentation for details on what the keywords to this decorator do.
  194. See the :ref:`deprecation-policy` document for details on when and how
  195. things should be deprecated in SymPy.
  196. Examples
  197. ========
  198. >>> from sympy.utilities.decorator import deprecated
  199. >>> from sympy import simplify
  200. >>> @deprecated("""\
  201. ... The simplify_this(expr) function is deprecated. Use simplify(expr)
  202. ... instead.""", deprecated_since_version="1.1",
  203. ... active_deprecations_target='simplify-this-deprecation')
  204. ... def simplify_this(expr):
  205. ... """
  206. ... Simplify ``expr``.
  207. ...
  208. ... .. deprecated:: 1.1
  209. ...
  210. ... The ``simplify_this`` function is deprecated. Use :func:`simplify`
  211. ... instead. See its documentation for more information. See
  212. ... :ref:`simplify-this-deprecation` for details.
  213. ...
  214. ... """
  215. ... return simplify(expr)
  216. >>> from sympy.abc import x
  217. >>> simplify_this(x*(x + 1) - x**2) # doctest: +SKIP
  218. <stdin>:1: SymPyDeprecationWarning:
  219. <BLANKLINE>
  220. The simplify_this(expr) function is deprecated. Use simplify(expr)
  221. instead.
  222. <BLANKLINE>
  223. See https://docs.sympy.org/latest/explanation/active-deprecations.html#simplify-this-deprecation
  224. for details.
  225. <BLANKLINE>
  226. This has been deprecated since SymPy version 1.1. It
  227. will be removed in a future version of SymPy.
  228. <BLANKLINE>
  229. simplify_this(x)
  230. x
  231. See Also
  232. ========
  233. sympy.utilities.exceptions.SymPyDeprecationWarning
  234. sympy.utilities.exceptions.sympy_deprecation_warning
  235. sympy.utilities.exceptions.ignore_warnings
  236. sympy.testing.pytest.warns_deprecated_sympy
  237. '''
  238. decorator_kwargs = {"deprecated_since_version": deprecated_since_version,
  239. "active_deprecations_target": active_deprecations_target}
  240. def deprecated_decorator(wrapped):
  241. if hasattr(wrapped, '__mro__'): # wrapped is actually a class
  242. class wrapper(wrapped):
  243. __doc__ = wrapped.__doc__
  244. __module__ = wrapped.__module__
  245. _sympy_deprecated_func = wrapped
  246. if '__new__' in wrapped.__dict__:
  247. def __new__(cls, *args, **kwargs):
  248. sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel)
  249. return super().__new__(cls, *args, **kwargs)
  250. else:
  251. def __init__(self, *args, **kwargs):
  252. sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel)
  253. super().__init__(*args, **kwargs)
  254. wrapper.__name__ = wrapped.__name__
  255. else:
  256. @wraps(wrapped)
  257. def wrapper(*args, **kwargs):
  258. sympy_deprecation_warning(message, **decorator_kwargs, stacklevel=stacklevel)
  259. return wrapped(*args, **kwargs)
  260. wrapper._sympy_deprecated_func = wrapped
  261. return wrapper
  262. return deprecated_decorator