Repository: tornadoweb/tornado
Pull request #3201 (2022-11-27 20:07:42)

all: Support lazy imports of submodules
A getattr hook in the top-level "tornado" package now imports submodules automatically, eliminating the need to explicitly reference multiple submodules in imports


Patch:
diff --git a/README.rst b/README.rst
index 9b33a10b..1c689f5c 100644
--- a/README.rst
+++ b/README.rst
@@ -21,8 +21,7 @@ Here is a simple "Hello, world" example web app for Tornado:
 .. code-block:: python
 
     import asyncio
-
-    import tornado.web
+    import tornado
 
     class MainHandler(tornado.web.RequestHandler):
         def get(self):
diff --git a/demos/blog/blog.py b/demos/blog/blog.py
index 66e42b69..215c4050 100755
--- a/demos/blog/blog.py
+++ b/demos/blog/blog.py
@@ -21,12 +21,7 @@ import markdown
 import os.path
 import psycopg2
 import re
-import tornado.escape
-import tornado.httpserver
-import tornado.ioloop
-import tornado.locks
-import tornado.options
-import tornado.web
+import tornado
 import unicodedata
 
 from tornado.options import define, options
diff --git a/demos/chat/chatdemo.py b/demos/chat/chatdemo.py
index f96ac051..28c12108 100755
--- a/demos/chat/chatdemo.py
+++ b/demos/chat/chatdemo.py
@@ -15,9 +15,7 @@
 # under the License.
 
 import asyncio
-import tornado.escape
-import tornado.locks
-import tornado.web
+import tornado
 import os.path
 import uuid
 
diff --git a/demos/facebook/facebook.py b/demos/facebook/facebook.py
index c23c0247..480c8028 100755
--- a/demos/facebook/facebook.py
+++ b/demos/facebook/facebook.py
@@ -16,11 +16,7 @@
 
 import asyncio
 import os.path
-import tornado.auth
-import tornado.escape
-import tornado.httpserver
-import tornado.options
-import tornado.web
+import tornado
 
 from tornado.options import define, options
 
diff --git a/demos/file_upload/file_receiver.py b/demos/file_upload/file_receiver.py
index 360c8aa5..5390715e 100755
--- a/demos/file_upload/file_receiver.py
+++ b/demos/file_upload/file_receiver.py
@@ -12,7 +12,7 @@ import asyncio
 import logging
 from urllib.parse import unquote
 
-import tornado.web
+import tornado
 from tornado import options
 
 
diff --git a/demos/helloworld/helloworld.py b/demos/helloworld/helloworld.py
index e19b61bb..f33440cf 100755
--- a/demos/helloworld/helloworld.py
+++ b/demos/helloworld/helloworld.py
@@ -15,9 +15,7 @@
 # under the License.
 
 import asyncio
-import tornado.httpserver
-import tornado.options
-import tornado.web
+import tornado
 
 from tornado.options import define, options
 
diff --git a/demos/websocket/chatdemo.py b/demos/websocket/chatdemo.py
index 594a7c5e..05781c75 100755
--- a/demos/websocket/chatdemo.py
+++ b/demos/websocket/chatdemo.py
@@ -20,10 +20,7 @@ Authentication, error handling, etc are left as an exercise for the reader :)
 
 import asyncio
 import logging
-import tornado.escape
-import tornado.options
-import tornado.web
-import tornado.websocket
+import tornado
 import os.path
 import uuid
 
diff --git a/docs/auth.rst b/docs/auth.rst
index dfbe3ed9..50339481 100644
--- a/docs/auth.rst
+++ b/docs/auth.rst
@@ -3,7 +3,7 @@
 
 .. testsetup::
 
-   import tornado.auth, tornado.gen, tornado.web
+   import tornado
 
 .. automodule:: tornado.auth
 
diff --git a/docs/guide/security.rst b/docs/guide/security.rst
index 008d614e..ea2b87ff 100644
--- a/docs/guide/security.rst
+++ b/docs/guide/security.rst
@@ -3,8 +3,7 @@ Authentication and security
 
 .. testsetup::
 
-   import tornado.auth
-   import tornado.web
+   import tornado
 
 Cookies and secure cookies
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/guide/structure.rst b/docs/guide/structure.rst
index 42a8e63b..ad927606 100644
--- a/docs/guide/structure.rst
+++ b/docs/guide/structure.rst
@@ -2,7 +2,7 @@
 
 .. testsetup::
 
-   import tornado.web
+   import tornado
 
 Structure of a Tornado web application
 ======================================
@@ -17,8 +17,7 @@ A minimal "hello world" example looks something like this:
 .. testcode::
 
     import asyncio
-
-    import tornado.web
+    import tornado
 
     class MainHandler(tornado.web.RequestHandler):
         def get(self):
diff --git a/docs/guide/templates.rst b/docs/guide/templates.rst
index 61ce753e..73440dae 100644
--- a/docs/guide/templates.rst
+++ b/docs/guide/templates.rst
@@ -3,7 +3,7 @@ Templates and UI
 
 .. testsetup::
 
-   import tornado.web
+   import tornado
 
 Tornado includes a simple, fast, and flexible templating language.
 This section describes that language as well as related issues
diff --git a/docs/index.rst b/docs/index.rst
index 024bc393..c33fb0b0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -32,8 +32,7 @@ Hello, world
 Here is a simple "Hello, world" example web app for Tornado::
 
     import asyncio
-
-    import tornado.web
+    import tornado
 
     class MainHandler(tornado.web.RequestHandler):
         def get(self):
diff --git a/docs/websocket.rst b/docs/websocket.rst
index 76bc0522..b56a4ec3 100644
--- a/docs/websocket.rst
+++ b/docs/websocket.rst
@@ -3,7 +3,7 @@
 
 .. testsetup::
 
-   import tornado.websocket
+   import tornado
 
 .. automodule:: tornado.websocket
 
diff --git a/tornado/__init__.py b/tornado/__init__.py
index f93e8e2a..060b836a 100644
--- a/tornado/__init__.py
+++ b/tornado/__init__.py
@@ -24,3 +24,44 @@
 # number has been incremented)
 version = "6.3.dev1"
 version_info = (6, 3, 0, -100)
+
+import importlib
+import typing
+
+__all__ = [
+    "auth",
+    "autoreload",
+    "concurrent",
+    "curl_httpclient",
+    "escape",
+    "gen",
+    "http1connection",
+    "httpclient",
+    "httpserver",
+    "httputil",
+    "ioloop",
+    "iostream",
+    "locale",
+    "locks",
+    "log",
+    "netutil",
+    "options",
+    "platform",
+    "process",
+    "queues",
+    "routing",
+    "simple_httpclient",
+    "tcpclient",
+    "tcpserver",
+    "template",
+    "testing",
+    "util",
+    "web",
+]
+
+
+# Copied from https://peps.python.org/pep-0562/
+def __getattr__(name: str) -> typing.Any:
+    if name in __all__:
+        return importlib.import_module("." + name, __name__)
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index 2c05755d..25609790 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -83,7 +83,7 @@ class IOLoop(Configurable):
         import functools
         import socket
 
-        import tornado.ioloop
+        import tornado
         from tornado.iostream import IOStream
 
         async def handle_connection(connection, address):
diff --git a/tornado/iostream.py b/tornado/iostream.py
index 96b47f5b..a408be59 100644
--- a/tornado/iostream.py
+++ b/tornado/iostream.py
@@ -1069,9 +1069,8 @@ class IOStream(BaseIOStream):
 
     .. testcode::
 
-        import tornado.ioloop
-        import tornado.iostream
         import socket
+        import tornado
 
         async def main():
             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
diff --git a/tornado/options.py b/tornado/options.py
index 6ec58996..b8296691 100644
--- a/tornado/options.py
+++ b/tornado/options.py
@@ -56,7 +56,7 @@ Your ``main()`` method can parse the command line or parse a config file with
 either `parse_command_line` or `parse_config_file`::
 
     import myapp.db, myapp.server
-    import tornado.options
+    import tornado
 
     if __name__ == '__main__':
         tornado.options.parse_command_line()
diff --git a/tornado/web.py b/tornado/web.py
index cd6a81b4..75bb46c9 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -23,7 +23,7 @@ Here is a simple "Hello, world" example app:
 .. testcode::
 
     import asyncio
-    import tornado.web
+    import tornado
 
     class MainHandler(tornado.web.RequestHandler):
         def get(self):
diff --git a/tornado/websocket.py b/tornado/websocket.py
index 43142b32..1d42e10b 100644
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -23,7 +23,6 @@ import hashlib
 import os
 import sys
 import struct
-import tornado.escape
 import tornado.web
 from urllib.parse import urlparse
 import zlib


Test patch:
diff --git a/tornado/test/escape_test.py b/tornado/test/escape_test.py
index d8f95e42..d067f645 100644
--- a/tornado/test/escape_test.py
+++ b/tornado/test/escape_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-import tornado.escape
+import tornado
 from tornado.escape import (
     utf8,
     xhtml_escape,
diff --git a/tornado/test/import_test.py b/tornado/test/import_test.py
index cae10381..bff661d7 100644
--- a/tornado/test/import_test.py
+++ b/tornado/test/import_test.py
@@ -11,38 +11,31 @@ _import_everything = b"""
 import asyncio
 asyncio.set_event_loop(None)
 
-import tornado.auth
-import tornado.autoreload
-import tornado.concurrent
-import tornado.escape
-import tornado.gen
-import tornado.http1connection
-import tornado.httpclient
-import tornado.httpserver
-import tornado.httputil
-import tornado.ioloop
-import tornado.iostream
-import tornado.locale
-import tornado.log
-import tornado.netutil
-import tornado.options
-import tornado.process
-import tornado.simple_httpclient
-import tornado.tcpserver
-import tornado.tcpclient
-import tornado.template
-import tornado.testing
-import tornado.util
-import tornado.web
-import tornado.websocket
-import tornado.wsgi
+import importlib
+import tornado
 
-try:
-    import pycurl
-except ImportError:
-    pass
-else:
-    import tornado.curl_httpclient
+for mod in tornado.__all__:
+    if mod == "curl_httpclient":
+        # This module has extra dependencies; skip it if they're not installed.
+        try:
+            import pycurl
+        except ImportError:
+            continue
+    importlib.import_module(f"tornado.{mod}")
+"""
+
+_import_lazy = b"""
+import sys
+import tornado
+
+if "tornado.web" in sys.modules:
+    raise Exception("unexpected eager import")
+
+# Trigger a lazy import by referring to something in a submodule.
+tornado.web.RequestHandler
+
+if "tornado.web" not in sys.modules:
+    raise Exception("lazy import did not update sys.modules")
 """
 
 
@@ -56,6 +49,12 @@ class ImportTest(unittest.TestCase):
         proc.communicate(_import_everything)
         self.assertEqual(proc.returncode, 0)
 
+    def test_lazy_import(self):
+        # Test that submodules can be referenced lazily after "import tornado"
+        proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE)
+        proc.communicate(_import_lazy)
+        self.assertEqual(proc.returncode, 0)
+
     def test_import_aliases(self):
         # Ensure we don't delete formerly-documented aliases accidentally.
         import tornado.ioloop
diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py
index 0cbc13c6..ec8ee121 100644
--- a/tornado/test/util_test.py
+++ b/tornado/test/util_test.py
@@ -4,7 +4,7 @@ import sys
 import datetime
 import unittest
 
-import tornado.escape
+import tornado
 from tornado.escape import utf8
 from tornado.util import (
     raise_exc_info,