scaffold.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. from __future__ import annotations
  2. import importlib.util
  3. import os
  4. import pathlib
  5. import sys
  6. import typing as t
  7. from collections import defaultdict
  8. from functools import update_wrapper
  9. from jinja2 import BaseLoader
  10. from jinja2 import FileSystemLoader
  11. from werkzeug.exceptions import default_exceptions
  12. from werkzeug.exceptions import HTTPException
  13. from werkzeug.utils import cached_property
  14. from .. import typing as ft
  15. from ..helpers import get_root_path
  16. from ..templating import _default_template_ctx_processor
  17. if t.TYPE_CHECKING: # pragma: no cover
  18. from click import Group
  19. # a singleton sentinel value for parameter defaults
  20. _sentinel = object()
  21. F = t.TypeVar("F", bound=t.Callable[..., t.Any])
  22. T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any])
  23. T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable)
  24. T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable)
  25. T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable)
  26. T_template_context_processor = t.TypeVar(
  27. "T_template_context_processor", bound=ft.TemplateContextProcessorCallable
  28. )
  29. T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable)
  30. T_url_value_preprocessor = t.TypeVar(
  31. "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable
  32. )
  33. T_route = t.TypeVar("T_route", bound=ft.RouteCallable)
  34. def setupmethod(f: F) -> F:
  35. f_name = f.__name__
  36. def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any:
  37. self._check_setup_finished(f_name)
  38. return f(self, *args, **kwargs)
  39. return t.cast(F, update_wrapper(wrapper_func, f))
  40. class Scaffold:
  41. """Common behavior shared between :class:`~flask.Flask` and
  42. :class:`~flask.blueprints.Blueprint`.
  43. :param import_name: The import name of the module where this object
  44. is defined. Usually :attr:`__name__` should be used.
  45. :param static_folder: Path to a folder of static files to serve.
  46. If this is set, a static route will be added.
  47. :param static_url_path: URL prefix for the static route.
  48. :param template_folder: Path to a folder containing template files.
  49. for rendering. If this is set, a Jinja loader will be added.
  50. :param root_path: The path that static, template, and resource files
  51. are relative to. Typically not set, it is discovered based on
  52. the ``import_name``.
  53. .. versionadded:: 2.0
  54. """
  55. cli: Group
  56. name: str
  57. _static_folder: str | None = None
  58. _static_url_path: str | None = None
  59. def __init__(
  60. self,
  61. import_name: str,
  62. static_folder: str | os.PathLike[str] | None = None,
  63. static_url_path: str | None = None,
  64. template_folder: str | os.PathLike[str] | None = None,
  65. root_path: str | None = None,
  66. ):
  67. #: The name of the package or module that this object belongs
  68. #: to. Do not change this once it is set by the constructor.
  69. self.import_name = import_name
  70. self.static_folder = static_folder # type: ignore
  71. self.static_url_path = static_url_path
  72. #: The path to the templates folder, relative to
  73. #: :attr:`root_path`, to add to the template loader. ``None`` if
  74. #: templates should not be added.
  75. self.template_folder = template_folder
  76. if root_path is None:
  77. root_path = get_root_path(self.import_name)
  78. #: Absolute path to the package on the filesystem. Used to look
  79. #: up resources contained in the package.
  80. self.root_path = root_path
  81. #: A dictionary mapping endpoint names to view functions.
  82. #:
  83. #: To register a view function, use the :meth:`route` decorator.
  84. #:
  85. #: This data structure is internal. It should not be modified
  86. #: directly and its format may change at any time.
  87. self.view_functions: dict[str, ft.RouteCallable] = {}
  88. #: A data structure of registered error handlers, in the format
  89. #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is
  90. #: the name of a blueprint the handlers are active for, or
  91. #: ``None`` for all requests. The ``code`` key is the HTTP
  92. #: status code for ``HTTPException``, or ``None`` for
  93. #: other exceptions. The innermost dictionary maps exception
  94. #: classes to handler functions.
  95. #:
  96. #: To register an error handler, use the :meth:`errorhandler`
  97. #: decorator.
  98. #:
  99. #: This data structure is internal. It should not be modified
  100. #: directly and its format may change at any time.
  101. self.error_handler_spec: dict[
  102. ft.AppOrBlueprintKey,
  103. dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]],
  104. ] = defaultdict(lambda: defaultdict(dict))
  105. #: A data structure of functions to call at the beginning of
  106. #: each request, in the format ``{scope: [functions]}``. The
  107. #: ``scope`` key is the name of a blueprint the functions are
  108. #: active for, or ``None`` for all requests.
  109. #:
  110. #: To register a function, use the :meth:`before_request`
  111. #: decorator.
  112. #:
  113. #: This data structure is internal. It should not be modified
  114. #: directly and its format may change at any time.
  115. self.before_request_funcs: dict[
  116. ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable]
  117. ] = defaultdict(list)
  118. #: A data structure of functions to call at the end of each
  119. #: request, in the format ``{scope: [functions]}``. The
  120. #: ``scope`` key is the name of a blueprint the functions are
  121. #: active for, or ``None`` for all requests.
  122. #:
  123. #: To register a function, use the :meth:`after_request`
  124. #: decorator.
  125. #:
  126. #: This data structure is internal. It should not be modified
  127. #: directly and its format may change at any time.
  128. self.after_request_funcs: dict[
  129. ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]]
  130. ] = defaultdict(list)
  131. #: A data structure of functions to call at the end of each
  132. #: request even if an exception is raised, in the format
  133. #: ``{scope: [functions]}``. The ``scope`` key is the name of a
  134. #: blueprint the functions are active for, or ``None`` for all
  135. #: requests.
  136. #:
  137. #: To register a function, use the :meth:`teardown_request`
  138. #: decorator.
  139. #:
  140. #: This data structure is internal. It should not be modified
  141. #: directly and its format may change at any time.
  142. self.teardown_request_funcs: dict[
  143. ft.AppOrBlueprintKey, list[ft.TeardownCallable]
  144. ] = defaultdict(list)
  145. #: A data structure of functions to call to pass extra context
  146. #: values when rendering templates, in the format
  147. #: ``{scope: [functions]}``. The ``scope`` key is the name of a
  148. #: blueprint the functions are active for, or ``None`` for all
  149. #: requests.
  150. #:
  151. #: To register a function, use the :meth:`context_processor`
  152. #: decorator.
  153. #:
  154. #: This data structure is internal. It should not be modified
  155. #: directly and its format may change at any time.
  156. self.template_context_processors: dict[
  157. ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable]
  158. ] = defaultdict(list, {None: [_default_template_ctx_processor]})
  159. #: A data structure of functions to call to modify the keyword
  160. #: arguments passed to the view function, in the format
  161. #: ``{scope: [functions]}``. The ``scope`` key is the name of a
  162. #: blueprint the functions are active for, or ``None`` for all
  163. #: requests.
  164. #:
  165. #: To register a function, use the
  166. #: :meth:`url_value_preprocessor` decorator.
  167. #:
  168. #: This data structure is internal. It should not be modified
  169. #: directly and its format may change at any time.
  170. self.url_value_preprocessors: dict[
  171. ft.AppOrBlueprintKey,
  172. list[ft.URLValuePreprocessorCallable],
  173. ] = defaultdict(list)
  174. #: A data structure of functions to call to modify the keyword
  175. #: arguments when generating URLs, in the format
  176. #: ``{scope: [functions]}``. The ``scope`` key is the name of a
  177. #: blueprint the functions are active for, or ``None`` for all
  178. #: requests.
  179. #:
  180. #: To register a function, use the :meth:`url_defaults`
  181. #: decorator.
  182. #:
  183. #: This data structure is internal. It should not be modified
  184. #: directly and its format may change at any time.
  185. self.url_default_functions: dict[
  186. ft.AppOrBlueprintKey, list[ft.URLDefaultCallable]
  187. ] = defaultdict(list)
  188. def __repr__(self) -> str:
  189. return f"<{type(self).__name__} {self.name!r}>"
  190. def _check_setup_finished(self, f_name: str) -> None:
  191. raise NotImplementedError
  192. @property
  193. def static_folder(self) -> str | None:
  194. """The absolute path to the configured static folder. ``None``
  195. if no static folder is set.
  196. """
  197. if self._static_folder is not None:
  198. return os.path.join(self.root_path, self._static_folder)
  199. else:
  200. return None
  201. @static_folder.setter
  202. def static_folder(self, value: str | os.PathLike[str] | None) -> None:
  203. if value is not None:
  204. value = os.fspath(value).rstrip(r"\/")
  205. self._static_folder = value
  206. @property
  207. def has_static_folder(self) -> bool:
  208. """``True`` if :attr:`static_folder` is set.
  209. .. versionadded:: 0.5
  210. """
  211. return self.static_folder is not None
  212. @property
  213. def static_url_path(self) -> str | None:
  214. """The URL prefix that the static route will be accessible from.
  215. If it was not configured during init, it is derived from
  216. :attr:`static_folder`.
  217. """
  218. if self._static_url_path is not None:
  219. return self._static_url_path
  220. if self.static_folder is not None:
  221. basename = os.path.basename(self.static_folder)
  222. return f"/{basename}".rstrip("/")
  223. return None
  224. @static_url_path.setter
  225. def static_url_path(self, value: str | None) -> None:
  226. if value is not None:
  227. value = value.rstrip("/")
  228. self._static_url_path = value
  229. @cached_property
  230. def jinja_loader(self) -> BaseLoader | None:
  231. """The Jinja loader for this object's templates. By default this
  232. is a class :class:`jinja2.loaders.FileSystemLoader` to
  233. :attr:`template_folder` if it is set.
  234. .. versionadded:: 0.5
  235. """
  236. if self.template_folder is not None:
  237. return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
  238. else:
  239. return None
  240. def _method_route(
  241. self,
  242. method: str,
  243. rule: str,
  244. options: dict[str, t.Any],
  245. ) -> t.Callable[[T_route], T_route]:
  246. if "methods" in options:
  247. raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
  248. return self.route(rule, methods=[method], **options)
  249. @setupmethod
  250. def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  251. """Shortcut for :meth:`route` with ``methods=["GET"]``.
  252. .. versionadded:: 2.0
  253. """
  254. return self._method_route("GET", rule, options)
  255. @setupmethod
  256. def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  257. """Shortcut for :meth:`route` with ``methods=["POST"]``.
  258. .. versionadded:: 2.0
  259. """
  260. return self._method_route("POST", rule, options)
  261. @setupmethod
  262. def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  263. """Shortcut for :meth:`route` with ``methods=["PUT"]``.
  264. .. versionadded:: 2.0
  265. """
  266. return self._method_route("PUT", rule, options)
  267. @setupmethod
  268. def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  269. """Shortcut for :meth:`route` with ``methods=["DELETE"]``.
  270. .. versionadded:: 2.0
  271. """
  272. return self._method_route("DELETE", rule, options)
  273. @setupmethod
  274. def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  275. """Shortcut for :meth:`route` with ``methods=["PATCH"]``.
  276. .. versionadded:: 2.0
  277. """
  278. return self._method_route("PATCH", rule, options)
  279. @setupmethod
  280. def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
  281. """Decorate a view function to register it with the given URL
  282. rule and options. Calls :meth:`add_url_rule`, which has more
  283. details about the implementation.
  284. .. code-block:: python
  285. @app.route("/")
  286. def index():
  287. return "Hello, World!"
  288. See :ref:`url-route-registrations`.
  289. The endpoint name for the route defaults to the name of the view
  290. function if the ``endpoint`` parameter isn't passed.
  291. The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and
  292. ``OPTIONS`` are added automatically.
  293. :param rule: The URL rule string.
  294. :param options: Extra options passed to the
  295. :class:`~werkzeug.routing.Rule` object.
  296. """
  297. def decorator(f: T_route) -> T_route:
  298. endpoint = options.pop("endpoint", None)
  299. self.add_url_rule(rule, endpoint, f, **options)
  300. return f
  301. return decorator
  302. @setupmethod
  303. def add_url_rule(
  304. self,
  305. rule: str,
  306. endpoint: str | None = None,
  307. view_func: ft.RouteCallable | None = None,
  308. provide_automatic_options: bool | None = None,
  309. **options: t.Any,
  310. ) -> None:
  311. """Register a rule for routing incoming requests and building
  312. URLs. The :meth:`route` decorator is a shortcut to call this
  313. with the ``view_func`` argument. These are equivalent:
  314. .. code-block:: python
  315. @app.route("/")
  316. def index():
  317. ...
  318. .. code-block:: python
  319. def index():
  320. ...
  321. app.add_url_rule("/", view_func=index)
  322. See :ref:`url-route-registrations`.
  323. The endpoint name for the route defaults to the name of the view
  324. function if the ``endpoint`` parameter isn't passed. An error
  325. will be raised if a function has already been registered for the
  326. endpoint.
  327. The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is
  328. always added automatically, and ``OPTIONS`` is added
  329. automatically by default.
  330. ``view_func`` does not necessarily need to be passed, but if the
  331. rule should participate in routing an endpoint name must be
  332. associated with a view function at some point with the
  333. :meth:`endpoint` decorator.
  334. .. code-block:: python
  335. app.add_url_rule("/", endpoint="index")
  336. @app.endpoint("index")
  337. def index():
  338. ...
  339. If ``view_func`` has a ``required_methods`` attribute, those
  340. methods are added to the passed and automatic methods. If it
  341. has a ``provide_automatic_methods`` attribute, it is used as the
  342. default if the parameter is not passed.
  343. :param rule: The URL rule string.
  344. :param endpoint: The endpoint name to associate with the rule
  345. and view function. Used when routing and building URLs.
  346. Defaults to ``view_func.__name__``.
  347. :param view_func: The view function to associate with the
  348. endpoint name.
  349. :param provide_automatic_options: Add the ``OPTIONS`` method and
  350. respond to ``OPTIONS`` requests automatically.
  351. :param options: Extra options passed to the
  352. :class:`~werkzeug.routing.Rule` object.
  353. """
  354. raise NotImplementedError
  355. @setupmethod
  356. def endpoint(self, endpoint: str) -> t.Callable[[F], F]:
  357. """Decorate a view function to register it for the given
  358. endpoint. Used if a rule is added without a ``view_func`` with
  359. :meth:`add_url_rule`.
  360. .. code-block:: python
  361. app.add_url_rule("/ex", endpoint="example")
  362. @app.endpoint("example")
  363. def example():
  364. ...
  365. :param endpoint: The endpoint name to associate with the view
  366. function.
  367. """
  368. def decorator(f: F) -> F:
  369. self.view_functions[endpoint] = f
  370. return f
  371. return decorator
  372. @setupmethod
  373. def before_request(self, f: T_before_request) -> T_before_request:
  374. """Register a function to run before each request.
  375. For example, this can be used to open a database connection, or
  376. to load the logged in user from the session.
  377. .. code-block:: python
  378. @app.before_request
  379. def load_user():
  380. if "user_id" in session:
  381. g.user = db.session.get(session["user_id"])
  382. The function will be called without any arguments. If it returns
  383. a non-``None`` value, the value is handled as if it was the
  384. return value from the view, and further request handling is
  385. stopped.
  386. This is available on both app and blueprint objects. When used on an app, this
  387. executes before every request. When used on a blueprint, this executes before
  388. every request that the blueprint handles. To register with a blueprint and
  389. execute before every request, use :meth:`.Blueprint.before_app_request`.
  390. """
  391. self.before_request_funcs.setdefault(None, []).append(f)
  392. return f
  393. @setupmethod
  394. def after_request(self, f: T_after_request) -> T_after_request:
  395. """Register a function to run after each request to this object.
  396. The function is called with the response object, and must return
  397. a response object. This allows the functions to modify or
  398. replace the response before it is sent.
  399. If a function raises an exception, any remaining
  400. ``after_request`` functions will not be called. Therefore, this
  401. should not be used for actions that must execute, such as to
  402. close resources. Use :meth:`teardown_request` for that.
  403. This is available on both app and blueprint objects. When used on an app, this
  404. executes after every request. When used on a blueprint, this executes after
  405. every request that the blueprint handles. To register with a blueprint and
  406. execute after every request, use :meth:`.Blueprint.after_app_request`.
  407. """
  408. self.after_request_funcs.setdefault(None, []).append(f)
  409. return f
  410. @setupmethod
  411. def teardown_request(self, f: T_teardown) -> T_teardown:
  412. """Register a function to be called when the request context is
  413. popped. Typically this happens at the end of each request, but
  414. contexts may be pushed manually as well during testing.
  415. .. code-block:: python
  416. with app.test_request_context():
  417. ...
  418. When the ``with`` block exits (or ``ctx.pop()`` is called), the
  419. teardown functions are called just before the request context is
  420. made inactive.
  421. When a teardown function was called because of an unhandled
  422. exception it will be passed an error object. If an
  423. :meth:`errorhandler` is registered, it will handle the exception
  424. and the teardown will not receive it.
  425. Teardown functions must avoid raising exceptions. If they
  426. execute code that might fail they must surround that code with a
  427. ``try``/``except`` block and log any errors.
  428. The return values of teardown functions are ignored.
  429. This is available on both app and blueprint objects. When used on an app, this
  430. executes after every request. When used on a blueprint, this executes after
  431. every request that the blueprint handles. To register with a blueprint and
  432. execute after every request, use :meth:`.Blueprint.teardown_app_request`.
  433. """
  434. self.teardown_request_funcs.setdefault(None, []).append(f)
  435. return f
  436. @setupmethod
  437. def context_processor(
  438. self,
  439. f: T_template_context_processor,
  440. ) -> T_template_context_processor:
  441. """Registers a template context processor function. These functions run before
  442. rendering a template. The keys of the returned dict are added as variables
  443. available in the template.
  444. This is available on both app and blueprint objects. When used on an app, this
  445. is called for every rendered template. When used on a blueprint, this is called
  446. for templates rendered from the blueprint's views. To register with a blueprint
  447. and affect every template, use :meth:`.Blueprint.app_context_processor`.
  448. """
  449. self.template_context_processors[None].append(f)
  450. return f
  451. @setupmethod
  452. def url_value_preprocessor(
  453. self,
  454. f: T_url_value_preprocessor,
  455. ) -> T_url_value_preprocessor:
  456. """Register a URL value preprocessor function for all view
  457. functions in the application. These functions will be called before the
  458. :meth:`before_request` functions.
  459. The function can modify the values captured from the matched url before
  460. they are passed to the view. For example, this can be used to pop a
  461. common language code value and place it in ``g`` rather than pass it to
  462. every view.
  463. The function is passed the endpoint name and values dict. The return
  464. value is ignored.
  465. This is available on both app and blueprint objects. When used on an app, this
  466. is called for every request. When used on a blueprint, this is called for
  467. requests that the blueprint handles. To register with a blueprint and affect
  468. every request, use :meth:`.Blueprint.app_url_value_preprocessor`.
  469. """
  470. self.url_value_preprocessors[None].append(f)
  471. return f
  472. @setupmethod
  473. def url_defaults(self, f: T_url_defaults) -> T_url_defaults:
  474. """Callback function for URL defaults for all view functions of the
  475. application. It's called with the endpoint and values and should
  476. update the values passed in place.
  477. This is available on both app and blueprint objects. When used on an app, this
  478. is called for every request. When used on a blueprint, this is called for
  479. requests that the blueprint handles. To register with a blueprint and affect
  480. every request, use :meth:`.Blueprint.app_url_defaults`.
  481. """
  482. self.url_default_functions[None].append(f)
  483. return f
  484. @setupmethod
  485. def errorhandler(
  486. self, code_or_exception: type[Exception] | int
  487. ) -> t.Callable[[T_error_handler], T_error_handler]:
  488. """Register a function to handle errors by code or exception class.
  489. A decorator that is used to register a function given an
  490. error code. Example::
  491. @app.errorhandler(404)
  492. def page_not_found(error):
  493. return 'This page does not exist', 404
  494. You can also register handlers for arbitrary exceptions::
  495. @app.errorhandler(DatabaseError)
  496. def special_exception_handler(error):
  497. return 'Database connection failed', 500
  498. This is available on both app and blueprint objects. When used on an app, this
  499. can handle errors from every request. When used on a blueprint, this can handle
  500. errors from requests that the blueprint handles. To register with a blueprint
  501. and affect every request, use :meth:`.Blueprint.app_errorhandler`.
  502. .. versionadded:: 0.7
  503. Use :meth:`register_error_handler` instead of modifying
  504. :attr:`error_handler_spec` directly, for application wide error
  505. handlers.
  506. .. versionadded:: 0.7
  507. One can now additionally also register custom exception types
  508. that do not necessarily have to be a subclass of the
  509. :class:`~werkzeug.exceptions.HTTPException` class.
  510. :param code_or_exception: the code as integer for the handler, or
  511. an arbitrary exception
  512. """
  513. def decorator(f: T_error_handler) -> T_error_handler:
  514. self.register_error_handler(code_or_exception, f)
  515. return f
  516. return decorator
  517. @setupmethod
  518. def register_error_handler(
  519. self,
  520. code_or_exception: type[Exception] | int,
  521. f: ft.ErrorHandlerCallable,
  522. ) -> None:
  523. """Alternative error attach function to the :meth:`errorhandler`
  524. decorator that is more straightforward to use for non decorator
  525. usage.
  526. .. versionadded:: 0.7
  527. """
  528. exc_class, code = self._get_exc_class_and_code(code_or_exception)
  529. self.error_handler_spec[None][code][exc_class] = f
  530. @staticmethod
  531. def _get_exc_class_and_code(
  532. exc_class_or_code: type[Exception] | int,
  533. ) -> tuple[type[Exception], int | None]:
  534. """Get the exception class being handled. For HTTP status codes
  535. or ``HTTPException`` subclasses, return both the exception and
  536. status code.
  537. :param exc_class_or_code: Any exception class, or an HTTP status
  538. code as an integer.
  539. """
  540. exc_class: type[Exception]
  541. if isinstance(exc_class_or_code, int):
  542. try:
  543. exc_class = default_exceptions[exc_class_or_code]
  544. except KeyError:
  545. raise ValueError(
  546. f"'{exc_class_or_code}' is not a recognized HTTP"
  547. " error code. Use a subclass of HTTPException with"
  548. " that code instead."
  549. ) from None
  550. else:
  551. exc_class = exc_class_or_code
  552. if isinstance(exc_class, Exception):
  553. raise TypeError(
  554. f"{exc_class!r} is an instance, not a class. Handlers"
  555. " can only be registered for Exception classes or HTTP"
  556. " error codes."
  557. )
  558. if not issubclass(exc_class, Exception):
  559. raise ValueError(
  560. f"'{exc_class.__name__}' is not a subclass of Exception."
  561. " Handlers can only be registered for Exception classes"
  562. " or HTTP error codes."
  563. )
  564. if issubclass(exc_class, HTTPException):
  565. return exc_class, exc_class.code
  566. else:
  567. return exc_class, None
  568. def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str:
  569. """Internal helper that returns the default endpoint for a given
  570. function. This always is the function name.
  571. """
  572. assert view_func is not None, "expected view func if endpoint is not provided."
  573. return view_func.__name__
  574. def _find_package_path(import_name: str) -> str:
  575. """Find the path that contains the package or module."""
  576. root_mod_name, _, _ = import_name.partition(".")
  577. try:
  578. root_spec = importlib.util.find_spec(root_mod_name)
  579. if root_spec is None:
  580. raise ValueError("not found")
  581. except (ImportError, ValueError):
  582. # ImportError: the machinery told us it does not exist
  583. # ValueError:
  584. # - the module name was invalid
  585. # - the module name is __main__
  586. # - we raised `ValueError` due to `root_spec` being `None`
  587. return os.getcwd()
  588. if root_spec.submodule_search_locations:
  589. if root_spec.origin is None or root_spec.origin == "namespace":
  590. # namespace package
  591. package_spec = importlib.util.find_spec(import_name)
  592. if package_spec is not None and package_spec.submodule_search_locations:
  593. # Pick the path in the namespace that contains the submodule.
  594. package_path = pathlib.Path(
  595. os.path.commonpath(package_spec.submodule_search_locations)
  596. )
  597. search_location = next(
  598. location
  599. for location in root_spec.submodule_search_locations
  600. if package_path.is_relative_to(location)
  601. )
  602. else:
  603. # Pick the first path.
  604. search_location = root_spec.submodule_search_locations[0]
  605. return os.path.dirname(search_location)
  606. else:
  607. # package with __init__.py
  608. return os.path.dirname(os.path.dirname(root_spec.origin))
  609. else:
  610. # module
  611. return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value]
  612. def find_package(import_name: str) -> tuple[str | None, str]:
  613. """Find the prefix that a package is installed under, and the path
  614. that it would be imported from.
  615. The prefix is the directory containing the standard directory
  616. hierarchy (lib, bin, etc.). If the package is not installed to the
  617. system (:attr:`sys.prefix`) or a virtualenv (``site-packages``),
  618. ``None`` is returned.
  619. The path is the entry in :attr:`sys.path` that contains the package
  620. for import. If the package is not installed, it's assumed that the
  621. package was imported from the current working directory.
  622. """
  623. package_path = _find_package_path(import_name)
  624. py_prefix = os.path.abspath(sys.prefix)
  625. # installed to the system
  626. if pathlib.PurePath(package_path).is_relative_to(py_prefix):
  627. return py_prefix, package_path
  628. site_parent, site_folder = os.path.split(package_path)
  629. # installed to a virtualenv
  630. if site_folder.lower() == "site-packages":
  631. parent, folder = os.path.split(site_parent)
  632. # Windows (prefix/lib/site-packages)
  633. if folder.lower() == "lib":
  634. return parent, package_path
  635. # Unix (prefix/lib/pythonX.Y/site-packages)
  636. if os.path.basename(parent).lower() == "lib":
  637. return os.path.dirname(parent), package_path
  638. # something else (prefix/site-packages)
  639. return site_parent, package_path
  640. # not installed
  641. return None, package_path