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

Side by Side Diff: cms/bin/test_server.py

Issue 29912588: Issue 7019 - [CMS] Refactor `test_server.py` (Closed)
Patch Set: Addressed comments from Patch Set #3 Created Oct. 23, 2018, 4:42 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
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
17
18 import os
16 import mimetypes 19 import mimetypes
17 import os
18 import sys
19 import argparse 20 import argparse
20 21
21 import jinja2 22 import jinja2
22 23
24 from cms.converters import converters
23 from cms.utils import process_page 25 from cms.utils import process_page
24 from cms.sources import create_source 26 from cms.sources import create_source
25 from cms.converters import converters
26
27 source = None
28 address = None
29 port = None
30 27
31 UNICODE_ENCODING = 'utf-8' 28 UNICODE_ENCODING = 'utf-8'
32 29
33 ERROR_TEMPLATE = ''' 30 ERROR_TEMPLATE = '''
34 <html> 31 <html>
35 <head> 32 <head>
36 <title>{{status}}</title> 33 <title>{{status}}</title>
37 </head> 34 </head>
38 <body> 35 <body>
39 <h1>{{status}}</h1> 36 <h1>{{status}}</h1>
40 {% set code = status.split()|first|int %} 37 {% set code = status.split()|first|int %}
41 {% if code == 404 %} 38 {% if code == 404 %}
42 <p>No page found for the address {{uri}}.</p> 39 <p>No page found for the address {{uri}}.</p>
43 {% elif code == 500 %} 40 {% elif code == 500 %}
44 <p>An error occurred while processing the request for {{uri}}:</p> 41 <p>An error occurred while processing the request for {{uri}}:</p>
45 <pre>{{error}}</pre> 42 <pre>{{error}}</pre>
46 {% endif %} 43 {% endif %}
47 </body> 44 </body>
48 </html>''' 45 </html>'''
49 46
50 # Initilize the mimetypes modules manually for consistent behavior, 47 # Initialize the mimetypes modules manually for consistent behavior,
51 # ignoring local files and Windows Registry. 48 # ignoring local files and Windows Registry.
52 mimetypes.init([]) 49 mimetypes.init([])
53 50
54 51
55 def get_page(path): 52 class DynamicServerHandler:
56 path = path.strip('/') 53 """General-purpose WSGI server handler that generates pages on request.
57 if path == '': 54
58 path = source.read_config().get('general', 'defaultlocale') 55 Parameters
59 if '/' in path: 56 ----------
60 locale, page = path.split('/', 1) 57 host: str
61 else: 58 The host where the server will run.
62 locale, page = path, '' 59 port: int
63 60 The TCP port the server will run on.
64 default_page = source.read_config().get('general', 'defaultpage') 61 source_dir: str
65 alternative_page = '/'.join([page, default_page]).lstrip('/') 62 The path to the website contents.
66 63
67 for format in converters.iterkeys(): 64 """
68 for p in (page, alternative_page): 65
69 if source.has_page(p, format): 66 def __init__(self, host, port, source_dir):
70 site_url = 'http://{}:{}'.format(address, port) 67 self.host = host
71 return (p, process_page(source, locale, p, format, site_url)) 68 self.port = port
72 if source.has_localizable_file(locale, page): 69 self.source = create_source(source_dir)
73 return (page, source.read_localizable_file(locale, page)) 70 self.full_url = 'http://{0}:{1}'.format(host, port)
74 71
75 return (None, None) 72 def _get_data(self, path):
76 73 """Read the data corresponding to a website path.
77 74
78 def has_conflicting_pages(page): 75 Parameters
79 pages = [p for p, _ in source.list_pages()] 76 ----------
80 pages.extend(source.list_localizable_files()) 77 path: str
81 78 The path to the page to get the data for.
82 if pages.count(page) > 1: 79
83 return True 80 Returns
84 if any(p.startswith(page + '/') or page.startswith(p + '/') for p in pages): 81 -------
85 return True 82 str or bytes
86 return False 83 The data corresponding to the path we're trying to access.
87 84
88 85 """
89 def get_data(path): 86 if self.source.has_static(path):
90 if source.has_static(path): 87 return self.source.read_static(path)
91 return source.read_static(path) 88
92 89 page, data = self._get_page(path)
93 page, data = get_page(path) 90
94 if page and has_conflicting_pages(page): 91 if page and self._has_conflicts(page):
95 raise Exception('The requested page conflicts with another page') 92 raise Exception('The requested page conflicts with another page.')
96 return data 93
97 94 return data
98 95
99 def show_error(start_response, status, **kwargs): 96 def _get_page(self, path):
100 env = jinja2.Environment(autoescape=True) 97 """Construct a page and return its contents.
101 template = env.from_string(ERROR_TEMPLATE) 98
102 mime = 'text/html; encoding=%s' % UNICODE_ENCODING 99 Parameters
103 start_response(status, [('Content-Type', mime)]) 100 ----------
104 for fragment in template.stream(status=status, **kwargs): 101 path: str
105 yield fragment.encode(UNICODE_ENCODING) 102 The path of the page we want to construct.
106 103
107 104 Returns
108 def handler(environ, start_response): 105 -------
109 path = environ.get('PATH_INFO') 106 (page_name, page_contents): (str, str)
110 107
111 data = get_data(path) 108 """
112 if data is None: 109 path = path.strip('/')
113 return show_error(start_response, '404 Not Found', uri=path) 110 if path == '':
114 111 locale, page = self.source.read_config().get('general',
115 mime = mimetypes.guess_type(path)[0] or 'text/html' 112 'defaultlocale'), ''
116 113 elif '/' in path:
117 if isinstance(data, unicode): 114 locale, page = path.split('/', 1)
118 data = data.encode(UNICODE_ENCODING) 115 else:
119 mime = '%s; charset=%s' % (mime, UNICODE_ENCODING) 116 locale, page = path, ''
120 117
121 start_response('200 OK', [('Content-Type', mime)]) 118 default_page = self.source.read_config().get('general', 'defaultpage')
122 return [data] 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.
230
231 Returns
232 -------
233 argparse.Namespace
Vasily Kuznetsov 2018/10/24 13:31:53 Cool! That's quite some detail, many people don't
234 With the script arguments, as parsed.
235
236 """
237 parser = argparse.ArgumentParser(
238 description='CMS development server created to test pages locally and '
239 'on-the-fly.',
240 )
241
242 parser.add_argument('path', default=os.curdir, nargs='?',
243 help='Path to the website we intend to run. If not '
244 'provided, defaults, to the current directory.')
245 parser.add_argument('--host', default='localhost',
246 help='Address of the host the server will listen on. '
247 'Defaults to "localhost".')
248 parser.add_argument('--port', default=5000, type=int,
249 help='TCP port the server will listen on. Default '
250 '5000.')
251
252 return parser.parse_args()
253
254
255 def run_werkzeug_server(handler, **kw):
256 """Set up a server that uses `werkzeug`.
257
258 Parameters
259 ----------
260 handler: DynamicServerHandler
261 Defines the parameters and methods required to handle requests.
262
263 Raises
264 ------
265 ImportError
266 If the package `werkzeug` is not installed
267
268 """
269 from werkzeug.serving import run_simple
270 import logging
271
272 def run(*args, **kwargs):
273 # The werkzeug logger must be configured before the
274 # root logger. Also we must prevent it from propagating
275 # messages, otherwise messages are logged twice.
276 logger = logging.getLogger('werkzeug')
277 logger.propagate = False
278 logger.setLevel(logging.INFO)
279 logger.addHandler(logging.StreamHandler())
280
281 run_simple(threaded=True, *args, **kwargs)
282
283 run(handler.host, handler.port, handler, **kw)
284
285
286 def run_builtins_server(handler, **kw):
287 """Configure a server that only uses builtin packages.
288
289 Parameters
290 ----------
291 handler: DynamicServerHandler
292 Defines the parameters and methods required to handle requests.
293
294 """
295 from SocketServer import ThreadingMixIn
296 from wsgiref.simple_server import WSGIServer, make_server
297
298 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
299 daemon_threads = True
300
301 def run(host, port, app, **kwargs):
302 def wrapper(environ, start_response):
303 try:
304 return app(environ, start_response)
305 except Exception as e:
306 return handler.get_error_page(
307 start_response, '500 Internal Server Error',
308 uri=environ.get('PATH_INFO'), error=e,
309 )
310
311 server = make_server(host, port, wrapper, ThreadedWSGIServer)
312 print(' * Running on {0}:{1}'.format(*server.server_address))
313 server.serve_forever()
314
315 run(handler.host, handler.port, handler, **kw)
316
317
318 def main():
319 args = parse_arguments()
320 handler = DynamicServerHandler(args.host, args.port, args.path)
321
322 try:
323 run_werkzeug_server(handler, use_reloader=True, use_debugger=True)
324 except ImportError:
325 run_builtins_server(handler)
123 326
124 327
125 if __name__ == '__main__': 328 if __name__ == '__main__':
126 329 main()
127 parser = argparse.ArgumentParser(description='CMS development server created to test pages locally and on-the-fly')
128 parser.add_argument('path', nargs='?', default=os.curdir)
129 parser.add_argument('-a', '--address', default='localhost', help='Address of the interface the server will listen on')
130 parser.add_argument('-p', '--port', type=int, default=5000, help='TCP port t he server will listen on')
131 args = parser.parse_args()
132
133 source = create_source(args.path)
134 address = args.address
135 port = args.port
136
137 try:
138 from werkzeug.serving import ThreadedWSGIServer, run_simple
139
140 # see https://github.com/mitsuhiko/werkzeug/pull/770
141 ThreadedWSGIServer.daemon_threads = True
142
143 def run(*args, **kwargs):
144 # The werkzeug logger must be configured before the
145 # root logger. Also we must prevent it from propagating
146 # messages, otherwise messages are logged twice.
147 import logging
148 logger = logging.getLogger('werkzeug')
149 logger.propagate = False
150 logger.setLevel(logging.INFO)
151 logger.addHandler(logging.StreamHandler())
152
153 run_simple(threaded=True, *args, **kwargs)
154 except ImportError:
155 from SocketServer import ThreadingMixIn
156 from wsgiref.simple_server import WSGIServer, make_server
157
158 class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
159 daemon_threads = True
160
161 def run(host, port, app, **kwargs):
162 def wrapper(environ, start_response):
163 try:
164 return app(environ, start_response)
165 except Exception as e:
166 return show_error(start_response, '500 Internal Server Error ',
167 uri=environ.get('PATH_INFO'), error=e)
168
169 server = make_server(host, port, wrapper, ThreadedWSGIServer)
170 print ' * Running on http://%s:%i/' % server.server_address
171 server.serve_forever()
172
173 run(address, port, handler, use_reloader=True, use_debugger=True)
OLDNEW
« no previous file with comments | « .hgignore ('k') | tests/conftest.py » ('j') | tests/test_dynamic_server.py » ('J')

Powered by Google App Engine
This is Rietveld