Skip to content

Commit 462bce9

Browse files
authored
Add support for setting binding address (#708)
1 parent 649b2c8 commit 462bce9

File tree

9 files changed

+61
-27
lines changed

9 files changed

+61
-27
lines changed

aiohttp_devtools/cli.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,23 @@ def cli() -> None:
2828
'env variable AIO_LIVERELOAD')
2929
browser_cache_help = ("When disabled (the default), sends no-cache headers to "
3030
"disable browser caching.")
31+
bind_address_help = "Network address to listen, default localhost. env variable: AIO_BIND_ADDRESS"
3132

3233

3334
@cli.command()
3435
@click.argument('path', type=_dir_existing, required=True)
3536
@click.option('--livereload/--no-livereload', envvar='AIO_LIVERELOAD', default=True, help=livereload_help)
37+
@click.option("-b", "--bind", "bind_address", envvar="AIO_BIND_ADDRESS", default="localhost", help=bind_address_help)
3638
@click.option('-p', '--port', default=8000, type=int)
3739
@click.option('-v', '--verbose', is_flag=True, help=verbose_help)
3840
@click.option("--browser-cache/--no-browser-cache", envvar="AIO_BROWSER_CACHE", default=False,
3941
help=browser_cache_help)
40-
def serve(path: str, livereload: bool, port: int, verbose: bool, browser_cache: bool) -> None:
42+
def serve(path: str, livereload: bool, bind_address: str, port: int, verbose: bool, browser_cache: bool) -> None:
4143
"""
4244
Serve static files from a directory.
4345
"""
4446
setup_logging(verbose)
45-
run_app(**serve_static(static_path=path, livereload=livereload, port=port,
47+
run_app(**serve_static(static_path=path, livereload=livereload, bind_address=bind_address, port=port,
4648
browser_cache=browser_cache))
4749

4850

@@ -55,7 +57,7 @@ def serve(path: str, livereload: bool, port: int, verbose: bool, browser_cache:
5557
"added to the server, instead of via signals (this is the default on Windows). "
5658
"env variable: AIO_SHUTDOWN_BY_URL")
5759
host_help = ('host used when referencing livereload and static files, if blank host is taken from the request header '
58-
'with default of localhost. env variable AIO_HOST')
60+
"with default of bind network address. env variable AIO_HOST")
5961
app_factory_help = ('name of the app factory to create an aiohttp.web.Application with, if missing default app-factory '
6062
'names are tried. This can be either a function with signature '
6163
'"def create_app(loop): -> Application" or "def create_app(): -> Application" '
@@ -75,6 +77,7 @@ def serve(path: str, livereload: bool, port: int, verbose: bool, browser_cache:
7577
@click.option('--livereload/--no-livereload', envvar='AIO_LIVERELOAD', default=None, help=livereload_help)
7678
@click.option('--host', default=INFER_HOST, help=host_help)
7779
@click.option('--app-factory', 'app_factory_name', envvar='AIO_APP_FACTORY', help=app_factory_help)
80+
@click.option("-b", "--bind", "bind_address", envvar="AIO_BIND_ADDRESS", default="localhost", help=bind_address_help)
7881
@click.option('-p', '--port', 'main_port', envvar='AIO_PORT', type=click.INT, help=port_help)
7982
@click.option('--aux-port', envvar='AIO_AUX_PORT', type=click.INT, help=aux_port_help)
8083
@click.option('-v', '--verbose', is_flag=True, help=verbose_help)

aiohttp_devtools/runserver/config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def __init__(self, *,
4242
path_prefix: str = "/_devtools",
4343
app_factory_name: Optional[str] = None,
4444
host: str = INFER_HOST,
45+
bind_address: str = "localhost",
4546
main_port: int = 8000,
4647
aux_port: Optional[int] = None,
4748
browser_cache: bool = False):
@@ -73,7 +74,15 @@ def __init__(self, *,
7374
self.path_prefix = path_prefix
7475
self.app_factory_name = app_factory_name
7576
self.infer_host = host == INFER_HOST
76-
self.host = 'localhost' if self.infer_host else host
77+
78+
if not self.infer_host:
79+
self.host = host
80+
elif bind_address == "0.0.0.0":
81+
self.host = "localhost"
82+
else:
83+
self.host = bind_address
84+
85+
self.bind_address = bind_address
7786
self.main_port = main_port
7887
self.aux_port = aux_port or (main_port + 1)
7988
self.browser_cache = browser_cache
@@ -190,5 +199,5 @@ async def load_app(self, app_factory: AppFactory) -> web.Application:
190199

191200
def __str__(self) -> str:
192201
fields = ("py_file", "static_path", "static_url", "livereload", "shutdown_by_url",
193-
"path_prefix", "app_factory_name", "host", "main_port", "aux_port")
202+
"path_prefix", "app_factory_name", "host", "bind_address", "main_port", "aux_port")
194203
return 'Config:\n' + '\n'.join(' {0}: {1!r}'.format(f, getattr(self, f)) for f in fields)

aiohttp_devtools/runserver/main.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ..logs import rs_dft_logger as logger
1010
from .config import Config
1111
from .log_handlers import AuxAccessLogger
12-
from .serve import HOST, check_port_open, create_auxiliary_app
12+
from .serve import check_port_open, create_auxiliary_app
1313
from .watch import AppTask, LiveReloadTask
1414

1515

@@ -33,7 +33,7 @@ def runserver(**config_kwargs: Any) -> RunServer:
3333
config = Config(**config_kwargs)
3434
config.import_app_factory()
3535

36-
asyncio.run(check_port_open(config.main_port))
36+
asyncio.run(check_port_open(config.main_port, host=config.bind_address))
3737

3838
aux_app = create_auxiliary_app(
3939
static_path=config.static_path_str,
@@ -56,11 +56,11 @@ def runserver(**config_kwargs: Any) -> RunServer:
5656
rel_path = config.static_path.relative_to(os.getcwd())
5757
logger.info('serving static files from ./%s/ at %s%s', rel_path, url, config.static_url)
5858

59-
return {"app": aux_app, "host": HOST, "port": config.aux_port,
59+
return {"app": aux_app, "host": config.bind_address, "port": config.aux_port,
6060
"shutdown_timeout": 0.01, "access_log_class": AuxAccessLogger}
6161

6262

63-
def serve_static(*, static_path: str, livereload: bool = True, port: int = 8000,
63+
def serve_static(*, static_path: str, livereload: bool = True, bind_address: str = "localhost", port: int = 8000,
6464
browser_cache: bool = False) -> RunServer:
6565
logger.debug('Config: path="%s", livereload=%s, port=%s', static_path, livereload, port)
6666

@@ -73,6 +73,6 @@ def serve_static(*, static_path: str, livereload: bool = True, port: int = 8000,
7373
app.cleanup_ctx.append(livereload_manager.cleanup_ctx)
7474

7575
livereload_status = 'ON' if livereload else 'OFF'
76-
logger.info('Serving "%s" at http://localhost:%d, livereload %s', static_path, port, livereload_status)
77-
return {"app": app, "host": HOST, "port": port,
76+
logger.info('Serving "%s" at http://%s:%d, livereload %s', static_path, bind_address, port, livereload_status)
77+
return {"app": app, "host": bind_address, "port": port,
7878
"shutdown_timeout": 0.01, "access_log_class": AuxAccessLogger}

aiohttp_devtools/runserver/serve.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
LIVE_RELOAD_HOST_SNIPPET = '\n<script src="http://{}:{}/livereload.js"></script>\n'
3434
LIVE_RELOAD_LOCAL_SNIPPET = b'\n<script src="/livereload.js"></script>\n'
35-
HOST = '0.0.0.0'
3635

3736
LAST_RELOAD = web.AppKey("LAST_RELOAD", List[float])
3837
LIVERELOAD_SCRIPT = web.AppKey("LIVERELOAD_SCRIPT", bytes)
@@ -129,17 +128,17 @@ def shutdown() -> NoReturn:
129128
_set_static_url(app, static_url)
130129

131130

132-
async def check_port_open(port: int, delay: float = 1) -> None:
131+
async def check_port_open(port: int, host: str = "0.0.0.0", delay: float = 1) -> None:
133132
loop = asyncio.get_running_loop()
134133
# the "s = socket.socket; s.bind" approach sometimes says a port is in use when it's not
135134
# this approach replicates aiohttp so should always give the same answer
136135
for i in range(5, 0, -1):
137136
try:
138-
server = await loop.create_server(asyncio.Protocol, host=HOST, port=port)
137+
server = await loop.create_server(asyncio.Protocol, host=host, port=port)
139138
except OSError as e:
140139
if e.errno != EADDRINUSE:
141140
raise
142-
dft_logger.warning('port %d is already in use, waiting %d...', port, i)
141+
dft_logger.warning("%s:%d is already in use, waiting %d...", host, port, i)
143142
await asyncio.sleep(delay)
144143
else:
145144
server.close()
@@ -170,7 +169,7 @@ def serve_main_app(config: Config, tty_path: Optional[str]) -> None:
170169
with asyncio.Runner() as runner:
171170
app_runner = runner.run(create_main_app(config, app_factory))
172171
try:
173-
runner.run(start_main_app(app_runner, config.main_port))
172+
runner.run(start_main_app(app_runner, config.bind_address, config.main_port))
174173
runner.get_loop().run_forever()
175174
except KeyboardInterrupt:
176175
pass
@@ -181,7 +180,7 @@ def serve_main_app(config: Config, tty_path: Optional[str]) -> None:
181180
loop = asyncio.new_event_loop()
182181
runner = loop.run_until_complete(create_main_app(config, app_factory))
183182
try:
184-
loop.run_until_complete(start_main_app(runner, config.main_port))
183+
loop.run_until_complete(start_main_app(runner, config.bind_address, config.main_port))
185184
loop.run_forever()
186185
except KeyboardInterrupt: # pragma: no cover
187186
pass
@@ -194,13 +193,13 @@ async def create_main_app(config: Config, app_factory: AppFactory) -> web.AppRun
194193
app = await config.load_app(app_factory)
195194
modify_main_app(app, config)
196195

197-
await check_port_open(config.main_port)
196+
await check_port_open(config.main_port, host=config.bind_address)
198197
return web.AppRunner(app, access_log_class=AccessLogger, shutdown_timeout=0.1)
199198

200199

201-
async def start_main_app(runner: web.AppRunner, port: int) -> None:
200+
async def start_main_app(runner: web.AppRunner, host: str, port: int) -> None:
202201
await runner.setup()
203-
site = web.TCPSite(runner, host=HOST, port=port)
202+
site = web.TCPSite(runner, host=host, port=port)
204203
await site.start()
205204

206205

aiohttp_devtools/runserver/watch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async def _src_reload_when_live(self, checks: int) -> None:
107107
assert self._app is not None and self._session is not None
108108

109109
if self._app[WS]:
110-
url = 'http://localhost:{.main_port}/?_checking_alive=1'.format(self._config)
110+
url = "http://{0.host}:{0.main_port}/?_checking_alive=1".format(self._config)
111111
logger.debug('checking app at "%s" is running before prompting reload...', url)
112112
for i in range(checks):
113113
await asyncio.sleep(0.1)
@@ -141,7 +141,7 @@ async def _stop_dev_server(self) -> None:
141141
if self._process.is_alive():
142142
logger.debug('stopping server process...')
143143
if self._config.shutdown_by_url: # Workaround for signals not working on Windows
144-
url = "http://localhost:{}{}/shutdown".format(self._config.main_port, self._config.path_prefix)
144+
url = "http://{0.host}:{0.main_port}{0.path_prefix}/shutdown".format(self._config)
145145
logger.debug("Attempting to stop process via shutdown endpoint {}".format(url))
146146
try:
147147
with suppress(ClientConnectionError):

tests/test_runserver_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ async def test_load_simple_app(tmpworkdir):
1313
Config(app_path='app.py')
1414

1515

16+
def test_infer_host(tmpworkdir):
17+
mktree(tmpworkdir, SIMPLE_APP)
18+
bind_config = Config(app_path="app.py", bind_address="192.168.1.1")
19+
assert bind_config.infer_host is True
20+
assert bind_config.host == "192.168.1.1"
21+
bind_any = Config(app_path="app.py", bind_address="0.0.0.0")
22+
assert bind_any.infer_host is True
23+
assert bind_any.host == "localhost"
24+
25+
26+
def test_host_override_addr(tmpworkdir):
27+
mktree(tmpworkdir, SIMPLE_APP)
28+
config = Config(app_path="app.py", host="foobar.com", bind_address="192.168.1.1")
29+
assert config.infer_host is False
30+
assert config.host == "foobar.com"
31+
assert config.bind_address == "192.168.1.1"
32+
33+
1634
@forked
1735
async def test_create_app_wrong_name(tmpworkdir):
1836
mktree(tmpworkdir, SIMPLE_APP)

tests/test_runserver_main.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ def create_app():
5858
})
5959
loop = asyncio.new_event_loop()
6060
asyncio.set_event_loop(loop)
61-
args = runserver(app_path='app.py', static_path='static_dir')
61+
args = runserver(app_path="app.py", static_path="static_dir", bind_address="0.0.0.0")
6262
aux_app = args["app"]
6363
aux_port = args["port"]
64+
runapp_host = args["host"]
6465
assert isinstance(aux_app, aiohttp.web.Application)
6566
assert aux_port == 8001
67+
assert runapp_host == "0.0.0.0"
6668
for startup in aux_app.on_startup:
6769
loop.run_until_complete(startup(aux_app))
6870

@@ -108,8 +110,10 @@ async def hello(request):
108110
args = runserver(app_path="app.py", host="foobar.com", main_port=0, aux_port=8001)
109111
aux_app = args["app"]
110112
aux_port = args["port"]
113+
runapp_host = args["host"]
111114
assert isinstance(aux_app, aiohttp.web.Application)
112115
assert aux_port == 8001
116+
assert runapp_host == "localhost"
113117
assert len(aux_app.on_startup) == 1
114118
assert len(aux_app.on_shutdown) == 1
115119
assert len(aux_app.cleanup_ctx) == 1
@@ -202,7 +206,7 @@ async def test_serve_main_app(tmpworkdir, mocker):
202206

203207
config = Config(app_path="app.py", main_port=0)
204208
runner = await create_main_app(config, config.import_app_factory())
205-
await start_main_app(runner, config.main_port)
209+
await start_main_app(runner, config.bind_address, config.main_port)
206210

207211
mock_modify_main_app.assert_called_with(mock.ANY, config)
208212

@@ -226,7 +230,7 @@ async def hello(request):
226230

227231
config = Config(app_path="app.py", main_port=0)
228232
runner = await create_main_app(config, config.import_app_factory())
229-
await start_main_app(runner, config.main_port)
233+
await start_main_app(runner, config.bind_address, config.main_port)
230234

231235
mock_modify_main_app.assert_called_with(mock.ANY, config)
232236

tests/test_runserver_serve.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121

2222
async def test_check_port_open(unused_tcp_port_factory):
2323
port = unused_tcp_port_factory()
24-
await check_port_open(port, 0.001)
24+
await check_port_open(port, delay=0.001)
2525

2626

2727
async def test_check_port_not_open(unused_tcp_port_factory):
2828
port = unused_tcp_port_factory()
2929
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
3030
sock.bind(('0.0.0.0', port))
3131
with pytest.raises(AiohttpDevException):
32-
await check_port_open(port, 0.001)
32+
await check_port_open(port, delay=0.001)
3333

3434

3535
async def test_aux_reload(smart_caplog):

tests/test_runserver_watch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ async def test_reload_server_running(aiohttp_client, mocker):
107107
mock_src_reload = mocker.patch('aiohttp_devtools.runserver.watch.src_reload', return_value=create_future())
108108
cli = await aiohttp_client(app)
109109
config = MagicMock()
110+
config.host = "localhost"
110111
config.main_port = cli.server.port
111112

112113
app_task = AppTask(config)

0 commit comments

Comments
 (0)