fresco 3.0.0__tar.gz → 3.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {fresco-3.0.0 → fresco-3.9.0}/CHANGELOG.rst +95 -0
  2. {fresco-3.0.0/fresco.egg-info → fresco-3.9.0}/PKG-INFO +8 -12
  3. fresco-3.9.0/fresco/__init__.py +127 -0
  4. {fresco-3.0.0 → fresco-3.9.0}/fresco/cookie.py +0 -1
  5. {fresco-3.0.0 → fresco-3.9.0}/fresco/core.py +83 -48
  6. {fresco-3.0.0 → fresco-3.9.0}/fresco/decorators.py +7 -6
  7. fresco-3.9.0/fresco/defaults.py +1 -0
  8. {fresco-3.0.0 → fresco-3.9.0}/fresco/middleware.py +46 -26
  9. {fresco-3.0.0 → fresco-3.9.0}/fresco/multidict.py +67 -72
  10. fresco-3.9.0/fresco/options.py +536 -0
  11. {fresco-3.0.0 → fresco-3.9.0}/fresco/request.py +200 -70
  12. {fresco-3.0.0 → fresco-3.9.0}/fresco/requestcontext.py +19 -9
  13. {fresco-3.0.0 → fresco-3.9.0}/fresco/response.py +114 -159
  14. {fresco-3.0.0 → fresco-3.9.0}/fresco/routeargs.py +23 -9
  15. {fresco-3.0.0 → fresco-3.9.0}/fresco/routing.py +247 -99
  16. {fresco-3.0.0 → fresco-3.9.0}/fresco/static.py +1 -1
  17. {fresco-3.0.0 → fresco-3.9.0}/fresco/subrequests.py +67 -54
  18. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/fixtures.py +6 -0
  19. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_cookie.py +1 -3
  20. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_core.py +30 -56
  21. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_decorators.py +1 -3
  22. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_middleware.py +0 -4
  23. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_multidict.py +3 -5
  24. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_options.py +137 -10
  25. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_request.py +40 -22
  26. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_requestcontext.py +0 -1
  27. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_response.py +21 -6
  28. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_routeargs.py +0 -5
  29. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_routing.py +149 -51
  30. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_static.py +2 -3
  31. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_subrequests.py +53 -4
  32. fresco-3.9.0/fresco/tests/util/__init__.py +0 -0
  33. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/test_http.py +32 -46
  34. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/test_urls.py +25 -9
  35. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/test_wsgi.py +0 -2
  36. fresco-3.9.0/fresco/types.py +32 -0
  37. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/cache.py +2 -1
  38. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/common.py +1 -3
  39. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/contentencodings.py +3 -2
  40. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/http.py +164 -105
  41. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/io.py +3 -4
  42. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/urls.py +52 -18
  43. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/wsgi.py +26 -16
  44. {fresco-3.0.0 → fresco-3.9.0/fresco.egg-info}/PKG-INFO +8 -12
  45. {fresco-3.0.0 → fresco-3.9.0}/fresco.egg-info/SOURCES.txt +4 -3
  46. fresco-3.9.0/pyproject.toml +47 -0
  47. fresco-3.9.0/setup.cfg +4 -0
  48. fresco-3.0.0/fresco/__init__.py +0 -27
  49. fresco-3.0.0/fresco/options.py +0 -300
  50. fresco-3.0.0/fresco/typing.py +0 -11
  51. fresco-3.0.0/setup.cfg +0 -31
  52. fresco-3.0.0/setup.py +0 -17
  53. {fresco-3.0.0 → fresco-3.9.0}/LICENSE.txt +0 -0
  54. {fresco-3.0.0 → fresco-3.9.0}/MANIFEST.in +0 -0
  55. {fresco-3.0.0 → fresco-3.9.0}/README.rst +0 -0
  56. {fresco-3.0.0 → fresco-3.9.0}/fresco/exceptions.py +0 -0
  57. /fresco-3.0.0/fresco/tests/__init__.py → /fresco-3.9.0/fresco/py.typed +0 -0
  58. {fresco-3.0.0/fresco/tests/util → fresco-3.9.0/fresco/tests}/__init__.py +0 -0
  59. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/test_exceptions.py +0 -0
  60. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/form_data.py +0 -0
  61. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/test_common.py +0 -0
  62. {fresco-3.0.0 → fresco-3.9.0}/fresco/tests/util/test_security.py +0 -0
  63. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/__init__.py +0 -0
  64. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/file.py +0 -0
  65. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/object.py +0 -0
  66. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/security.py +0 -0
  67. {fresco-3.0.0 → fresco-3.9.0}/fresco/util/textproc.py +0 -0
  68. {fresco-3.0.0 → fresco-3.9.0}/fresco.egg-info/dependency_links.txt +0 -0
  69. {fresco-3.0.0 → fresco-3.9.0}/fresco.egg-info/top_level.txt +0 -0
@@ -1,6 +1,101 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ 3.9.0 (released 2026-02-02)
5
+ ---------------------------
6
+
7
+ - Bugfix: fix request.get and request.getint type annotations
8
+ - Allow multiple methods to be routed using kwargs, eg ``Route(GET_POST=my_view)``
9
+ - Add support for Python 3.13, 3.14 and drop support for Python 3.9
10
+
11
+ 3.8.0 (released 2025-07-17)
12
+ ---------------------------
13
+
14
+ - Add Response.bad_request ``message`` argument
15
+ - Add warnings when loading option files creates undefined keys
16
+
17
+ 3.7.0 (released 2025-05-05)
18
+ ---------------------------
19
+
20
+ - Add a ``fresco.util.url.add_query`` function
21
+
22
+ 3.6.0 (released 2025-05-03)
23
+ ---------------------------
24
+
25
+ - Replace ``fresco.DEFAULT_CHARSET`` with ``fresco.defaults.DEFAULT_CHARSET``
26
+ - Add a ``required`` argument to Request.get
27
+ - Add new request.get* functions: ``getfloat``, ``getbool`` and ``getdecimal``
28
+
29
+ 3.5.0 (released 2025-02-25)
30
+ ---------------------------
31
+
32
+ - Refactor middleware.XForwarded to clean up error tracebacks and add a new ``force_https`` option
33
+ - Add new ``fresco.options.override_options`` function to aid testing
34
+ - Automatically add a ``request`` argument where the routed function's first
35
+ positional parameter is annotated as type ``fresco.request.Request``.
36
+ view
37
+
38
+ 3.4.0 (released 2024-10-16)
39
+ ---------------------------
40
+
41
+ - Add new ``ALL`` shortcut method, allowing all HTTP methods to be routed to a
42
+ view
43
+
44
+ 3.3.4 (released 2024-08-06)
45
+ ---------------------------
46
+
47
+ - Bugfix: ensure option files are loaded in a well defined order
48
+
49
+ 3.3.3 (released 2024-05-01)
50
+ ---------------------------
51
+
52
+ - Bugfix: request.now returns the correct time on non-UTC systems
53
+ - Add support for Python 3.12 and drop support for 3.8
54
+
55
+ 3.3.2 (released 2023-05-31)
56
+ ---------------------------
57
+
58
+ - Bugfix: a fresco app routed through route_wsgi now receives a fresh request
59
+ and environ dict, isolated from the containing app.
60
+
61
+ 3.3.1 (released 2023-05-18)
62
+ ---------------------------
63
+
64
+ - The ``route_wsgi`` and ``route_all`` methods now only match at a
65
+ path separator boundary (``'/'``).
66
+ - Bugfix: subrequest now works with views routed via ``RRoute`` (and so expect
67
+ an initial ``request`` argument)
68
+
69
+ 3.3.0 (released 2023-05-02)
70
+ ----------------------------
71
+
72
+ - Options: fresco.options.Options can now take a list of paths as its first argument
73
+ - Options: tags can now be specified with environment variable substitutions
74
+ (eg "{FOO}" would load files tagged with the current value of the 'FOO'
75
+ environment variable)
76
+ - Options: add ``fresco.options.list_from_str`` and ``fresco.options.dict_from_options``
77
+
78
+ 3.2.0 (released 2023-04-16)
79
+ ---------------------------
80
+
81
+ - Bugfix: fixed cases where the close methods of WSGI content iterators were
82
+ not called, notably when using subrequests
83
+ - Bugfix: fix ``ResourceWarnings`` caused by unclosed temporary files
84
+ - Added ``route_class`` argument to ``RouteCollection.route``
85
+ - Modified ``RouteCollection.route_wsgi`` to allow it to take a string path to
86
+ the WSGI callable instead of the callable itself, making its signature more
87
+ consistent with ``RouteCollection.route``
88
+ - Added ``fresco.process_request_once``
89
+ - ``request.make_url`` and ``fresco.util.url.make_query`` now drop items from
90
+ the generated query string if the value is ``None``
91
+
92
+ 3.1.0 (released 2022-05-04)
93
+ ---------------------------
94
+
95
+ - Bugfix: passing URL paths to subrequests no longer raises an exception
96
+ - Added an ``_env`` argument to ``fresco.subrequest.subrequest`` to allow custom
97
+ WSGI environ keys to be passed to the subrequest
98
+
4
99
  3.0.0 (released 2022-05-02)
5
100
  ---------------------------
6
101
 
@@ -1,23 +1,21 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: fresco
3
- Version: 3.0.0
3
+ Version: 3.9.0
4
4
  Summary: A Web/WSGI micro-framework
5
- Home-page: https://ollycope.com/software/fresco/latest/
6
- Author: Oliver Cope
7
- Author-email: oliver@redgecko.org
8
- License: Apache
9
- Keywords: wsgi web www framework
10
- Platform: UNKNOWN
5
+ Author-email: Oliver Cope <oliver@redgecko.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://ollycope.com/software/fresco/latest/
8
+ Keywords: wsgi,web,www,framework
11
9
  Classifier: Development Status :: 5 - Production/Stable
12
10
  Classifier: Environment :: Web Environment
13
11
  Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: Apache Software License
15
12
  Classifier: Operating System :: OS Independent
16
13
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: Implementation :: PyPy
18
14
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
15
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
16
+ Description-Content-Type: text/x-rst
20
17
  License-File: LICENSE.txt
18
+ Dynamic: license-file
21
19
 
22
20
  Fresco, a web micro-framework for Python
23
21
  ========================================
@@ -46,5 +44,3 @@ Read the
46
44
  <https://ollycope.com/software/fresco/latest/>`_ for
47
45
  more about the framework, or
48
46
  visit the `source repo <https://sr.ht/~olly/fresco/>`_.
49
-
50
-
@@ -0,0 +1,127 @@
1
+ # Copyright 2015 Oliver Cope
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ from fresco.request import Request
16
+ from fresco.request import currentrequest
17
+ from fresco.requestcontext import context
18
+ from fresco.response import Response
19
+ from fresco.core import FrescoApp
20
+ from fresco.core import urlfor
21
+ from fresco.defaults import DEFAULT_CHARSET
22
+ from fresco.options import Options
23
+ from fresco.routing import Route
24
+ from fresco.routing import RRoute
25
+ from fresco.routing import RouteCollection
26
+ from fresco.routing import DelegateRoute
27
+ from fresco.routing import routefor
28
+ from fresco.routing import ALL_METHODS
29
+ from fresco.routing import GET
30
+ from fresco.routing import HEAD
31
+ from fresco.routing import POST
32
+ from fresco.routing import PUT
33
+ from fresco.routing import DELETE
34
+ from fresco.routing import OPTIONS
35
+ from fresco.routing import TRACE
36
+ from fresco.routing import CONNECT
37
+ from fresco.routing import VERSION_CONTROL
38
+ from fresco.routing import REPORT
39
+ from fresco.routing import CHECKOUT
40
+ from fresco.routing import CHECKIN
41
+ from fresco.routing import UNCHECKOUT
42
+ from fresco.routing import MKWORKSPACE
43
+ from fresco.routing import UPDATE
44
+ from fresco.routing import LABEL
45
+ from fresco.routing import MERGE
46
+ from fresco.routing import BASELINE_CONTROL
47
+ from fresco.routing import MKACTIVITY
48
+ from fresco.routing import ORDERPATCH
49
+ from fresco.routing import ACL
50
+ from fresco.routing import SEARCH
51
+ from fresco.routing import PATCH
52
+ from fresco.routeargs import routearg
53
+ from fresco.routeargs import FormArg
54
+ from fresco.routeargs import PostArg
55
+ from fresco.routeargs import QueryArg
56
+ from fresco.routeargs import GetArg
57
+ from fresco.routeargs import CookieArg
58
+ from fresco.routeargs import SessionArg
59
+ from fresco.routeargs import RequestObject
60
+ from fresco.routeargs import FormData
61
+ from fresco.routeargs import PostData
62
+ from fresco.routeargs import QueryData
63
+ from fresco.routeargs import GetData
64
+ from fresco.middleware import XForwarded
65
+ from fresco.subrequests import subrequest
66
+ from fresco.subrequests import subrequest_bytes
67
+ from fresco.subrequests import subrequest_raw
68
+ from fresco.util.common import object_or_404
69
+
70
+
71
+ __version__ = "3.9.0"
72
+ __all__ = [
73
+ "Request",
74
+ "currentrequest",
75
+ "context",
76
+ "Response",
77
+ "FrescoApp",
78
+ "urlfor",
79
+ "ALL_METHODS",
80
+ "DEFAULT_CHARSET",
81
+ "GET",
82
+ "HEAD",
83
+ "POST",
84
+ "PUT",
85
+ "DELETE",
86
+ "OPTIONS",
87
+ "TRACE",
88
+ "CONNECT",
89
+ "VERSION_CONTROL",
90
+ "REPORT",
91
+ "CHECKOUT",
92
+ "CHECKIN",
93
+ "UNCHECKOUT",
94
+ "MKWORKSPACE",
95
+ "UPDATE",
96
+ "LABEL",
97
+ "MERGE",
98
+ "BASELINE_CONTROL",
99
+ "MKACTIVITY",
100
+ "ORDERPATCH",
101
+ "ACL",
102
+ "SEARCH",
103
+ "PATCH",
104
+ "DelegateRoute",
105
+ "Options",
106
+ "Route",
107
+ "RouteCollection",
108
+ "RRoute",
109
+ "routearg",
110
+ "FormArg",
111
+ "PostArg",
112
+ "QueryArg",
113
+ "GetArg",
114
+ "CookieArg",
115
+ "SessionArg",
116
+ "RequestObject",
117
+ "FormData",
118
+ "PostData",
119
+ "QueryData",
120
+ "GetData",
121
+ "routefor",
122
+ "XForwarded",
123
+ "subrequest",
124
+ "subrequest_bytes",
125
+ "subrequest_raw",
126
+ "object_or_404",
127
+ ]
@@ -163,7 +163,6 @@ def parse_cookie_header(cookie_string, unquote=unquote):
163
163
  cookies = []
164
164
 
165
165
  for part in cookie_string.split(";"):
166
-
167
166
  try:
168
167
  k, v = part.strip().split("=", 1)
169
168
  except ValueError:
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  #
15
15
  from functools import partial
16
+ from functools import wraps
16
17
  from typing import Callable
17
18
  from typing import Dict
18
19
  from typing import List
@@ -20,6 +21,7 @@ from typing import Tuple
20
21
  from typing import Type
21
22
  from typing import Set
22
23
  from typing import Union
24
+ import typing as t
23
25
  import contextlib
24
26
  import logging
25
27
  import sys
@@ -31,7 +33,10 @@ from fresco.util.http import encode_multipart
31
33
  from fresco.util.urls import normpath, make_query
32
34
  from fresco.util.common import fq_path
33
35
  from fresco.util.wsgi import make_environ
34
- from fresco.typing import WSGICallable
36
+ from fresco.types import WSGIApplication
37
+ from fresco.types import HeaderList
38
+ from fresco.types import OptionalExcInfo
39
+ from fresco.types import WriteCallable
35
40
 
36
41
  from fresco.exceptions import ResponseException
37
42
  from fresco.requestcontext import context
@@ -44,10 +49,12 @@ from fresco.routing import (
44
49
  )
45
50
  from fresco.options import Options
46
51
 
47
- __all__ = ("FrescoApp", "urlfor")
52
+ __all__ = ("FrescoApp", "urlfor", "context")
48
53
 
49
54
  logger = logging.getLogger(__name__)
50
55
 
56
+ ExcInfo = tuple[t.Type[BaseException], BaseException, types.TracebackType]
57
+
51
58
 
52
59
  class FrescoApp(RouteCollection):
53
60
  """\
@@ -64,7 +71,6 @@ class FrescoApp(RouteCollection):
64
71
  request_class = Request
65
72
 
66
73
  def __init__(self, *args, **kwargs):
67
-
68
74
  views = kwargs.pop("views", None)
69
75
  path = kwargs.pop("path", None)
70
76
  super(FrescoApp, self).__init__(*args, **kwargs)
@@ -114,7 +120,9 @@ class FrescoApp(RouteCollection):
114
120
  #: If a function returns a value (other than ``None``),
115
121
  #: this value will be
116
122
  #: returned as the response instead of calling the scheduled view.
117
- self.process_http_error_response_handlers: List[Tuple[int, Callable]] = []
123
+ self.process_http_error_response_handlers: list[
124
+ tuple[t.Optional[int], Callable[[Request, Response], t.Optional[Response]]]
125
+ ] = []
118
126
 
119
127
  #: Functions to be called if an exception is raised during a view
120
128
  #: Each function will be passed ``request, exc_info``.
@@ -122,7 +130,9 @@ class FrescoApp(RouteCollection):
122
130
  #: this value will be
123
131
  #: returned as the response and the error will not be propagated.
124
132
  #: If all exception handlers return None then the error will be raised
125
- self.process_exception_handlers: List[Tuple[Exception, Callable]] = []
133
+ self.process_exception_handlers: list[
134
+ tuple[Type[Exception], Callable[[Request, ExcInfo], Union[Response, None]]]
135
+ ] = []
126
136
 
127
137
  #: Functions to be called at the end of request processing,
128
138
  #: after all content has been output.
@@ -150,12 +160,12 @@ class FrescoApp(RouteCollection):
150
160
 
151
161
  def get_response(
152
162
  self,
153
- request,
154
- path,
155
- method,
163
+ request: Request,
164
+ path: str,
165
+ method: str,
156
166
  currentcontext=context.currentcontext,
157
167
  normpath=normpath,
158
- ):
168
+ ) -> Response:
159
169
  ctx = currentcontext()
160
170
  ctx["app"] = self
161
171
  environ = request.environ
@@ -177,19 +187,19 @@ class FrescoApp(RouteCollection):
177
187
 
178
188
  try:
179
189
  for traversal in self.get_route_traversals(path, method, request):
190
+ route = traversal.route
180
191
  try:
181
- route = traversal.route
182
192
  environ["wsgiorg.routing_args"] = (
183
193
  traversal.args,
184
194
  traversal.kwargs,
185
195
  )
186
- view = route.getview(method)
196
+ view = traversal.view
187
197
  ctx["view_self"] = getattr(view, "__self__", None)
188
198
  ctx["route_traversal"] = traversal
189
199
  if self.logger:
190
200
  self.logger.info(
191
201
  "matched route: %s %r => %r",
192
- request.method,
202
+ method,
193
203
  path,
194
204
  fq_path(view),
195
205
  )
@@ -258,12 +268,12 @@ class FrescoApp(RouteCollection):
258
268
 
259
269
  # Is the URL just missing a trailing '/'?
260
270
  if not path or path[-1] != "/":
261
- for _ in self.get_methods(request, path + "/"):
271
+ if self.get_methods(request, path + "/"):
262
272
  return Response.unrestricted_redirect_permanent(path + "/")
263
273
 
264
274
  return Response.not_found()
265
275
 
266
- def view(self, request=None) -> Response:
276
+ def view(self, request: t.Optional[Request] = None) -> Response:
267
277
  request = request or context.request
268
278
  try:
269
279
  path = request.path_info
@@ -271,9 +281,7 @@ class FrescoApp(RouteCollection):
271
281
  response = e.response
272
282
  else:
273
283
  response = self.get_response(
274
- request,
275
- path,
276
- request.method
284
+ request, path, request.environ["REQUEST_METHOD"]
277
285
  )
278
286
 
279
287
  for f in self.process_response_handlers:
@@ -290,7 +298,9 @@ class FrescoApp(RouteCollection):
290
298
 
291
299
  return response
292
300
 
293
- def handle_http_error_response(self, request, response):
301
+ def handle_http_error_response(
302
+ self, request: Request, response: Response
303
+ ) -> Response:
294
304
  """
295
305
  Call any process_http_error_response handlers and return the
296
306
  (potentially modified) response object.
@@ -306,8 +316,8 @@ class FrescoApp(RouteCollection):
306
316
  self.log_exception(request)
307
317
  return response
308
318
 
309
- def get_methods(self, request, path):
310
- """\
319
+ def get_methods(self, request: Request, path: str) -> Set[str]:
320
+ """
311
321
  Return the HTTP methods valid in routes to the given path
312
322
  """
313
323
  methods: Set[str] = set()
@@ -325,13 +335,7 @@ class FrescoApp(RouteCollection):
325
335
  exc_info=exc_info,
326
336
  )
327
337
 
328
- def handle_exception(
329
- self, request, allow_reraise=True
330
- ) -> Union[
331
- Response,
332
- Tuple[Type[BaseException], BaseException, types.TracebackType],
333
- ]:
334
-
338
+ def handle_exception(self, request, allow_reraise=True) -> Response:
335
339
  exc_info = sys.exc_info()
336
340
  if exc_info[0] is None:
337
341
  raise AssertionError(
@@ -348,10 +352,7 @@ class FrescoApp(RouteCollection):
348
352
  # server handle it
349
353
  if allow_reraise and not have_error_handlers:
350
354
  raise exc_info[1].with_traceback(exc_info[2]) # type: ignore
351
- response: Union[
352
- Response,
353
- Tuple[Type[BaseException], BaseException, types.TracebackType],
354
- ] = Response.internal_server_error()
355
+ response: Response = Response.internal_server_error()
355
356
 
356
357
  if not self.process_exception_handlers:
357
358
  self.log_exception(request, exc_info)
@@ -408,7 +409,7 @@ class FrescoApp(RouteCollection):
408
409
  self.reset_wsgi_app()
409
410
  self._middleware.insert(position, (middleware, args, kwargs))
410
411
 
411
- def make_wsgi_app(self, wsgi_app=None, use_middleware=True) -> WSGICallable:
412
+ def make_wsgi_app(self, wsgi_app=None, use_middleware=True) -> WSGIApplication:
412
413
  """
413
414
  Return a WSGI (PEP-3333) compliant application that drives this
414
415
  FrescoApp object.
@@ -421,7 +422,7 @@ class FrescoApp(RouteCollection):
421
422
  """
422
423
  if wsgi_app is None:
423
424
 
424
- def wsgi_app(
425
+ def _wsgi_app(
425
426
  environ,
426
427
  start_response,
427
428
  view=self.view,
@@ -430,15 +431,18 @@ class FrescoApp(RouteCollection):
430
431
  request = request_class(environ)
431
432
  return view(request)(environ, start_response)
432
433
 
434
+ else:
435
+ _wsgi_app = wsgi_app
436
+
433
437
  if use_middleware:
434
438
  for m, m_args, m_kwargs in self._middleware:
435
- wsgi_app = m(wsgi_app, *m_args, **m_kwargs)
439
+ _wsgi_app = m(_wsgi_app, *m_args, **m_kwargs)
436
440
 
437
441
  def fresco_wsgi_app(
438
442
  environ,
439
443
  start_response,
440
444
  frescoapp=self,
441
- wsgi_app=wsgi_app,
445
+ wsgi_app=_wsgi_app,
442
446
  request_class=self.request_class,
443
447
  process_teardown_handlers=self.process_teardown_handlers,
444
448
  call_process_teardown_handlers=self.call_process_teardown_handlers,
@@ -450,8 +454,7 @@ class FrescoApp(RouteCollection):
450
454
  iterator = None
451
455
  try:
452
456
  iterator = wsgi_app(environ, start_response)
453
- for item in iterator:
454
- yield item
457
+ yield from iterator
455
458
  except Exception:
456
459
  exc_info = sys.exc_info()
457
460
  try:
@@ -464,14 +467,15 @@ class FrescoApp(RouteCollection):
464
467
  def exc_start_response(s, h, exc_info=exc_info):
465
468
  return start_response(s, h, exc_info)
466
469
 
467
- for item in response(environ, exc_start_response):
468
- yield item
470
+ yield from response(environ, exc_start_response)
469
471
  finally:
470
472
  del exc_info
471
473
  finally:
472
474
  try:
473
475
  if process_teardown_handlers:
474
476
  call_process_teardown_handlers(request)
477
+ for item in request.teardown_handlers:
478
+ item()
475
479
  finally:
476
480
  try:
477
481
  close = getattr(iterator, "close", None)
@@ -562,6 +566,27 @@ class FrescoApp(RouteCollection):
562
566
  except Exception:
563
567
  self.log_exception(request)
564
568
 
569
+ def process_request_once(
570
+ self, func: Callable[[Request], t.Optional[Response]]
571
+ ) -> Callable[[Request], t.Optional[Response]]:
572
+ """
573
+ Register a ``process_request`` hook function that is called only once
574
+
575
+ When running fresco with multiple worker threads/processes the hook
576
+ function will be called at most once per worker.
577
+ """
578
+
579
+ @self.process_request
580
+ @wraps(func)
581
+ def process_request_once(request: Request) -> t.Optional[Response]:
582
+ try:
583
+ self.process_request_handlers.remove(process_request_once)
584
+ except ValueError:
585
+ return None
586
+ return func(request)
587
+
588
+ return func
589
+
565
590
  def process_request(self, func):
566
591
  """
567
592
  Register a ``process_request`` hook function
@@ -583,7 +608,11 @@ class FrescoApp(RouteCollection):
583
608
  self.process_response_handlers.append(func)
584
609
  return func
585
610
 
586
- def process_exception(self, func, exc_type=Exception):
611
+ def process_exception(
612
+ self,
613
+ func: Callable[[Request, ExcInfo], Union[Response, None]],
614
+ exc_type: Type[Exception] = Exception,
615
+ ):
587
616
  """
588
617
  Register a ``process_exception`` hook function
589
618
  """
@@ -649,33 +678,39 @@ class FrescoApp(RouteCollection):
649
678
  start_response("200 OK", [])
650
679
  yield b""
651
680
 
652
- def fake_start_response(status, headers, exc_info=None):
681
+ def fake_start_response(
682
+ status: str, headers: HeaderList, exc_info: OptionalExcInfo = None
683
+ ) -> WriteCallable:
653
684
  return lambda s: None
654
685
 
655
686
  environ = make_environ(url, environ, wsgi_input, **kwargs)
656
687
  app = self.make_wsgi_app(wsgi_app=fake_app, use_middleware=middleware)
657
688
  result = app(environ, fake_start_response)
658
- close = getattr(result, "close", lambda: None)
689
+ close = getattr(result, "close", None)
659
690
  content_iterator = iter(result)
660
- next(content_iterator, None)
661
- yield context
662
- list(content_iterator)
663
- close()
691
+ try:
692
+ next(content_iterator, None)
693
+ yield context
694
+ list(content_iterator)
695
+ finally:
696
+ if close is not None:
697
+ close()
664
698
 
665
699
  def requestcontext_with_payload(
666
700
  self, url="/", data=None, environ=None, files=None, multipart=False, **kwargs
667
701
  ):
668
-
669
702
  if files:
670
703
  multipart = True
671
704
 
672
705
  if multipart:
673
706
  wsgi_input, headers = encode_multipart(data, files)
674
707
  kwargs.update(headers)
675
- elif hasattr(data, "read"):
708
+ elif isinstance(data, t.BinaryIO):
676
709
  wsgi_input = data.read()
677
710
  elif isinstance(data, bytes):
678
711
  wsgi_input = data
712
+ elif data is None:
713
+ wsgi_input = b""
679
714
  else:
680
715
  wsgi_input = make_query(data).encode("ascii")
681
716
 
@@ -14,20 +14,23 @@
14
14
  #
15
15
  import sys
16
16
  from functools import wraps
17
+ import typing as t
17
18
 
18
19
  from fresco import Response
19
20
 
20
21
  _marker = object()
21
22
 
22
23
 
23
- def onerror(exceptions, handler):
24
- """\
24
+ def onerror(
25
+ exceptions: t.Union[t.Type[Exception], tuple[t.Type[Exception], ...]], handler
26
+ ) -> t.Callable:
27
+ """
25
28
  Return a decorator that can replace or update the return value of the
26
29
  function if an exception is raised
27
30
  """
28
31
 
29
32
  try:
30
- if isinstance(exceptions, Exception):
33
+ if isinstance(exceptions, type):
31
34
  exceptions = (exceptions,)
32
35
  except TypeError:
33
36
  pass
@@ -79,9 +82,7 @@ def json_response(
79
82
  def json_response_decorator(func):
80
83
  @wraps(func)
81
84
  def json_response_decorated(*fa, **fkw):
82
- return Response.json(
83
- func(*fa, **fkw), indent, separators, **kwargs
84
- )
85
+ return Response.json(func(*fa, **fkw), indent, separators, **kwargs)
85
86
 
86
87
  return json_response_decorated
87
88
 
@@ -0,0 +1 @@
1
+ DEFAULT_CHARSET = "UTF-8"