Skip to content

Commit 3542bc2

Browse files
committed
Add server entry-point.
1 parent c84dd48 commit 3542bc2

File tree

3 files changed

+278
-3
lines changed

3 files changed

+278
-3
lines changed

harserver/app.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import argparse
2+
import asyncio
3+
import logging
4+
5+
import aiohttp.web
6+
7+
8+
def entry_point():
9+
"""
10+
Application entry point.
11+
12+
This function creates a server, parses command-line arguments, and
13+
runs the server until terminated.
14+
15+
"""
16+
opts = _parse_arguments()
17+
logging.basicConfig(
18+
level=logging.DEBUG if opts.debug else logging.INFO,
19+
format='%(levelname)1.1s %(name)s: %(message)s',
20+
)
21+
if opts.verbose:
22+
logging.getLogger(__package__).setLevel(logging.DEBUG)
23+
24+
asyncio.run(_main(opts), debug=opts.debug)
25+
26+
27+
async def _main(opts):
28+
"""Create the application and run it on the event loop.
29+
30+
:param argparse.Namespace opts: parsed command-line options
31+
32+
N.B. we need to create the application **after** the event loop
33+
is running to ensure that :class:`asyncio.Event` is associated with
34+
the appropriate event loop.
35+
36+
"""
37+
app = _create_app(opts)
38+
app.on_shutdown.append(app['finished'].set)
39+
40+
runner = aiohttp.web.AppRunner(app, handle_signals=True)
41+
await runner.setup()
42+
app['runner'] = runner
43+
44+
site = aiohttp.web.TCPSite(runner, host=opts.listen, port=opts.port_number)
45+
await site.start()
46+
await app['finished'].wait()
47+
48+
49+
def _parse_arguments():
50+
"""Parse command-line parameters.
51+
52+
:return: parsed command-line parameters
53+
:rtype: argparse.Namespace
54+
55+
"""
56+
parser = argparse.ArgumentParser(
57+
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
58+
parser.add_argument(
59+
'--listen',
60+
'-l',
61+
type=str,
62+
default='0.0.0.0',
63+
help='local IP address to listen on')
64+
parser.add_argument(
65+
'--port-number',
66+
'-p',
67+
type=int,
68+
default=8080,
69+
help='port number to bind to')
70+
parser.add_argument(
71+
'--debug',
72+
'-d',
73+
action='store_true',
74+
default=False,
75+
help='enable global debug logging')
76+
parser.add_argument(
77+
'--verbose',
78+
'-v',
79+
action='store_true',
80+
default=False,
81+
help='enable application-level debug logging')
82+
83+
return parser.parse_args()
84+
85+
86+
def _create_app(opts):
87+
"""Create the application instance.
88+
89+
:param argparse.Namespace opts: parsed command-line options
90+
:return: application instance with routes configured
91+
:rtype: aiohttp.web.Application
92+
93+
A :class:`asyncio.Event` instance is stored in the application under
94+
the *finished* key. This event will be set when the application is
95+
going to terminate.
96+
97+
"""
98+
app = aiohttp.web.Application()
99+
app['finished'] = asyncio.Event()
100+
return app

setup.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
'aiohttp==3.4.4',
1818
'yarl==1.3.0',
1919
],
20+
entry_points={
21+
'console_scripts': ['har-server=harserver.app:entry_point'],
22+
},
2023
extras_require={
2124
'dev': [
25+
'asynctest==0.12.2',
2226
'coverage==4.5.2',
2327
'coveralls==1.5.1',
2428
'flake8==3.6.0',
@@ -30,9 +34,7 @@
3034
'wheel==0.32.3',
3135
'yapf==0.25.0',
3236
],
33-
'docs': [
34-
'Sphinx==1.8.2',
35-
],
37+
'docs': ['Sphinx==1.8.2'],
3638
},
3739
project_urls={
3840
'Builds':

tests/test_app.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import asyncio
2+
import unittest.mock
3+
4+
import aiohttp.web
5+
import asynctest.mock
6+
7+
from harserver import app
8+
9+
10+
class PatchingMixin(unittest.TestCase):
11+
def setUp(self):
12+
super().setUp()
13+
self._patchers = []
14+
15+
def tearDown(self):
16+
super().tearDown()
17+
for patcher in self._patchers:
18+
patcher.stop()
19+
self._patchers.clear()
20+
21+
def add_patch(self, target, **kwargs):
22+
patcher = unittest.mock.patch(target, **kwargs)
23+
self._patchers.append(patcher)
24+
return patcher.start()
25+
26+
27+
class EntryPointTests(PatchingMixin, unittest.TestCase):
28+
def setUp(self):
29+
super().setUp()
30+
self.argparse_patch = self.add_patch('harserver.app.argparse')
31+
self.asyncio_patch = self.add_patch('harserver.app.asyncio')
32+
self.main_patch = self.add_patch('harserver.app._main')
33+
self.add_patch('harserver.app.logging')
34+
35+
def test_that_main_uses_argument_parser(self):
36+
app.entry_point()
37+
38+
self.argparse_patch.ArgumentParser.assert_called()
39+
parser = self.argparse_patch.ArgumentParser.return_value
40+
parser.parse_args.assert_called_once_with()
41+
42+
def test_that_main_adds_debug_parameter(self):
43+
app.entry_point()
44+
45+
parser = self.argparse_patch.ArgumentParser.return_value
46+
parser.add_argument.assert_any_call(
47+
'--debug',
48+
'-d',
49+
action='store_true',
50+
help=unittest.mock.ANY,
51+
default=unittest.mock.ANY)
52+
53+
def test_that_main_adds_listen_parameter(self):
54+
app.entry_point()
55+
56+
parser = self.argparse_patch.ArgumentParser.return_value
57+
parser.add_argument.assert_any_call(
58+
'--listen',
59+
'-l',
60+
type=str,
61+
default=unittest.mock.ANY,
62+
help=unittest.mock.ANY)
63+
64+
def test_that_main_adds_port_parameter(self):
65+
app.entry_point()
66+
67+
parser = self.argparse_patch.ArgumentParser.return_value
68+
parser.add_argument.assert_any_call(
69+
'--port-number',
70+
'-p',
71+
type=int,
72+
default=unittest.mock.ANY,
73+
help=unittest.mock.ANY)
74+
75+
def test_that_main_adds_verbose_parameter(self):
76+
app.entry_point()
77+
78+
parser = self.argparse_patch.ArgumentParser.return_value
79+
parser.add_argument.assert_any_call(
80+
'--verbose',
81+
'-v',
82+
action='store_true',
83+
help=unittest.mock.ANY,
84+
default=unittest.mock.ANY)
85+
86+
def test_that_log_leveL_defaults_to_info(self):
87+
parser = self.argparse_patch.ArgumentParser.return_value
88+
parser.parse_args.return_value.debug = False
89+
parser.parse_args.return_value.verbose = False
90+
91+
app.entry_point()
92+
93+
app.logging.basicConfig.assert_called_once_with(
94+
level=app.logging.INFO, format=unittest.mock.ANY)
95+
96+
def test_that_debug_option_sets_log_level(self):
97+
parser = self.argparse_patch.ArgumentParser.return_value
98+
parser.parse_args.return_value.debug = True
99+
parser.parse_args.return_value.verbose = False
100+
101+
app.entry_point()
102+
103+
app.logging.basicConfig.assert_called_once_with(
104+
level=app.logging.DEBUG, format=unittest.mock.ANY)
105+
106+
def test_that_verbose_option_sets_log_level(self):
107+
parser = self.argparse_patch.ArgumentParser.return_value
108+
parser.parse_args.return_value.debug = False
109+
parser.parse_args.return_value.verbose = True
110+
111+
app.entry_point()
112+
113+
app.logging.getLogger.assert_any_call('harserver')
114+
logger = app.logging.getLogger.return_value
115+
logger.setLevel.assert_called_once_with(app.logging.DEBUG)
116+
117+
def test_that_entry_point_runs_application(self):
118+
parser = self.argparse_patch.ArgumentParser.return_value
119+
opts = parser.parse_args.return_value
120+
121+
app.entry_point()
122+
123+
self.main_patch.assert_called_once_with(opts)
124+
self.asyncio_patch.run.assert_called_once_with(
125+
self.main_patch.return_value, debug=opts.debug)
126+
127+
128+
class CreateAppTests(unittest.TestCase):
129+
def test_that_create_app_returns_application(self):
130+
maybe_app = app._create_app(unittest.mock.Mock())
131+
self.assertIsInstance(maybe_app, aiohttp.web.Application)
132+
133+
def test_that_finished_event_is_added(self):
134+
app_instance = app._create_app(unittest.mock.Mock())
135+
self.assertIsInstance(app_instance['finished'], asyncio.Event)
136+
137+
138+
class MainTests(PatchingMixin, asynctest.TestCase):
139+
def setUp(self):
140+
super().setUp()
141+
self.opts = unittest.mock.Mock()
142+
self.opts.listen = '0.0.0.0'
143+
self.opts.port_number = 0
144+
self.app_instance = app._create_app(self.opts)
145+
self.app_instance['finished'].set() # stop the app immediately
146+
147+
self.add_patch(
148+
'harserver.app._create_app', return_value=self.app_instance)
149+
150+
async def test_running_application(self):
151+
await app._main(self.opts)
152+
153+
async def test_that_run_uses_app_runner(self):
154+
runner_cls = self.add_patch('harserver.app.aiohttp.web.AppRunner')
155+
runner = runner_cls.return_value
156+
runner.setup = asynctest.mock.CoroutineMock()
157+
158+
await app._main(self.opts)
159+
runner_cls.assert_called_once_with(
160+
self.app_instance, handle_signals=True)
161+
self.assertIs(runner, self.app_instance['runner'])
162+
163+
async def test_that_run_adds_initial_site(self):
164+
site_cls = self.add_patch('harserver.app.aiohttp.web.TCPSite')
165+
site = site_cls.return_value
166+
site.start = asynctest.mock.CoroutineMock()
167+
168+
await app._main(self.opts)
169+
site_cls.assert_called_once_with(
170+
self.app_instance['runner'],
171+
host=self.opts.listen,
172+
port=self.opts.port_number)
173+
site.start.assert_called_once_with()

0 commit comments

Comments
 (0)