Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: cms/bin/test_server.py

Issue 29912588: Issue 7019 - [CMS] Refactor `test_server.py` (Closed)
Left Patch Set: Created Oct. 16, 2018, 11:42 a.m.
Right Patch Set: Addressed docstring nit Created Oct. 29, 2018, 11 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « .hgignore ('k') | tests/conftest.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 # This file is part of the Adblock Plus web scripts, 1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-present eyeo GmbH 2 # Copyright (C) 2006-present eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details. 11 # GNU General Public License for more details.
12 # 12 #
13 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 15
16 from __future__ import print_function 16 from __future__ import print_function
17 17
18 import os 18 import os
19 import mimetypes 19 import mimetypes
20 from argparse import ArgumentParser 20 import argparse
21 21
22 import jinja2 22 import jinja2
23 23
24 from cms.converters import converters 24 from cms.converters import converters
25 from cms.utils import process_page 25 from cms.utils import process_page
26 from cms.sources import create_source 26 from cms.sources import create_source
27
28
29 class Parameters:
Vasily Kuznetsov 2018/10/16 13:18:23 As we discussed via IRC, let's make this whole thi
Tudor Avram 2018/10/18 13:44:05 Done.
30 source = None
31 host = None
32 port = None
33 base_url = None
34
35
36 MIME_FORMAT = '{}'
37 27
38 UNICODE_ENCODING = 'utf-8' 28 UNICODE_ENCODING = 'utf-8'
39 29
40 ERROR_TEMPLATE = ''' 30 ERROR_TEMPLATE = '''
41 <html> 31 <html>
42 <head> 32 <head>
43 <title>{{status}}</title> 33 <title>{{status}}</title>
44 </head> 34 </head>
45 <body> 35 <body>
46 <h1>{{status}}</h1> 36 <h1>{{status}}</h1>
47 {% set code = status.split()|first|int %} 37 {% set code = status.split()|first|int %}
48 {% if code == 404 %} 38 {% if code == 404 %}
49 <p>No page found for the address {{uri}}.</p> 39 <p>No page found for the address {{uri}}.</p>
50 {% elif code == 500 %} 40 {% elif code == 500 %}
51 <p>An error occurred while processing the request for {{uri}}:</p> 41 <p>An error occurred while processing the request for {{uri}}:</p>
52 <pre>{{error}}</pre> 42 <pre>{{error}}</pre>
53 {% endif %} 43 {% endif %}
54 </body> 44 </body>
55 </html>''' 45 </html>'''
56 46
57 # Initialize the mimetypes modules manually for consistent behavior, 47 # Initialize the mimetypes modules manually for consistent behavior,
58 # ignoring local files and Windows Registry. 48 # ignoring local files and Windows Registry.
59 mimetypes.init([]) 49 mimetypes.init([])
60 50
61 51
62 def get_data(path): 52 class DynamicServerHandler:
63 """Read the data corresponding to a page. 53 """General-purpose WSGI server handler that generates pages on request.
64 54
65 Parameters 55 Parameters
66 ---------- 56 ----------
67 path: str 57 host: str
68 The path to the page to get the data for. 58 The host where the server will run.
59 port: int
60 The TCP port the server will run on.
61 source_dir: str
62 The path to the website contents.
63
64 """
65
66 def __init__(self, host, port, source_dir):
67 self.host = host
68 self.port = port
69 self.source = create_source(source_dir)
70 self.full_url = 'http://{0}:{1}'.format(host, port)
71
72 def _get_data(self, path):
73 """Read the data corresponding to a website path.
74
75 Parameters
76 ----------
77 path: str
78 The path to the page to get the data for.
79
80 Returns
81 -------
82 str or bytes
83 The data corresponding to the path we're trying to access.
84
85 """
86 if self.source.has_static(path):
87 return self.source.read_static(path)
88
89 page, data = self._get_page(path)
90
91 if page and self._has_conflicts(page):
92 raise Exception('The requested page conflicts with another page.')
93
94 return data
95
96 def _get_page(self, path):
97 """Construct a page and return its contents.
98
99 Parameters
100 ----------
101 path: str
102 The path of the page we want to construct.
103
104 Returns
105 -------
106 (page_name, page_contents): (str, str)
107
108 """
109 path = path.strip('/')
110 if path == '':
111 locale, page = self.source.read_config().get('general',
112 'defaultlocale'), ''
113 elif '/' in path:
114 locale, page = path.split('/', 1)
115 else:
116 locale, page = path, ''
117
118 default_page = self.source.read_config().get('general', 'defaultpage')
119 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')]
120
121 for page_format in converters.iterkeys():
122 for p in possible_pages:
123 if self.source.has_page(p, page_format):
124 return p, process_page(self.source, locale, p, page_format,
125 self.full_url)
126
127 if self.source.has_localizable_file(locale, page):
128 return page, self.source.read_localizable_file(locale, page)
129
130 return None, None
131
132 def _has_conflicts(self, page):
133 """Check if a page has conflicts.
134
135 A page has conflicts if there are other pages with the same name.
136
137 Parameters
138 ----------
139 page: str
140 The path of the page we're checking for conflicts.
141
142 Returns
143 -------
144 bool
145 True - if the page has conflicts
146 False - otherwise
147
148 """
149 pages = [p for p, _ in self.source.list_pages()]
150 pages.extend(self.source.list_localizable_files())
151
152 if pages.count(page) > 1:
153 return True
154 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in
155 pages):
156 return True
157 return False
158
159 def get_error_page(self, start_response, status, **kw):
160 """Create and display an error page.
161
162 Parameters
163 ----------
164 start_response: function
165 It will be called before constructing the error page, to setup
166 things like the status of the response and the headers.
167 status: str
168 The status of the response we're sending the error page with.
169 Needs to have the following format:
170 "<status_code> <status_message>"
171 kw: dict
172 Any additional arguments that will be passed onto the `stream`
173 method of a `jinja2 Template`.
174
175 Returns
176 -------
177 generator of utf8 strings
178 Fragments of the corresponding error HTML template.
179
180 """
181 env = jinja2.Environment(autoescape=True)
182 page_template = env.from_string(ERROR_TEMPLATE)
183 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING)
184
185 start_response(status, [('Content-Type', mime)])
186
187 for fragment in page_template.stream(status=status, **kw):
188 yield fragment.encode(UNICODE_ENCODING)
189
190 def __call__(self, environ, start_response):
191 """Execute the handler, according to the WSGI standards.
192
193 Parameters
194 ---------
195 environ: dict
196 The environment under which the page is requested.
197 The requested page must be under the `PATH_INFO` key.
198 start_response: function
199 Used to initiate a response. Must take two arguments, in this
200 order:
201 - Response status, in the format "<code> <message>".
202 - Response headers, as a list of tuples.
203
204 Returns
205 -------
206 list of str
207 With the data for a specific page.
208
209 """
210 path = environ.get('PATH_INFO')
211
212 data = self._get_data(path)
213
214 if data is None:
215 return self.get_error_page(start_response, '404 Not Found',
216 uri=path)
217
218 mime = mimetypes.guess_type(path)[0] or 'text/html'
219
220 if isinstance(data, unicode):
221 data = data.encode(UNICODE_ENCODING)
222 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING)
223
224 start_response('200 OK', [('Content-Type', mime)])
225 return [data]
226
227
228 def parse_arguments():
229 """Set up and parse the arguments required by the script.
69 230
70 Returns 231 Returns
71 ------- 232 -------
72 str/ bytes 233 argparse.Namespace
73 The data corresponding to the page we're trying to open. 234 With the script arguments, as parsed.
74 235
75 """ 236 """
76 if Parameters.source.has_static(path): 237 parser = argparse.ArgumentParser(
77 return Parameters.source.read_static(path) 238 description='CMS development server created to test pages locally and '
78 239 'on-the-fly.',
79 page, data = get_page(path) 240 )
80 if page and has_conflicts(page):
81 raise Exception('The requested page conflicts with another page')
82 return data
83
84
85 def get_page(path):
86 """Construct a page and return its contents.
87
88 Parameters
89 ----------
90 path: str
91 The path of the page we want to construct.
92
93 Returns
94 -------
95 (str, str)
96 With the following format:
97 <page_name, page_contents>
98
99 """
100 path = path.strip('/')
101 if path == '':
102 locale, page = Parameters.source.read_config().get(
103 'general', 'defaultlocale'), ''
104 elif '/' in path:
105 locale, page = path.split('/', 1)
106 else:
107 locale, page = path, ''
108
109 default_page = Parameters.source.read_config().get('general',
110 'defaultpage')
111 possible_pages = [page, '/'.join([page, default_page]).lstrip('/')]
112
113 for page_format in converters.iterkeys():
114 for p in possible_pages:
115 if Parameters.source.has_page(p, page_format):
116 return p, process_page(Parameters.source, locale, p,
117 page_format, Parameters.base_url)
118
119 if Parameters.source.has_localizable_file(locale, page):
120 return page, Parameters.source.read_localizable_file(locale, page)
121
122 return None, None
123
124
125 def has_conflicts(page):
126 """Check if a page has conflicts.
127
128 A page has conflicts if there are other pages with the same name.
129 Parameters
130 ----------
131 page: str
132 The path of the page we're checking for conflicts.
133
134 Returns
135 -------
136 bool
137 True - if the page has conflicts
138 False - otherwise
139
140 """
141 pages = [p for p, _ in Parameters.source.list_pages()]
142 pages.extend(Parameters.source.list_localizable_files())
143
144 if pages.count(page) > 1:
145 return True
146 if any(p.startswith(page + '/') or page.startswith(p + '/')
147 for p in pages):
148 return True
149 return False
150
151
152 def get_error_page(start_response, status, **kw):
153 """Create and display an error page.
154
155 Parameters
156 ----------
157 start_response: function
158 It will be called before constructing the error page, to setup
159 things like the status of the response and the headers.
160 status: str
161 The status of the response we're sending the error page with.
162 Needs to have the following format: "<status_code> <status_message>"
163 kw: dict
164 Any additional arguments that will be passed onto the `stream` method
165 of a `jinja2 Template`.
166
167 Returns
168 -------
169 generator
170 of utf8 strings - fragments of the corresponding error HTML template.
171
172 """
173 env = jinja2.Environment(autoescape=True)
174 page_template = env.from_string(ERROR_TEMPLATE)
175 mime = 'text/html; encoding={}'.format(UNICODE_ENCODING)
176
177 start_response(status, [('Content-Type', mime)])
178
179 for fragment in page_template.stream(status=status, **kw):
180 yield fragment.encode(UNICODE_ENCODING)
181
182
183 def set_parameters():
184 """Set the arguments required to run the script.
185
186 Performs the following actions:
187 1. Setup the script's argument parser
188 2. Read the arguments provided when running the script
189 3. Set the fields of the Parameters namespace
190
191 """
192 parser = ArgumentParser(description='CMS development server created to '
193 'test pages locally and on-the-fly')
194 241
195 parser.add_argument('path', default=os.curdir, nargs='?', 242 parser.add_argument('path', default=os.curdir, nargs='?',
196 help='Path to the website we intend to run. If not ' 243 help='Path to the website we intend to run. If not '
197 'provided, defaults, to the current directory.') 244 'provided, defaults, to the current directory.')
198 parser.add_argument('--host', default='localhost', 245 parser.add_argument('--host', default='localhost',
199 help='Address of the host the server will listen on. ' 246 help='Address of the host the server will listen on. '
200 'Defaults to "localhost".') 247 'Defaults to "localhost".')
201 parser.add_argument('--port', default=5000, type=int, 248 parser.add_argument('--port', default=5000, type=int,
202 help='TCP port the server will listen on. Default ' 249 help='TCP port the server will listen on. Default '
203 '5000.') 250 '5000.')
204 251
205 args = parser.parse_args() 252 return parser.parse_args()
206 253
207 Parameters.source = create_source(args.path) 254
208 Parameters.host = args.host 255 def run_werkzeug_server(handler, **kw):
209 Parameters.port = args.port 256 """Set up a server that uses `werkzeug`.
210 Parameters.base_url = 'http://{0}:{1}'.format(args.host, args.port)
211
212
213 def handler(environ, start_response):
214 """Handle a request for a page.
215 257
216 Parameters 258 Parameters
217 ---------- 259 ----------
218 environ: dict 260 handler: DynamicServerHandler
219 The environment under which the request si made. 261 Defines the parameters and methods required to handle requests.
220 start_response: function
221 Used to initiate a request response.
222
223 Returns
224 -------
225 [str]
226 With the response body.
227
228 """
229 path = environ.get('PATH_INFO')
230
231 data = get_data(path)
232
233 if data is None:
234 return get_error_page(start_response, '404 Not Found', uri=path)
235
236 mime = mimetypes.guess_type(path)[0] or 'text/html'
237
238 if isinstance(data, unicode):
239 data = data.encode(UNICODE_ENCODING)
240 mime = '{0}; charset={1}'.format(mime, UNICODE_ENCODING)
241
242 start_response('200 OK', [('Content-Type', mime)])
243 return [data]
244
245
246 def make_werkzeug_server():
247 """Set up a server that uses `werkzeug`.
248
249 Returns
250 -------
251 function
252 Used to run the server
253 262
254 Raises 263 Raises
255 ------ 264 ------
256 ImportError 265 ImportError
257 If the package `werkzeug` is not installed 266 If the package `werkzeug` is not installed
258 267
259 """ 268 """
260 from werkzeug.serving import run_simple 269 from werkzeug.serving import run_simple
261 270 import logging
262 def run_func(*args, **kwargs): 271
272 def run(*args, **kwargs):
263 # The werkzeug logger must be configured before the 273 # The werkzeug logger must be configured before the
264 # root logger. Also we must prevent it from propagating 274 # root logger. Also we must prevent it from propagating
265 # messages, otherwise messages are logged twice. 275 # messages, otherwise messages are logged twice.
266 import logging
267 logger = logging.getLogger('werkzeug') 276 logger = logging.getLogger('werkzeug')
268 logger.propagate = False 277 logger.propagate = False
269 logger.setLevel(logging.INFO) 278 logger.setLevel(logging.INFO)
270 logger.addHandler(logging.StreamHandler()) 279 logger.addHandler(logging.StreamHandler())
271 280
272 run_simple(threaded=True, *args, **kwargs) 281 run_simple(threaded=True, *args, **kwargs)
273 282
274 return run_func 283 run(handler.host, handler.port, handler, **kw)
275 284
276 285
277 def make_builtins_server(): 286 def run_builtins_server(handler, **kw):
278 """Configure a server that only uses builtin packages. 287 """Configure a server that only uses builtin packages.
279 288
280 Returns 289 Parameters
281 ------- 290 ----------
282 function 291 handler: DynamicServerHandler
283 Used to run the server. 292 Defines the parameters and methods required to handle requests.
284 293
285 """ 294 """
286 from SocketServer import ThreadingMixIn 295 from SocketServer import ThreadingMixIn
287 from wsgiref.simple_server import WSGIServer, make_server 296 from wsgiref.simple_server import WSGIServer, make_server
288 297
289 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer): 298 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
290 daemon_threads = True 299 daemon_threads = True
291 300
292 def run(host, port, app, **kwargs): 301 def run(host, port, app, **kwargs):
293 def wrapper(environ, start_response): 302 def wrapper(environ, start_response):
294 try: 303 try:
295 return app(environ, start_response) 304 return app(environ, start_response)
296 except Exception as e: 305 except Exception as e:
297 return get_error_page(start_response, 306 return handler.get_error_page(
298 '500 Internal Server Error', 307 start_response, '500 Internal Server Error',
299 uri=environ.get('PATH_INFO'), error=e) 308 uri=environ.get('PATH_INFO'), error=e,
300 309 )
301 server = make_server(host, port, wrapper, 310
302 ThreadedWSGIServer) 311 server = make_server(host, port, wrapper, ThreadedWSGIServer)
303 print(' * Running on {0}:{1}'.format(*server.server_address)) 312 print(' * Running on {0}:{1}'.format(*server.server_address))
304 server.serve_forever() 313 server.serve_forever()
305 314
306 return run 315 run(handler.host, handler.port, handler, **kw)
307 316
308 317
309 def main(): 318 def main():
310 set_parameters() 319 args = parse_arguments()
320 handler = DynamicServerHandler(args.host, args.port, args.path)
321
311 try: 322 try:
312 run = make_werkzeug_server() 323 run_werkzeug_server(handler, use_reloader=True, use_debugger=True)
313 except ImportError: 324 except ImportError:
314 run = make_builtins_server() 325 run_builtins_server(handler)
315 run(Parameters.host, Parameters.port, handler, use_reloader=True,
316 use_debugger=True)
317 326
318 327
319 if __name__ == '__main__': 328 if __name__ == '__main__':
320 main() 329 main()
LEFTRIGHT

Powered by Google App Engine
This is Rietveld