PageRenderTime 45ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/astropy/utils/tests/test_decorators.py

https://github.com/astropy/astropy
Python | 771 lines | 751 code | 5 blank | 15 comment | 0 complexity | 5801379c754e47aab2c76d09bb5d0332 MD5 | raw file
  1. # Licensed under a 3-clause BSD style license - see LICENSE.rst
  2. import concurrent.futures
  3. import inspect
  4. import pickle
  5. import pytest
  6. from astropy.utils.decorators import (deprecated_attribute, deprecated,
  7. sharedmethod, classproperty, lazyproperty,
  8. format_doc, deprecated_renamed_argument)
  9. from astropy.utils.exceptions import (AstropyDeprecationWarning,
  10. AstropyPendingDeprecationWarning,
  11. AstropyUserWarning)
  12. class NewDeprecationWarning(AstropyDeprecationWarning):
  13. """
  14. New Warning subclass to be used to test the deprecated decorator's
  15. ``warning_type`` parameter.
  16. """
  17. def test_deprecated_attribute():
  18. class DummyClass:
  19. def __init__(self):
  20. self._foo = 42
  21. self._bar = 4242
  22. self._message = '42'
  23. self._alternative = [42]
  24. self._pending = {42}
  25. def set_private(self):
  26. self._foo = 100
  27. self._bar = 1000
  28. self._message = '100'
  29. self._alternative = [100]
  30. self._pending = {100}
  31. foo = deprecated_attribute('foo', '0.2')
  32. bar = deprecated_attribute('bar', '0.2',
  33. warning_type=NewDeprecationWarning)
  34. alternative = deprecated_attribute('alternative', '0.2',
  35. alternative='other')
  36. message = deprecated_attribute('message', '0.2', message='MSG')
  37. pending = deprecated_attribute('pending', '0.2', pending=True)
  38. dummy = DummyClass()
  39. with pytest.warns(AstropyDeprecationWarning, match="The foo attribute is "
  40. "deprecated and may be removed in a future version.") as w:
  41. dummy.foo
  42. assert len(w) == 1
  43. with pytest.warns(NewDeprecationWarning, match="The bar attribute is "
  44. "deprecated and may be removed in a future version.") as w:
  45. dummy.bar
  46. assert len(w) == 1
  47. with pytest.warns(AstropyDeprecationWarning, match="MSG"):
  48. dummy.message
  49. with pytest.warns(AstropyDeprecationWarning, match=r"Use other instead\."):
  50. dummy.alternative
  51. with pytest.warns(AstropyPendingDeprecationWarning):
  52. dummy.pending
  53. dummy.set_private()
  54. # This needs to be defined outside of the test function, because we
  55. # want to try to pickle it.
  56. @deprecated('100.0')
  57. class TA:
  58. """
  59. This is the class docstring.
  60. """
  61. def __init__(self):
  62. """
  63. This is the __init__ docstring
  64. """
  65. pass
  66. class TMeta(type):
  67. metaclass_attr = 1
  68. @deprecated('100.0')
  69. class TB(metaclass=TMeta):
  70. pass
  71. @deprecated('100.0', warning_type=NewDeprecationWarning)
  72. class TC:
  73. """
  74. This class has the custom warning.
  75. """
  76. pass
  77. def test_deprecated_class():
  78. orig_A = TA.__bases__[0]
  79. # The only thing that should be different about the new class
  80. # is __doc__, __init__, __bases__ and __subclasshook__.
  81. # and __init_subclass__ for Python 3.6+.
  82. for x in dir(orig_A):
  83. if x not in ('__doc__', '__init__', '__bases__', '__dict__',
  84. '__subclasshook__', '__init_subclass__'):
  85. assert getattr(TA, x) == getattr(orig_A, x)
  86. with pytest.warns(AstropyDeprecationWarning) as w:
  87. TA()
  88. assert len(w) == 1
  89. if TA.__doc__ is not None:
  90. assert 'function' not in TA.__doc__
  91. assert 'deprecated' in TA.__doc__
  92. assert 'function' not in TA.__init__.__doc__
  93. assert 'deprecated' in TA.__init__.__doc__
  94. # Make sure the object is picklable
  95. pickle.dumps(TA)
  96. with pytest.warns(NewDeprecationWarning) as w:
  97. TC()
  98. assert len(w) == 1
  99. def test_deprecated_class_with_new_method():
  100. """
  101. Test that a class with __new__ method still works even if it accepts
  102. additional arguments.
  103. This previously failed because the deprecated decorator would wrap objects
  104. __init__ which takes no arguments.
  105. """
  106. @deprecated('1.0')
  107. class A:
  108. def __new__(cls, a):
  109. return super().__new__(cls)
  110. # Creating an instance should work but raise a DeprecationWarning
  111. with pytest.warns(AstropyDeprecationWarning) as w:
  112. A(1)
  113. assert len(w) == 1
  114. @deprecated('1.0')
  115. class B:
  116. def __new__(cls, a):
  117. return super().__new__(cls)
  118. def __init__(self, a):
  119. pass
  120. # Creating an instance should work but raise a DeprecationWarning
  121. with pytest.warns(AstropyDeprecationWarning) as w:
  122. B(1)
  123. assert len(w) == 1
  124. def test_deprecated_class_with_super():
  125. """
  126. Regression test for an issue where classes that used ``super()`` in their
  127. ``__init__`` did not actually call the correct class's ``__init__`` in the
  128. MRO.
  129. """
  130. @deprecated('100.0')
  131. class TB:
  132. def __init__(self, a, b):
  133. super().__init__()
  134. with pytest.warns(AstropyDeprecationWarning) as w:
  135. TB(1, 2)
  136. assert len(w) == 1
  137. if TB.__doc__ is not None:
  138. assert 'function' not in TB.__doc__
  139. assert 'deprecated' in TB.__doc__
  140. assert 'function' not in TB.__init__.__doc__
  141. assert 'deprecated' in TB.__init__.__doc__
  142. def test_deprecated_class_with_custom_metaclass():
  143. """
  144. Regression test for an issue where deprecating a class with a metaclass
  145. other than type did not restore the metaclass properly.
  146. """
  147. with pytest.warns(AstropyDeprecationWarning) as w:
  148. TB()
  149. assert len(w) == 1
  150. assert type(TB) is TMeta
  151. assert TB.metaclass_attr == 1
  152. def test_deprecated_static_and_classmethod():
  153. """
  154. Regression test for issue introduced by
  155. https://github.com/astropy/astropy/pull/2811 and mentioned also here:
  156. https://github.com/astropy/astropy/pull/2580#issuecomment-51049969
  157. where it appears that deprecated staticmethods didn't work on Python 2.6.
  158. """
  159. class A:
  160. """Docstring"""
  161. @deprecated('1.0')
  162. @staticmethod
  163. def B():
  164. pass
  165. @deprecated('1.0')
  166. @classmethod
  167. def C(cls):
  168. pass
  169. with pytest.warns(AstropyDeprecationWarning) as w:
  170. A.B()
  171. assert len(w) == 1
  172. if A.__doc__ is not None:
  173. assert 'deprecated' in A.B.__doc__
  174. with pytest.warns(AstropyDeprecationWarning) as w:
  175. A.C()
  176. assert len(w) == 1
  177. if A.__doc__ is not None:
  178. assert 'deprecated' in A.C.__doc__
  179. def test_deprecated_argument():
  180. # Tests the decorator with function, method, staticmethod and classmethod.
  181. class Test:
  182. @classmethod
  183. @deprecated_renamed_argument('clobber', 'overwrite', '1.3')
  184. def test1(cls, overwrite):
  185. return overwrite
  186. @staticmethod
  187. @deprecated_renamed_argument('clobber', 'overwrite', '1.3')
  188. def test2(overwrite):
  189. return overwrite
  190. @deprecated_renamed_argument('clobber', 'overwrite', '1.3')
  191. def test3(self, overwrite):
  192. return overwrite
  193. @deprecated_renamed_argument('clobber', 'overwrite', '1.3',
  194. warning_type=NewDeprecationWarning)
  195. def test4(self, overwrite):
  196. return overwrite
  197. @deprecated_renamed_argument('clobber', 'overwrite', '1.3', relax=False)
  198. def test1(overwrite):
  199. return overwrite
  200. for method in [Test().test1, Test().test2, Test().test3, Test().test4, test1]:
  201. # As positional argument only
  202. assert method(1) == 1
  203. # As new keyword argument
  204. assert method(overwrite=1) == 1
  205. # Using the deprecated name
  206. with pytest.warns(AstropyDeprecationWarning, match=r"1\.3") as w:
  207. assert method(clobber=1) == 1
  208. assert len(w) == 1
  209. assert 'test_decorators.py' in str(w[0].filename)
  210. if method.__name__ == 'test4':
  211. assert issubclass(w[0].category, NewDeprecationWarning)
  212. # Using both. Both keyword
  213. with pytest.raises(TypeError), pytest.warns(AstropyDeprecationWarning):
  214. method(clobber=2, overwrite=1)
  215. # One positional, one keyword
  216. with pytest.raises(TypeError), pytest.warns(AstropyDeprecationWarning):
  217. method(1, clobber=2)
  218. def test_deprecated_argument_custom_message():
  219. @deprecated_renamed_argument('foo', 'bar', '4.0', message='Custom msg')
  220. def test(bar=0):
  221. pass
  222. with pytest.warns(AstropyDeprecationWarning, match='Custom msg'):
  223. test(foo=0)
  224. def test_deprecated_argument_in_kwargs():
  225. # To rename an argument that is consumed by "kwargs" the "arg_in_kwargs"
  226. # parameter is used.
  227. @deprecated_renamed_argument('clobber', 'overwrite', '1.3',
  228. arg_in_kwargs=True)
  229. def test(**kwargs):
  230. return kwargs['overwrite']
  231. # As positional argument only
  232. with pytest.raises(TypeError):
  233. test(1)
  234. # As new keyword argument
  235. assert test(overwrite=1) == 1
  236. # Using the deprecated name
  237. with pytest.warns(AstropyDeprecationWarning, match=r"1\.3") as w:
  238. assert test(clobber=1) == 1
  239. assert len(w) == 1
  240. assert 'test_decorators.py' in str(w[0].filename)
  241. # Using both. Both keyword
  242. with pytest.raises(TypeError), pytest.warns(AstropyDeprecationWarning):
  243. test(clobber=2, overwrite=1)
  244. # One positional, one keyword
  245. with pytest.raises(TypeError), pytest.warns(AstropyDeprecationWarning):
  246. test(1, clobber=2)
  247. def test_deprecated_argument_relaxed():
  248. # Relax turns the TypeError if both old and new keyword are used into
  249. # a warning.
  250. @deprecated_renamed_argument('clobber', 'overwrite', '1.3', relax=True)
  251. def test(overwrite):
  252. return overwrite
  253. # As positional argument only
  254. assert test(1) == 1
  255. # As new keyword argument
  256. assert test(overwrite=1) == 1
  257. # Using the deprecated name
  258. with pytest.warns(AstropyDeprecationWarning, match=r"1\.3") as w:
  259. assert test(clobber=1) == 1
  260. assert len(w) == 1
  261. # Using both. Both keyword
  262. with pytest.warns(AstropyUserWarning) as w:
  263. assert test(clobber=2, overwrite=1) == 1
  264. assert len(w) == 2
  265. assert '"clobber" was deprecated' in str(w[0].message)
  266. assert '"clobber" and "overwrite" keywords were set' in str(w[1].message)
  267. # One positional, one keyword
  268. with pytest.warns(AstropyUserWarning) as w:
  269. assert test(1, clobber=2) == 1
  270. assert len(w) == 2
  271. assert '"clobber" was deprecated' in str(w[0].message)
  272. assert '"clobber" and "overwrite" keywords were set' in str(w[1].message)
  273. def test_deprecated_argument_pending():
  274. # Relax turns the TypeError if both old and new keyword are used into
  275. # a warning.
  276. @deprecated_renamed_argument('clobber', 'overwrite', '1.3', pending=True)
  277. def test(overwrite):
  278. return overwrite
  279. # As positional argument only
  280. assert test(1) == 1
  281. # As new keyword argument
  282. assert test(overwrite=1) == 1
  283. # Using the deprecated name
  284. assert test(clobber=1) == 1
  285. # Using both. Both keyword
  286. assert test(clobber=2, overwrite=1) == 1
  287. # One positional, one keyword
  288. assert test(1, clobber=2) == 1
  289. def test_deprecated_argument_multi_deprecation():
  290. @deprecated_renamed_argument(['x', 'y', 'z'], ['a', 'b', 'c'],
  291. [1.3, 1.2, 1.3], relax=True)
  292. def test(a, b, c):
  293. return a, b, c
  294. with pytest.warns(AstropyDeprecationWarning) as w:
  295. assert test(x=1, y=2, z=3) == (1, 2, 3)
  296. assert len(w) == 3
  297. # Make sure relax is valid for all arguments
  298. with pytest.warns(AstropyUserWarning) as w:
  299. assert test(x=1, y=2, z=3, b=3) == (1, 3, 3)
  300. assert len(w) == 4
  301. with pytest.warns(AstropyUserWarning) as w:
  302. assert test(x=1, y=2, z=3, a=3) == (3, 2, 3)
  303. assert len(w) == 4
  304. with pytest.warns(AstropyUserWarning) as w:
  305. assert test(x=1, y=2, z=3, c=5) == (1, 2, 5)
  306. assert len(w) == 4
  307. def test_deprecated_argument_multi_deprecation_2():
  308. @deprecated_renamed_argument(['x', 'y', 'z'], ['a', 'b', 'c'],
  309. [1.3, 1.2, 1.3], relax=[True, True, False])
  310. def test(a, b, c):
  311. return a, b, c
  312. with pytest.warns(AstropyUserWarning) as w:
  313. assert test(x=1, y=2, z=3, b=3) == (1, 3, 3)
  314. assert len(w) == 4
  315. with pytest.warns(AstropyUserWarning) as w:
  316. assert test(x=1, y=2, z=3, a=3) == (3, 2, 3)
  317. assert len(w) == 4
  318. with pytest.raises(TypeError), pytest.warns(AstropyUserWarning):
  319. assert test(x=1, y=2, z=3, c=5) == (1, 2, 5)
  320. def test_deprecated_argument_not_allowed_use():
  321. # If the argument is supposed to be inside the kwargs one needs to set the
  322. # arg_in_kwargs parameter. Without it it raises a TypeError.
  323. with pytest.raises(TypeError):
  324. @deprecated_renamed_argument('clobber', 'overwrite', '1.3')
  325. def test1(**kwargs):
  326. return kwargs['overwrite']
  327. # Cannot replace "*args".
  328. with pytest.raises(TypeError):
  329. @deprecated_renamed_argument('overwrite', 'args', '1.3')
  330. def test2(*args):
  331. return args
  332. # Cannot replace "**kwargs".
  333. with pytest.raises(TypeError):
  334. @deprecated_renamed_argument('overwrite', 'kwargs', '1.3')
  335. def test3(**kwargs):
  336. return kwargs
  337. def test_deprecated_argument_remove():
  338. @deprecated_renamed_argument('x', None, '2.0', alternative='astropy.y')
  339. def test(dummy=11, x=3):
  340. return dummy, x
  341. with pytest.warns(AstropyDeprecationWarning, match=r"Use astropy\.y instead") as w:
  342. assert test(x=1) == (11, 1)
  343. assert len(w) == 1
  344. with pytest.warns(AstropyDeprecationWarning) as w:
  345. assert test(x=1, dummy=10) == (10, 1)
  346. assert len(w) == 1
  347. with pytest.warns(AstropyDeprecationWarning, match=r'Use astropy\.y instead'):
  348. test(121, 1) == (121, 1)
  349. assert test() == (11, 3)
  350. assert test(121) == (121, 3)
  351. assert test(dummy=121) == (121, 3)
  352. def test_sharedmethod_reuse_on_subclasses():
  353. """
  354. Regression test for an issue where sharedmethod would bind to one class
  355. for all time, causing the same method not to work properly on other
  356. subclasses of that class.
  357. It has the same problem when the same sharedmethod is called on different
  358. instances of some class as well.
  359. """
  360. class AMeta(type):
  361. def foo(cls):
  362. return cls.x
  363. class A:
  364. x = 3
  365. def __init__(self, x):
  366. self.x = x
  367. @sharedmethod
  368. def foo(self):
  369. return self.x
  370. a1 = A(1)
  371. a2 = A(2)
  372. assert a1.foo() == 1
  373. assert a2.foo() == 2
  374. # Similar test now, but for multiple subclasses using the same sharedmethod
  375. # as a classmethod
  376. assert A.foo() == 3
  377. class B(A):
  378. x = 5
  379. assert B.foo() == 5
  380. def test_classproperty_docstring():
  381. """
  382. Tests that the docstring is set correctly on classproperties.
  383. This failed previously due to a bug in Python that didn't always
  384. set __doc__ properly on instances of property subclasses.
  385. """
  386. class A:
  387. # Inherits docstring from getter
  388. @classproperty
  389. def foo(cls):
  390. """The foo."""
  391. return 1
  392. assert A.__dict__['foo'].__doc__ == "The foo."
  393. class B:
  394. # Use doc passed to classproperty constructor
  395. def _get_foo(cls): return 1
  396. foo = classproperty(_get_foo, doc="The foo.")
  397. assert B.__dict__['foo'].__doc__ == "The foo."
  398. @pytest.mark.slow
  399. def test_classproperty_lazy_threadsafe(fast_thread_switching):
  400. """
  401. Test that a class property with lazy=True is thread-safe.
  402. """
  403. workers = 8
  404. with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
  405. # This is testing for race conditions, so try many times in the
  406. # hope that we'll get the timing right.
  407. for p in range(10000):
  408. class A:
  409. @classproperty(lazy=True)
  410. def foo(cls):
  411. nonlocal calls
  412. calls += 1
  413. return object()
  414. # Have all worker threads query in parallel
  415. calls = 0
  416. futures = [executor.submit(lambda: A.foo) for i in range(workers)]
  417. # Check that only one call happened and they all received it
  418. values = [future.result() for future in futures]
  419. assert calls == 1
  420. assert values[0] is not None
  421. assert values == [values[0]] * workers
  422. @pytest.mark.slow
  423. def test_lazyproperty_threadsafe(fast_thread_switching):
  424. """
  425. Test thread safety of lazyproperty.
  426. """
  427. # This test is generally similar to test_classproperty_lazy_threadsafe
  428. # above. See there for comments.
  429. class A:
  430. def __init__(self):
  431. self.calls = 0
  432. @lazyproperty
  433. def foo(self):
  434. self.calls += 1
  435. return object()
  436. workers = 8
  437. with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
  438. for p in range(10000):
  439. a = A()
  440. futures = [executor.submit(lambda: a.foo) for i in range(workers)]
  441. values = [future.result() for future in futures]
  442. assert a.calls == 1
  443. assert a.foo is not None
  444. assert values == [a.foo] * workers
  445. def test_format_doc_stringInput_simple():
  446. # Simple tests with string input
  447. docstring_fail = ''
  448. # Raises an valueerror if input is empty
  449. with pytest.raises(ValueError):
  450. @format_doc(docstring_fail)
  451. def testfunc_fail():
  452. pass
  453. docstring = 'test'
  454. # A first test that replaces an empty docstring
  455. @format_doc(docstring)
  456. def testfunc_1():
  457. pass
  458. assert inspect.getdoc(testfunc_1) == docstring
  459. # Test that it replaces an existing docstring
  460. @format_doc(docstring)
  461. def testfunc_2():
  462. '''not test'''
  463. pass
  464. assert inspect.getdoc(testfunc_2) == docstring
  465. def test_format_doc_stringInput_format():
  466. # Tests with string input and formatting
  467. docstring = 'yes {0} no {opt}'
  468. # Raises an indexerror if not given the formatted args and kwargs
  469. with pytest.raises(IndexError):
  470. @format_doc(docstring)
  471. def testfunc1():
  472. pass
  473. # Test that the formatting is done right
  474. @format_doc(docstring, '/', opt='= life')
  475. def testfunc2():
  476. pass
  477. assert inspect.getdoc(testfunc2) == 'yes / no = life'
  478. # Test that we can include the original docstring
  479. docstring2 = 'yes {0} no {__doc__}'
  480. @format_doc(docstring2, '/')
  481. def testfunc3():
  482. '''= 2 / 2 * life'''
  483. pass
  484. assert inspect.getdoc(testfunc3) == 'yes / no = 2 / 2 * life'
  485. def test_format_doc_objectInput_simple():
  486. # Simple tests with object input
  487. def docstring_fail():
  488. pass
  489. # Self input while the function has no docstring raises an error
  490. with pytest.raises(ValueError):
  491. @format_doc(docstring_fail)
  492. def testfunc_fail():
  493. pass
  494. def docstring0():
  495. '''test'''
  496. pass
  497. # A first test that replaces an empty docstring
  498. @format_doc(docstring0)
  499. def testfunc_1():
  500. pass
  501. assert inspect.getdoc(testfunc_1) == inspect.getdoc(docstring0)
  502. # Test that it replaces an existing docstring
  503. @format_doc(docstring0)
  504. def testfunc_2():
  505. '''not test'''
  506. pass
  507. assert inspect.getdoc(testfunc_2) == inspect.getdoc(docstring0)
  508. def test_format_doc_objectInput_format():
  509. # Tests with object input and formatting
  510. def docstring():
  511. '''test {0} test {opt}'''
  512. pass
  513. # Raises an indexerror if not given the formatted args and kwargs
  514. with pytest.raises(IndexError):
  515. @format_doc(docstring)
  516. def testfunc_fail():
  517. pass
  518. # Test that the formatting is done right
  519. @format_doc(docstring, '+', opt='= 2 * test')
  520. def testfunc2():
  521. pass
  522. assert inspect.getdoc(testfunc2) == 'test + test = 2 * test'
  523. # Test that we can include the original docstring
  524. def docstring2():
  525. '''test {0} test {__doc__}'''
  526. pass
  527. @format_doc(docstring2, '+')
  528. def testfunc3():
  529. '''= 4 / 2 * test'''
  530. pass
  531. assert inspect.getdoc(testfunc3) == 'test + test = 4 / 2 * test'
  532. def test_format_doc_selfInput_simple():
  533. # Simple tests with self input
  534. # Self input while the function has no docstring raises an error
  535. with pytest.raises(ValueError):
  536. @format_doc(None)
  537. def testfunc_fail():
  538. pass
  539. # Test that it keeps an existing docstring
  540. @format_doc(None)
  541. def testfunc_1():
  542. '''not test'''
  543. pass
  544. assert inspect.getdoc(testfunc_1) == 'not test'
  545. def test_format_doc_selfInput_format():
  546. # Tests with string input which is '__doc__' (special case) and formatting
  547. # Raises an indexerror if not given the formatted args and kwargs
  548. with pytest.raises(IndexError):
  549. @format_doc(None)
  550. def testfunc_fail():
  551. '''dum {0} dum {opt}'''
  552. pass
  553. # Test that the formatting is done right
  554. @format_doc(None, 'di', opt='da dum')
  555. def testfunc1():
  556. '''dum {0} dum {opt}'''
  557. pass
  558. assert inspect.getdoc(testfunc1) == 'dum di dum da dum'
  559. # Test that we cannot recursively insert the original documentation
  560. @format_doc(None, 'di')
  561. def testfunc2():
  562. '''dum {0} dum {__doc__}'''
  563. pass
  564. assert inspect.getdoc(testfunc2) == 'dum di dum '
  565. def test_format_doc_onMethod():
  566. # Check if the decorator works on methods too, to spice it up we try double
  567. # decorator
  568. docstring = 'what we do {__doc__}'
  569. class TestClass:
  570. @format_doc(docstring)
  571. @format_doc(None, 'strange.')
  572. def test_method(self):
  573. '''is {0}'''
  574. pass
  575. assert inspect.getdoc(TestClass.test_method) == 'what we do is strange.'
  576. def test_format_doc_onClass():
  577. # Check if the decorator works on classes too
  578. docstring = 'what we do {__doc__} {0}{opt}'
  579. @format_doc(docstring, 'strange', opt='.')
  580. class TestClass:
  581. '''is'''
  582. pass
  583. assert inspect.getdoc(TestClass) == 'what we do is strange.'