Merge branch '24-pagelet-initialization' into 'staging'

Resolve "Pagelet initialization"

See merge request s3lph/matemat!36
This commit is contained in:
s3lph 2018-08-20 22:48:48 +00:00
commit cc8b7d091e
8 changed files with 159 additions and 6 deletions

View file

@ -10,8 +10,9 @@ test:
stage: test
script:
- pip3 install -r requirements.txt
- sudo -u matemat python3 -m coverage run --branch -m unittest discover matemat
- sudo -u matemat python3 -m coverage report -m --include 'matemat/*' --omit '*/test_*.py'
- sudo -u matemat python3 -m coverage run --rcfile=setup.cfg -m unittest discover matemat
- sudo -u matemat python3 -m coverage combine
- sudo -u matemat python3 -m coverage report --rcfile=setup.cfg
codestyle:
stage: test

2
doc

@ -1 +1 @@
Subproject commit 9449a6dc39843969d3b549f05848b5857c23cfa3
Subproject commit 0cf3d59c8b37f84e915f5e30e7447f0611cc1238

View file

@ -8,5 +8,5 @@ server will attempt to serve the request with a static resource in a previously
from .requestargs import RequestArgument, RequestArguments
from .responses import PageletResponse, RedirectResponse, TemplateResponse
from .httpd import MatematWebserver, HttpHandler, pagelet
from .httpd import MatematWebserver, HttpHandler, pagelet, pagelet_init
from .config import parse_config_file

View file

@ -1,5 +1,5 @@
from typing import Any, Callable, Dict, Tuple, Type, Union
from typing import Any, Callable, Dict, Set, Tuple, Type, Union
import logging
import os
@ -38,6 +38,8 @@ _PAGELET_PATHS: Dict[str, Callable[[str, # HTTP method (GET, POST, ...)
bytes, str, # Response body: will assign HTTP/1.0 200 OK
PageletResponse, # A generic response
]]] = dict()
# The pagelet initialization functions, to be executed upon startup
_PAGELET_INIT_FUNCTIONS: Set[Callable[[Dict[str, str], logging.Logger], None]] = set()
# Inactivity timeout for client sessions
_SESSION_TIMEOUT: int = 3600
@ -94,6 +96,28 @@ def pagelet(path: str):
return http_handler
def pagelet_init(fun: Callable[[Dict[str, str], logging.Logger], None]):
"""
Annotate a function to act as a pagelet initialization function. The function will be called when the webserver is
started. The function should set up everything required for pagelets to operate correctly, e.g. providing default
values for configuration items, or performing database schema migrations.
Any exception thrown by an initialization function will cause the web server to seize operation. Multiple
initialization functions can be used.
The function must have the following signature:
(config: Dict[str, str], logger: logging.Logger) -> None
config: The mutable dictionary of variables read from the [Pagelets] section of the configuration file.
logger: The server's logger instance.
returns: Nothing.
:param fun: The function to annotate
"""
_PAGELET_INIT_FUNCTIONS.add(fun)
class MatematHTTPServer(HTTPServer):
"""
A http.server.HTTPServer subclass that acts as a container for data that must be persistent between requests.
@ -184,8 +208,20 @@ class MatematWebserver(object):
def start(self) -> None:
"""
Start the web server. This call blocks while the server is running.
Call all pagelet initialization functions and start the web server. This call blocks while the server is
running. If any exception is raised in the initialization phase, the program is terminated with a non-zero
exit code.
"""
try:
# Run all pagelet initialization functions
for fun in _PAGELET_INIT_FUNCTIONS:
fun(self._httpd.pagelet_variables, self._httpd.logger)
except BaseException as e:
# If an error occurs, log it and terminate
self._httpd.logger.exception(e)
self._httpd.logger.critical('An initialization pagelet raised an error. Stopping.')
raise e
# If pagelet initialization went fine, start the HTTP server
self._httpd.serve_forever()

View file

@ -4,6 +4,7 @@ This package contains the pagelet functions served by the Matemat software.
A new pagelet function must be imported here to be automatically loaded when the server is started.
"""
from .initialization import initialization
from .main import main_page
from .login import login_page
from .logout import logout

View file

@ -0,0 +1,28 @@
from typing import Dict
import logging
from matemat.webserver import pagelet_init
from matemat.db import MatematDatabase
@pagelet_init
def initialization(config: Dict[str, str],
logger: logging.Logger) -> None:
"""
The pagelet initialization function. Makes sure everything is ready for operation. If anything fails here, the web
server won't resume operation.
"""
# Set default values for missing config items
if 'InstanceName' not in config:
config['InstanceName'] = 'Matemat'
logger.warning('Property \'InstanceName\' not set, using \'Matemat\'')
if 'UploadDir' not in config:
config['UploadDir'] = './static/upload/'
logger.warning('Property \'UploadDir\' not set, using \'./static/upload/\'')
if 'DatabaseFile' not in config:
config['DatabaseFile'] = './matemat.db'
logger.warning('Property \'DatabaseFile\' not set, using \'./matemat.db\'')
with MatematDatabase(config['DatabaseFile']):
# Connect to the database to create it and perform any schema migrations
pass

View file

@ -0,0 +1,68 @@
from typing import Any, Dict
import unittest
import http.client
import logging
import threading
from matemat.webserver import MatematWebserver, RequestArguments, pagelet_init, pagelet
@pagelet('/just/testing/init')
def init_test_pagelet(method: str,
path: str,
args: RequestArguments,
session_vars: Dict[str, Any],
headers: Dict[str, str],
pagelet_variables: Dict[str, str]):
return pagelet_variables['Unit-Test']
_INIT_FAIL = False
@pagelet_init
def init(config: Dict[str, str],
logger: logging.Logger):
if _INIT_FAIL:
raise ValueError('This error should be raised!')
config['Unit-Test'] = 'Pagelet Init Test'
class TestPageletInitialization(unittest.TestCase):
def setUp(self):
self.srv = MatematWebserver('::1', 0, '/nonexistent', '/nonexistent', {}, {},
logging.NOTSET, logging.NullHandler())
self.srv_port = int(self.srv._httpd.socket.getsockname()[1])
self.timer = threading.Timer(5.0, self.srv._httpd.shutdown)
self.timer.start()
def tearDown(self):
self.timer.cancel()
if self.srv is not None:
self.srv._httpd.socket.close()
global _INIT_FAIL
_INIT_FAIL = False
def test_pagelet_init_ok(self):
"""
Test successful pagelet initialization
"""
thread = threading.Thread(target=self.srv.start)
thread.start()
con = http.client.HTTPConnection(f'[::1]:{self.srv_port}')
con.request('GET', '/just/testing/init')
response = con.getresponse().read()
self.srv._httpd.shutdown()
self.assertEqual(b'Pagelet Init Test', response)
def test_pagelet_init_fail(self):
"""
Test unsuccessful pagelet initialization
"""
global _INIT_FAIL
_INIT_FAIL = True
with self.assertRaises(ValueError):
self.srv.start()

View file

@ -1,3 +1,22 @@
#
# PyCodestyle
#
[pycodestyle]
max-line-length = 120
statistics = True
#
# Coverage
#
[run]
branch = True
parallel = True
source = matemat/
[report]
show_missing = True
include = matemat/*
omit = */test/*.py