fresco 3.3.4__py3-none-any.whl → 3.5.0__py3-none-any.whl

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.

Potentially problematic release.


This version of fresco might be problematic. Click here for more details.

fresco/__init__.py CHANGED
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
- __version__ = "3.3.4"
15
+ __version__ = "3.5.0"
16
16
 
17
17
  DEFAULT_CHARSET = "UTF-8"
18
18
 
fresco/core.py CHANGED
@@ -46,7 +46,7 @@ from fresco.routing import (
46
46
  )
47
47
  from fresco.options import Options
48
48
 
49
- __all__ = ("FrescoApp", "urlfor")
49
+ __all__ = ("FrescoApp", "urlfor", "context")
50
50
 
51
51
  logger = logging.getLogger(__name__)
52
52
 
@@ -184,13 +184,13 @@ class FrescoApp(RouteCollection):
184
184
  traversal.args,
185
185
  traversal.kwargs,
186
186
  )
187
- view = route.getview(method)
187
+ view = traversal.view
188
188
  ctx["view_self"] = getattr(view, "__self__", None)
189
189
  ctx["route_traversal"] = traversal
190
190
  if self.logger:
191
191
  self.logger.info(
192
192
  "matched route: %s %r => %r",
193
- request.method,
193
+ method,
194
194
  path,
195
195
  fq_path(view),
196
196
  )
fresco/middleware.py CHANGED
@@ -12,11 +12,17 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
+ import typing as t
16
+
17
+ from fresco.typing import WSGICallable
18
+ from fresco.typing import WSGIEnviron
19
+ from fresco.typing import StartResponse
20
+
15
21
  __all__ = ["XForwarded"]
16
22
 
17
23
 
18
24
  class XForwarded(object):
19
- """\
25
+ """
20
26
  Modify the WSGI environment so that the X_FORWARDED_* headers are observed
21
27
  and generated URIs are correct in a proxied environment.
22
28
 
@@ -33,6 +39,13 @@ class XForwarded(object):
33
39
  the wsgi.url_scheme is modified to ``https`` and ``HTTPS`` is set to
34
40
  ``on``.
35
41
 
42
+ :param trusted:
43
+ List of IP addresses trusted to set the HTTP_X_FORWARDED_* headers
44
+
45
+ :param force_https:
46
+ If True, the following environ keys will be set unconditionally:
47
+ ``"HTTPS": "on"`` and ``"wsgi.url_scheme": "https"`` will be set
48
+
36
49
  Example::
37
50
 
38
51
  >>> from fresco import FrescoApp, context, GET, Response
@@ -68,24 +81,36 @@ class XForwarded(object):
68
81
  u'URL is https://real-name/; REMOTE_ADDR is 1.2.3.4'
69
82
  """
70
83
 
71
- def __init__(self, app, trusted=None):
84
+ def __init__(
85
+ self,
86
+ app: WSGICallable,
87
+ trusted: t.Optional[t.Iterable[str]] = None,
88
+ force_https: t.Optional[bool] = None,
89
+ ) -> None:
72
90
  self.app = app
91
+ self.force_https = force_https
73
92
  if trusted:
74
93
  self.trusted = set(trusted)
75
94
  else:
76
95
  self.trusted = set()
77
96
 
78
- def __call__(self, environ, start_response):
79
- """\
97
+ def __call__(
98
+ self, environ: WSGIEnviron, start_response: StartResponse
99
+ ) -> t.Iterable[bytes]:
100
+ """
80
101
  Call the WSGI app, passing it a modified environ
81
102
  """
82
103
  env = environ.get
83
- is_ssl = (
84
- env("HTTP_X_FORWARDED_PROTO") == "https"
85
- or env("HTTP_X_FORWARDED_SSL") == "on"
86
- )
87
104
 
105
+ if self.force_https is None:
106
+ is_ssl = (
107
+ env("HTTP_X_FORWARDED_PROTO") == "https"
108
+ or env("HTTP_X_FORWARDED_SSL") == "on"
109
+ )
110
+ else:
111
+ is_ssl = self.force_https
88
112
  host = env("HTTP_X_FORWARDED_HOST")
113
+
89
114
  if host is not None:
90
115
  if ":" in host:
91
116
  port = host.split(":")[1]
@@ -99,21 +124,17 @@ class XForwarded(object):
99
124
  environ["wsgi.url_scheme"] = "https"
100
125
  environ["HTTPS"] = "on"
101
126
 
102
- try:
103
- forwards = environ["HTTP_X_FORWARDED_FOR"].split(", ") + [
104
- env("REMOTE_ADDR", "")
105
- ]
106
- except KeyError:
107
- # No X-Forwarded-For header?
108
- return self.app(environ, start_response)
109
-
110
- if self.trusted:
111
- for ip in forwards[::-1]:
112
- # Find the first non-trusted ip; this is our remote address
113
- if ip not in self.trusted:
114
- environ["REMOTE_ADDR"] = ip
115
- break
116
- else:
117
- environ["REMOTE_ADDR"] = forwards[0]
127
+ forwarded_for = env("HTTP_X_FORWARDED_FOR")
128
+ if forwarded_for:
129
+ addrs = forwarded_for.split(", ")
130
+
131
+ if self.trusted:
132
+ for ip in addrs[::-1]:
133
+ # Find the first non-trusted ip; this is our remote address
134
+ if ip not in self.trusted:
135
+ environ["REMOTE_ADDR"] = ip
136
+ break
137
+ else:
138
+ environ["REMOTE_ADDR"] = addrs[0]
118
139
 
119
140
  return self.app(environ, start_response)
fresco/options.py CHANGED
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  #
15
+ import contextlib
15
16
  import inspect
17
+ import itertools
16
18
  import json
17
19
  import logging
18
20
  import typing as t
@@ -217,9 +219,11 @@ class Options(dict):
217
219
  for ts in sorted(
218
220
  tagged_sources,
219
221
  key=(
220
- lambda ts: ([], ts[1])
221
- if len(ts[0]) == 0
222
- else (sorted(tags.index(t) for t in ts[0]), ts[1])
222
+ lambda ts: (
223
+ ([], ts[1])
224
+ if len(ts[0]) == 0 else
225
+ (sorted(tags.index(t) for t in ts[0]), ts[1])
226
+ )
223
227
  )
224
228
  )
225
229
  ]
@@ -314,6 +318,45 @@ class Options(dict):
314
318
  self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
315
319
 
316
320
 
321
+ @contextlib.contextmanager
322
+ def override_options(options, other=None, **kwargs):
323
+ """
324
+ Context manager that updates the given Options object with new values.
325
+ On exit, the old values will be restored.
326
+
327
+ This function is provided to assist with writing tests. It directly
328
+ modifies the given options object and does not prevent other threads from
329
+ accessing the modified values.
330
+ """
331
+ saved: list[tuple[str, t.Any]] = []
332
+ items: t.Iterable[tuple[str, t.Any]] = []
333
+ if other is not None:
334
+
335
+ keys = getattr(other, "keys", None)
336
+ if keys and callable(keys):
337
+ items = ((k, other[k]) for k in keys())
338
+
339
+ if kwargs:
340
+ items = itertools.chain(items, kwargs.items())
341
+
342
+ NOT_PRESENT = object()
343
+
344
+ for k, v in items:
345
+ if k in options:
346
+ saved.append((k, options[k]))
347
+ else:
348
+ saved.append((k, NOT_PRESENT))
349
+
350
+ options[k] = v
351
+
352
+ yield options
353
+ for k, v in saved:
354
+ if v is NOT_PRESENT:
355
+ del options[k]
356
+ else:
357
+ options[k] = v
358
+
359
+
317
360
  def parse_value(
318
361
  options: Mapping,
319
362
  v: str,
fresco/py.typed ADDED
File without changes
fresco/routing.py CHANGED
@@ -14,6 +14,7 @@
14
14
  #
15
15
  import re
16
16
  import sys
17
+ import inspect
17
18
  import warnings
18
19
  from copy import copy
19
20
  from collections import defaultdict
@@ -38,6 +39,7 @@ from fresco.response import Response
38
39
  from fresco.request import Request
39
40
  from fresco.requestcontext import context
40
41
  from fresco.routeargs import RouteArg
42
+ from fresco.typing import WSGICallable
41
43
  from fresco.util.cache import make_cache
42
44
  from fresco.util.common import fq_path
43
45
  from fresco.util.urls import join_path
@@ -107,6 +109,9 @@ ALL_METHODS = HTTP_METHODS = set(
107
109
  + RFC5789_METHODS
108
110
  )
109
111
 
112
+ #: Shortcut for specifying all methods as a kwarg
113
+ ALL = "ALL"
114
+
110
115
  __all__ = [
111
116
  "ALL_METHODS",
112
117
  "Pattern",
@@ -125,7 +130,7 @@ PathMatch = namedtuple(
125
130
 
126
131
 
127
132
  class RouteTraversal(
128
- namedtuple("RouteTraversal", "route args kwargs collections_traversed")
133
+ namedtuple("RouteTraversal", "route view args kwargs collections_traversed")
129
134
  ):
130
135
  """
131
136
  Encapsulate a route traversal.
@@ -134,6 +139,7 @@ class RouteTraversal(
134
139
 
135
140
  - ``route`` the final route traversed, allowing access to the view
136
141
  associated with the path.
142
+ - ``view``, the resolved view callable
137
143
  - ``args`` - positional args to be passed to the view. This is a
138
144
  combination of args extracted from the path and any added when
139
145
  constructing the route.
@@ -203,7 +209,7 @@ class RouteTraversal(
203
209
  #: An item of RouteTraversal.collections_traversed
204
210
  TraversedCollection = namedtuple(
205
211
  "TraversedCollection",
206
- "collection path route args kwargs " "traversal_args traversal_kwargs",
212
+ "collection path route args kwargs traversal_args traversal_kwargs",
207
213
  )
208
214
 
209
215
 
@@ -705,8 +711,7 @@ class Route(object):
705
711
  lambda: defaultdict(list)
706
712
  )
707
713
 
708
- #: Always provide an positional ``request`` argument to views
709
- provide_request = False
714
+ provide_request: Optional[bool] = None
710
715
 
711
716
  def __init__(
712
717
  self,
@@ -727,7 +732,9 @@ class Route(object):
727
732
  :param pattern: A string that can be compiled into a path pattern
728
733
  :param methods: The list of HTTP methods the view is bound to
729
734
  ('GET', 'POST', etc)
730
- :param view: The view function.
735
+ :param view:
736
+ The view function, or a string identifier which will later be resolved.
737
+
731
738
  :param kwargs: A dictionary of default keyword arguments to pass
732
739
  to the view callable
733
740
  :param args: Positional arguments to pass to the view callable
@@ -744,8 +751,11 @@ class Route(object):
744
751
  by a view will cause the current response to be
745
752
  discarded with routing continuing to the next
746
753
  available route.
747
- :param provide_request: If True, provide the current request as the
748
- first argument to the view callable.
754
+ :param provide_request:
755
+ If True, provide the current request as the first argument to the
756
+ view callable. Defaults to ``None``, which will autodetect if the
757
+ view function has an initial parameter of type
758
+ :class:`~fresco.request.Request`
749
759
  :param **_kwargs: Keyword arguments matching HTTP method names
750
760
  (GET, POST etc) can used to specify views
751
761
  associated with those methods.
@@ -803,10 +813,16 @@ class Route(object):
803
813
  "HTTP methods must be specified as a string or iterable"
804
814
  )
805
815
  for m in methods:
816
+ if m == ALL:
817
+ methods = ALL_METHODS
818
+ break
806
819
  if m not in ALL_METHODS:
807
820
  raise ValueError("{!r} is not a valid HTTP method".format(m))
808
821
  method_view_map.update((m, view) for m in methods)
809
822
 
823
+ if view := _kwargs.pop("ALL", None):
824
+ method_view_map |= {m: view for m in ALL_METHODS}
825
+
810
826
  for method in ALL_METHODS:
811
827
  if method in _kwargs:
812
828
  method_view_map[method] = _kwargs.pop(method)
@@ -895,14 +911,14 @@ class Route(object):
895
911
  newroute.fallthrough_statuses = {int(s) for s in status_codes}
896
912
  return newroute
897
913
 
898
- def match(self, path, method):
914
+ def match(self, path: str, method: t.Optional[str]) -> t.Optional[PathMatch]:
899
915
  if method and method not in self.methods:
900
916
  return None
901
917
  return self.pattern.match(path)
902
918
 
903
- def getview(self, method: str) -> Callable:
904
- """\
905
- Return the raw view callable.
919
+ def getview(self, method: str) -> Callable[..., Any]:
920
+ """
921
+ Resolve and return the raw view callable.
906
922
  """
907
923
  try:
908
924
  return self._cached_views[method]
@@ -922,6 +938,12 @@ class Route(object):
922
938
  uview = getattr(self.instance, uview)
923
939
 
924
940
  self._cached_views[method] = uview
941
+ if self.provide_request is None:
942
+ if callable(uview):
943
+ self.provide_request = _has_request_parameter(uview)
944
+ else:
945
+ self.provide_request = False
946
+
925
947
  return uview
926
948
 
927
949
  @classmethod
@@ -1385,8 +1407,8 @@ class RouteCollection(MutableSequence):
1385
1407
  raise exc
1386
1408
 
1387
1409
  def _get_routes(
1388
- self, key: Tuple[t.Optional[str], str]
1389
- ) -> t.Sequence[t.Tuple[Route, PathMatch]]:
1410
+ self, key: tuple[t.Optional[str], str]
1411
+ ) -> t.Sequence[tuple[Route, PathMatch]]:
1390
1412
  method, path = key
1391
1413
  routes = ((r, r.match(path, method)) for r in self.__routes__)
1392
1414
  return [(r, t) for (r, t) in routes if t is not None]
@@ -1428,6 +1450,12 @@ class RouteCollection(MutableSequence):
1428
1450
 
1429
1451
  # View function arguments extracted while traversing the path
1430
1452
  traversal_args, traversal_kwargs = result.args, result.kwargs
1453
+ if method is None:
1454
+ for m in route.viewspecs:
1455
+ view = route.getview(m)
1456
+ break
1457
+ else:
1458
+ view = route.getview(method)
1431
1459
 
1432
1460
  # Process any args/kwargs defined in the Route declaration.
1433
1461
  if request:
@@ -1459,7 +1487,7 @@ class RouteCollection(MutableSequence):
1459
1487
  raise exc
1460
1488
 
1461
1489
  r = self.route_class("/", ALL_METHODS, raiser)
1462
- yield RouteTraversal(r, (), {}, [(self, "", r)])
1490
+ yield RouteTraversal(r, r.getview("GET"), (), {}, [(self, "", r)])
1463
1491
  continue
1464
1492
 
1465
1493
  for sub in sub_routes.get_route_traversals(
@@ -1484,17 +1512,21 @@ class RouteCollection(MutableSequence):
1484
1512
  # Dynamic routes consume their arguments when creating the
1485
1513
  # sub RouteCollection.
1486
1514
  if route.dynamic:
1487
- yield RouteTraversal(sub.route, sub.args, sub.kwargs, traversed)
1515
+ yield RouteTraversal(
1516
+ sub.route, sub.view, sub.args, sub.kwargs, traversed
1517
+ )
1488
1518
  else:
1489
1519
  yield RouteTraversal(
1490
1520
  sub.route,
1521
+ sub.view,
1491
1522
  args + sub.args,
1492
- dict(kwargs, **sub.kwargs),
1523
+ kwargs | sub.kwargs,
1493
1524
  traversed,
1494
1525
  )
1495
1526
  else:
1496
1527
  yield RouteTraversal(
1497
1528
  route,
1529
+ view,
1498
1530
  args,
1499
1531
  kwargs,
1500
1532
  [
@@ -1563,7 +1595,7 @@ class RouteCollection(MutableSequence):
1563
1595
  def route_wsgi(
1564
1596
  self,
1565
1597
  path: str,
1566
- wsgiapp: t.Union[t.Callable, str],
1598
+ wsgiapp: t.Union[WSGICallable, str],
1567
1599
  rewrite_script_name: bool = True,
1568
1600
  *args,
1569
1601
  **kwargs,
@@ -1591,7 +1623,7 @@ class RouteCollection(MutableSequence):
1591
1623
  resolved_wsgi_app = None
1592
1624
 
1593
1625
  def fresco_wsgi_view(
1594
- request,
1626
+ request: Request,
1595
1627
  path=path,
1596
1628
  ws_path=ws_path,
1597
1629
  rewrite_script_name=rewrite_script_name,
@@ -1766,3 +1798,24 @@ def register_converter(name, registry=ExtensiblePattern):
1766
1798
  return cls
1767
1799
 
1768
1800
  return register_converter
1801
+
1802
+
1803
+ def _has_request_parameter(func: Callable[..., Any]) -> bool:
1804
+ """
1805
+ Return True if the given function has an initial parameter of type Request
1806
+ """
1807
+
1808
+ def _is_request_annotation(a: Any) -> bool:
1809
+ if isinstance(a, type) and issubclass(a, Request):
1810
+ return True
1811
+
1812
+ origin = t.get_origin(a)
1813
+ if origin is t.Union and any(
1814
+ _is_request_annotation(arg) for arg in t.get_args(a)
1815
+ ):
1816
+ return True
1817
+ return False
1818
+
1819
+ sig = inspect.signature(func)
1820
+ firstparam = next(iter(sig.parameters.values()), None)
1821
+ return bool(firstparam and _is_request_annotation(firstparam.annotation))
fresco/subrequests.py CHANGED
@@ -1,7 +1,5 @@
1
1
  from itertools import chain
2
2
  from typing import Any
3
- from typing import Dict
4
- from typing import Tuple
5
3
 
6
4
  from fresco.core import context
7
5
  from fresco.request import Request
@@ -262,8 +260,8 @@ def resolve_viewspec(viewspec, *args, **kwargs):
262
260
 
263
261
 
264
262
  def _get_args_for_route(
265
- route: Route, request: Request, args: Tuple, kwargs: Dict[str, Any]
266
- ) -> Tuple[Tuple, Dict[str, Any]]:
263
+ route: Route, request: Request, args: tuple[Any, ...], kwargs: dict[str, Any]
264
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
267
265
  """
268
266
  Return the args/kwargs required to pass to the view callable for ``route``.
269
267
  """
@@ -24,6 +24,7 @@ import sys
24
24
  import pytest
25
25
 
26
26
  from fresco.options import Options
27
+ from fresco.options import override_options
27
28
  from fresco.options import parse_key_value_pairs
28
29
  from fresco.options import dict_from_options
29
30
 
@@ -104,6 +105,25 @@ class TestOptions(object):
104
105
  assert isinstance(Options().copy(), Options)
105
106
 
106
107
 
108
+ class TestOverrideOptions:
109
+
110
+ def test_override_options_with_object(self):
111
+ options = Options(foo=1)
112
+ with override_options(options, {"foo": 2, "bar": "a"}):
113
+ assert options["foo"] == 2
114
+ assert options["bar"] == "a"
115
+ assert options["foo"] == 1
116
+ assert "bar" not in options
117
+
118
+ def test_override_options_with_kwargs(self):
119
+ options = Options(foo=1)
120
+ with override_options(options, foo=2, bar="a"):
121
+ assert options["foo"] == 2
122
+ assert options["bar"] == "a"
123
+ assert options["foo"] == 1
124
+ assert "bar" not in options
125
+
126
+
107
127
  class TestLoadKeyValuePairs:
108
128
  def test_it_loads_strings(self):
109
129
  assert parse_key_value_pairs({}, ["a=b"]) == {"a": "b"}
@@ -24,11 +24,15 @@ import tms
24
24
 
25
25
  from fresco import FrescoApp
26
26
  from fresco.core import urlfor
27
+ from fresco.request import Request
27
28
  from fresco.exceptions import NotFound
28
29
  from fresco.response import Response
29
30
  from fresco.routing import ALL_METHODS
31
+ from fresco.routing import ALL
30
32
  from fresco.routing import GET
33
+ from fresco.routing import OPTIONS
31
34
  from fresco.routing import POST
35
+ from fresco.routing import _has_request_parameter
32
36
  from fresco.routing import (
33
37
  Route,
34
38
  DelegateRoute,
@@ -114,6 +118,21 @@ class TestRouteConstructor(object):
114
118
  with pytest.raises(ValueError):
115
119
  Route("/", ["GET", "FOO"], lambda: None)
116
120
 
121
+ def test_ALL_kwarg_routes_all_methods(self):
122
+ def view():
123
+ return Response()
124
+
125
+ app = FrescoApp()
126
+ app.route("/x", ALL, view)
127
+ assert len(list(app.get_route_traversals("/x", GET))) == 1
128
+ assert len(list(app.get_route_traversals("/x", POST))) == 1
129
+ assert len(list(app.get_route_traversals("/x", OPTIONS))) == 1
130
+
131
+ app.route("/y", ALL=view)
132
+ assert len(list(app.get_route_traversals("/y", GET))) == 1
133
+ assert len(list(app.get_route_traversals("/y", POST))) == 1
134
+ assert len(list(app.get_route_traversals("/y", OPTIONS))) == 1
135
+
117
136
 
118
137
  class TestRouteBeforeHooks(object):
119
138
  def test_hook_is_called(self):
@@ -330,29 +349,29 @@ class TestPredicates(object):
330
349
 
331
350
  class TestRouteNames(object):
332
351
  def test_name_present_in_route_keys(self):
333
- r = Route("/", GET, None, name="foo")
352
+ r = Route("/", GET, lambda: None, name="foo")
334
353
  assert "foo" in list(r.route_keys())
335
354
 
336
355
  def test_name_with_other_kwargs(self):
337
- r = Route("/", GET, None, name="foo", x="bar")
356
+ r = Route("/", GET, lambda: None, name="foo", x="bar")
338
357
  assert "foo" in list(r.route_keys())
339
358
 
340
359
  def test_name_cannot_contain_colon(self):
341
360
  with pytest.raises(ValueError):
342
- Route("/", GET, None, name="foo:bar")
361
+ Route("/", GET, lambda: None, name="foo:bar")
343
362
 
344
363
 
345
364
  class TestRouteCollection(object):
346
365
  def test_it_adds_routes_from_constructor(self):
347
- r1 = Route("/1", GET, None, name="1")
348
- r2 = Route("/2", POST, None, name="2")
366
+ r1 = Route("/1", GET, lambda: None, name="1")
367
+ r2 = Route("/2", POST, lambda: None, name="2")
349
368
  rc = RouteCollection([r1, r2])
350
369
  assert [r.name for r in rc] == ["1", "2"]
351
370
 
352
371
  def test_it_adds_routecollections_from_constructor(self):
353
- r1 = Route("/", GET, None, name="1")
354
- r2 = Route("/", POST, None, name="2")
355
- r3 = Route("/", POST, None, name="3")
372
+ r1 = Route("/", GET, lambda: None, name="1")
373
+ r2 = Route("/", POST, lambda: None, name="2")
374
+ r3 = Route("/", POST, lambda: None, name="3")
356
375
  rc = RouteCollection([r1, RouteCollection([r2, r3])])
357
376
  assert [r.name for r in rc] == ["1", "2", "3"]
358
377
 
@@ -411,7 +430,7 @@ class TestRouteCollection(object):
411
430
  a.add_route(a_delegate_route)
412
431
  b.add_route(b_delegate_route)
413
432
 
414
- r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", None))
433
+ r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", "GET"))
415
434
 
416
435
  assert r.collections_traversed == [
417
436
  (a, "", a_delegate_route, (), {}, (), {}),
@@ -1124,3 +1143,51 @@ class TestRouteAll:
1124
1143
  assert len(list(app.get_route_traversals("/x", GET))) == 1
1125
1144
  assert len(list(app.get_route_traversals("/x/y", GET))) == 1
1126
1145
  assert len(list(app.get_route_traversals("/xy", GET))) == 0
1146
+
1147
+
1148
+ class TestRequestParamter:
1149
+
1150
+ def test_has_request_parameter(self):
1151
+
1152
+ def a(request: Request):
1153
+ pass
1154
+
1155
+ def b(request: t.Optional[Request]):
1156
+ pass
1157
+
1158
+ def c(request: t.Union[Request, None]):
1159
+ pass
1160
+
1161
+ def d(request: t.Union[None, Request]):
1162
+ pass
1163
+
1164
+ def e(request: int):
1165
+ pass
1166
+
1167
+ def f(request):
1168
+ pass
1169
+
1170
+ assert _has_request_parameter(a) is True
1171
+ assert _has_request_parameter(b) is True
1172
+ assert _has_request_parameter(c) is True
1173
+ assert _has_request_parameter(d) is True
1174
+ assert _has_request_parameter(e) is False
1175
+ assert _has_request_parameter(f) is False
1176
+
1177
+ def test_request_is_provided_automatically(self):
1178
+
1179
+ def a(request: Request):
1180
+ return Response("a")
1181
+
1182
+ def b():
1183
+ return Response("b")
1184
+
1185
+ app = FrescoApp()
1186
+ app.route("/a", GET=a)
1187
+ app.route("/b", GET=b)
1188
+
1189
+ with app.requestcontext("/a"):
1190
+ assert app.view().content == "a"
1191
+
1192
+ with app.requestcontext("/b"):
1193
+ assert app.view().content == "b"
fresco/typing.py CHANGED
@@ -1,11 +1,17 @@
1
1
  from types import TracebackType
2
2
  from typing import Any
3
3
  from typing import Callable
4
- from typing import Dict
5
4
  from typing import Iterable
6
- from typing import List
7
- from typing import Tuple
8
5
 
9
- WSGICallable = Callable[[Dict[str, Any], Callable], Iterable[bytes]]
10
- ExcInfoTuple = Tuple[type, BaseException, TracebackType]
11
- HeadersList = List[Tuple[str, str]]
6
+ HeaderList = list[tuple[str, str]]
7
+ HeadersList = HeaderList
8
+ WSGIEnviron = dict[str, Any]
9
+ StartResponse = Callable[[str, HeaderList], None]
10
+ WSGICallable = Callable[
11
+ [
12
+ WSGIEnviron,
13
+ StartResponse,
14
+ ],
15
+ Iterable[bytes]
16
+ ]
17
+ ExcInfoTuple = tuple[type, BaseException, TracebackType]
fresco/util/wsgi.py CHANGED
@@ -27,7 +27,7 @@ from typing import Tuple
27
27
  import sys
28
28
 
29
29
  from fresco.typing import ExcInfoTuple
30
- from fresco.typing import HeadersList
30
+ from fresco.typing import HeaderList
31
31
  from fresco.typing import WSGICallable
32
32
 
33
33
  logger = logging.getLogger(__name__)
@@ -407,16 +407,16 @@ def make_environ(url="/", environ=None, wsgi_input=b"", **kwargs):
407
407
 
408
408
  def apply_request(
409
409
  request, wsgicallable: WSGICallable
410
- ) -> Tuple[str, HeadersList, Optional[ExcInfoTuple], List[bytes]]:
410
+ ) -> Tuple[str, HeaderList, Optional[ExcInfoTuple], List[bytes]]:
411
411
  """
412
412
  Execute ``wsgicallable`` with the given request, exhaust and close the
413
413
  content iterator and return the result.
414
414
  """
415
415
 
416
- _start_response_result: List[Tuple[str, HeadersList, Optional[ExcInfoTuple]]] = []
416
+ _start_response_result: List[Tuple[str, HeaderList, Optional[ExcInfoTuple]]] = []
417
417
 
418
418
  def start_response(
419
- status: str, headers: HeadersList, exc_info: Optional[ExcInfoTuple] = None
419
+ status: str, headers: HeaderList, exc_info: Optional[ExcInfoTuple] = None
420
420
  ):
421
421
  _start_response_result.append((status, headers, exc_info))
422
422
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: fresco
3
- Version: 3.3.4
3
+ Version: 3.5.0
4
4
  Summary: A Web/WSGI micro-framework
5
5
  Author-email: Oliver Cope <oliver@redgecko.org>
6
6
  License: Apache
@@ -1,20 +1,21 @@
1
- fresco/__init__.py,sha256=DkBKfTdD88kXZYGi5Pv686Es1twTgZ4fVO3mA2nf0GQ,3520
1
+ fresco/__init__.py,sha256=qd3y9P7W77xtuDx9WtYQt7wIHeRNUobog6zq8W4J-zg,3520
2
2
  fresco/cookie.py,sha256=Qnx8yOjU4LUJ1fqi7YvqbhAA01rCsclJGl_fxI68slw,7055
3
- fresco/core.py,sha256=mwYR6UP0zRSjcFlNZf51-otX9EcmvgjEZ7GDVQoQexg,26615
3
+ fresco/core.py,sha256=kBf_JY8wqvBSQzxP0pFUr0sOnvUoDltittL-5xDUlC8,26611
4
4
  fresco/decorators.py,sha256=84NUpRJ-M7GK6wDl42bmCRSvgoWzCsy1huyvGnSAPPw,3232
5
5
  fresco/exceptions.py,sha256=KE-LoYUGnho6KltzkU6cnm9vUiUhAiDIjPqn5ba-YCA,4410
6
- fresco/middleware.py,sha256=uCAuOtBnvaVBJGrQa8ecvLkSZ6aSKmWaJqxB8Rv4SsQ,4173
6
+ fresco/middleware.py,sha256=XMpakA1YMZJqwHQyyxgw6TFhecDRTrUSSeNLDEkY8qA,4808
7
7
  fresco/multidict.py,sha256=0CaNNIcFnZ1hLk3NExhNvjc_BtK4zVB26L9gP_7MeNM,13362
8
- fresco/options.py,sha256=0RzwO1eyCUC70GxKFnCHNKDyZwsKc9Q-AmtlMAnfZ-Q,14004
8
+ fresco/options.py,sha256=gzFzTv6vib16WazRxT1s2xD91f8IRfCGKijx3BbE1gU,15163
9
+ fresco/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
10
  fresco/request.py,sha256=dC7pMg-4Kxa6PcqaaWFL4JviANQotuBjoKwlxZYywRY,27048
10
11
  fresco/requestcontext.py,sha256=P-SkKJkKLYVqNiR2zwooRROnSnE2VMj2P2-eD5DW5Qg,3504
11
12
  fresco/response.py,sha256=ADKHbtAGyhwtaUJxB7m_1nqVdZRKUryebmG4FDUjZVY,37072
12
13
  fresco/routeargs.py,sha256=dxNlqbQ1FrgIY6OCFzcEMdZ0OVyjlMQdQGLmUJgdPYU,10176
13
- fresco/routing.py,sha256=kTFqJ9Nn9cOHlul-lHqKDurBwqlEO4fg8tertb1jkoI,58709
14
+ fresco/routing.py,sha256=BfUaC9xD9BL_RqRPMIkMNOdLbhbSgzWSJWooktRoeqc,60515
14
15
  fresco/static.py,sha256=9SKQ3P1YFTP45Qiic-ycvkpKRvqIURp1XSzPazTmYLI,2513
15
- fresco/subrequests.py,sha256=wFJWLuhVAcei5imYc5NBSCBaHBEm-X4_XswPtK3O4Zw,11081
16
+ fresco/subrequests.py,sha256=zQlKJRZJVbfkxc0cQp3qoBFZH9pPFq77DgnYAJJvgAI,11052
16
17
  fresco/types.py,sha256=UHITRMDoS90s2zV2wNpqLFhRWESfaBAMuQnL4c21mqY,106
17
- fresco/typing.py,sha256=jQ1r_wme54J08XZUdmuuW4M8ve-gEhm1Wriwctls3r8,347
18
+ fresco/typing.py,sha256=uQWLElgVCJSZ3X2OkGNu-1ihmPvytyqSLguPB6tm-Pc,412
18
19
  fresco/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  fresco/tests/fixtures.py,sha256=eyo2zPivB3fItDkrJqWnOCvIS_A1q1JEhT4AutAB--o,1871
20
21
  fresco/tests/test_cookie.py,sha256=HTLmNCjcPoZDeFnZAzY3lJPeduzgU4mi9C-74eSQCec,2171
@@ -23,12 +24,12 @@ fresco/tests/test_decorators.py,sha256=VFXHo1gm2jldQXeaEF3NNo5fnpdJ-LXc8-vNymPJK
23
24
  fresco/tests/test_exceptions.py,sha256=R0Tn86m33iTKecZ69TgH4CqY9XSFP0FsLMH10O5Jth8,973
24
25
  fresco/tests/test_middleware.py,sha256=D_sWfX-w3bhItOm54nB_cuYPGoWopISvZCFIuMX68cU,3137
25
26
  fresco/tests/test_multidict.py,sha256=uDwDYII0dvVxaEyDO85zRTWlIWw3LO3StzYemJVm0E0,7565
26
- fresco/tests/test_options.py,sha256=Zx1tLVLeok8tO3dWua2PyTPZgeWbXFMXtN3FnorBmC8,10998
27
+ fresco/tests/test_options.py,sha256=zKJveuxTz4HL9NeQYwe8qaki8WQw65ghhW3cLnXfaVI,11659
27
28
  fresco/tests/test_request.py,sha256=hoANrergrohlAeTbQufDMfIbvYURsPyPjCxOVKz7bVo,16389
28
29
  fresco/tests/test_requestcontext.py,sha256=t8hm-lzIk85ryb3sdlpVoPQyLDWpCjB86dg8nVG1yRw,3115
29
30
  fresco/tests/test_response.py,sha256=MrhHIDg81pJlTeEcn2rGtU-i59s1KzEccF81u4Up6xs,8934
30
31
  fresco/tests/test_routeargs.py,sha256=VMWUbrXegTLN9Tx2AcrpbjAAEaxAANzkcy02SmpFOmY,8162
31
- fresco/tests/test_routing.py,sha256=yfZGon6vKwszFp3pCP2Mtnf3W3u4WdELdvVcgIKHOVU,37890
32
+ fresco/tests/test_routing.py,sha256=DkHP518f6UVAuG2pNVAyoyEcc2ej8T-Kw-moqkMB9gI,39851
32
33
  fresco/tests/test_static.py,sha256=y73dqzE2flpACQ_dvNhDzk_WlvNQfkhYF_8YY4MMGDo,4686
33
34
  fresco/tests/test_subrequests.py,sha256=7rluJnw-elXRXfrzvAQvGBHRBU93zwnL827mTxBGd3Y,7909
34
35
  fresco/tests/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -49,9 +50,9 @@ fresco/util/object.py,sha256=FjYNfPHzvBqq1rn0Y6As-2AVZ_SZOjH-lrSy4EbYmHY,370
49
50
  fresco/util/security.py,sha256=nXEdoCak_2c4OA1L1wGwhZygS22s2fzwR0Kp-DdwKZg,1058
50
51
  fresco/util/textproc.py,sha256=e5jLTofKCqdm6_Fy8XOyR43AJr5APtL59Kd8cNA9PrQ,2309
51
52
  fresco/util/urls.py,sha256=aaVovLyXNlVoGviiLN94ImqXf-LTQs_xooEIyi3LBc4,9195
52
- fresco/util/wsgi.py,sha256=2cr2b92RpvPSgS_P46X2mlYepoFg9Qji-V-fzxpSNzw,12971
53
- fresco-3.3.4.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
54
- fresco-3.3.4.dist-info/METADATA,sha256=v9NcXIhYp5MqENOecCV7XIsFrg7sC9GdRmIHuS912HI,1549
55
- fresco-3.3.4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
56
- fresco-3.3.4.dist-info/top_level.txt,sha256=p_1aMce5Shjq9fIfdbB-aN8wCDhjF_iYnn98bUebbII,7
57
- fresco-3.3.4.dist-info/RECORD,,
53
+ fresco/util/wsgi.py,sha256=gllmrw9um9ecs_sFxXy-Uwh7M9YAdcBNw33S63AhB8E,12967
54
+ fresco-3.5.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
55
+ fresco-3.5.0.dist-info/METADATA,sha256=mL1SE-OBfPOlYjc7YBSbt6ftilgaol-pUNAYqt3k0A0,1549
56
+ fresco-3.5.0.dist-info/WHEEL,sha256=nn6H5-ilmfVryoAQl3ZQ2l8SH5imPWFpm1A5FgEuFV4,91
57
+ fresco-3.5.0.dist-info/top_level.txt,sha256=p_1aMce5Shjq9fIfdbB-aN8wCDhjF_iYnn98bUebbII,7
58
+ fresco-3.5.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (75.8.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5