codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import typing as t4 5from jinja2.loaders import BaseLoader6from werkzeug.routing import RequestRedirect7 8from .blueprints import Blueprint9from .globals import request_ctx10from .sansio.app import App11 12if t.TYPE_CHECKING:13 from .sansio.scaffold import Scaffold14 from .wrappers import Request15 16 17class UnexpectedUnicodeError(AssertionError, UnicodeError):18 """Raised in places where we want some better error reporting for19 unexpected unicode or binary data.20 """21 22 23class DebugFilesKeyError(KeyError, AssertionError):24 """Raised from request.files during debugging. The idea is that it can25 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.msg48 49 50class FormDataRoutingRedirect(AssertionError):51 """This exception is raised in debug mode if a routing redirect52 would cause the browser to drop the method or body. This happens53 when method is not GET, HEAD or OPTIONS and the status code is not54 307 or 308.55 """56 57 def __init__(self, request: Request) -> None:58 exc = request.routing_exception59 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 81def attach_enctype_error_multidict(request: Request) -> None:82 """Patch ``request.files.__getitem__`` to raise a descriptive error83 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 raise97 98 raise DebugFilesKeyError(request, key).with_traceback(99 e.__traceback__100 ) from None101 102 newcls.__name__ = oldcls.__name__103 newcls.__module__ = oldcls.__module__104 request.files.__class__ = newcls105 106 107def _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 continue112 if isinstance(value, (tuple, list)):113 if not all(isinstance(x, str) for x in value):114 continue115 yield f"{key}:"116 for item in value:117 yield f" - {item}"118 continue119 elif not isinstance(value, (str, int, float, bool)):120 continue121 yield f"{key}: {value!r}"122 123 124def 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 = 0138 blueprint = None139 if request_ctx and request_ctx.request.blueprint is not None:140 blueprint = request_ctx.request.blueprint141 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 += 1160 info.append(f" -> {detail}")161 162 seems_fishy = False163 if total_found == 0:164 info.append("Error: the template could not be found.")165 seems_fishy = True166 elif total_found > 1:167 info.append("Warning: multiple loaders returned a match for the template.")168 seems_fishy = True169 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 