Add reloading on addtional directories (#2167)
This commit is contained in:
parent
83c746ee57
commit
5bb9aa0c2c
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class SanicArgumentParser(ArgumentParser):
|
||||||
def add_bool_arguments(self, *args, **kwargs):
|
def add_bool_arguments(self, *args, **kwargs):
|
||||||
group = self.add_mutually_exclusive_group()
|
group = self.add_mutually_exclusive_group()
|
||||||
group.add_argument(*args, action="store_true", **kwargs)
|
group.add_argument(*args, action="store_true", **kwargs)
|
||||||
kwargs["help"] = "no " + kwargs["help"]
|
kwargs["help"] = f"no {kwargs['help']}\n "
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,15 @@ def main():
|
||||||
parser = SanicArgumentParser(
|
parser = SanicArgumentParser(
|
||||||
prog="sanic",
|
prog="sanic",
|
||||||
description=BASE_LOGO,
|
description=BASE_LOGO,
|
||||||
formatter_class=RawDescriptionHelpFormatter,
|
formatter_class=lambda prog: RawTextHelpFormatter(
|
||||||
|
prog, max_help_position=33
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-H",
|
"-H",
|
||||||
|
@ -51,13 +59,24 @@ def main():
|
||||||
dest="unix",
|
dest="unix",
|
||||||
type=str,
|
type=str,
|
||||||
default="",
|
default="",
|
||||||
help="location of unix socket",
|
help="location of unix socket\n ",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
|
||||||
|
)
|
||||||
|
parser.add_bool_arguments(
|
||||||
|
"--access-logs", dest="access_log", help="display access logs"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--factory",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Treat app as an application factory, "
|
||||||
|
"i.e. a () -> <Sanic app> callable\n "
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w",
|
"-w",
|
||||||
|
@ -65,32 +84,23 @@ def main():
|
||||||
dest="workers",
|
dest="workers",
|
||||||
type=int,
|
type=int,
|
||||||
default=1,
|
default=1,
|
||||||
help="number of worker processes [default 1]",
|
help="number of worker processes [default 1]\n ",
|
||||||
)
|
)
|
||||||
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
|
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-r",
|
"-r",
|
||||||
|
"--reload",
|
||||||
"--auto-reload",
|
"--auto-reload",
|
||||||
dest="auto_reload",
|
dest="auto_reload",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Watch source directory for file changes and reload on changes",
|
help="Watch source directory for file changes and reload on changes",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--factory",
|
"-R",
|
||||||
action="store_true",
|
"--reload-dir",
|
||||||
help=(
|
dest="path",
|
||||||
"Treat app as an application factory, "
|
action="append",
|
||||||
"i.e. a () -> <Sanic app> callable."
|
help="Extra directories to watch and reload on changes\n ",
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-v",
|
|
||||||
"--version",
|
|
||||||
action="version",
|
|
||||||
version=f"Sanic {__version__}; Routing {__routing_version__}",
|
|
||||||
)
|
|
||||||
parser.add_bool_arguments(
|
|
||||||
"--access-logs", dest="access_log", help="display access logs"
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"module", help="path to your Sanic app. Example: path.to.server:app"
|
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||||
|
@ -140,6 +150,17 @@ def main():
|
||||||
}
|
}
|
||||||
if args.auto_reload:
|
if args.auto_reload:
|
||||||
kwargs["auto_reload"] = True
|
kwargs["auto_reload"] = True
|
||||||
|
|
||||||
|
if args.path:
|
||||||
|
if args.auto_reload or args.debug:
|
||||||
|
kwargs["reload_dir"] = args.path
|
||||||
|
else:
|
||||||
|
error_logger.warning(
|
||||||
|
"Ignoring '--reload-dir' since auto reloading was not "
|
||||||
|
"enabled. If you would like to watch directories for "
|
||||||
|
"changes, consider using --debug or --auto-reload."
|
||||||
|
)
|
||||||
|
|
||||||
app.run(**kwargs)
|
app.run(**kwargs)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if module_name.startswith(e.name):
|
if module_name.startswith(e.name):
|
||||||
|
|
18
sanic/app.py
18
sanic/app.py
|
@ -14,6 +14,7 @@ from asyncio.futures import Future
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from pathlib import Path
|
||||||
from socket import socket
|
from socket import socket
|
||||||
from ssl import Purpose, SSLContext, create_default_context
|
from ssl import Purpose, SSLContext, create_default_context
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
@ -105,6 +106,7 @@ class Sanic(BaseSanic):
|
||||||
"name",
|
"name",
|
||||||
"named_request_middleware",
|
"named_request_middleware",
|
||||||
"named_response_middleware",
|
"named_response_middleware",
|
||||||
|
"reload_dirs",
|
||||||
"request_class",
|
"request_class",
|
||||||
"request_middleware",
|
"request_middleware",
|
||||||
"response_middleware",
|
"response_middleware",
|
||||||
|
@ -168,6 +170,7 @@ class Sanic(BaseSanic):
|
||||||
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
self.listeners: Dict[str, List[ListenerType]] = defaultdict(list)
|
||||||
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
|
||||||
|
self.reload_dirs: Set[Path] = set()
|
||||||
self.request_class = request_class
|
self.request_class = request_class
|
||||||
self.request_middleware: Deque[MiddlewareType] = deque()
|
self.request_middleware: Deque[MiddlewareType] = deque()
|
||||||
self.response_middleware: Deque[MiddlewareType] = deque()
|
self.response_middleware: Deque[MiddlewareType] = deque()
|
||||||
|
@ -389,7 +392,7 @@ class Sanic(BaseSanic):
|
||||||
if self.config.EVENT_AUTOREGISTER:
|
if self.config.EVENT_AUTOREGISTER:
|
||||||
self.signal_router.reset()
|
self.signal_router.reset()
|
||||||
self.add_signal(None, event)
|
self.add_signal(None, event)
|
||||||
signal = self.signal_router.name_index.get(event)
|
signal = self.signal_router.name_index[event]
|
||||||
self.signal_router.finalize()
|
self.signal_router.finalize()
|
||||||
else:
|
else:
|
||||||
raise NotFound("Could not find signal %s" % event)
|
raise NotFound("Could not find signal %s" % event)
|
||||||
|
@ -846,6 +849,7 @@ class Sanic(BaseSanic):
|
||||||
access_log: Optional[bool] = None,
|
access_log: Optional[bool] = None,
|
||||||
unix: Optional[str] = None,
|
unix: Optional[str] = None,
|
||||||
loop: None = None,
|
loop: None = None,
|
||||||
|
reload_dir: Optional[Union[List[str], str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run the HTTP Server and listen until keyboard interrupt or term
|
Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
|
@ -880,6 +884,18 @@ class Sanic(BaseSanic):
|
||||||
:type unix: str
|
:type unix: str
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
if reload_dir:
|
||||||
|
if isinstance(reload_dir, str):
|
||||||
|
reload_dir = [reload_dir]
|
||||||
|
|
||||||
|
for directory in reload_dir:
|
||||||
|
direc = Path(directory)
|
||||||
|
if not direc.is_dir():
|
||||||
|
logger.warning(
|
||||||
|
f"Directory {directory} could not be located"
|
||||||
|
)
|
||||||
|
self.reload_dirs.add(Path(directory))
|
||||||
|
|
||||||
if loop is not None:
|
if loop is not None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"loop is not a valid argument. To use an existing loop, "
|
"loop is not a valid argument. To use an existing loop, "
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import itertools
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -59,6 +60,20 @@ def restart_with_reloader():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_file(filename, mtimes):
|
||||||
|
need_reload = False
|
||||||
|
|
||||||
|
mtime = os.stat(filename).st_mtime
|
||||||
|
old_time = mtimes.get(filename)
|
||||||
|
if old_time is None:
|
||||||
|
mtimes[filename] = mtime
|
||||||
|
elif mtime > old_time:
|
||||||
|
mtimes[filename] = mtime
|
||||||
|
need_reload = True
|
||||||
|
|
||||||
|
return need_reload
|
||||||
|
|
||||||
|
|
||||||
def watchdog(sleep_interval, app):
|
def watchdog(sleep_interval, app):
|
||||||
"""Watch project files, restart worker process if a change happened.
|
"""Watch project files, restart worker process if a change happened.
|
||||||
|
|
||||||
|
@ -85,17 +100,16 @@ def watchdog(sleep_interval, app):
|
||||||
while True:
|
while True:
|
||||||
need_reload = False
|
need_reload = False
|
||||||
|
|
||||||
for filename in _iter_module_files():
|
for filename in itertools.chain(
|
||||||
|
_iter_module_files(),
|
||||||
|
*(d.glob("**/*") for d in app.reload_dirs),
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
mtime = os.stat(filename).st_mtime
|
check = _check_file(filename, mtimes)
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
old_time = mtimes.get(filename)
|
if check:
|
||||||
if old_time is None:
|
|
||||||
mtimes[filename] = mtime
|
|
||||||
elif mtime > old_time:
|
|
||||||
mtimes[filename] = mtime
|
|
||||||
need_reload = True
|
need_reload = True
|
||||||
|
|
||||||
if need_reload:
|
if need_reload:
|
||||||
|
|
|
@ -33,7 +33,7 @@ def capture(command):
|
||||||
"fake.server:app",
|
"fake.server:app",
|
||||||
"fake.server:create_app()",
|
"fake.server:create_app()",
|
||||||
"fake.server.create_app()",
|
"fake.server.create_app()",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
def test_server_run(appname):
|
def test_server_run(appname):
|
||||||
command = ["sanic", appname]
|
command = ["sanic", appname]
|
||||||
|
|
|
@ -23,6 +23,8 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
flags = 0
|
flags = 0
|
||||||
|
|
||||||
|
TIMER_DELAY = 2
|
||||||
|
|
||||||
|
|
||||||
def terminate(proc):
|
def terminate(proc):
|
||||||
if flags:
|
if flags:
|
||||||
|
@ -56,6 +58,40 @@ def write_app(filename, **runargs):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def write_json_config_app(filename, jsonfile, **runargs):
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(
|
||||||
|
dedent(
|
||||||
|
f"""\
|
||||||
|
import os
|
||||||
|
from sanic import Sanic
|
||||||
|
import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
with open("{jsonfile}", "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
app.config.update_config(config)
|
||||||
|
|
||||||
|
app.route("/")(lambda x: x)
|
||||||
|
|
||||||
|
@app.listener("after_server_start")
|
||||||
|
def complete(*args):
|
||||||
|
print("complete", os.getpid(), app.config.FOO)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(**{runargs!r})
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(filename):
|
||||||
|
text = secrets.token_urlsafe()
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(f"""{{"FOO": "{text}"}}""")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def scanner(proc):
|
def scanner(proc):
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = line.decode().strip()
|
line = line.decode().strip()
|
||||||
|
@ -90,9 +126,10 @@ async def test_reloader_live(runargs, mode):
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
filename = os.path.join(tmpdir, "reloader.py")
|
filename = os.path.join(tmpdir, "reloader.py")
|
||||||
text = write_app(filename, **runargs)
|
text = write_app(filename, **runargs)
|
||||||
proc = Popen(argv[mode], cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
command = argv[mode]
|
||||||
|
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
||||||
try:
|
try:
|
||||||
timeout = Timer(5, terminate, [proc])
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
||||||
timeout.start()
|
timeout.start()
|
||||||
# Python apparently keeps using the old source sometimes if
|
# Python apparently keeps using the old source sometimes if
|
||||||
# we don't sleep before rewrite (pycache timestamp problem?)
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
||||||
|
@ -107,3 +144,40 @@ async def test_reloader_live(runargs, mode):
|
||||||
terminate(proc)
|
terminate(proc)
|
||||||
with suppress(TimeoutExpired):
|
with suppress(TimeoutExpired):
|
||||||
proc.wait(timeout=3)
|
proc.wait(timeout=3)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"runargs, mode",
|
||||||
|
[
|
||||||
|
(dict(port=42102, auto_reload=True), "script"),
|
||||||
|
(dict(port=42103, debug=True), "module"),
|
||||||
|
({}, "sanic"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_reloader_live_with_dir(runargs, mode):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
filename = os.path.join(tmpdir, "reloader.py")
|
||||||
|
config_file = os.path.join(tmpdir, "config.json")
|
||||||
|
runargs["reload_dir"] = tmpdir
|
||||||
|
write_json_config_app(filename, config_file, **runargs)
|
||||||
|
text = write_file(config_file)
|
||||||
|
command = argv[mode]
|
||||||
|
if mode == "sanic":
|
||||||
|
command += ["--reload-dir", tmpdir]
|
||||||
|
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
||||||
|
try:
|
||||||
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
||||||
|
timeout.start()
|
||||||
|
# Python apparently keeps using the old source sometimes if
|
||||||
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
||||||
|
sleep(1)
|
||||||
|
line = scanner(proc)
|
||||||
|
assert text in next(line)
|
||||||
|
# Edit source code and try again
|
||||||
|
text = write_file(config_file)
|
||||||
|
assert text in next(line)
|
||||||
|
finally:
|
||||||
|
timeout.cancel()
|
||||||
|
terminate(proc)
|
||||||
|
with suppress(TimeoutExpired):
|
||||||
|
proc.wait(timeout=3)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user