util_test.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. import re
  2. import sys
  3. import datetime
  4. import textwrap
  5. import unittest
  6. import tornado
  7. from tornado.escape import utf8
  8. from tornado.util import (
  9. raise_exc_info,
  10. Configurable,
  11. exec_in,
  12. ArgReplacer,
  13. timedelta_to_seconds,
  14. import_object,
  15. re_unescape,
  16. )
  17. from typing import cast, Dict, Any
  18. class RaiseExcInfoTest(unittest.TestCase):
  19. def test_two_arg_exception(self):
  20. # This test would fail on python 3 if raise_exc_info were simply
  21. # a three-argument raise statement, because TwoArgException
  22. # doesn't have a "copy constructor"
  23. class TwoArgException(Exception):
  24. def __init__(self, a, b):
  25. super().__init__()
  26. self.a, self.b = a, b
  27. try:
  28. raise TwoArgException(1, 2)
  29. except TwoArgException:
  30. exc_info = sys.exc_info()
  31. try:
  32. raise_exc_info(exc_info)
  33. self.fail("didn't get expected exception")
  34. except TwoArgException as e:
  35. self.assertIs(e, exc_info[1])
  36. class TestConfigurable(Configurable):
  37. @classmethod
  38. def configurable_base(cls):
  39. return TestConfigurable
  40. @classmethod
  41. def configurable_default(cls):
  42. return TestConfig1
  43. class TestConfig1(TestConfigurable):
  44. def initialize(self, pos_arg=None, a=None):
  45. self.a = a
  46. self.pos_arg = pos_arg
  47. class TestConfig2(TestConfigurable):
  48. def initialize(self, pos_arg=None, b=None):
  49. self.b = b
  50. self.pos_arg = pos_arg
  51. class TestConfig3(TestConfigurable):
  52. # TestConfig3 is a configuration option that is itself configurable.
  53. @classmethod
  54. def configurable_base(cls):
  55. return TestConfig3
  56. @classmethod
  57. def configurable_default(cls):
  58. return TestConfig3A
  59. class TestConfig3A(TestConfig3):
  60. def initialize(self, a=None):
  61. self.a = a
  62. class TestConfig3B(TestConfig3):
  63. def initialize(self, b=None):
  64. self.b = b
  65. class ConfigurableTest(unittest.TestCase):
  66. def setUp(self):
  67. self.saved = TestConfigurable._save_configuration()
  68. self.saved3 = TestConfig3._save_configuration()
  69. def tearDown(self):
  70. TestConfigurable._restore_configuration(self.saved)
  71. TestConfig3._restore_configuration(self.saved3)
  72. def checkSubclasses(self):
  73. # no matter how the class is configured, it should always be
  74. # possible to instantiate the subclasses directly
  75. self.assertIsInstance(TestConfig1(), TestConfig1)
  76. self.assertIsInstance(TestConfig2(), TestConfig2)
  77. obj = TestConfig1(a=1)
  78. self.assertEqual(obj.a, 1)
  79. obj2 = TestConfig2(b=2)
  80. self.assertEqual(obj2.b, 2)
  81. def test_default(self):
  82. # In these tests we combine a typing.cast to satisfy mypy with
  83. # a runtime type-assertion. Without the cast, mypy would only
  84. # let us access attributes of the base class.
  85. obj = cast(TestConfig1, TestConfigurable())
  86. self.assertIsInstance(obj, TestConfig1)
  87. self.assertIsNone(obj.a)
  88. obj = cast(TestConfig1, TestConfigurable(a=1))
  89. self.assertIsInstance(obj, TestConfig1)
  90. self.assertEqual(obj.a, 1)
  91. self.checkSubclasses()
  92. def test_config_class(self):
  93. TestConfigurable.configure(TestConfig2)
  94. obj = cast(TestConfig2, TestConfigurable())
  95. self.assertIsInstance(obj, TestConfig2)
  96. self.assertIsNone(obj.b)
  97. obj = cast(TestConfig2, TestConfigurable(b=2))
  98. self.assertIsInstance(obj, TestConfig2)
  99. self.assertEqual(obj.b, 2)
  100. self.checkSubclasses()
  101. def test_config_str(self):
  102. TestConfigurable.configure("tornado.test.util_test.TestConfig2")
  103. obj = cast(TestConfig2, TestConfigurable())
  104. self.assertIsInstance(obj, TestConfig2)
  105. self.assertIsNone(obj.b)
  106. obj = cast(TestConfig2, TestConfigurable(b=2))
  107. self.assertIsInstance(obj, TestConfig2)
  108. self.assertEqual(obj.b, 2)
  109. self.checkSubclasses()
  110. def test_config_args(self):
  111. TestConfigurable.configure(None, a=3)
  112. obj = cast(TestConfig1, TestConfigurable())
  113. self.assertIsInstance(obj, TestConfig1)
  114. self.assertEqual(obj.a, 3)
  115. obj = cast(TestConfig1, TestConfigurable(42, a=4))
  116. self.assertIsInstance(obj, TestConfig1)
  117. self.assertEqual(obj.a, 4)
  118. self.assertEqual(obj.pos_arg, 42)
  119. self.checkSubclasses()
  120. # args bound in configure don't apply when using the subclass directly
  121. obj = TestConfig1()
  122. self.assertIsNone(obj.a)
  123. def test_config_class_args(self):
  124. TestConfigurable.configure(TestConfig2, b=5)
  125. obj = cast(TestConfig2, TestConfigurable())
  126. self.assertIsInstance(obj, TestConfig2)
  127. self.assertEqual(obj.b, 5)
  128. obj = cast(TestConfig2, TestConfigurable(42, b=6))
  129. self.assertIsInstance(obj, TestConfig2)
  130. self.assertEqual(obj.b, 6)
  131. self.assertEqual(obj.pos_arg, 42)
  132. self.checkSubclasses()
  133. # args bound in configure don't apply when using the subclass directly
  134. obj = TestConfig2()
  135. self.assertIsNone(obj.b)
  136. def test_config_multi_level(self):
  137. TestConfigurable.configure(TestConfig3, a=1)
  138. obj = cast(TestConfig3A, TestConfigurable())
  139. self.assertIsInstance(obj, TestConfig3A)
  140. self.assertEqual(obj.a, 1)
  141. TestConfigurable.configure(TestConfig3)
  142. TestConfig3.configure(TestConfig3B, b=2)
  143. obj2 = cast(TestConfig3B, TestConfigurable())
  144. self.assertIsInstance(obj2, TestConfig3B)
  145. self.assertEqual(obj2.b, 2)
  146. def test_config_inner_level(self):
  147. # The inner level can be used even when the outer level
  148. # doesn't point to it.
  149. obj = TestConfig3()
  150. self.assertIsInstance(obj, TestConfig3A)
  151. TestConfig3.configure(TestConfig3B)
  152. obj = TestConfig3()
  153. self.assertIsInstance(obj, TestConfig3B)
  154. # Configuring the base doesn't configure the inner.
  155. obj2 = TestConfigurable()
  156. self.assertIsInstance(obj2, TestConfig1)
  157. TestConfigurable.configure(TestConfig2)
  158. obj3 = TestConfigurable()
  159. self.assertIsInstance(obj3, TestConfig2)
  160. obj = TestConfig3()
  161. self.assertIsInstance(obj, TestConfig3B)
  162. class UnicodeLiteralTest(unittest.TestCase):
  163. def test_unicode_escapes(self):
  164. self.assertEqual(utf8("\u00e9"), b"\xc3\xa9")
  165. class ExecInTest(unittest.TestCase):
  166. def test_no_inherit_future(self):
  167. # Two files: the first has "from __future__ import annotations", and it executes the second
  168. # which doesn't. The second file should not be affected by the first's __future__ imports.
  169. #
  170. # The annotations future became available in python 3.7 but has been replaced by PEP 649, so
  171. # it should remain supported but off-by-default for the foreseeable future.
  172. code1 = textwrap.dedent(
  173. """
  174. from __future__ import annotations
  175. from tornado.util import exec_in
  176. exec_in(code2, globals())
  177. """
  178. )
  179. code2 = textwrap.dedent(
  180. """
  181. def f(x: int) -> int:
  182. return x + 1
  183. output[0] = f.__annotations__
  184. """
  185. )
  186. # Make a mutable container to pass the result back to the caller
  187. output = [None]
  188. exec_in(code1, dict(code2=code2, output=output))
  189. # If the annotations future were in effect, these would be strings instead of the int type
  190. # object.
  191. self.assertEqual(output[0], {"x": int, "return": int})
  192. class ArgReplacerTest(unittest.TestCase):
  193. def setUp(self):
  194. def function(x, y, callback=None, z=None):
  195. pass
  196. self.replacer = ArgReplacer(function, "callback")
  197. def test_omitted(self):
  198. args = (1, 2)
  199. kwargs: Dict[str, Any] = dict()
  200. self.assertIsNone(self.replacer.get_old_value(args, kwargs))
  201. self.assertEqual(
  202. self.replacer.replace("new", args, kwargs),
  203. (None, (1, 2), dict(callback="new")),
  204. )
  205. def test_position(self):
  206. args = (1, 2, "old", 3)
  207. kwargs: Dict[str, Any] = dict()
  208. self.assertEqual(self.replacer.get_old_value(args, kwargs), "old")
  209. self.assertEqual(
  210. self.replacer.replace("new", args, kwargs),
  211. ("old", [1, 2, "new", 3], dict()),
  212. )
  213. def test_keyword(self):
  214. args = (1,)
  215. kwargs = dict(y=2, callback="old", z=3)
  216. self.assertEqual(self.replacer.get_old_value(args, kwargs), "old")
  217. self.assertEqual(
  218. self.replacer.replace("new", args, kwargs),
  219. ("old", (1,), dict(y=2, callback="new", z=3)),
  220. )
  221. class TimedeltaToSecondsTest(unittest.TestCase):
  222. def test_timedelta_to_seconds(self):
  223. time_delta = datetime.timedelta(hours=1)
  224. self.assertEqual(timedelta_to_seconds(time_delta), 3600.0)
  225. class ImportObjectTest(unittest.TestCase):
  226. def test_import_member(self):
  227. self.assertIs(import_object("tornado.escape.utf8"), utf8)
  228. def test_import_member_unicode(self):
  229. self.assertIs(import_object("tornado.escape.utf8"), utf8)
  230. def test_import_module(self):
  231. self.assertIs(import_object("tornado.escape"), tornado.escape)
  232. def test_import_module_unicode(self):
  233. # The internal implementation of __import__ differs depending on
  234. # whether the thing being imported is a module or not.
  235. # This variant requires a byte string in python 2.
  236. self.assertIs(import_object("tornado.escape"), tornado.escape)
  237. class ReUnescapeTest(unittest.TestCase):
  238. def test_re_unescape(self):
  239. test_strings = ("/favicon.ico", "index.html", "Hello, World!", "!$@#%;")
  240. for string in test_strings:
  241. self.assertEqual(string, re_unescape(re.escape(string)))
  242. def test_re_unescape_raises_error_on_invalid_input(self):
  243. with self.assertRaises(ValueError):
  244. re_unescape("\\d")
  245. with self.assertRaises(ValueError):
  246. re_unescape("\\b")
  247. with self.assertRaises(ValueError):
  248. re_unescape("\\Z")
  249. class VersionInfoTest(unittest.TestCase):
  250. def assert_version_info_compatible(self, version, version_info):
  251. # We map our version identifier string (a subset of
  252. # https://packaging.python.org/en/latest/specifications/version-specifiers/#public-version-identifiers)
  253. # to a 4-tuple of integers for easy comparisons. The last component is
  254. # 0 for a final release, negative for a pre-release, and would be positive for a
  255. # post-release if we did any of those. This test is not a promise that these are the
  256. # only formats we will ever use, but it does catch accidents like
  257. # https://github.com/tornadoweb/tornado/issues/3406.
  258. major = minor = patch = "0"
  259. is_pre = False
  260. if m := re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", version):
  261. # Regular 3-component version number
  262. major, minor, patch = m.groups()
  263. elif m := re.fullmatch(r"(\d+)\.(\d+)", version):
  264. # Two-component version number, equivalent to major.minor.0
  265. major, minor = m.groups()
  266. elif m := re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:\.dev|a|b|rc)\d+", version):
  267. # Pre-release 3-component version number.
  268. major, minor, patch = m.groups()
  269. is_pre = True
  270. elif m := re.fullmatch(r"(\d+)\.(\d+)(?:\.dev|a|b|rc)\d+", version):
  271. # Pre-release 2-component version number.
  272. major, minor = m.groups()
  273. is_pre = True
  274. else:
  275. self.fail(f"Unrecognized version format: {version}")
  276. self.assertEqual(version_info[:3], (int(major), int(minor), int(patch)))
  277. if is_pre:
  278. self.assertLess(int(version_info[3]), 0)
  279. else:
  280. self.assertEqual(int(version_info[3]), 0)
  281. def test_version_info_compatible(self):
  282. self.assert_version_info_compatible("6.5.0", (6, 5, 0, 0))
  283. self.assert_version_info_compatible("6.5", (6, 5, 0, 0))
  284. self.assert_version_info_compatible("6.5.1", (6, 5, 1, 0))
  285. self.assert_version_info_compatible("6.6.dev1", (6, 6, 0, -100))
  286. self.assert_version_info_compatible("6.6a1", (6, 6, 0, -100))
  287. self.assert_version_info_compatible("6.6b1", (6, 6, 0, -100))
  288. self.assert_version_info_compatible("6.6rc1", (6, 6, 0, -100))
  289. self.assertRaises(
  290. AssertionError, self.assert_version_info_compatible, "6.5.0", (6, 5, 0, 1)
  291. )
  292. self.assertRaises(
  293. AssertionError, self.assert_version_info_compatible, "6.5.0", (6, 4, 0, 0)
  294. )
  295. self.assertRaises(
  296. AssertionError, self.assert_version_info_compatible, "6.5.1", (6, 5, 0, 1)
  297. )
  298. def test_current_version(self):
  299. self.assert_version_info_compatible(tornado.version, tornado.version_info)