Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
autoreload.py351 linesDownload Raw Back to tornado
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""Automatically restart the server when a source file is modified.
17
18Most applications should not access this module directly.  Instead,
19pass the keyword argument ``autoreload=True`` to the
20`tornado.web.Application` constructor (or ``debug=True``, which
21enables this setting and several others).  This will enable autoreload
22mode as well as checking for changes to templates and static
23resources.  Note that restarting is a destructive operation and any
24requests in progress will be aborted when the process restarts.  (If
25you want to disable autoreload while using other debug-mode features,
26pass both ``debug=True`` and ``autoreload=False``).
27
28This module can also be used as a command-line wrapper around scripts
29such as unit test runners.  See the `main` method for details.
30
31The command-line wrapper and Application debug modes can be used together.
32This combination is encouraged as the wrapper catches syntax errors and
33other import-time failures, while debug mode catches changes once
34the server has started.
35
36This module will not work correctly when `.HTTPServer`'s multi-process
37mode is used.
38
39Reloading loses any Python interpreter command-line arguments (e.g. ``-u``)
40because it re-executes Python using ``sys.executable`` and ``sys.argv``.
41Additionally, modifying these variables will cause reloading to behave
42incorrectly.
43
44"""
45
46import os
47import sys
48
49# sys.path handling
50# -----------------
51#
52# If a module is run with "python -m", the current directory (i.e. "")
53# is automatically prepended to sys.path, but not if it is run as
54# "path/to/file.py".  The processing for "-m" rewrites the former to
55# the latter, so subsequent executions won't have the same path as the
56# original.
57#
58# Conversely, when run as path/to/file.py, the directory containing
59# file.py gets added to the path, which can cause confusion as imports
60# may become relative in spite of the future import.
61#
62# We address the former problem by reconstructing the original command
63# line before re-execution so the new process will
64# see the correct path.  We attempt to address the latter problem when
65# tornado.autoreload is run as __main__.
66
67if __name__ == "__main__":
68    # This sys.path manipulation must come before our imports (as much
69    # as possible - if we introduced a tornado.sys or tornado.os
70    # module we'd be in trouble), or else our imports would become
71    # relative again despite the future import.
72    #
73    # There is a separate __main__ block at the end of the file to call main().
74    if sys.path[0] == os.path.dirname(__file__):
75        del sys.path[0]
76
77import functools
78import importlib.abc
79import os
80import pkgutil
81import sys
82import traceback
83import types
84import subprocess
85import weakref
86
87from tornado import ioloop
88from tornado.log import gen_log
89from tornado import process
90
91try:
92    import signal
93except ImportError:
94    signal = None  # type: ignore
95
96from typing import Callable, Dict, Optional, List, Union
97
98# os.execv is broken on Windows and can't properly parse command line
99# arguments and executable name if they contain whitespaces. subprocess
100# fixes that behavior.
101_has_execv = sys.platform != "win32"
102
103_watched_files = set()
104_reload_hooks = []
105_reload_attempted = False
106_io_loops: "weakref.WeakKeyDictionary[ioloop.IOLoop, bool]" = (
107    weakref.WeakKeyDictionary()
108)
109_autoreload_is_main = False
110_original_argv: Optional[List[str]] = None
111_original_spec = None
112
113
114def start(check_time: int = 500) -> None:
115    """Begins watching source files for changes.
116
117    .. versionchanged:: 5.0
118       The ``io_loop`` argument (deprecated since version 4.1) has been removed.
119    """
120    io_loop = ioloop.IOLoop.current()
121    if io_loop in _io_loops:
122        return
123    _io_loops[io_loop] = True
124    if len(_io_loops) > 1:
125        gen_log.warning("tornado.autoreload started more than once in the same process")
126    modify_times: Dict[str, float] = {}
127    callback = functools.partial(_reload_on_update, modify_times)
128    scheduler = ioloop.PeriodicCallback(callback, check_time)
129    scheduler.start()
130
131
132def wait() -> None:
133    """Wait for a watched file to change, then restart the process.
134
135    Intended to be used at the end of scripts like unit test runners,
136    to run the tests again after any source file changes (but see also
137    the command-line interface in `main`)
138    """
139    io_loop = ioloop.IOLoop()
140    io_loop.add_callback(start)
141    io_loop.start()
142
143
144def watch(filename: str) -> None:
145    """Add a file to the watch list.
146
147    All imported modules are watched by default.
148    """
149    _watched_files.add(filename)
150
151
152def add_reload_hook(fn: Callable[[], None]) -> None:
153    """Add a function to be called before reloading the process.
154
155    Note that for open file and socket handles it is generally
156    preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
157    `os.set_inheritable`) instead of using a reload hook to close them.
158    """
159    _reload_hooks.append(fn)
160
161
162def _reload_on_update(modify_times: Dict[str, float]) -> None:
163    if _reload_attempted:
164        # We already tried to reload and it didn't work, so don't try again.
165        return
166    if process.task_id() is not None:
167        # We're in a child process created by fork_processes.  If child
168        # processes restarted themselves, they'd all restart and then
169        # all call fork_processes again.
170        return
171    for module in list(sys.modules.values()):
172        # Some modules play games with sys.modules (e.g. email/__init__.py
173        # in the standard library), and occasionally this can cause strange
174        # failures in getattr.  Just ignore anything that's not an ordinary
175        # module.
176        if not isinstance(module, types.ModuleType):
177            continue
178        path = getattr(module, "__file__", None)
179        if not path:
180            continue
181        if path.endswith(".pyc") or path.endswith(".pyo"):
182            path = path[:-1]
183        _check_file(modify_times, path)
184    for path in _watched_files:
185        _check_file(modify_times, path)
186
187
188def _check_file(modify_times: Dict[str, float], path: str) -> None:
189    try:
190        modified = os.stat(path).st_mtime
191    except Exception:
192        return
193    if path not in modify_times:
194        modify_times[path] = modified
195        return
196    if modify_times[path] != modified:
197        gen_log.info("%s modified; restarting server", path)
198        _reload()
199
200
201def _reload() -> None:
202    global _reload_attempted
203    _reload_attempted = True
204    for fn in _reload_hooks:
205        fn()
206    if sys.platform != "win32":
207        # Clear the alarm signal set by
208        # ioloop.set_blocking_log_threshold so it doesn't fire
209        # after the exec.
210        signal.setitimer(signal.ITIMER_REAL, 0, 0)
211    # sys.path fixes: see comments at top of file.  If __main__.__spec__
212    # exists, we were invoked with -m and the effective path is about to
213    # change on re-exec.  Reconstruct the original command line to
214    # ensure that the new process sees the same path we did.
215    if _autoreload_is_main:
216        assert _original_argv is not None
217        spec = _original_spec
218        argv = _original_argv
219    else:
220        spec = getattr(sys.modules["__main__"], "__spec__", None)
221        argv = sys.argv
222    if spec and spec.name != "__main__":
223        # __spec__ is set in two cases: when running a module, and when running a directory. (when
224        # running a file, there is no spec). In the former case, we must pass -m to maintain the
225        # module-style behavior (setting sys.path), even though python stripped -m from its argv at
226        # startup. If sys.path is exactly __main__, we're running a directory and should fall
227        # through to the non-module behavior.
228        #
229        # Some of this, including the use of exactly __main__ as a spec for directory mode,
230        # is documented at https://docs.python.org/3/library/runpy.html#runpy.run_path
231        argv = ["-m", spec.name] + argv[1:]
232
233    if not _has_execv:
234        subprocess.Popen([sys.executable] + argv)
235        os._exit(0)
236    else:
237        os.execv(sys.executable, [sys.executable] + argv)
238
239
240_USAGE = """
241  python -m tornado.autoreload -m module.to.run [args...]
242  python -m tornado.autoreload path/to/script.py [args...]
243"""
244
245
246def main() -> None:
247    """Command-line wrapper to re-run a script whenever its source changes.
248
249    Scripts may be specified by filename or module name::
250
251        python -m tornado.autoreload -m tornado.test.runtests
252        python -m tornado.autoreload tornado/test/runtests.py
253
254    Running a script with this wrapper is similar to calling
255    `tornado.autoreload.wait` at the end of the script, but this wrapper
256    can catch import-time problems like syntax errors that would otherwise
257    prevent the script from reaching its call to `wait`.
258    """
259    # Remember that we were launched with autoreload as main.
260    # The main module can be tricky; set the variables both in our globals
261    # (which may be __main__) and the real importable version.
262    #
263    # We use optparse instead of the newer argparse because we want to
264    # mimic the python command-line interface which requires stopping
265    # parsing at the first positional argument. optparse supports
266    # this but as far as I can tell argparse does not.
267    import optparse
268    import tornado.autoreload
269
270    global _autoreload_is_main
271    global _original_argv, _original_spec
272    tornado.autoreload._autoreload_is_main = _autoreload_is_main = True
273    original_argv = sys.argv
274    tornado.autoreload._original_argv = _original_argv = original_argv
275    original_spec = getattr(sys.modules["__main__"], "__spec__", None)
276    tornado.autoreload._original_spec = _original_spec = original_spec
277
278    parser = optparse.OptionParser(
279        prog="python -m tornado.autoreload",
280        usage=_USAGE,
281        epilog="Either -m or a path must be specified, but not both",
282    )
283    parser.disable_interspersed_args()
284    parser.add_option("-m", dest="module", metavar="module", help="module to run")
285    parser.add_option(
286        "--until-success",
287        action="store_true",
288        help="stop reloading after the program exist successfully (status code 0)",
289    )
290    opts, rest = parser.parse_args()
291    if opts.module is None:
292        if not rest:
293            print("Either -m or a path must be specified", file=sys.stderr)
294            sys.exit(1)
295        path = rest[0]
296        sys.argv = rest[:]
297    else:
298        path = None
299        sys.argv = [sys.argv[0]] + rest
300
301    # SystemExit.code is typed funny: https://github.com/python/typeshed/issues/8513
302    # All we care about is truthiness
303    exit_status: Union[int, str, None] = 1
304    try:
305        import runpy
306
307        if opts.module is not None:
308            runpy.run_module(opts.module, run_name="__main__", alter_sys=True)
309        else:
310            assert path is not None
311            runpy.run_path(path, run_name="__main__")
312    except SystemExit as e:
313        exit_status = e.code
314        gen_log.info("Script exited with status %s", e.code)
315    except Exception as e:
316        gen_log.warning("Script exited with uncaught exception", exc_info=True)
317        # If an exception occurred at import time, the file with the error
318        # never made it into sys.modules and so we won't know to watch it.
319        # Just to make sure we've covered everything, walk the stack trace
320        # from the exception and watch every file.
321        for filename, lineno, name, line in traceback.extract_tb(sys.exc_info()[2]):
322            watch(filename)
323        if isinstance(e, SyntaxError):
324            # SyntaxErrors are special:  their innermost stack frame is fake
325            # so extract_tb won't see it and we have to get the filename
326            # from the exception object.
327            if e.filename is not None:
328                watch(e.filename)
329    else:
330        exit_status = 0
331        gen_log.info("Script exited normally")
332    # restore sys.argv so subsequent executions will include autoreload
333    sys.argv = original_argv
334
335    if opts.module is not None:
336        assert opts.module is not None
337        # runpy did a fake import of the module as __main__, but now it's
338        # no longer in sys.modules.  Figure out where it is and watch it.
339        loader = pkgutil.get_loader(opts.module)
340        if loader is not None and isinstance(loader, importlib.abc.FileLoader):
341            watch(loader.get_filename())
342    if opts.until_success and not exit_status:
343        return
344    wait()
345
346
347if __name__ == "__main__":
348    # See also the other __main__ block at the top of the file, which modifies
349    # sys.path before our imports
350    main()
351 
codekingpro/portable-devtools · Team Ai