fresco 3.4.0__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 +1 -1
- fresco/core.py +3 -3
- fresco/middleware.py +45 -24
- fresco/options.py +41 -0
- fresco/py.typed +0 -0
- fresco/routing.py +62 -18
- fresco/subrequests.py +2 -4
- fresco/tests/test_options.py +20 -0
- fresco/tests/test_routing.py +59 -9
- fresco/typing.py +12 -6
- fresco/util/wsgi.py +4 -4
- {fresco-3.4.0.dist-info → fresco-3.5.0.dist-info}/METADATA +2 -2
- {fresco-3.4.0.dist-info → fresco-3.5.0.dist-info}/RECORD +16 -15
- {fresco-3.4.0.dist-info → fresco-3.5.0.dist-info}/WHEEL +1 -1
- {fresco-3.4.0.dist-info → fresco-3.5.0.dist-info}/LICENSE.txt +0 -0
- {fresco-3.4.0.dist-info → fresco-3.5.0.dist-info}/top_level.txt +0 -0
fresco/__init__.py
CHANGED
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 =
|
|
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
|
-
|
|
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__(
|
|
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__(
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
@@ -316,6 +318,45 @@ class Options(dict):
|
|
|
316
318
|
self.update_from_dict(dict(inspect.getmembers(ob)), load_all)
|
|
317
319
|
|
|
318
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
|
+
|
|
319
360
|
def parse_value(
|
|
320
361
|
options: Mapping,
|
|
321
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
|
|
@@ -128,7 +130,7 @@ PathMatch = namedtuple(
|
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
class RouteTraversal(
|
|
131
|
-
namedtuple("RouteTraversal", "route args kwargs collections_traversed")
|
|
133
|
+
namedtuple("RouteTraversal", "route view args kwargs collections_traversed")
|
|
132
134
|
):
|
|
133
135
|
"""
|
|
134
136
|
Encapsulate a route traversal.
|
|
@@ -137,6 +139,7 @@ class RouteTraversal(
|
|
|
137
139
|
|
|
138
140
|
- ``route`` the final route traversed, allowing access to the view
|
|
139
141
|
associated with the path.
|
|
142
|
+
- ``view``, the resolved view callable
|
|
140
143
|
- ``args`` - positional args to be passed to the view. This is a
|
|
141
144
|
combination of args extracted from the path and any added when
|
|
142
145
|
constructing the route.
|
|
@@ -206,7 +209,7 @@ class RouteTraversal(
|
|
|
206
209
|
#: An item of RouteTraversal.collections_traversed
|
|
207
210
|
TraversedCollection = namedtuple(
|
|
208
211
|
"TraversedCollection",
|
|
209
|
-
"collection path route args kwargs
|
|
212
|
+
"collection path route args kwargs traversal_args traversal_kwargs",
|
|
210
213
|
)
|
|
211
214
|
|
|
212
215
|
|
|
@@ -708,8 +711,7 @@ class Route(object):
|
|
|
708
711
|
lambda: defaultdict(list)
|
|
709
712
|
)
|
|
710
713
|
|
|
711
|
-
|
|
712
|
-
provide_request = False
|
|
714
|
+
provide_request: Optional[bool] = None
|
|
713
715
|
|
|
714
716
|
def __init__(
|
|
715
717
|
self,
|
|
@@ -730,7 +732,9 @@ class Route(object):
|
|
|
730
732
|
:param pattern: A string that can be compiled into a path pattern
|
|
731
733
|
:param methods: The list of HTTP methods the view is bound to
|
|
732
734
|
('GET', 'POST', etc)
|
|
733
|
-
:param view:
|
|
735
|
+
:param view:
|
|
736
|
+
The view function, or a string identifier which will later be resolved.
|
|
737
|
+
|
|
734
738
|
:param kwargs: A dictionary of default keyword arguments to pass
|
|
735
739
|
to the view callable
|
|
736
740
|
:param args: Positional arguments to pass to the view callable
|
|
@@ -747,8 +751,11 @@ class Route(object):
|
|
|
747
751
|
by a view will cause the current response to be
|
|
748
752
|
discarded with routing continuing to the next
|
|
749
753
|
available route.
|
|
750
|
-
:param provide_request:
|
|
751
|
-
|
|
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`
|
|
752
759
|
:param **_kwargs: Keyword arguments matching HTTP method names
|
|
753
760
|
(GET, POST etc) can used to specify views
|
|
754
761
|
associated with those methods.
|
|
@@ -904,14 +911,14 @@ class Route(object):
|
|
|
904
911
|
newroute.fallthrough_statuses = {int(s) for s in status_codes}
|
|
905
912
|
return newroute
|
|
906
913
|
|
|
907
|
-
def match(self, path, method):
|
|
914
|
+
def match(self, path: str, method: t.Optional[str]) -> t.Optional[PathMatch]:
|
|
908
915
|
if method and method not in self.methods:
|
|
909
916
|
return None
|
|
910
917
|
return self.pattern.match(path)
|
|
911
918
|
|
|
912
|
-
def getview(self, method: str) -> Callable:
|
|
913
|
-
"""
|
|
914
|
-
|
|
919
|
+
def getview(self, method: str) -> Callable[..., Any]:
|
|
920
|
+
"""
|
|
921
|
+
Resolve and return the raw view callable.
|
|
915
922
|
"""
|
|
916
923
|
try:
|
|
917
924
|
return self._cached_views[method]
|
|
@@ -931,6 +938,12 @@ class Route(object):
|
|
|
931
938
|
uview = getattr(self.instance, uview)
|
|
932
939
|
|
|
933
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
|
+
|
|
934
947
|
return uview
|
|
935
948
|
|
|
936
949
|
@classmethod
|
|
@@ -1394,8 +1407,8 @@ class RouteCollection(MutableSequence):
|
|
|
1394
1407
|
raise exc
|
|
1395
1408
|
|
|
1396
1409
|
def _get_routes(
|
|
1397
|
-
self, key:
|
|
1398
|
-
) -> t.Sequence[
|
|
1410
|
+
self, key: tuple[t.Optional[str], str]
|
|
1411
|
+
) -> t.Sequence[tuple[Route, PathMatch]]:
|
|
1399
1412
|
method, path = key
|
|
1400
1413
|
routes = ((r, r.match(path, method)) for r in self.__routes__)
|
|
1401
1414
|
return [(r, t) for (r, t) in routes if t is not None]
|
|
@@ -1437,6 +1450,12 @@ class RouteCollection(MutableSequence):
|
|
|
1437
1450
|
|
|
1438
1451
|
# View function arguments extracted while traversing the path
|
|
1439
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)
|
|
1440
1459
|
|
|
1441
1460
|
# Process any args/kwargs defined in the Route declaration.
|
|
1442
1461
|
if request:
|
|
@@ -1468,7 +1487,7 @@ class RouteCollection(MutableSequence):
|
|
|
1468
1487
|
raise exc
|
|
1469
1488
|
|
|
1470
1489
|
r = self.route_class("/", ALL_METHODS, raiser)
|
|
1471
|
-
yield RouteTraversal(r, (), {}, [(self, "", r)])
|
|
1490
|
+
yield RouteTraversal(r, r.getview("GET"), (), {}, [(self, "", r)])
|
|
1472
1491
|
continue
|
|
1473
1492
|
|
|
1474
1493
|
for sub in sub_routes.get_route_traversals(
|
|
@@ -1493,17 +1512,21 @@ class RouteCollection(MutableSequence):
|
|
|
1493
1512
|
# Dynamic routes consume their arguments when creating the
|
|
1494
1513
|
# sub RouteCollection.
|
|
1495
1514
|
if route.dynamic:
|
|
1496
|
-
yield RouteTraversal(
|
|
1515
|
+
yield RouteTraversal(
|
|
1516
|
+
sub.route, sub.view, sub.args, sub.kwargs, traversed
|
|
1517
|
+
)
|
|
1497
1518
|
else:
|
|
1498
1519
|
yield RouteTraversal(
|
|
1499
1520
|
sub.route,
|
|
1521
|
+
sub.view,
|
|
1500
1522
|
args + sub.args,
|
|
1501
|
-
|
|
1523
|
+
kwargs | sub.kwargs,
|
|
1502
1524
|
traversed,
|
|
1503
1525
|
)
|
|
1504
1526
|
else:
|
|
1505
1527
|
yield RouteTraversal(
|
|
1506
1528
|
route,
|
|
1529
|
+
view,
|
|
1507
1530
|
args,
|
|
1508
1531
|
kwargs,
|
|
1509
1532
|
[
|
|
@@ -1572,7 +1595,7 @@ class RouteCollection(MutableSequence):
|
|
|
1572
1595
|
def route_wsgi(
|
|
1573
1596
|
self,
|
|
1574
1597
|
path: str,
|
|
1575
|
-
wsgiapp: t.Union[
|
|
1598
|
+
wsgiapp: t.Union[WSGICallable, str],
|
|
1576
1599
|
rewrite_script_name: bool = True,
|
|
1577
1600
|
*args,
|
|
1578
1601
|
**kwargs,
|
|
@@ -1600,7 +1623,7 @@ class RouteCollection(MutableSequence):
|
|
|
1600
1623
|
resolved_wsgi_app = None
|
|
1601
1624
|
|
|
1602
1625
|
def fresco_wsgi_view(
|
|
1603
|
-
request,
|
|
1626
|
+
request: Request,
|
|
1604
1627
|
path=path,
|
|
1605
1628
|
ws_path=ws_path,
|
|
1606
1629
|
rewrite_script_name=rewrite_script_name,
|
|
@@ -1775,3 +1798,24 @@ def register_converter(name, registry=ExtensiblePattern):
|
|
|
1775
1798
|
return cls
|
|
1776
1799
|
|
|
1777
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:
|
|
266
|
-
) ->
|
|
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
|
"""
|
fresco/tests/test_options.py
CHANGED
|
@@ -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"}
|
fresco/tests/test_routing.py
CHANGED
|
@@ -24,6 +24,7 @@ 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,6 +32,7 @@ from fresco.routing import ALL
|
|
|
31
32
|
from fresco.routing import GET
|
|
32
33
|
from fresco.routing import OPTIONS
|
|
33
34
|
from fresco.routing import POST
|
|
35
|
+
from fresco.routing import _has_request_parameter
|
|
34
36
|
from fresco.routing import (
|
|
35
37
|
Route,
|
|
36
38
|
DelegateRoute,
|
|
@@ -347,29 +349,29 @@ class TestPredicates(object):
|
|
|
347
349
|
|
|
348
350
|
class TestRouteNames(object):
|
|
349
351
|
def test_name_present_in_route_keys(self):
|
|
350
|
-
r = Route("/", GET, None, name="foo")
|
|
352
|
+
r = Route("/", GET, lambda: None, name="foo")
|
|
351
353
|
assert "foo" in list(r.route_keys())
|
|
352
354
|
|
|
353
355
|
def test_name_with_other_kwargs(self):
|
|
354
|
-
r = Route("/", GET, None, name="foo", x="bar")
|
|
356
|
+
r = Route("/", GET, lambda: None, name="foo", x="bar")
|
|
355
357
|
assert "foo" in list(r.route_keys())
|
|
356
358
|
|
|
357
359
|
def test_name_cannot_contain_colon(self):
|
|
358
360
|
with pytest.raises(ValueError):
|
|
359
|
-
Route("/", GET, None, name="foo:bar")
|
|
361
|
+
Route("/", GET, lambda: None, name="foo:bar")
|
|
360
362
|
|
|
361
363
|
|
|
362
364
|
class TestRouteCollection(object):
|
|
363
365
|
def test_it_adds_routes_from_constructor(self):
|
|
364
|
-
r1 = Route("/1", GET, None, name="1")
|
|
365
|
-
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")
|
|
366
368
|
rc = RouteCollection([r1, r2])
|
|
367
369
|
assert [r.name for r in rc] == ["1", "2"]
|
|
368
370
|
|
|
369
371
|
def test_it_adds_routecollections_from_constructor(self):
|
|
370
|
-
r1 = Route("/", GET, None, name="1")
|
|
371
|
-
r2 = Route("/", POST, None, name="2")
|
|
372
|
-
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")
|
|
373
375
|
rc = RouteCollection([r1, RouteCollection([r2, r3])])
|
|
374
376
|
assert [r.name for r in rc] == ["1", "2", "3"]
|
|
375
377
|
|
|
@@ -428,7 +430,7 @@ class TestRouteCollection(object):
|
|
|
428
430
|
a.add_route(a_delegate_route)
|
|
429
431
|
b.add_route(b_delegate_route)
|
|
430
432
|
|
|
431
|
-
r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey",
|
|
433
|
+
r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", "GET"))
|
|
432
434
|
|
|
433
435
|
assert r.collections_traversed == [
|
|
434
436
|
(a, "", a_delegate_route, (), {}, (), {}),
|
|
@@ -1141,3 +1143,51 @@ class TestRouteAll:
|
|
|
1141
1143
|
assert len(list(app.get_route_traversals("/x", GET))) == 1
|
|
1142
1144
|
assert len(list(app.get_route_traversals("/x/y", GET))) == 1
|
|
1143
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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,
|
|
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,
|
|
416
|
+
_start_response_result: List[Tuple[str, HeaderList, Optional[ExcInfoTuple]]] = []
|
|
417
417
|
|
|
418
418
|
def start_response(
|
|
419
|
-
status: str, headers:
|
|
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,20 +1,21 @@
|
|
|
1
|
-
fresco/__init__.py,sha256=
|
|
1
|
+
fresco/__init__.py,sha256=qd3y9P7W77xtuDx9WtYQt7wIHeRNUobog6zq8W4J-zg,3520
|
|
2
2
|
fresco/cookie.py,sha256=Qnx8yOjU4LUJ1fqi7YvqbhAA01rCsclJGl_fxI68slw,7055
|
|
3
|
-
fresco/core.py,sha256=
|
|
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=
|
|
6
|
+
fresco/middleware.py,sha256=XMpakA1YMZJqwHQyyxgw6TFhecDRTrUSSeNLDEkY8qA,4808
|
|
7
7
|
fresco/multidict.py,sha256=0CaNNIcFnZ1hLk3NExhNvjc_BtK4zVB26L9gP_7MeNM,13362
|
|
8
|
-
fresco/options.py,sha256=
|
|
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=
|
|
14
|
+
fresco/routing.py,sha256=BfUaC9xD9BL_RqRPMIkMNOdLbhbSgzWSJWooktRoeqc,60515
|
|
14
15
|
fresco/static.py,sha256=9SKQ3P1YFTP45Qiic-ycvkpKRvqIURp1XSzPazTmYLI,2513
|
|
15
|
-
fresco/subrequests.py,sha256=
|
|
16
|
+
fresco/subrequests.py,sha256=zQlKJRZJVbfkxc0cQp3qoBFZH9pPFq77DgnYAJJvgAI,11052
|
|
16
17
|
fresco/types.py,sha256=UHITRMDoS90s2zV2wNpqLFhRWESfaBAMuQnL4c21mqY,106
|
|
17
|
-
fresco/typing.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
53
|
-
fresco-3.
|
|
54
|
-
fresco-3.
|
|
55
|
-
fresco-3.
|
|
56
|
-
fresco-3.
|
|
57
|
-
fresco-3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|