show_urls.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. # -*- coding: utf-8 -*-
  2. import functools
  3. import json
  4. import re
  5. import django
  6. from django.conf import settings
  7. from django.contrib.admindocs.views import simplify_regex
  8. from django.core.exceptions import ViewDoesNotExist
  9. from django.core.management.base import BaseCommand, CommandError
  10. from django.utils import translation
  11. from django_extensions.management.color import color_style, no_style
  12. from django_extensions.management.utils import signalcommand
  13. if django.VERSION >= (2, 0):
  14. from django.urls import URLPattern, URLResolver # type: ignore
  15. class RegexURLPattern: # type: ignore
  16. pass
  17. class RegexURLResolver: # type: ignore
  18. pass
  19. class LocaleRegexURLResolver: # type: ignore
  20. pass
  21. def describe_pattern(p):
  22. return str(p.pattern)
  23. else:
  24. try:
  25. from django.urls import RegexURLPattern, RegexURLResolver, LocaleRegexURLResolver # type: ignore
  26. except ImportError:
  27. from django.core.urlresolvers import RegexURLPattern, RegexURLResolver, LocaleRegexURLResolver # type: ignore
  28. class URLPattern: # type: ignore
  29. pass
  30. class URLResolver: # type: ignore
  31. pass
  32. def describe_pattern(p):
  33. return p.regex.pattern
  34. FMTR = {
  35. 'dense': "{url}\t{module}\t{url_name}\t{decorator}",
  36. 'table': "{url},{module},{url_name},{decorator}",
  37. 'aligned': "{url},{module},{url_name},{decorator}",
  38. 'verbose': "{url}\n\tController: {module}\n\tURL Name: {url_name}\n\tDecorators: {decorator}\n",
  39. 'json': '',
  40. 'pretty-json': ''
  41. }
  42. class Command(BaseCommand):
  43. help = "Displays all of the url matching routes for the project."
  44. def add_arguments(self, parser):
  45. super().add_arguments(parser)
  46. parser.add_argument(
  47. "--unsorted", "-u", action="store_true", dest="unsorted",
  48. help="Show urls unsorted but same order as found in url patterns"
  49. )
  50. parser.add_argument(
  51. "--language", "-l", dest="language",
  52. help="Only show this language code (useful for i18n_patterns)"
  53. )
  54. parser.add_argument(
  55. "--decorator", "-d", action="append", dest="decorator", default=[],
  56. help="Show the presence of given decorator on views"
  57. )
  58. parser.add_argument(
  59. "--format", "-f", dest="format_style", default="dense",
  60. help="Style of the output. Choices: %s" % FMTR.keys()
  61. )
  62. parser.add_argument(
  63. "--urlconf", "-c", dest="urlconf", default="ROOT_URLCONF",
  64. help="Set the settings URL conf variable to use"
  65. )
  66. @signalcommand
  67. def handle(self, *args, **options):
  68. style = no_style() if options['no_color'] else color_style()
  69. language = options['language']
  70. if language is not None:
  71. translation.activate(language)
  72. self.LANGUAGES = [(code, name) for code, name in getattr(settings, 'LANGUAGES', []) if code == language]
  73. else:
  74. self.LANGUAGES = getattr(settings, 'LANGUAGES', ((None, None), ))
  75. decorator = options['decorator']
  76. if not decorator:
  77. decorator = ['login_required']
  78. format_style = options['format_style']
  79. if format_style not in FMTR:
  80. raise CommandError(
  81. "Format style '%s' does not exist. Options: %s" % (
  82. format_style,
  83. ", ".join(sorted(FMTR.keys())),
  84. )
  85. )
  86. pretty_json = format_style == 'pretty-json'
  87. if pretty_json:
  88. format_style = 'json'
  89. fmtr = FMTR[format_style]
  90. urlconf = options['urlconf']
  91. views = []
  92. if not hasattr(settings, urlconf):
  93. raise CommandError("Settings module {} does not have the attribute {}.".format(settings, urlconf))
  94. try:
  95. urlconf = __import__(getattr(settings, urlconf), {}, {}, [''])
  96. except Exception as e:
  97. if options['traceback']:
  98. import traceback
  99. traceback.print_exc()
  100. raise CommandError("Error occurred while trying to load %s: %s" % (getattr(settings, urlconf), str(e)))
  101. view_functions = self.extract_views_from_urlpatterns(urlconf.urlpatterns)
  102. for (func, regex, url_name) in view_functions:
  103. if hasattr(func, '__globals__'):
  104. func_globals = func.__globals__
  105. elif hasattr(func, 'func_globals'):
  106. func_globals = func.func_globals
  107. else:
  108. func_globals = {}
  109. decorators = [d for d in decorator if d in func_globals]
  110. if isinstance(func, functools.partial):
  111. func = func.func
  112. decorators.insert(0, 'functools.partial')
  113. if hasattr(func, '__name__'):
  114. func_name = func.__name__
  115. elif hasattr(func, '__class__'):
  116. func_name = '%s()' % func.__class__.__name__
  117. else:
  118. func_name = re.sub(r' at 0x[0-9a-f]+', '', repr(func))
  119. module = '{0}.{1}'.format(func.__module__, func_name)
  120. url_name = url_name or ''
  121. url = simplify_regex(regex)
  122. decorator = ', '.join(decorators)
  123. if format_style == 'json':
  124. views.append({"url": url, "module": module, "name": url_name, "decorators": decorator})
  125. else:
  126. views.append(fmtr.format(
  127. module='{0}.{1}'.format(style.MODULE(func.__module__), style.MODULE_NAME(func_name)),
  128. url_name=style.URL_NAME(url_name),
  129. url=style.URL(url),
  130. decorator=decorator,
  131. ).strip())
  132. if not options['unsorted'] and format_style != 'json':
  133. views = sorted(views)
  134. if format_style == 'aligned':
  135. views = [row.split(',', 3) for row in views]
  136. widths = [len(max(columns, key=len)) for columns in zip(*views)]
  137. views = [
  138. ' '.join('{0:<{1}}'.format(cdata, width) for width, cdata in zip(widths, row))
  139. for row in views
  140. ]
  141. elif format_style == 'table':
  142. # Reformat all data and show in a table format
  143. views = [row.split(',', 3) for row in views]
  144. widths = [len(max(columns, key=len)) for columns in zip(*views)]
  145. table_views = []
  146. header = (style.MODULE_NAME('URL'), style.MODULE_NAME('Module'), style.MODULE_NAME('Name'), style.MODULE_NAME('Decorator'))
  147. table_views.append(
  148. ' | '.join('{0:<{1}}'.format(title, width) for width, title in zip(widths, header))
  149. )
  150. table_views.append('-+-'.join('-' * width for width in widths))
  151. for row in views:
  152. table_views.append(
  153. ' | '.join('{0:<{1}}'.format(cdata, width) for width, cdata in zip(widths, row))
  154. )
  155. # Replace original views so we can return the same object
  156. views = table_views
  157. elif format_style == 'json':
  158. if pretty_json:
  159. return json.dumps(views, indent=4)
  160. return json.dumps(views)
  161. return "\n".join([v for v in views]) + "\n"
  162. def extract_views_from_urlpatterns(self, urlpatterns, base='', namespace=None):
  163. """
  164. Return a list of views from a list of urlpatterns.
  165. Each object in the returned list is a three-tuple: (view_func, regex, name)
  166. """
  167. views = []
  168. for p in urlpatterns:
  169. if isinstance(p, (URLPattern, RegexURLPattern)):
  170. try:
  171. if not p.name:
  172. name = p.name
  173. elif namespace:
  174. name = '{0}:{1}'.format(namespace, p.name)
  175. else:
  176. name = p.name
  177. pattern = describe_pattern(p)
  178. views.append((p.callback, base + pattern, name))
  179. except ViewDoesNotExist:
  180. continue
  181. elif isinstance(p, (URLResolver, RegexURLResolver)):
  182. try:
  183. patterns = p.url_patterns
  184. except ImportError:
  185. continue
  186. if namespace and p.namespace:
  187. _namespace = '{0}:{1}'.format(namespace, p.namespace)
  188. else:
  189. _namespace = (p.namespace or namespace)
  190. pattern = describe_pattern(p)
  191. if isinstance(p, LocaleRegexURLResolver):
  192. for language in self.LANGUAGES:
  193. with translation.override(language[0]):
  194. views.extend(self.extract_views_from_urlpatterns(patterns, base + pattern, namespace=_namespace))
  195. else:
  196. views.extend(self.extract_views_from_urlpatterns(patterns, base + pattern, namespace=_namespace))
  197. elif hasattr(p, '_get_callback'):
  198. try:
  199. views.append((p._get_callback(), base + describe_pattern(p), p.name))
  200. except ViewDoesNotExist:
  201. continue
  202. elif hasattr(p, 'url_patterns') or hasattr(p, '_get_url_patterns'):
  203. try:
  204. patterns = p.url_patterns
  205. except ImportError:
  206. continue
  207. views.extend(self.extract_views_from_urlpatterns(patterns, base + describe_pattern(p), namespace=namespace))
  208. else:
  209. raise TypeError("%s does not appear to be a urlpattern object" % p)
  210. return views