Michael Merickel
2018-10-15 0c29cf2df41600d3906d521c72991c7686018b71
commit | author | age
126470 1 import fnmatch
616f40 2 import argparse
337960 3 import sys
d58614 4 import textwrap
b8ba0f 5 import re
337960 6
e028e0 7 from zope.interface import Interface
CM 8
337960 9 from pyramid.paster import bootstrap
678790 10 from pyramid.compat import string_types
e028e0 11 from pyramid.interfaces import IRouteRequest
228a5e 12 from pyramid.config import not_
1dfd12 13
678790 14 from pyramid.scripts.common import get_config_loader
49fb77 15 from pyramid.scripts.common import parse_vars
1dfd12 16 from pyramid.static import static_view
e028e0 17 from pyramid.view import _find_views
e51dd0 18
JA 19
20 PAD = 3
1dfd12 21 ANY_KEY = '*'
JA 22 UNKNOWN_KEY = '<unknown>'
e51dd0 23
337960 24
d29151 25 def main(argv=sys.argv, quiet=False):
CM 26     command = PRoutesCommand(argv, quiet)
d58614 27     return command.run()
1dfd12 28
JA 29
30 def _get_pattern(route):
31     pattern = route.pattern
32
33     if not pattern.startswith('/'):
34         pattern = '/%s' % pattern
35     return pattern
36
37
a99d2d 38 def _get_print_format(fmt, max_name, max_pattern, max_view, max_method):
JA 39     print_fmt = ''
40     max_map = {
41         'name': max_name,
42         'pattern': max_pattern,
43         'view': max_view,
44         'method': max_method,
45     }
46     sizes = []
47
48     for index, col in enumerate(fmt):
49         size = max_map[col] + PAD
50         print_fmt += '{{%s: <{%s}}} ' % (col, index)
51         sizes.append(size)
52
53     return print_fmt.format(*sizes)
1dfd12 54
JA 55
56 def _get_request_methods(route_request_methods, view_request_methods):
228a5e 57     excludes = set()
JA 58
59     if route_request_methods:
60         route_request_methods = set(route_request_methods)
61
62     if view_request_methods:
63         view_request_methods = set(view_request_methods)
64
65         for method in view_request_methods.copy():
66             if method.startswith('!'):
67                 view_request_methods.remove(method)
68                 excludes.add(method[1:])
69
1dfd12 70     has_route_methods = route_request_methods is not None
JA 71     has_view_methods = len(view_request_methods) > 0
72     has_methods = has_route_methods or has_view_methods
73
74     if has_route_methods is False and has_view_methods is False:
75         request_methods = [ANY_KEY]
76     elif has_route_methods is False and has_view_methods is True:
77         request_methods = view_request_methods
78     elif has_route_methods is True and has_view_methods is False:
79         request_methods = route_request_methods
80     else:
228a5e 81         request_methods = route_request_methods.intersection(
JA 82             view_request_methods
1dfd12 83         )
228a5e 84
JA 85     request_methods = set(request_methods).difference(excludes)
1dfd12 86
JA 87     if has_methods and not request_methods:
88         request_methods = '<route mismatch>'
89     elif request_methods:
228a5e 90         if excludes and request_methods == set([ANY_KEY]):
JA 91             for exclude in excludes:
92                 request_methods.add('!%s' % exclude)
93
94         request_methods = ','.join(sorted(request_methods))
1dfd12 95
JA 96     return request_methods
97
98
99 def _get_view_module(view_callable):
100     if view_callable is None:
101         return UNKNOWN_KEY
102
103     if hasattr(view_callable, '__name__'):
104         if hasattr(view_callable, '__original_view__'):
105             original_view = view_callable.__original_view__
106         else:
107             original_view = None
108
109         if isinstance(original_view, static_view):
110             if original_view.package_name is not None:
111                 return '%s:%s' % (
112                     original_view.package_name,
0c29cf 113                     original_view.docroot,
1dfd12 114                 )
JA 115             else:
116                 return original_view.docroot
117         else:
118             view_name = view_callable.__name__
119     else:
120         # Currently only MultiView hits this,
121         # we could just not run _get_view_module
122         # for them and remove this logic
123         view_name = str(view_callable)
124
0c29cf 125     view_module = '%s.%s' % (view_callable.__module__, view_name)
1dfd12 126
JA 127     # If pyramid wraps something in wsgiapp or wsgiapp2 decorators
128     # that is currently returned as pyramid.router.decorator, lets
129     # hack a nice name in:
130     if view_module == 'pyramid.router.decorator':
131         view_module = '<wsgiapp>'
132
133     return view_module
134
135
136 def get_route_data(route, registry):
137     pattern = _get_pattern(route)
138
0c29cf 139     request_iface = registry.queryUtility(IRouteRequest, name=route.name)
1dfd12 140
JA 141     route_request_methods = None
e2274e 142     view_request_methods_order = []
JA 143     view_request_methods = {}
1dfd12 144     view_callable = None
JA 145
0c29cf 146     route_intr = registry.introspector.get('routes', route.name)
1dfd12 147
e2274e 148     if request_iface is None:
0c29cf 149         return [(route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY)]
1dfd12 150
e028e0 151     view_callables = _find_views(registry, request_iface, Interface, '')
CM 152     if view_callables:
153         view_callable = view_callables[0]
154     else:
155         view_callable = None
1dfd12 156     view_module = _get_view_module(view_callable)
e2274e 157
1dfd12 158     # Introspectables can be turned off, so there could be a chance
JA 159     # that we have no `route_intr` but we do have a route + callable
160     if route_intr is None:
161         view_request_methods[view_module] = []
e2274e 162         view_request_methods_order.append(view_module)
1dfd12 163     else:
582c2e 164         if route_intr.get('static', False) is True:
JA 165             return [
166                 (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY)
167             ]
1dfd12 168
582c2e 169         route_request_methods = route_intr['request_methods']
1dfd12 170         view_intr = registry.introspector.related(route_intr)
JA 171
172         if view_intr:
173             for view in view_intr:
174                 request_method = view.get('request_methods')
175
176                 if request_method is not None:
1bdb55 177                     if view.get('attr') is not None:
JA 178                         view_callable = getattr(view['callable'], view['attr'])
179                         view_module = '%s.%s' % (
180                             _get_view_module(view['callable']),
0c29cf 181                             view['attr'],
1bdb55 182                         )
JA 183                     else:
184                         view_callable = view['callable']
185                         view_module = _get_view_module(view_callable)
1dfd12 186
JA 187                     if view_module not in view_request_methods:
188                         view_request_methods[view_module] = []
e2274e 189                         view_request_methods_order.append(view_module)
1dfd12 190
JA 191                     if isinstance(request_method, string_types):
192                         request_method = (request_method,)
228a5e 193                     elif isinstance(request_method, not_):
JA 194                         request_method = ('!%s' % request_method.value,)
1dfd12 195
JA 196                     view_request_methods[view_module].extend(request_method)
197                 else:
198                     if view_module not in view_request_methods:
199                         view_request_methods[view_module] = []
e2274e 200                         view_request_methods_order.append(view_module)
1dfd12 201
JA 202         else:
203             view_request_methods[view_module] = []
e2274e 204             view_request_methods_order.append(view_module)
1dfd12 205
JA 206     final_routes = []
207
e2274e 208     for view_module in view_request_methods_order:
JA 209         methods = view_request_methods[view_module]
0c29cf 210         request_methods = _get_request_methods(route_request_methods, methods)
1dfd12 211
0c29cf 212         final_routes.append(
MM 213             (route.name, pattern, view_module, request_methods)
214         )
1dfd12 215
JA 216     return final_routes
e51dd0 217
337960 218
CM 219 class PRoutesCommand(object):
d58614 220     description = """\
CM 221     Print all URL dispatch routes used by a Pyramid application in the
337960 222     order in which they are evaluated.  Each route includes the name of the
CM 223     route, the pattern of the route, and the view callable which will be
224     invoked when the route is matched.
225
364280 226     This command accepts one positional argument named 'config_uri'.  It
d58614 227     specifies the PasteDeploy config file to use for the interactive
364280 228     shell. The format is 'inifile#name'. If the name is left off, 'main'
CM 229     will be assumed.  Example: 'proutes myapp.ini'.
337960 230
CM 231     """
678790 232     bootstrap = staticmethod(bootstrap)  # testing
MM 233     get_config_loader = staticmethod(get_config_loader)  # testing
337960 234     stdout = sys.stdout
616f40 235     parser = argparse.ArgumentParser(
df57ec 236         description=textwrap.dedent(description),
c9b2fa 237         formatter_class=argparse.RawDescriptionHelpFormatter,
0c29cf 238     )
MM 239     parser.add_argument(
240         '-g',
241         '--glob',
242         action='store',
243         dest='glob',
244         default='',
245         help='Display routes matching glob pattern',
246     )
337960 247
0c29cf 248     parser.add_argument(
MM 249         '-f',
250         '--format',
251         action='store',
252         dest='format',
253         default='',
254         help=(
255             'Choose which columns to display, this will '
256             'override the format key in the [proutes] ini '
257             'section'
258         ),
259     )
616f40 260
SP 261     parser.add_argument(
262         'config_uri',
307ee4 263         nargs='?',
SP 264         default=None,
616f40 265         help='The URI to the configuration file.',
0c29cf 266     )
a99d2d 267
e96b2b 268     parser.add_argument(
a86675 269         'config_vars',
e96b2b 270         nargs='*',
SP 271         default=(),
6721ff 272         help="Variables required by the config file. For example, "
0c29cf 273         "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
MM 274         "passed here.",
275     )
e96b2b 276
d29151 277     def __init__(self, argv, quiet=False):
616f40 278         self.args = self.parser.parse_args(argv[1:])
d29151 279         self.quiet = quiet
0c29cf 280         self.available_formats = ['name', 'pattern', 'view', 'method']
a99d2d 281         self.column_format = self.available_formats
JA 282
283     def validate_formats(self, formats):
284         invalid_formats = []
285         for fmt in formats:
286             if fmt not in self.available_formats:
287                 invalid_formats.append(fmt)
288
0c29cf 289         msg = 'You provided invalid formats %s, ' 'Available formats are %s'
a99d2d 290
JA 291         if invalid_formats:
292             msg = msg % (invalid_formats, self.available_formats)
293             self.out(msg)
294             return False
295
296         return True
297
678790 298     def proutes_file_config(self, loader, global_conf=None):
MM 299         settings = loader.get_settings('proutes', global_conf)
300         format = settings.get('format')
301         if format:
302             cols = re.split(r'[,|\s\n]+', format)
303             self.column_format = [x.strip() for x in cols]
337960 304
1dfd12 305     def out(self, msg):  # pragma: no cover
d29151 306         if not self.quiet:
5cf9fc 307             print(msg)
1dfd12 308
JA 309     def _get_mapper(self, registry):
310         from pyramid.config import Configurator
0c29cf 311
1dfd12 312         config = Configurator(registry=registry)
JA 313         return config.get_routes_mapper()
e51dd0 314
d29151 315     def run(self, quiet=False):
83fb10 316         if not self.args.config_uri:
d29151 317             self.out('requires a config file argument')
d58614 318             return 2
49fb77 319
a4f18f 320         config_uri = self.args.config_uri
678790 321         config_vars = parse_vars(self.args.config_vars)
MM 322         loader = self.get_config_loader(config_uri)
323         loader.setup_logging(config_vars)
324         self.proutes_file_config(loader, config_vars)
325
326         env = self.bootstrap(config_uri, options=config_vars)
337960 327         registry = env['registry']
CM 328         mapper = self._get_mapper(registry)
a99d2d 329
616f40 330         if self.args.format:
SP 331             columns = self.args.format.split(',')
a99d2d 332             self.column_format = [x.strip() for x in columns]
JA 333
334         is_valid = self.validate_formats(self.column_format)
335
336         if is_valid is False:
337             return 2
e51dd0 338
1dfd12 339         if mapper is None:
JA 340             return 0
e51dd0 341
1dfd12 342         max_name = len('Name')
JA 343         max_pattern = len('Pattern')
344         max_view = len('View')
345         max_method = len('Method')
360463 346
582c2e 347         routes = mapper.get_routes(include_static=True)
e51dd0 348
1dfd12 349         if len(routes) == 0:
JA 350             return 0
e51dd0 351
0c29cf 352         mapped_routes = [
MM 353             {
354                 'name': 'Name',
355                 'pattern': 'Pattern',
356                 'view': 'View',
357                 'method': 'Method',
358             },
359             {
360                 'name': '----',
361                 'pattern': '-------',
362                 'view': '----',
363                 'method': '------',
364             },
365         ]
e51dd0 366
1dfd12 367         for route in routes:
JA 368             route_data = get_route_data(route, registry)
e51dd0 369
1dfd12 370             for name, pattern, view, method in route_data:
616f40 371                 if self.args.glob:
0c29cf 372                     match = fnmatch.fnmatch(
MM 373                         name, self.args.glob
374                     ) or fnmatch.fnmatch(pattern, self.args.glob)
126470 375                     if not match:
MA 376                         continue
377
1dfd12 378                 if len(name) > max_name:
JA 379                     max_name = len(name)
e51dd0 380
JA 381                 if len(pattern) > max_pattern:
382                     max_pattern = len(pattern)
383
1dfd12 384                 if len(view) > max_view:
JA 385                     max_view = len(view)
e51dd0 386
1dfd12 387                 if len(method) > max_method:
JA 388                     max_method = len(method)
e51dd0 389
0c29cf 390                 mapped_routes.append(
MM 391                     {
392                         'name': name,
393                         'pattern': pattern,
394                         'view': view,
395                         'method': method,
396                     }
397                 )
e51dd0 398
a99d2d 399         fmt = _get_print_format(
JA 400             self.column_format, max_name, max_pattern, max_view, max_method
401         )
1dfd12 402
JA 403         for route in mapped_routes:
a99d2d 404             self.out(fmt.format(**route))
e51dd0 405
d58614 406         return 0
337960 407
1dfd12 408
JA 409 if __name__ == '__main__':  # pragma: no cover
40d54e 410     sys.exit(main() or 0)