Skip to content

Commit

Permalink
Merge pull request #740 from jashandeep-sohi/exec-web
Browse files Browse the repository at this point in the history
Add aiohttp.web command-line interface to serve apps
asvetlov committed Jan 19, 2016
2 parents 3ba958f + 699b1ef commit 6f4ed33
Showing 4 changed files with 252 additions and 6 deletions.
52 changes: 52 additions & 0 deletions aiohttp/web.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import warnings
import sys


from . import hdrs
@@ -16,6 +17,8 @@
from .web_exceptions import * # noqa
from .web_urldispatcher import * # noqa
from .web_ws import * # noqa
from argparse import ArgumentParser
from importlib import import_module


__all__ = (web_reqrep.__all__ +
@@ -322,3 +325,52 @@ def run_app(app, *, host='0.0.0.0', port=None,
loop.run_until_complete(handler.finish_connections(shutdown_timeout))
loop.run_until_complete(app.cleanup())
loop.close()


def main(argv):
arg_parser = ArgumentParser(
description="aiohttp.web Application server",
prog="aiohttp.web"
)
arg_parser.add_argument(
"entry_func",
help=("Callable returning the `aiohttp.web.Application` instance to "
"run. Should be specified in the 'module:function' syntax."),
metavar="entry-func"
)
arg_parser.add_argument(
"-H", "--hostname",
help="TCP/IP hostname to serve on (default: %(default)r)",
default="localhost"
)
arg_parser.add_argument(
"-P", "--port",
help="TCP/IP port to serve on (default: %(default)r)",
type=int,
default="8080"
)
args, extra_args = arg_parser.parse_known_args(argv)

# Import logic
mod_str, _, func_str = args.entry_func.partition(":")
if not func_str or not mod_str:
arg_parser.error(
"'entry-func' not in 'module:function' syntax"
)
if mod_str.startswith("."):
arg_parser.error("relative module names not supported")
try:
module = import_module(mod_str)
except ImportError:
arg_parser.error("module %r not found" % mod_str)
try:
func = getattr(module, func_str)
except AttributeError:
arg_parser.error("module %r has no attribute %r" % (mod_str, func_str))

app = func(extra_args)
run_app(app, host=args.hostname, port=args.port)
arg_parser.exit(message="Stopped\n")

if __name__ == "__main__":
main(sys.argv)
31 changes: 25 additions & 6 deletions docs/web.rst
Original file line number Diff line number Diff line change
@@ -34,13 +34,34 @@ particular *HTTP method* and *path*::

After that, run the application by :func:`run_app` call::

run_app(app)
web.run_app(app)

That's it. Now, head over to ``http://localhost:8080/`` to see the results.

.. seealso:: :ref:`aiohttp-web-graceful-shutdown` section
explains what :func:`run_app` does and how implement
complex server initialization/finalization from scratch.
.. seealso::

:ref:`aiohttp-web-graceful-shutdown` section explains what :func:`run_app`
does and how to implement complex server initialization/finalization
from scratch.


.. _aiohttp-web-cli:

Command Line Interface (CLI)
----------------------------
:mod:`aiohttp.web` implements a basic CLI for quickly serving an
:class:`Application` in *development* over TCP/IP::

$ python -m aiohttp.web -n localhost -p 8080 package.module.init_func

``package.module.init_func`` should be an importable :term:`callable` that
accepts a list of any non-parsed command-line arguments and returns an
:class:`Application` instance after setting it up::

def init_function(args):
app = web.Application()
app.router.add_route("GET", "/", index_handler)
return app


.. _aiohttp-web-handler:
@@ -858,8 +879,6 @@ finalizing. It's pretty close to :func:`run_app` utility function::
loop.close()




CORS support
------------

54 changes: 54 additions & 0 deletions examples/web_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Example of serving an Application using the `aiohttp.web` CLI.
Serve this app using::
$ python -m aiohttp.web -H localhost -P 8080 --repeat 10 web_app.init \
> "Hello World"
Here ``--repeat`` & ``"Hello World"`` are application specific command-line
arguments. `aiohttp.web` only parses & consumes the command-line arguments it
needs (i.e. ``-H``, ``-P`` & ``entry-func``) and passes on any additional
arguments to the `web_app.init` function for processing.
"""

from aiohttp.web import Application, Response
from argparse import ArgumentParser


def display_message(req):
args = req.app["args"]
text = "\n".join([args.message] * args.repeat)
return Response(text=text)


def init(args):
arg_parser = ArgumentParser(
prog="aiohttp.web ...", description="Application CLI", add_help=False
)

# Positional argument
arg_parser.add_argument(
"message",
help="message to print"
)

# Optional argument
arg_parser.add_argument(
"--repeat",
help="number of times to repeat message", type=int, default="1"
)

# Avoid conflict with -h from `aiohttp.web` CLI parser
arg_parser.add_argument(
"--app-help",
help="show this message and exit", action="help"
)

parsed_args = arg_parser.parse_args(args)

app = Application()
app["args"] = parsed_args
app.router.add_route('GET', '/', display_message)

return app
121 changes: 121 additions & 0 deletions tests/test_web_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import pytest


from aiohttp import web
from unittest import mock


@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_empty(error):
argv = [""]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with(
"'entry-func' not in 'module:function' syntax"
)


@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_only_module(error):
argv = ["test"]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with(
"'entry-func' not in 'module:function' syntax"
)


@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_only_function(error):
argv = [":test"]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with(
"'entry-func' not in 'module:function' syntax"
)


@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_only_seperator(error):
argv = [":"]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with(
"'entry-func' not in 'module:function' syntax"
)


@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_relative_module(error):
argv = [".a.b:c"]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with("relative module names not supported")


@mock.patch("aiohttp.web.import_module", side_effect=ImportError)
@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_non_existent_module(error, import_module):
argv = ["alpha.beta:func"]

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with("module %r not found" % "alpha.beta")


@mock.patch("aiohttp.web.import_module")
@mock.patch("aiohttp.web.ArgumentParser.error", side_effect=SystemExit)
def test_entry_func_non_existent_attribute(error, import_module):
argv = ["alpha.beta:func"]
module = import_module("alpha.beta")
del module.func

with pytest.raises(SystemExit):
web.main(argv)

error.assert_called_with(
"module %r has no attribute %r" % ("alpha.beta", "func")
)


@mock.patch("aiohttp.web.run_app")
@mock.patch("aiohttp.web.import_module")
def test_entry_func_call(import_module, run_app):
argv = ("-H testhost -P 6666 --extra-optional-eins alpha.beta:func "
"--extra-optional-zwei extra positional args").split()
module = import_module("alpha.beta")

with pytest.raises(SystemExit):
web.main(argv)

module.func.assert_called_with(
("--extra-optional-eins --extra-optional-zwei extra positional "
"args").split()
)


@mock.patch("aiohttp.web.run_app")
@mock.patch("aiohttp.web.import_module")
@mock.patch("aiohttp.web.ArgumentParser.exit", side_effect=SystemExit)
def test_running_application(exit, import_module, run_app):
argv = ("-H testhost -P 6666 --extra-optional-eins alpha.beta:func "
"--extra-optional-zwei extra positional args").split()
module = import_module("alpha.beta")
app = module.func()

with pytest.raises(SystemExit):
web.main(argv)

run_app.assert_called_with(app, host="testhost", port=6666)
exit.assert_called_with(message="Stopped\n")

0 comments on commit 6f4ed33

Please sign in to comment.