]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
test: Partially typecheck tests for annotated modules
authorBen Darnell <ben@bendarnell.com>
Sun, 29 Jul 2018 16:44:30 +0000 (12:44 -0400)
committerBen Darnell <ben@bendarnell.com>
Sat, 11 Aug 2018 15:54:44 +0000 (11:54 -0400)
The only type annotations added were those required by mypy. This
found some errors in the original annotations.

setup.cfg
tornado/httputil.py
tornado/test/escape_test.py
tornado/test/httputil_test.py
tornado/test/util_test.py
tornado/util.py

index 5ea4c87552da5c92f47b4a68632df17c37bdc2c7..4097ecf482137b7d12bed266d2ad70a5542df539 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,3 +9,14 @@ disallow_untyped_defs = True
 
 [mypy-tornado.escape]
 disallow_untyped_defs = True
+
+# It's generally too tedious to require type annotations in tests, but
+# we do want to type check them as much as type inference allows.
+[mypy-tornado.test.util_test]
+check_untyped_defs = True
+
+[mypy-tornado.test.httputil_test]
+check_untyped_defs = True
+
+[mypy-tornado.test.escape_test]
+check_untyped_defs = True
index 72f1eb6b73920967d412ccaea63b44780e1d4695..d5dc675bdc058605b565b7efa474ae790384dcfa 100644 (file)
@@ -347,7 +347,7 @@ class HTTPServerRequest(object):
 
     def __init__(self, method: str=None, uri: str=None, version: str="HTTP/1.0",
                  headers: HTTPHeaders=None, body: bytes=None, host: str=None,
-                 files: Dict[str, 'HTTPFile']=None, connection: 'HTTPConnection'=None,
+                 files: Dict[str, List['HTTPFile']]=None, connection: 'HTTPConnection'=None,
                  start_line: 'RequestStartLine'=None, server_connection: object=None) -> None:
         if start_line is not None:
             method, uri, version = start_line
@@ -578,7 +578,7 @@ class HTTPConnection(object):
         raise NotImplementedError()
 
 
-def url_concat(url: str, args: Union[Dict[str, str], List[Tuple[str, str]],
+def url_concat(url: str, args: Union[None, Dict[str, str], List[Tuple[str, str]],
                                      Tuple[Tuple[str, str], ...]]) -> str:
     """Concatenate url and arguments regardless of whether
     url has existing query parameters.
@@ -702,7 +702,7 @@ def _int_or_none(val: str) -> Optional[int]:
 
 
 def parse_body_arguments(content_type: str, body: bytes, arguments: Dict[str, List[bytes]],
-                         files: Dict[str, HTTPFile], headers: HTTPHeaders=None) -> None:
+                         files: Dict[str, List[HTTPFile]], headers: HTTPHeaders=None) -> None:
     """Parses a form request body.
 
     Supports ``application/x-www-form-urlencoded`` and
@@ -739,7 +739,7 @@ def parse_body_arguments(content_type: str, body: bytes, arguments: Dict[str, Li
 
 
 def parse_multipart_form_data(boundary: bytes, data: bytes, arguments: Dict[str, List[bytes]],
-                              files: Dict[str, HTTPFile]) -> None:
+                              files: Dict[str, List[HTTPFile]]) -> None:
     """Parses a ``multipart/form-data`` body.
 
     The ``boundary`` and ``data`` parameters are both byte strings.
@@ -783,7 +783,7 @@ def parse_multipart_form_data(boundary: bytes, data: bytes, arguments: Dict[str,
         name = disp_params["name"]
         if disp_params.get("filename"):
             ctype = headers.get("Content-Type", "application/unknown")
-            files.setdefault(name, []).append(HTTPFile(  # type: ignore
+            files.setdefault(name, []).append(HTTPFile(
                 filename=disp_params["filename"], body=value,
                 content_type=ctype))
         else:
index 6881850dde0381cf41f9b5a95be66a30d7afef76..5c458f08bc61608a9e93246a4bbcc3dce542bec4 100644 (file)
@@ -7,6 +7,8 @@ from tornado.escape import (
 )
 from tornado.util import unicode_type
 
+from typing import List, Tuple, Union, Dict, Any  # noqa
+
 linkify_tests = [
     # (input, linkify_kwargs, expected_output)
 
@@ -132,7 +134,7 @@ linkify_tests = [
     ("www.external-link.com",
      {"extra_params": lambda href: '    rel="nofollow" class="external"  '},
      u'<a href="http://www.external-link.com" rel="nofollow" class="external">www.external-link.com</a>'),  # noqa: E501
-]
+]  # type: List[Tuple[Union[str, bytes], Dict[str, Any], str]]
 
 
 class EscapeTestCase(unittest.TestCase):
@@ -152,7 +154,7 @@ class EscapeTestCase(unittest.TestCase):
 
             (u"<\u00e9>", u"&lt;\u00e9&gt;"),
             (b"<\xc3\xa9>", b"&lt;\xc3\xa9&gt;"),
-        ]
+        ]  # type: List[Tuple[Union[str, bytes], Union[str, bytes]]]
         for unescaped, escaped in tests:
             self.assertEqual(utf8(xhtml_escape(unescaped)), utf8(escaped))
             self.assertEqual(utf8(unescaped), utf8(xhtml_unescape(escaped)))
@@ -178,7 +180,7 @@ class EscapeTestCase(unittest.TestCase):
 
             # unicode strings become utf8
             (u'\u00e9', '%C3%A9'),
-        ]
+        ]  # type: List[Tuple[Union[str, bytes], str]]
         for unescaped, escaped in tests:
             self.assertEqual(url_escape(unescaped), escaped)
 
index cec6017fed43277b4afa64ef93e4176b1b2b7263..d6d3e5c0ba84db77fe283f00e83e397774e21ac8 100644 (file)
@@ -2,7 +2,7 @@
 from tornado.httputil import (
     url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp,
     HTTPServerRequest, parse_request_start_line, parse_cookie, qs_to_qsl,
-    HTTPInputError,
+    HTTPInputError, HTTPFile
 )
 from tornado.escape import utf8, native_str
 from tornado.log import gen_log
@@ -16,6 +16,17 @@ import time
 import urllib.parse
 import unittest
 
+from typing import Tuple, Dict, List
+
+
+def form_data_args() -> Tuple[Dict[str, List[bytes]], Dict[str, List[HTTPFile]]]:
+    """Return two empty dicts suitable for use with parse_multipart_form_data.
+
+    mypy insists on type annotations for dict literals, so this lets us avoid
+    the verbose types throughout this test.
+    """
+    return {}, {}
+
 
 class TestUrlConcat(unittest.TestCase):
     def test_url_concat_no_query_params(self):
@@ -122,8 +133,7 @@ Content-Disposition: form-data; name="files"; filename="ab.txt"
 
 Foo
 --1234--""".replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         parse_multipart_form_data(b"1234", data, args, files)
         file = files["files"][0]
         self.assertEqual(file["filename"], "ab.txt")
@@ -137,8 +147,7 @@ Content-Disposition: form-data; name=files; filename=ab.txt
 
 Foo
 --1234--""".replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         parse_multipart_form_data(b"1234", data, args, files)
         file = files["files"][0]
         self.assertEqual(file["filename"], "ab.txt")
@@ -155,15 +164,14 @@ Foo
                      ]
         for filename in filenames:
             logging.debug("trying filename %r", filename)
-            data = """\
+            str_data = """\
 --1234
 Content-Disposition: form-data; name="files"; filename="%s"
 
 Foo
 --1234--""" % filename.replace('\\', '\\\\').replace('"', '\\"')
-            data = utf8(data.replace("\n", "\r\n"))
-            args = {}
-            files = {}
+            data = utf8(str_data.replace("\n", "\r\n"))
+            args, files = form_data_args()
             parse_multipart_form_data(b"1234", data, args, files)
             file = files["files"][0]
             self.assertEqual(file["filename"], filename)
@@ -176,8 +184,7 @@ Content-Disposition: form-data; name="files"; filename="ab.txt"; filename*=UTF-8
 
 Foo
 --1234--""".replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         parse_multipart_form_data(b"1234", data, args, files)
         file = files["files"][0]
         self.assertEqual(file["filename"], u"áb.txt")
@@ -190,8 +197,7 @@ Content-Disposition: form-data; name="files"; filename="ab.txt"
 
 Foo
 --1234--'''.replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         parse_multipart_form_data(b'"1234"', data, args, files)
         file = files["files"][0]
         self.assertEqual(file["filename"], "ab.txt")
@@ -203,8 +209,7 @@ Foo
 
 Foo
 --1234--'''.replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         with ExpectLog(gen_log, "multipart/form-data missing headers"):
             parse_multipart_form_data(b"1234", data, args, files)
         self.assertEqual(files, {})
@@ -216,8 +221,7 @@ Content-Disposition: invalid; name="files"; filename="ab.txt"
 
 Foo
 --1234--'''.replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         with ExpectLog(gen_log, "Invalid multipart/form-data"):
             parse_multipart_form_data(b"1234", data, args, files)
         self.assertEqual(files, {})
@@ -228,8 +232,7 @@ Foo
 Content-Disposition: form-data; name="files"; filename="ab.txt"
 
 Foo--1234--'''.replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         with ExpectLog(gen_log, "Invalid multipart/form-data"):
             parse_multipart_form_data(b"1234", data, args, files)
         self.assertEqual(files, {})
@@ -241,8 +244,7 @@ Content-Disposition: form-data; filename="ab.txt"
 
 Foo
 --1234--""".replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         with ExpectLog(gen_log, "multipart/form-data value missing name"):
             parse_multipart_form_data(b"1234", data, args, files)
         self.assertEqual(files, {})
@@ -258,8 +260,7 @@ Content-Disposition: form-data; name="files"; filename="ab.txt"
 Foo
 --1234--
 """.replace(b"\n", b"\r\n")
-        args = {}
-        files = {}
+        args, files = form_data_args()
         parse_multipart_form_data(b"1234", data, args, files)
         file = files["files"][0]
         self.assertEqual(file["filename"], "ab.txt")
@@ -436,7 +437,7 @@ class HTTPServerRequestTest(unittest.TestCase):
         self.assertIsInstance(requets.body, bytes)
 
     def test_repr_does_not_contain_headers(self):
-        request = HTTPServerRequest(uri='/', headers={'Canary': 'Coal Mine'})
+        request = HTTPServerRequest(uri='/', headers=HTTPHeaders({'Canary': ['Coal Mine']}))
         self.assertTrue('Canary' not in repr(request))
 
 
index 59b13ad01ea2bff46622d8545d62a8c660b4b71d..5768208831bd9dc3d47abec963884481a111d57c 100644 (file)
@@ -12,6 +12,12 @@ from tornado.util import (
     timedelta_to_seconds, import_object, re_unescape, is_finalizing
 )
 
+import typing
+from typing import cast
+
+if typing.TYPE_CHECKING:
+    from typing import Dict, Any  # noqa: F401
+
 
 class RaiseExcInfoTest(unittest.TestCase):
     def test_two_arg_exception(self):
@@ -94,15 +100,18 @@ class ConfigurableTest(unittest.TestCase):
 
         obj = TestConfig1(a=1)
         self.assertEqual(obj.a, 1)
-        obj = TestConfig2(b=2)
-        self.assertEqual(obj.b, 2)
+        obj2 = TestConfig2(b=2)
+        self.assertEqual(obj2.b, 2)
 
     def test_default(self):
-        obj = TestConfigurable()
+        # In these tests we combine a typing.cast to satisfy mypy with
+        # a runtime type-assertion. Without the cast, mypy would only
+        # let us access attributes of the base class.
+        obj = cast(TestConfig1, TestConfigurable())
         self.assertIsInstance(obj, TestConfig1)
         self.assertIs(obj.a, None)
 
-        obj = TestConfigurable(a=1)
+        obj = cast(TestConfig1, TestConfigurable(a=1))
         self.assertIsInstance(obj, TestConfig1)
         self.assertEqual(obj.a, 1)
 
@@ -110,11 +119,11 @@ class ConfigurableTest(unittest.TestCase):
 
     def test_config_class(self):
         TestConfigurable.configure(TestConfig2)
-        obj = TestConfigurable()
+        obj = cast(TestConfig2, TestConfigurable())
         self.assertIsInstance(obj, TestConfig2)
         self.assertIs(obj.b, None)
 
-        obj = TestConfigurable(b=2)
+        obj = cast(TestConfig2, TestConfigurable(b=2))
         self.assertIsInstance(obj, TestConfig2)
         self.assertEqual(obj.b, 2)
 
@@ -122,11 +131,11 @@ class ConfigurableTest(unittest.TestCase):
 
     def test_config_str(self):
         TestConfigurable.configure('tornado.test.util_test.TestConfig2')
-        obj = TestConfigurable()
+        obj = cast(TestConfig2, TestConfigurable())
         self.assertIsInstance(obj, TestConfig2)
         self.assertIs(obj.b, None)
 
-        obj = TestConfigurable(b=2)
+        obj = cast(TestConfig2, TestConfigurable(b=2))
         self.assertIsInstance(obj, TestConfig2)
         self.assertEqual(obj.b, 2)
 
@@ -134,11 +143,11 @@ class ConfigurableTest(unittest.TestCase):
 
     def test_config_args(self):
         TestConfigurable.configure(None, a=3)
-        obj = TestConfigurable()
+        obj = cast(TestConfig1, TestConfigurable())
         self.assertIsInstance(obj, TestConfig1)
         self.assertEqual(obj.a, 3)
 
-        obj = TestConfigurable(42, a=4)
+        obj = cast(TestConfig1, TestConfigurable(42, a=4))
         self.assertIsInstance(obj, TestConfig1)
         self.assertEqual(obj.a, 4)
         self.assertEqual(obj.pos_arg, 42)
@@ -150,11 +159,11 @@ class ConfigurableTest(unittest.TestCase):
 
     def test_config_class_args(self):
         TestConfigurable.configure(TestConfig2, b=5)
-        obj = TestConfigurable()
+        obj = cast(TestConfig2, TestConfigurable())
         self.assertIsInstance(obj, TestConfig2)
         self.assertEqual(obj.b, 5)
 
-        obj = TestConfigurable(42, b=6)
+        obj = cast(TestConfig2, TestConfigurable(42, b=6))
         self.assertIsInstance(obj, TestConfig2)
         self.assertEqual(obj.b, 6)
         self.assertEqual(obj.pos_arg, 42)
@@ -166,15 +175,15 @@ class ConfigurableTest(unittest.TestCase):
 
     def test_config_multi_level(self):
         TestConfigurable.configure(TestConfig3, a=1)
-        obj = TestConfigurable()
+        obj = cast(TestConfig3A, TestConfigurable())
         self.assertIsInstance(obj, TestConfig3A)
         self.assertEqual(obj.a, 1)
 
         TestConfigurable.configure(TestConfig3)
         TestConfig3.configure(TestConfig3B, b=2)
-        obj = TestConfigurable()
-        self.assertIsInstance(obj, TestConfig3B)
-        self.assertEqual(obj.b, 2)
+        obj2 = cast(TestConfig3B, TestConfigurable())
+        self.assertIsInstance(obj2, TestConfig3B)
+        self.assertEqual(obj2.b, 2)
 
     def test_config_inner_level(self):
         # The inner level can be used even when the outer level
@@ -187,12 +196,12 @@ class ConfigurableTest(unittest.TestCase):
         self.assertIsInstance(obj, TestConfig3B)
 
         # Configuring the base doesn't configure the inner.
-        obj = TestConfigurable()
-        self.assertIsInstance(obj, TestConfig1)
+        obj2 = TestConfigurable()
+        self.assertIsInstance(obj2, TestConfig1)
         TestConfigurable.configure(TestConfig2)
 
-        obj = TestConfigurable()
-        self.assertIsInstance(obj, TestConfig2)
+        obj3 = TestConfigurable()
+        self.assertIsInstance(obj3, TestConfig2)
 
         obj = TestConfig3()
         self.assertIsInstance(obj, TestConfig3B)
@@ -224,14 +233,14 @@ class ArgReplacerTest(unittest.TestCase):
 
     def test_omitted(self):
         args = (1, 2)
-        kwargs = dict()
+        kwargs = dict()  # type: Dict[str, Any]
         self.assertIs(self.replacer.get_old_value(args, kwargs), None)
         self.assertEqual(self.replacer.replace('new', args, kwargs),
                          (None, (1, 2), dict(callback='new')))
 
     def test_position(self):
         args = (1, 2, 'old', 3)
-        kwargs = dict()
+        kwargs = dict()  # type: Dict[str, Any]
         self.assertEqual(self.replacer.get_old_value(args, kwargs), 'old')
         self.assertEqual(self.replacer.replace('new', args, kwargs),
                          ('old', [1, 2, 'new', 3], dict()))
index 9991b3a5064a5f26bf84cabfff8bb8426b7489e7..c10133116b1c0e81b1e9cc736044f47f151641c0 100644 (file)
@@ -19,14 +19,14 @@ import typing
 import zlib
 
 from typing import (
-    Any, Optional, Dict, Mapping, List, Tuple, Match, Callable, Type,
+    Any, Optional, Dict, Mapping, List, Tuple, Match, Callable, Type, Sequence
 )
 
 if typing.TYPE_CHECKING:
     # Additional imports only used in type comments.
     # This lets us make these imports lazy.
     import datetime  # noqa
-    import types  # noqa
+    from types import TracebackType  # noqa
     from typing import Union  # noqa
     import unittest  # noqa
 
@@ -154,14 +154,24 @@ def exec_in(code: Any, glob: Dict[str, Any], loc: Mapping[str, Any]=None) -> Non
     exec(code, glob, loc)
 
 
-def raise_exc_info(exc_info):
-    # type: (Tuple[type, BaseException, types.TracebackType]) -> typing.NoReturn
+def raise_exc_info(
+        exc_info,  # type: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]]
+):
+    # type: (...) -> typing.NoReturn
+    #
+    # This function's type annotation must use comments instead of
+    # real annotations because typing.NoReturn does not exist in
+    # python 3.5's typing module. The formatting is funky because this
+    # is apparently what flake8 wants.
     try:
-        raise exc_info[1].with_traceback(exc_info[2])
+        if exc_info[1] is not None:
+            raise exc_info[1].with_traceback(exc_info[2])
+        else:
+            raise TypeError("raise_exc_info called with no exception")
     finally:
         # Clear the traceback reference from our stack frame to
         # minimize circular references that slow down GC.
-        exc_info = None  # type: ignore
+        exc_info = (None, None, None)
 
 
 def errno_from_exception(e: BaseException) -> Optional[int]:
@@ -367,7 +377,7 @@ class ArgReplacer(object):
                 return code.co_varnames[:code.co_argcount]
             raise
 
-    def get_old_value(self, args: List[Any], kwargs: Dict[str, Any], default: Any=None) -> Any:
+    def get_old_value(self, args: Sequence[Any], kwargs: Dict[str, Any], default: Any=None) -> Any:
         """Returns the old value of the named argument without replacing it.
 
         Returns ``default`` if the argument is not present.
@@ -377,8 +387,8 @@ class ArgReplacer(object):
         else:
             return kwargs.get(self.name, default)
 
-    def replace(self, new_value: Any, args: List[Any],
-                kwargs: Dict[str, Any]) -> Tuple[Any, List[Any], Dict[str, Any]]:
+    def replace(self, new_value: Any, args: Sequence[Any],
+                kwargs: Dict[str, Any]) -> Tuple[Any, Sequence[Any], Dict[str, Any]]:
         """Replace the named argument in ``args, kwargs`` with ``new_value``.
 
         Returns ``(old_value, args, kwargs)``.  The returned ``args`` and