diff --git a/tornado/autoreload.py b/tornado/autoreload.py index 262a30590240d85e4a8ad26a8c30a89310d04394..376b346e3dc6ab0ad55841e9747d14b6b7de1efb 100644 --- a/tornado/autoreload.py +++ b/tornado/autoreload.py @@ -325,24 +325,13 @@ def main() -> None: sys.argv = [sys.argv[0]] + rest try: - if opts.module is not None: - import runpy + import runpy + if opts.module is not None: runpy.run_module(opts.module, run_name="__main__", alter_sys=True) else: assert path is not None - with open(path) as f: - # Execute the script in our namespace instead of creating - # a new one so that something that tries to import __main__ - # (e.g. the unittest module) will see names defined in the - # script instead of just those defined in this module. - global __file__ - __file__ = path - # If __package__ is defined, imports may be incorrectly - # interpreted as relative to this module. - global __package__ - del __package__ - exec_in(f.read(), globals(), globals()) + runpy.run_path(path, run_name="__main__") except SystemExit as e: gen_log.info("Script exited with status %s", e.code) except Exception as e: diff --git a/tornado/test/autoreload_test.py b/tornado/test/autoreload_test.py index ff934520ddd3a144d1b1dab796de0ac677d13a63..025b6ed5df811e2fa08cfe2e1e63628c585cca9e 100644 --- a/tornado/test/autoreload_test.py +++ b/tornado/test/autoreload_test.py @@ -11,6 +11,9 @@ import unittest class AutoreloadTest(unittest.TestCase): def setUp(self): + # When these tests fail the output sometimes exceeds the default maxDiff. + self.maxDiff = 1024 + self.path = mkdtemp() # Each test app runs itself twice via autoreload. The first time it manually triggers @@ -124,38 +127,59 @@ exec(open("run_twice_magic.py").read()) } ) - with self.subTest(mode="module"): - # In module mode, the path is set to the parent directory and we can import testapp. - # Also, the __spec__.name is set to the fully qualified module name. - out = self.run_subprocess([sys.executable, "-m", "testapp"]) - self.assertEqual( - out, - ( - "import testapp succeeded\n" - + "Starting __name__='__main__', __spec__.name=testapp.__main__\n" - ) - * 2, - ) - - with self.subTest(mode="file"): - # When the __main__.py file is run directly, there is no qualified module spec and we - # cannot import testapp. - out = self.run_subprocess([sys.executable, "testapp/__main__.py"]) - self.assertEqual( - out, - "import testapp failed\nStarting __name__='__main__', __spec__.name=None\n" - * 2, - ) - - with self.subTest(mode="directory"): - # Running as a directory finds __main__.py like a module. It does not manipulate - # sys.path but it does set a spec with a name of exactly __main__. - out = self.run_subprocess([sys.executable, "testapp"]) - self.assertEqual( - out, - "import testapp failed\nStarting __name__='__main__', __spec__.name=__main__\n" - * 2, - ) + # The autoreload wrapper should support all the same modes as the python interpreter. + # The wrapper itself should have no effect on this test so we try all modes with and + # without it. + for wrapper in [False, True]: + with self.subTest(wrapper=wrapper): + with self.subTest(mode="module"): + if wrapper: + base_args = [sys.executable, "-m", "tornado.autoreload"] + else: + base_args = [sys.executable] + # In module mode, the path is set to the parent directory and we can import + # testapp. Also, the __spec__.name is set to the fully qualified module name. + out = self.run_subprocess(base_args + ["-m", "testapp"]) + self.assertEqual( + out, + ( + "import testapp succeeded\n" + + "Starting __name__='__main__', __spec__.name=testapp.__main__\n" + ) + * 2, + ) + + with self.subTest(mode="file"): + out = self.run_subprocess(base_args + ["testapp/__main__.py"]) + # In file mode, we do not expect the path to be set so we can import testapp, + # but when the wrapper is used the -m argument to the python interpreter + # does this for us. + expect_import = ( + "import testapp succeeded" + if wrapper + else "import testapp failed" + ) + # In file mode there is no qualified module spec. + self.assertEqual( + out, + f"{expect_import}\nStarting __name__='__main__', __spec__.name=None\n" + * 2, + ) + + with self.subTest(mode="directory"): + # Running as a directory finds __main__.py like a module. It does not manipulate + # sys.path but it does set a spec with a name of exactly __main__. + out = self.run_subprocess(base_args + ["testapp"]) + expect_import = ( + "import testapp succeeded" + if wrapper + else "import testapp failed" + ) + self.assertEqual( + out, + f"{expect_import}\nStarting __name__='__main__', __spec__.name=__main__\n" + * 2, + ) def test_reload_wrapper_preservation(self): # This test verifies that when `python -m tornado.autoreload` @@ -190,6 +214,7 @@ exec(open("run_twice_magic.py").read()) def test_reload_wrapper_args(self): main = """\ +import os import sys print(os.path.basename(sys.argv[0]))