From: Ben Darnell Date: Fri, 14 Jul 2023 00:57:11 +0000 (-0400) Subject: autoreload: Support the ability to run a directory instead of a module X-Git-Tag: v6.4.0b1~20^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d7621eebba60199f21ae8398de1b57273149b04b;p=thirdparty%2Ftornado.git autoreload: Support the ability to run a directory instead of a module Running a directory has some but not all of the behavior of running a module, including setting __spec__, so we must be careful not to break things by assuming that __spec__ means module mode. Fixes #2855 --- diff --git a/tornado/autoreload.py b/tornado/autoreload.py index 0ac449665..07b9112a6 100644 --- a/tornado/autoreload.py +++ b/tornado/autoreload.py @@ -225,7 +225,15 @@ def _reload() -> None: else: spec = getattr(sys.modules["__main__"], "__spec__", None) argv = sys.argv - if spec: + if spec and spec.name != "__main__": + # __spec__ is set in two cases: when running a module, and when running a directory. (when + # running a file, there is no spec). In the former case, we must pass -m to maintain the + # module-style behavior (setting sys.path), even though python stripped -m from its argv at + # startup. If sys.path is exactly __main__, we're running a directory and should fall + # through to the non-module behavior. + # + # Some of this, including the use of exactly __main__ as a spec for directory mode, + # is documented at https://docs.python.org/3/library/runpy.html#runpy.run_path argv = ["-m", spec.name] + argv[1:] else: path_prefix = "." + os.pathsep @@ -331,7 +339,7 @@ def main() -> None: # never made it into sys.modules and so we won't know to watch it. # Just to make sure we've covered everything, walk the stack trace # from the exception and watch every file. - for (filename, lineno, name, line) in traceback.extract_tb(sys.exc_info()[2]): + for filename, lineno, name, line in traceback.extract_tb(sys.exc_info()[2]): watch(filename) if isinstance(e, SyntaxError): # SyntaxErrors are special: their innermost stack frame is fake diff --git a/tornado/test/autoreload_test.py b/tornado/test/autoreload_test.py index fff602811..feee94f3b 100644 --- a/tornado/test/autoreload_test.py +++ b/tornado/test/autoreload_test.py @@ -24,17 +24,23 @@ class AutoreloadTest(unittest.TestCase): time.sleep(1) shutil.rmtree(self.path) - def test_reload_module(self): + def test_reload(self): main = """\ import os import sys from tornado import autoreload -# This import will fail if path is not set up correctly -import testapp +# In module mode, the path is set to the parent directory and we can import testapp. +try: + import testapp +except ImportError: + print("import testapp failed") +else: + print("import testapp succeeded") -print('Starting') +spec = getattr(sys.modules[__name__], '__spec__', None) +print(f"Starting {__name__=}, __spec__.name={getattr(spec, 'name', None)}") if 'TESTAPP_STARTED' not in os.environ: os.environ['TESTAPP_STARTED'] = '1' sys.stdout.flush() @@ -57,16 +63,62 @@ if 'TESTAPP_STARTED' not in os.environ: if "PYTHONPATH" in os.environ: pythonpath += os.pathsep + os.environ["PYTHONPATH"] - p = Popen( - [sys.executable, "-m", "testapp"], - stdout=subprocess.PIPE, - cwd=self.path, - env=dict(os.environ, PYTHONPATH=pythonpath), - universal_newlines=True, - encoding="utf-8", - ) - out = p.communicate()[0] - self.assertEqual(out, "Starting\nStarting\n") + 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. + p = Popen( + [sys.executable, "-m", "testapp"], + stdout=subprocess.PIPE, + cwd=self.path, + env=dict(os.environ, PYTHONPATH=pythonpath), + universal_newlines=True, + encoding="utf-8", + ) + out = p.communicate()[0] + 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. + p = Popen( + [sys.executable, "testapp/__main__.py"], + stdout=subprocess.PIPE, + cwd=self.path, + env=dict(os.environ, PYTHONPATH=pythonpath), + universal_newlines=True, + encoding="utf-8", + ) + out = p.communicate()[0] + 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__. + p = Popen( + [sys.executable, "testapp"], + stdout=subprocess.PIPE, + cwd=self.path, + env=dict(os.environ, PYTHONPATH=pythonpath), + universal_newlines=True, + encoding="utf-8", + ) + out = p.communicate()[0] + self.assertEqual( + out, + "import testapp failed\nStarting __name__='__main__', __spec__.name=__main__\n" + * 2, + ) def test_reload_wrapper_preservation(self): # This test verifies that when `python -m tornado.autoreload`