scaffold.py 30 KB

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