From 6c2aae090c5ddfa9a06c4e3c03c15484fbc43cd3 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 27 Nov 2022 15:06:08 -0500 Subject: [PATCH] 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 --- README.rst | 3 +- demos/blog/blog.py | 7 +--- demos/chat/chatdemo.py | 4 +- demos/facebook/facebook.py | 6 +-- demos/file_upload/file_receiver.py | 2 +- demos/helloworld/helloworld.py | 4 +- demos/websocket/chatdemo.py | 5 +-- docs/auth.rst | 2 +- docs/guide/security.rst | 3 +- docs/guide/structure.rst | 5 +-- docs/guide/templates.rst | 2 +- docs/index.rst | 3 +- docs/websocket.rst | 2 +- tornado/__init__.py | 41 ++++++++++++++++++++ tornado/ioloop.py | 2 +- tornado/iostream.py | 3 +- tornado/options.py | 2 +- tornado/test/escape_test.py | 2 +- tornado/test/import_test.py | 61 +++++++++++++++--------------- tornado/test/util_test.py | 2 +- tornado/web.py | 2 +- tornado/websocket.py | 1 - 22 files changed, 91 insertions(+), 73 deletions(-) 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/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, 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