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 +1 -1
- fresco/core.py +3 -3
- fresco/middleware.py +45 -24
- fresco/options.py +46 -3
- fresco/py.typed +0 -0
- fresco/routing.py +71 -18
- fresco/subrequests.py +2 -4
- fresco/tests/test_options.py +20 -0
- fresco/tests/test_routing.py +76 -9
- fresco/typing.py +12 -6
- fresco/util/wsgi.py +4 -4
- {fresco-3.3.4.dist-info → fresco-3.5.0.dist-info}/METADATA +2 -2
- {fresco-3.3.4.dist-info → fresco-3.5.0.dist-info}/RECORD +16 -15
- {fresco-3.3.4.dist-info → fresco-3.5.0.dist-info}/WHEEL +1 -1
- {fresco-3.3.4.dist-info → fresco-3.5.0.dist-info}/LICENSE.txt +0 -0
- {fresco-3.3.4.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
|
|
@@ -217,9 +219,11 @@ class Options(dict):
|
|
|
217
219
|
for ts in sorted(
|
|
218
220
|
tagged_sources,
|
|
219
221
|
key=(
|
|
220
|
-
lambda ts: (
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
748
|
-
|
|
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
|
-
|
|
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:
|
|
1389
|
-
) -> t.Sequence[
|
|
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(
|
|
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
|
-
|
|
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[
|
|
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:
|
|
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,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",
|
|
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
|
-
|
|
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
|