]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Add command-line interface to tornado.autoreload
authorBen Darnell <ben@bendarnell.com>
Sun, 26 Jun 2011 22:24:23 +0000 (15:24 -0700)
committerBen Darnell <ben@bendarnell.com>
Sun, 26 Jun 2011 22:24:23 +0000 (15:24 -0700)
tornado/autoreload.py
tornado/testing.py
website/sphinx/releases/next.rst

index 2ed0faec5cf4063519254aab19fc4d7c59272034..e22365747b33f0dac586eea5df454d4ea556fe97 100644 (file)
@@ -26,6 +26,8 @@ and Google AppEngine.  It also will not work correctly when HTTPServer's
 multi-process mode is used.
 """
 
+from __future__ import with_statement
+
 import functools
 import logging
 import os
@@ -51,6 +53,25 @@ def start(io_loop=None, check_time=500):
     scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
     scheduler.start()
 
+def wait():
+    """Wait for a watched file to change, then restart the process.
+
+    Intended to be used at the end of scripts like unit test runners,
+    to run the tests again after any source file changes (but see also
+    the command-line interface in `main`)
+    """
+    io_loop = ioloop.IOLoop()
+    start(io_loop)
+    io_loop.start()
+
+_watched_files = set()
+
+def watch(filename):
+    """Add a file to the watch list.
+
+    All imported modules are watched by default.
+    """
+    _watched_files.add(filename)
 
 _reload_attempted = False
 
@@ -69,13 +90,18 @@ def _reload_on_update(io_loop, modify_times):
         if not path: continue
         if path.endswith(".pyc") or path.endswith(".pyo"):
             path = path[:-1]
+        _check_file(io_loop, modify_times, path)
+    for path in _watched_files:
+        _check_file(io_loop, modify_times, path)
+
+def _check_file(io_loop, modify_times, path):
         try:
             modified = os.stat(path).st_mtime
         except:
-            continue
+            return
         if path not in modify_times:
             modify_times[path] = modified
-            continue
+            return
         if modify_times[path] != modified:
             logging.info("%s modified; restarting server", path)
             _reload_attempted = True
@@ -106,3 +132,61 @@ def _reload_on_update(io_loop, modify_times):
                 os.spawnv(os.P_NOWAIT, sys.executable,
                           [sys.executable] + sys.argv)
                 sys.exit(0)
+
+_USAGE = """\
+Usage:
+  python -m tornado.autoreload -m module.to.run [args...]
+  python -m tornado.autoreload path/to/script.py [args...]
+"""
+def main():
+    """Command-line wrapper to re-run a script whenever its source changes.
+    
+    Scripts may be specified by filename or module name::
+
+        python -m tornado.autoreload -m tornado.test.runtests
+        python -m tornado.autoreload tornado/test/runtests.py
+
+    Running a script with this wrapper is similar to calling
+    `tornado.autoreload.wait` at the end of the script, but this wrapper
+    can catch import-time problems like syntax errors that would otherwise
+    prevent the script from reaching its call to `wait`.
+    """
+    original_argv = sys.argv
+    sys.argv = sys.argv[:]
+    if len(sys.argv) >= 3 and sys.argv[1] == "-m":
+        mode = "module"
+        module = sys.argv[2]
+        del sys.argv[1:3]
+    elif len(sys.argv) >= 2:
+        mode = "script"
+        script = sys.argv[1]
+        sys.argv = sys.argv[1:]
+    else:
+        print >>sys.stderr, _USAGE
+        sys.exit(1)
+
+    try:
+        if mode == "module":
+            import runpy
+            runpy.run_module(module, run_name="__main__", alter_sys=True)
+        elif mode == "script":
+            with open(script) as f:
+                global __file__
+                __file__ = script
+                # Use globals as our "locals" dictionary so that
+                # something that tries to import __main__ (e.g. the unittest
+                # module) will see the right things.
+                exec f.read() in globals(), globals()
+                print "exec done"
+    except SystemExit, e:
+        logging.info("Script exited with status %s", e.code)
+    except Exception, e:
+        logging.warning("Script exited with uncaught exception", exc_info=True)
+        if isinstance(e, SyntaxError):
+            watch(e.filename)
+    sys.argv = original_argv
+    wait()
+    
+
+if __name__ == "__main__":
+    main()
index cb724d4f40c35e1dffdbbeb900fca2a7ea02e2af..f068b167dea61523bacec7556d60b83a42065c60 100644 (file)
@@ -294,11 +294,15 @@ class LogTrapTestCase(unittest.TestCase):
             handler.stream = old_stream
 
 def main():
-    """A simple test runner with autoreload support.
+    """A simple test runner.
+
+    This test runner is essentially equivalent to `unittest.main` from
+    the standard library, but adds support for tornado-style option
+    parsing and log formatting.
 
     The easiest way to run a test is via the command line::
 
-        python -m tornado.testing --autoreload tornado.test.stack_context_test
+        python -m tornado.testing tornado.test.stack_context_test
 
     See the standard library unittest module for ways in which tests can
     be specified.
@@ -310,19 +314,15 @@ def main():
     be overridden by naming a single test on the command line::
 
         # Runs all tests
-        tornado/test/runtests.py --autoreload
+        tornado/test/runtests.py
         # Runs one test
-        tornado/test/runtests.py --autoreload tornado.test.stack_context_test
+        tornado/test/runtests.py tornado.test.stack_context_test
 
-    If --autoreload is specified, the process will continue running
-    after the tests finish, and when any source file changes the tests
-    will be rerun.  Without --autoreload, the process will exit
-    once the tests finish (with an exit status of 0 for success and
-    non-zero for failures).
     """
     from tornado.options import define, options, parse_command_line
 
-    define('autoreload', type=bool, default=False)
+    define('autoreload', type=bool, default=False,
+           help="DEPRECATED: use tornado.autoreload.main instead")
     define('httpclient', type=str, default=None)
     argv = [sys.argv[0]] + parse_command_line(sys.argv)
 
@@ -353,10 +353,7 @@ def main():
             raise
     if options.autoreload:
         import tornado.autoreload
-        import tornado.ioloop
-        ioloop = tornado.ioloop.IOLoop()
-        tornado.autoreload.start(ioloop)
-        ioloop.start()
+        tornado.autoreload.wait()
 
 if __name__ == '__main__':
     main()
index a7961da48983796e4a4d46442782cfcb2b3a636d..15f20a9349e285d65e01c028ec42e3767d1e8fc0 100644 (file)
@@ -8,6 +8,11 @@ New features
 ~~~~~~~~~~~~
 
 * New method `tornado.iostream.IOStream.read_until_close`
+* `tornado.autoreload` has a new command-line interface which can be used
+  to wrap any script.  This replaces the ``--autoreload`` argument to
+  `tornado.testing.main` and is more robust against syntax errors.
+* `tornado.autoreload.watch` can be used to watch files other than
+  the sources of imported modules.
 * `tornado.ioloop.IOLoop` and `tornado.httpclient.HTTPClient` now have
   ``close()`` methods that should be used in applications that create
   and destroy many of these objects.