Source code for tornado.autoreload

#!/usr/bin/env python## Copyright 2009 Facebook## Licensed under the Apache License, Version 2.0 (the "License"); you may# not use this file except in compliance with the License. You may obtain# a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the# License for the specific language governing permissions and limitations# under the License."""Automatically restart the server when a source file is modified.Most applications should not access this module directly. Instead,pass the keyword argument ``autoreload=True`` to the`tornado.web.Application` constructor (or ``debug=True``, whichenables this setting and several others). This will enable autoreloadmode as well as checking for changes to templates and staticresources. Note that restarting is a destructive operation and anyrequests in progress will be aborted when the process restarts. (Ifyou want to disable autoreload while using other debug-mode features,pass both ``debug=True`` and ``autoreload=False``).This module can also be used as a command-line wrapper around scriptssuch as unit test runners. See the `main` method for details.The command-line wrapper and Application debug modes can be used together.This combination is encouraged as the wrapper catches syntax errors andother import-time failures, while debug mode catches changes oncethe server has started.This module depends on `.IOLoop`, so it will not work in WSGI applicationsand Google App Engine. It also will not work correctly when `.HTTPServer`'smulti-process mode is used.Reloading loses any Python interpreter command-line arguments (e.g. ``-u``)because it re-executes Python using ``sys.executable`` and ``sys.argv``.Additionally, modifying these variables will cause reloading to behaveincorrectly."""from__future__importabsolute_import,division,print_function,with_statementimportosimportsys# sys.path handling# -----------------## If a module is run with "python -m", the current directory (i.e. "")# is automatically prepended to sys.path, but not if it is run as# "path/to/file.py". The processing for "-m" rewrites the former to# the latter, so subsequent executions won't have the same path as the# original.## Conversely, when run as path/to/file.py, the directory containing# file.py gets added to the path, which can cause confusion as imports# may become relative in spite of the future import.## We address the former problem by setting the $PYTHONPATH environment# variable before re-execution so the new process will see the correct# path. We attempt to address the latter problem when tornado.autoreload# is run as __main__, although we can't fix the general case because# we cannot reliably reconstruct the original command line# (http://bugs.python.org/issue14208).if__name__=="__main__":# This sys.path manipulation must come before our imports (as much# as possible - if we introduced a tornado.sys or tornado.os# module we'd be in trouble), or else our imports would become# relative again despite the future import.## There is a separate __main__ block at the end of the file to call main().ifsys.path[0]==os.path.dirname(__file__):delsys.path[0]importfunctoolsimportloggingimportosimportpkgutil# type: ignoreimportsysimporttracebackimporttypesimportsubprocessimportweakreffromtornadoimportioloopfromtornado.logimportgen_logfromtornadoimportprocessfromtornado.utilimportexec_intry:importsignalexceptImportError:signal=None# os.execv is broken on Windows and can't properly parse command line# arguments and executable name if they contain whitespaces. subprocess# fixes that behavior.# This distinction is also important because when we use execv, we want to# close the IOLoop and all its file descriptors, to guard against any# file descriptors that were not set CLOEXEC. When execv is not available,# we must not close the IOLoop because we want the process to exit cleanly._has_execv=sys.platform!='win32'_watched_files=set()_reload_hooks=[]_reload_attempted=False_io_loops=weakref.WeakKeyDictionary()# type: ignore

[docs]defstart(io_loop=None,check_time=500):"""Begins watching source files for changes. .. versionchanged:: 4.1 The ``io_loop`` argument is deprecated. """io_loop=io_looporioloop.IOLoop.current()ifio_loopin_io_loops:return_io_loops[io_loop]=Trueiflen(_io_loops)>1:gen_log.warning("tornado.autoreload started more than once in the same process")if_has_execv:add_reload_hook(functools.partial(io_loop.close,all_fds=True))modify_times={}callback=functools.partial(_reload_on_update,modify_times)scheduler=ioloop.PeriodicCallback(callback,check_time,io_loop=io_loop)scheduler.start()

[docs]defwait():"""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()

[docs]defwatch(filename):"""Add a file to the watch list. All imported modules are watched by default. """_watched_files.add(filename)

[docs]defadd_reload_hook(fn):"""Add a function to be called before reloading the process. Note that for open file and socket handles it is generally preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or ``tornado.platform.auto.set_close_exec``) instead of using a reload hook to close them. """_reload_hooks.append(fn)

def_reload_on_update(modify_times):if_reload_attempted:# We already tried to reload and it didn't work, so don't try again.returnifprocess.task_id()isnotNone:# We're in a child process created by fork_processes. If child# processes restarted themselves, they'd all restart and then# all call fork_processes again.returnformoduleinlist(sys.modules.values()):# Some modules play games with sys.modules (e.g. email/__init__.py# in the standard library), and occasionally this can cause strange# failures in getattr. Just ignore anything that's not an ordinary# module.ifnotisinstance(module,types.ModuleType):continuepath=getattr(module,"__file__",None)ifnotpath:continueifpath.endswith(".pyc")orpath.endswith(".pyo"):path=path[:-1]_check_file(modify_times,path)forpathin_watched_files:_check_file(modify_times,path)def_check_file(modify_times,path):try:modified=os.stat(path).st_mtimeexceptException:returnifpathnotinmodify_times:modify_times[path]=modifiedreturnifmodify_times[path]!=modified:gen_log.info("%s modified; restarting server",path)_reload()def_reload():global_reload_attempted_reload_attempted=Trueforfnin_reload_hooks:fn()ifhasattr(signal,"setitimer"):# Clear the alarm signal set by# ioloop.set_blocking_log_threshold so it doesn't fire# after the exec.signal.setitimer(signal.ITIMER_REAL,0,0)# sys.path fixes: see comments at top of file. If sys.path[0] is an empty# string, we were (probably) invoked with -m and the effective path# is about to change on re-exec. Add the current directory to $PYTHONPATH# to ensure that the new process sees the same path we did.path_prefix='.'+os.pathsepif(sys.path[0]==''andnotos.environ.get("PYTHONPATH","").startswith(path_prefix)):os.environ["PYTHONPATH"]=(path_prefix+os.environ.get("PYTHONPATH",""))ifnot_has_execv:subprocess.Popen([sys.executable]+sys.argv)sys.exit(0)else:try:os.execv(sys.executable,[sys.executable]+sys.argv)exceptOSError:# Mac OS X versions prior to 10.6 do not support execv in# a process that contains multiple threads. Instead of# re-executing in the current process, start a new one# and cause the current process to exit. This isn't# ideal since the new process is detached from the parent# terminal and thus cannot easily be killed with ctrl-C,# but it's better than not being able to autoreload at# all.# Unfortunately the errno returned in this case does not# appear to be consistent, so we can't easily check for# this error specifically.os.spawnv(os.P_NOWAIT,sys.executable,[sys.executable]+sys.argv)# At this point the IOLoop has been closed and finally# blocks will experience errors if we allow the stack to# unwind, so just exit uncleanly.os._exit(0)_USAGE="""\Usage: python -m tornado.autoreload -m module.to.run [args...] python -m tornado.autoreload path/to/script.py [args...]"""

[docs]defmain():"""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.argvsys.argv=sys.argv[:]iflen(sys.argv)>=3andsys.argv[1]=="-m":mode="module"module=sys.argv[2]delsys.argv[1:3]eliflen(sys.argv)>=2:mode="script"script=sys.argv[1]sys.argv=sys.argv[1:]else:print(_USAGE,file=sys.stderr)sys.exit(1)try:ifmode=="module":importrunpyrunpy.run_module(module,run_name="__main__",alter_sys=True)elifmode=="script":withopen(script)asf:# 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__=script# If __package__ is defined, imports may be incorrectly# interpreted as relative to this module.global__package__del__package__exec_in(f.read(),globals(),globals())exceptSystemExitase:logging.basicConfig()gen_log.info("Script exited with status %s",e.code)exceptExceptionase:logging.basicConfig()gen_log.warning("Script exited with uncaught exception",exc_info=True)# If an exception occurred at import time, the file with the error# 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)intraceback.extract_tb(sys.exc_info()[2]):watch(filename)ifisinstance(e,SyntaxError):# SyntaxErrors are special: their innermost stack frame is fake# so extract_tb won't see it and we have to get the filename# from the exception object.watch(e.filename)else:logging.basicConfig()gen_log.info("Script exited normally")# restore sys.argv so subsequent executions will include autoreloadsys.argv=original_argvifmode=='module':# runpy did a fake import of the module as __main__, but now it's# no longer in sys.modules. Figure out where it is and watch it.loader=pkgutil.get_loader(module)ifloaderisnotNone:watch(loader.get_filename())wait()

if__name__=="__main__":# See also the other __main__ block at the top of the file, which modifies# sys.path before our importsmain()