Format: text/x-python
⬇ Download File (5.9 KB)
   1  from __future__ import annotations
   2  
   3  import typing as t
   4  
   5  from jinja2.loaders import BaseLoader
   6  from werkzeug.routing import RequestRedirect
   7  
   8  from .blueprints import Blueprint
   9  from .globals import request_ctx
  10  from .sansio.app import App
  11  
  12  if t.TYPE_CHECKING:
  13      from .sansio.scaffold import Scaffold
  14      from .wrappers import Request
  15  
  16  
  17  class UnexpectedUnicodeError(AssertionError, UnicodeError):
  18      """Raised in places where we want some better error reporting for
  19      unexpected unicode or binary data.
  20      """
  21  
  22  
  23  class DebugFilesKeyError(KeyError, AssertionError):
  24      """Raised from request.files during debugging.  The idea is that it can
  25      provide a better error message than just a generic KeyError/BadRequest.
  26      """
  27  
  28      def __init__(self, request: Request, key: str) -> None:
  29          form_matches = request.form.getlist(key)
  30          buf = [
  31              f"You tried to access the file {key!r} in the request.files"
  32              " dictionary but it does not exist. The mimetype for the"
  33              f" request is {request.mimetype!r} instead of"
  34              " 'multipart/form-data' which means that no file contents"
  35              " were transmitted. To fix this error you should provide"
  36              ' enctype="multipart/form-data" in your form.'
  37          ]
  38          if form_matches:
  39              names = ", ".join(repr(x) for x in form_matches)
  40              buf.append(
  41                  "\n\nThe browser instead transmitted some file names. "
  42                  f"This was submitted: {names}"
  43              )
  44          self.msg = "".join(buf)
  45  
  46      def __str__(self) -> str:
  47          return self.msg
  48  
  49  
  50  class FormDataRoutingRedirect(AssertionError):
  51      """This exception is raised in debug mode if a routing redirect
  52      would cause the browser to drop the method or body. This happens
  53      when method is not GET, HEAD or OPTIONS and the status code is not
  54      307 or 308.
  55      """
  56  
  57      def __init__(self, request: Request) -> None:
  58          exc = request.routing_exception
  59          assert isinstance(exc, RequestRedirect)
  60          buf = [
  61              f"A request was sent to '{request.url}', but routing issued"
  62              f" a redirect to the canonical URL '{exc.new_url}'."
  63          ]
  64  
  65          if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
  66              buf.append(
  67                  " The URL was defined with a trailing slash. Flask"
  68                  " will redirect to the URL with a trailing slash if it"
  69                  " was accessed without one."
  70              )
  71  
  72          buf.append(
  73              " Send requests to the canonical URL, or use 307 or 308 for"
  74              " routing redirects. Otherwise, browsers will drop form"
  75              " data.\n\n"
  76              "This exception is only raised in debug mode."
  77          )
  78          super().__init__("".join(buf))
  79  
  80  
  81  def attach_enctype_error_multidict(request: Request) -> None:
  82      """Patch ``request.files.__getitem__`` to raise a descriptive error
  83      about ``enctype=multipart/form-data``.
  84  
  85      :param request: The request to patch.
  86      :meta private:
  87      """
  88      oldcls = request.files.__class__
  89  
  90      class newcls(oldcls):  # type: ignore[valid-type, misc]
  91          def __getitem__(self, key: str) -> t.Any:
  92              try:
  93                  return super().__getitem__(key)
  94              except KeyError as e:
  95                  if key not in request.form:
  96                      raise
  97  
  98                  raise DebugFilesKeyError(request, key).with_traceback(
  99                      e.__traceback__
 100                  ) from None
 101  
 102      newcls.__name__ = oldcls.__name__
 103      newcls.__module__ = oldcls.__module__
 104      request.files.__class__ = newcls
 105  
 106  
 107  def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]:
 108      yield f"class: {type(loader).__module__}.{type(loader).__name__}"
 109      for key, value in sorted(loader.__dict__.items()):
 110          if key.startswith("_"):
 111              continue
 112          if isinstance(value, (tuple, list)):
 113              if not all(isinstance(x, str) for x in value):
 114                  continue
 115              yield f"{key}:"
 116              for item in value:
 117                  yield f"  - {item}"
 118              continue
 119          elif not isinstance(value, (str, int, float, bool)):
 120              continue
 121          yield f"{key}: {value!r}"
 122  
 123  
 124  def explain_template_loading_attempts(
 125      app: App,
 126      template: str,
 127      attempts: list[
 128          tuple[
 129              BaseLoader,
 130              Scaffold,
 131              tuple[str, str | None, t.Callable[[], bool] | None] | None,
 132          ]
 133      ],
 134  ) -> None:
 135      """This should help developers understand what failed"""
 136      info = [f"Locating template {template!r}:"]
 137      total_found = 0
 138      blueprint = None
 139      if request_ctx and request_ctx.request.blueprint is not None:
 140          blueprint = request_ctx.request.blueprint
 141  
 142      for idx, (loader, srcobj, triple) in enumerate(attempts):
 143          if isinstance(srcobj, App):
 144              src_info = f"application {srcobj.import_name!r}"
 145          elif isinstance(srcobj, Blueprint):
 146              src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})"
 147          else:
 148              src_info = repr(srcobj)
 149  
 150          info.append(f"{idx + 1:5}: trying loader of {src_info}")
 151  
 152          for line in _dump_loader_info(loader):
 153              info.append(f"       {line}")
 154  
 155          if triple is None:
 156              detail = "no match"
 157          else:
 158              detail = f"found ({triple[1] or '<string>'!r})"
 159              total_found += 1
 160          info.append(f"       -> {detail}")
 161  
 162      seems_fishy = False
 163      if total_found == 0:
 164          info.append("Error: the template could not be found.")
 165          seems_fishy = True
 166      elif total_found > 1:
 167          info.append("Warning: multiple loaders returned a match for the template.")
 168          seems_fishy = True
 169  
 170      if blueprint is not None and seems_fishy:
 171          info.append(
 172              "  The template was looked up from an endpoint that belongs"
 173              f" to the blueprint {blueprint!r}."
 174          )
 175          info.append("  Maybe you did not place a template in the right folder?")
 176          info.append("  See https://flask.palletsprojects.com/blueprints/#templates")
 177  
 178      app.logger.info("\n".join(info))
 179