compat.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. """
  2. The `compat` module provides support for backwards compatibility with older
  3. versions of Django/Python, and compatibility wrappers around optional packages.
  4. """
  5. import django
  6. from django.conf import settings
  7. from django.views.generic import View
  8. def unicode_http_header(value):
  9. # Coerce HTTP header value to unicode.
  10. if isinstance(value, bytes):
  11. return value.decode('iso-8859-1')
  12. return value
  13. def distinct(queryset, base):
  14. if settings.DATABASES[queryset.db]["ENGINE"] == "django.db.backends.oracle":
  15. # distinct analogue for Oracle users
  16. return base.filter(pk__in=set(queryset.values_list('pk', flat=True)))
  17. return queryset.distinct()
  18. # django.contrib.postgres requires psycopg2
  19. try:
  20. from django.contrib.postgres import fields as postgres_fields
  21. except ImportError:
  22. postgres_fields = None
  23. # coreapi is required for CoreAPI schema generation
  24. try:
  25. import coreapi
  26. except ImportError:
  27. coreapi = None
  28. # uritemplate is required for OpenAPI and CoreAPI schema generation
  29. try:
  30. import uritemplate
  31. except ImportError:
  32. uritemplate = None
  33. # coreschema is optional
  34. try:
  35. import coreschema
  36. except ImportError:
  37. coreschema = None
  38. # pyyaml is optional
  39. try:
  40. import yaml
  41. except ImportError:
  42. yaml = None
  43. # requests is optional
  44. try:
  45. import requests
  46. except ImportError:
  47. requests = None
  48. # PATCH method is not implemented by Django
  49. if 'patch' not in View.http_method_names:
  50. View.http_method_names = View.http_method_names + ['patch']
  51. # Markdown is optional (version 3.0+ required)
  52. try:
  53. import markdown
  54. HEADERID_EXT_PATH = 'markdown.extensions.toc'
  55. LEVEL_PARAM = 'baselevel'
  56. def apply_markdown(text):
  57. """
  58. Simple wrapper around :func:`markdown.markdown` to set the base level
  59. of '#' style headers to <h2>.
  60. """
  61. extensions = [HEADERID_EXT_PATH]
  62. extension_configs = {
  63. HEADERID_EXT_PATH: {
  64. LEVEL_PARAM: '2'
  65. }
  66. }
  67. md = markdown.Markdown(
  68. extensions=extensions, extension_configs=extension_configs
  69. )
  70. md_filter_add_syntax_highlight(md)
  71. return md.convert(text)
  72. except ImportError:
  73. apply_markdown = None
  74. markdown = None
  75. try:
  76. import pygments
  77. from pygments.formatters import HtmlFormatter
  78. from pygments.lexers import TextLexer, get_lexer_by_name
  79. def pygments_highlight(text, lang, style):
  80. lexer = get_lexer_by_name(lang, stripall=False)
  81. formatter = HtmlFormatter(nowrap=True, style=style)
  82. return pygments.highlight(text, lexer, formatter)
  83. def pygments_css(style):
  84. formatter = HtmlFormatter(style=style)
  85. return formatter.get_style_defs('.highlight')
  86. except ImportError:
  87. pygments = None
  88. def pygments_highlight(text, lang, style):
  89. return text
  90. def pygments_css(style):
  91. return None
  92. if markdown is not None and pygments is not None:
  93. # starting from this blogpost and modified to support current markdown extensions API
  94. # https://zerokspot.com/weblog/2008/06/18/syntax-highlighting-in-markdown-with-pygments/
  95. import re
  96. from markdown.preprocessors import Preprocessor
  97. class CodeBlockPreprocessor(Preprocessor):
  98. pattern = re.compile(
  99. r'^\s*``` *([^\n]+)\n(.+?)^\s*```', re.M | re.S)
  100. formatter = HtmlFormatter()
  101. def run(self, lines):
  102. def repl(m):
  103. try:
  104. lexer = get_lexer_by_name(m.group(1))
  105. except (ValueError, NameError):
  106. lexer = TextLexer()
  107. code = m.group(2).replace('\t', ' ')
  108. code = pygments.highlight(code, lexer, self.formatter)
  109. code = code.replace('\n\n', '\n&nbsp;\n').replace('\n', '<br />').replace('\\@', '@')
  110. return '\n\n%s\n\n' % code
  111. ret = self.pattern.sub(repl, "\n".join(lines))
  112. return ret.split("\n")
  113. def md_filter_add_syntax_highlight(md):
  114. md.preprocessors.register(CodeBlockPreprocessor(), 'highlight', 40)
  115. return True
  116. else:
  117. def md_filter_add_syntax_highlight(md):
  118. return False
  119. if django.VERSION >= (4, 2):
  120. # Django 4.2+: use the stock parse_header_parameters function
  121. # Note: Django 4.1 also has an implementation of parse_header_parameters
  122. # which is slightly different from the one in 4.2, it needs
  123. # the compatibility shim as well.
  124. from django.utils.http import parse_header_parameters
  125. else:
  126. # Django <= 4.1: create a compatibility shim for parse_header_parameters
  127. from django.http.multipartparser import parse_header
  128. def parse_header_parameters(line):
  129. # parse_header works with bytes, but parse_header_parameters
  130. # works with strings. Call encode to convert the line to bytes.
  131. main_value_pair, params = parse_header(line.encode())
  132. return main_value_pair, {
  133. # parse_header will convert *some* values to string.
  134. # parse_header_parameters converts *all* values to string.
  135. # Make sure all values are converted by calling decode on
  136. # any remaining non-string values.
  137. k: v if isinstance(v, str) else v.decode()
  138. for k, v in params.items()
  139. }
  140. # `separators` argument to `json.dumps()` differs between 2.x and 3.x
  141. # See: https://bugs.python.org/issue22767
  142. SHORT_SEPARATORS = (',', ':')
  143. LONG_SEPARATORS = (', ', ': ')
  144. INDENT_SEPARATORS = (',', ': ')