]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
autoreload: Support directories in CLI wrapper
authorBen Darnell <ben@bendarnell.com>
Sun, 23 Jul 2023 02:10:18 +0000 (22:10 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 23 Jul 2023 02:10:18 +0000 (22:10 -0400)
A previous commit added support for using autoreload within programs
that were started as directories; this commit supports them when
run with the -m tornado.autoreload wrapper.

This change may have side effects for file mode since we now use
runpy.run_path instead of executing the file by hand (I don't think
the run_path function existed when this code was originally written).

tornado/autoreload.py
tornado/test/autoreload_test.py

index 262a30590240d85e4a8ad26a8c30a89310d04394..376b346e3dc6ab0ad55841e9747d14b6b7de1efb 100644 (file)
@@ -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:
index ff934520ddd3a144d1b1dab796de0ac677d13a63..025b6ed5df811e2fa08cfe2e1e63628c585cca9e 100644 (file)
@@ -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]))