Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aiohttp.web command-line interface to serve apps #740

Merged
merged 7 commits into from
Jan 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand All @@ -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__ +
Expand Down Expand Up @@ -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
Expand Up @@ -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:
Expand Down Expand Up @@ -858,8 +879,6 @@ finalizing. It's pretty close to :func:`run_app` utility function::
loop.close()




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

Expand Down
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example should have a code for demonstrating how to parse args again via argparse.
I mean parse.parse_args(args) -- not every user know about the ability.
I want to encourage argparse usage, not ugly code like if args[0] == '--param' and args[1] == 'value'.

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")