fresco 3.3.4__py3-none-any.whl → 3.9.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.
- fresco/__init__.py +55 -56
- fresco/core.py +39 -27
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +45 -24
- fresco/multidict.py +35 -51
- fresco/options.py +188 -70
- fresco/py.typed +0 -0
- fresco/request.py +157 -36
- fresco/requestcontext.py +3 -0
- fresco/response.py +66 -57
- fresco/routeargs.py +23 -9
- fresco/routing.py +152 -74
- fresco/static.py +1 -1
- fresco/subrequests.py +3 -5
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +59 -15
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_response.py +16 -0
- fresco/tests/test_routing.py +113 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/tests/util/test_urls.py +20 -0
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +44 -12
- fresco/util/wsgi.py +15 -14
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/METADATA +4 -4
- fresco-3.9.0.dist-info/RECORD +58 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -11
- fresco-3.3.4.dist-info/RECORD +0 -57
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.3.4.dist-info → fresco-3.9.0.dist-info}/top_level.txt +0 -0
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,24 @@ class TestOptions(object):
|
|
|
104
105
|
assert isinstance(Options().copy(), Options)
|
|
105
106
|
|
|
106
107
|
|
|
108
|
+
class TestOverrideOptions:
|
|
109
|
+
def test_override_options_with_object(self):
|
|
110
|
+
options = Options(foo=1)
|
|
111
|
+
with override_options(options, {"foo": 2, "bar": "a"}):
|
|
112
|
+
assert options["foo"] == 2
|
|
113
|
+
assert options["bar"] == "a"
|
|
114
|
+
assert options["foo"] == 1
|
|
115
|
+
assert "bar" not in options
|
|
116
|
+
|
|
117
|
+
def test_override_options_with_kwargs(self):
|
|
118
|
+
options = Options(foo=1)
|
|
119
|
+
with override_options(options, foo=2, bar="a"):
|
|
120
|
+
assert options["foo"] == 2
|
|
121
|
+
assert options["bar"] == "a"
|
|
122
|
+
assert options["foo"] == 1
|
|
123
|
+
assert "bar" not in options
|
|
124
|
+
|
|
125
|
+
|
|
107
126
|
class TestLoadKeyValuePairs:
|
|
108
127
|
def test_it_loads_strings(self):
|
|
109
128
|
assert parse_key_value_pairs({}, ["a=b"]) == {"a": "b"}
|
|
@@ -143,7 +162,6 @@ class TestLoadKeyValuePairs:
|
|
|
143
162
|
|
|
144
163
|
|
|
145
164
|
class TestLoadOptions:
|
|
146
|
-
|
|
147
165
|
def check_loadoptions(self, tmpdir, files, sources="*", tags=[], expected={}):
|
|
148
166
|
"""
|
|
149
167
|
Write the files indicated in ``sources`` to the given temporary directory,
|
|
@@ -192,6 +210,11 @@ class TestLoadOptions:
|
|
|
192
210
|
def test_it_loads_py_files(self, tmpdir):
|
|
193
211
|
self.check_loadoptions(tmpdir, {"a.py": "x = 2 * 2"}, expected={"x": 4})
|
|
194
212
|
|
|
213
|
+
def test_py_files_have_options_in_namespace(self, tmpdir):
|
|
214
|
+
self.check_loadoptions(
|
|
215
|
+
tmpdir, {"a.py": "options['foo'] = 'bar'"}, expected={"foo": "bar"}
|
|
216
|
+
)
|
|
217
|
+
|
|
195
218
|
def test_it_selects_by_tag(self, tmpdir):
|
|
196
219
|
with self.check_loadoptions(
|
|
197
220
|
tmpdir,
|
|
@@ -217,14 +240,38 @@ class TestLoadOptions:
|
|
|
217
240
|
with self.check_loadoptions(
|
|
218
241
|
tmpdir,
|
|
219
242
|
{
|
|
220
|
-
"a": "a =
|
|
221
|
-
"a.dev.txt": "a = ${a}
|
|
222
|
-
"a.local.txt": "a = ${a}
|
|
223
|
-
"b.dev.txt": "a = ${a}
|
|
243
|
+
"a": "a = 'a'",
|
|
244
|
+
"a.0-dev.txt": "a = ${a} a.dev",
|
|
245
|
+
"a.local.txt": "a = ${a} a.local",
|
|
246
|
+
"b.dev.txt": "a = ${a} b.dev",
|
|
247
|
+
},
|
|
248
|
+
) as loadopts:
|
|
249
|
+
assert loadopts("*", ["dev", "local"]) == {"a": "a a.dev b.dev a.local"}
|
|
250
|
+
assert loadopts("*", ["local", "dev"]) == {"a": "a a.local a.dev b.dev"}
|
|
251
|
+
|
|
252
|
+
def test_it_loads_in_priority_order(self, tmpdir):
|
|
253
|
+
with self.check_loadoptions(
|
|
254
|
+
tmpdir,
|
|
255
|
+
{
|
|
256
|
+
"a": "a = 'a'",
|
|
257
|
+
"a.100-dev.txt": "a = ${a} a.100-dev",
|
|
258
|
+
"a.local.txt": "a = ${a} a.local",
|
|
259
|
+
"b.dev.txt": "a = ${a} b.dev",
|
|
224
260
|
},
|
|
225
261
|
) as loadopts:
|
|
226
|
-
assert loadopts("*", ["dev", "local"]) == {"a": "
|
|
227
|
-
assert loadopts("*", ["local", "dev"]) == {"a": "
|
|
262
|
+
assert loadopts("*", ["dev", "local"]) == {"a": "a b.dev a.local a.100-dev"}
|
|
263
|
+
assert loadopts("*", ["local", "dev"]) == {"a": "a a.local b.dev a.100-dev"}
|
|
264
|
+
|
|
265
|
+
def test_it_loads_in_priority_order_without_tags(self, tmpdir):
|
|
266
|
+
with self.check_loadoptions(
|
|
267
|
+
tmpdir,
|
|
268
|
+
{
|
|
269
|
+
"a": "a = a",
|
|
270
|
+
"b.100": "a = ${a} b",
|
|
271
|
+
"a.200.txt": "a = ${a} 100",
|
|
272
|
+
},
|
|
273
|
+
) as loadopts:
|
|
274
|
+
assert loadopts("*") == {"a": "a b 100"}
|
|
228
275
|
|
|
229
276
|
def test_it_loads_from_os_environ(self, tmpdir):
|
|
230
277
|
with setenv(a="2"):
|
|
@@ -252,7 +299,7 @@ class TestLoadOptions:
|
|
|
252
299
|
tmpdir,
|
|
253
300
|
{"a.txt": "a=1", "b.txt": "b=1"},
|
|
254
301
|
sources=["a.*", "b.*"],
|
|
255
|
-
expected={"a": 1, "b": 1}
|
|
302
|
+
expected={"a": 1, "b": 1},
|
|
256
303
|
)
|
|
257
304
|
|
|
258
305
|
def test_it_substitutes_from_environment_variables(self, tmpdir):
|
|
@@ -261,7 +308,7 @@ class TestLoadOptions:
|
|
|
261
308
|
tmpdir,
|
|
262
309
|
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
263
310
|
tags=["{FOO}"],
|
|
264
|
-
expected={"a": 2}
|
|
311
|
+
expected={"a": 2},
|
|
265
312
|
)
|
|
266
313
|
|
|
267
314
|
with setenv(FOO="baz"):
|
|
@@ -269,7 +316,7 @@ class TestLoadOptions:
|
|
|
269
316
|
tmpdir,
|
|
270
317
|
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
271
318
|
tags=["{FOO}"],
|
|
272
|
-
expected={"a": 1}
|
|
319
|
+
expected={"a": 1},
|
|
273
320
|
)
|
|
274
321
|
|
|
275
322
|
def test_it_allows_missing_environment_variables(self, tmpdir):
|
|
@@ -278,19 +325,16 @@ class TestLoadOptions:
|
|
|
278
325
|
tmpdir,
|
|
279
326
|
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
280
327
|
tags=["{FOO}"],
|
|
281
|
-
expected={"a": 1}
|
|
328
|
+
expected={"a": 1},
|
|
282
329
|
)
|
|
283
330
|
|
|
284
331
|
|
|
285
332
|
class TestDictFromOptions:
|
|
286
|
-
|
|
287
333
|
def test_it_splits_on_prefix(self):
|
|
288
|
-
|
|
289
334
|
options = Options(FOO_BAR=1, FOO_BAZ=2, FOO_BAR_BAZ=3, BAR=4)
|
|
290
335
|
assert dict_from_options("FOO_", options) == {"BAR": 1, "BAZ": 2, "BAR_BAZ": 3}
|
|
291
336
|
|
|
292
337
|
def test_it_splits_recursively(self):
|
|
293
|
-
|
|
294
338
|
options = Options(
|
|
295
339
|
A_A=1,
|
|
296
340
|
A_B_C_D=2,
|
|
@@ -303,7 +347,7 @@ class TestDictFromOptions:
|
|
|
303
347
|
"A": 1,
|
|
304
348
|
"B": {"C": {"D": 2}, "E": 3},
|
|
305
349
|
"F": {"G": {"H": 4}},
|
|
306
|
-
"I": 5
|
|
350
|
+
"I": 5,
|
|
307
351
|
}
|
|
308
352
|
|
|
309
353
|
|
fresco/tests/test_request.py
CHANGED
|
@@ -99,7 +99,7 @@ class TestRequestProperties(object):
|
|
|
99
99
|
CONTENT_LENGTH="2",
|
|
100
100
|
CONTENT_TYPE="text/plain; charset=UTF-8",
|
|
101
101
|
) as c:
|
|
102
|
-
c.request.body_bytes == b"\xc3a"
|
|
102
|
+
assert c.request.body_bytes == b"\xc3a"
|
|
103
103
|
|
|
104
104
|
def test_get_json_decodes_json(self):
|
|
105
105
|
with context(
|
|
@@ -107,7 +107,7 @@ class TestRequestProperties(object):
|
|
|
107
107
|
CONTENT_LENGTH="14",
|
|
108
108
|
CONTENT_TYPE="application/json",
|
|
109
109
|
) as c:
|
|
110
|
-
c.request.get_json() == {"foo": "bar"}
|
|
110
|
+
assert c.request.get_json() == {"foo": "bar"}
|
|
111
111
|
|
|
112
112
|
def test_get_json_ignores_mime_type(self):
|
|
113
113
|
with context(
|
|
@@ -115,7 +115,7 @@ class TestRequestProperties(object):
|
|
|
115
115
|
CONTENT_LENGTH="14",
|
|
116
116
|
CONTENT_TYPE="application/broken",
|
|
117
117
|
) as c:
|
|
118
|
-
c.request.get_json() == {"foo": "bar"}
|
|
118
|
+
assert c.request.get_json() == {"foo": "bar"}
|
|
119
119
|
|
|
120
120
|
def test_get_json_passes_args_to_decoder(self):
|
|
121
121
|
with context(
|
|
@@ -123,19 +123,30 @@ class TestRequestProperties(object):
|
|
|
123
123
|
CONTENT_LENGTH="10",
|
|
124
124
|
CONTENT_TYPE="application/broken",
|
|
125
125
|
) as c:
|
|
126
|
-
c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
|
|
126
|
+
assert c.request.get_json(parse_int=lambda s: s + "!") == {"foo": "1!"}
|
|
127
127
|
|
|
128
|
-
def
|
|
128
|
+
def test_get_required_raises_badrequest(self):
|
|
129
129
|
with context(
|
|
130
130
|
QUERY_STRING="x=10",
|
|
131
131
|
CONTENT_LENGTH="10",
|
|
132
132
|
CONTENT_TYPE="application/broken",
|
|
133
133
|
) as c:
|
|
134
|
-
|
|
135
|
-
assert
|
|
134
|
+
req = c.request
|
|
135
|
+
assert req.get("x", required=True) == "10"
|
|
136
136
|
with pytest.raises(exceptions.BadRequest):
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
req.get("y", required=True)
|
|
138
|
+
|
|
139
|
+
def test_get_does_type_conversion(self):
|
|
140
|
+
with context(
|
|
141
|
+
QUERY_STRING="x=10",
|
|
142
|
+
CONTENT_LENGTH="10",
|
|
143
|
+
CONTENT_TYPE="application/broken",
|
|
144
|
+
) as c:
|
|
145
|
+
req = c.request
|
|
146
|
+
assert req.get("x") == "10"
|
|
147
|
+
assert req.get("x", type=int) == 10
|
|
148
|
+
assert req.get("y", type=int) is None
|
|
149
|
+
assert req.get("y", "20", type=int) == "20"
|
|
139
150
|
|
|
140
151
|
def test_is_secure_returns_correct_value(self):
|
|
141
152
|
with context("https://example.org/") as c:
|
|
@@ -150,7 +161,7 @@ class TestRequestProperties(object):
|
|
|
150
161
|
def test_getint_raises_badrequest(self):
|
|
151
162
|
with pytest.raises(exceptions.BadRequest):
|
|
152
163
|
with context("http://example.org/") as c:
|
|
153
|
-
c.request.getint("a")
|
|
164
|
+
c.request.getint("a", required=True)
|
|
154
165
|
|
|
155
166
|
with pytest.raises(exceptions.BadRequest):
|
|
156
167
|
with context("http://example.org/?a=four") as c:
|
fresco/tests/test_response.py
CHANGED
|
@@ -87,6 +87,12 @@ class TestCannedResponses(object):
|
|
|
87
87
|
r = Response.redirect("http://bad.example.com/", "gloop", barfle=23)
|
|
88
88
|
assert r.get_header("Location") == "http://localhost/arfle/23"
|
|
89
89
|
|
|
90
|
+
def test_redirect_uses_fallback_for_empty_url(self):
|
|
91
|
+
app = FrescoApp()
|
|
92
|
+
with app.requestcontext():
|
|
93
|
+
r = Response.redirect("", "http://localhost/foo")
|
|
94
|
+
assert r.get_header("Location") == "http://localhost/foo"
|
|
95
|
+
|
|
90
96
|
def test_not_found(self):
|
|
91
97
|
with FrescoApp().requestcontext():
|
|
92
98
|
r = Response.not_found()
|
|
@@ -130,6 +136,16 @@ class TestCannedResponses(object):
|
|
|
130
136
|
assert r.status == "200 OK"
|
|
131
137
|
assert r.get_header("Content-Type") == "text/html"
|
|
132
138
|
|
|
139
|
+
def test_meta_refresh_to_view(self):
|
|
140
|
+
|
|
141
|
+
app = FrescoApp()
|
|
142
|
+
view = Mock(return_value=Response())
|
|
143
|
+
app.route("/arfle/<barfle:int>", GET, view)
|
|
144
|
+
with app.requestcontext():
|
|
145
|
+
r = Response.meta_refresh(view, barfle=42)
|
|
146
|
+
content = b"".join(r.content_iterator)
|
|
147
|
+
assert b'url=http://localhost/arfle/42"' in content
|
|
148
|
+
|
|
133
149
|
def test_json(self):
|
|
134
150
|
with FrescoApp().requestcontext():
|
|
135
151
|
r = Response.json(
|
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,
|
|
@@ -43,6 +47,10 @@ from fresco.routing import (
|
|
|
43
47
|
from . import fixtures
|
|
44
48
|
|
|
45
49
|
|
|
50
|
+
def dummyview() -> Response:
|
|
51
|
+
return Response()
|
|
52
|
+
|
|
53
|
+
|
|
46
54
|
def assert_method_bound_to(method, ob):
|
|
47
55
|
try:
|
|
48
56
|
assert method.__self__ is ob
|
|
@@ -114,6 +122,31 @@ class TestRouteConstructor(object):
|
|
|
114
122
|
with pytest.raises(ValueError):
|
|
115
123
|
Route("/", ["GET", "FOO"], lambda: None)
|
|
116
124
|
|
|
125
|
+
def test_ALL_kwarg_routes_all_methods(self):
|
|
126
|
+
def view():
|
|
127
|
+
return Response()
|
|
128
|
+
|
|
129
|
+
app = FrescoApp()
|
|
130
|
+
app.route("/x", ALL, view)
|
|
131
|
+
assert len(list(app.get_route_traversals("/x", GET))) == 1
|
|
132
|
+
assert len(list(app.get_route_traversals("/x", POST))) == 1
|
|
133
|
+
assert len(list(app.get_route_traversals("/x", OPTIONS))) == 1
|
|
134
|
+
|
|
135
|
+
app.route("/y", ALL=view)
|
|
136
|
+
assert len(list(app.get_route_traversals("/y", GET))) == 1
|
|
137
|
+
assert len(list(app.get_route_traversals("/y", POST))) == 1
|
|
138
|
+
assert len(list(app.get_route_traversals("/y", OPTIONS))) == 1
|
|
139
|
+
|
|
140
|
+
def test_multiple_methods_can_be_expressed_using_kwargs(self):
|
|
141
|
+
def view():
|
|
142
|
+
return Response()
|
|
143
|
+
|
|
144
|
+
app = FrescoApp()
|
|
145
|
+
app.route("/x", GET_POST=view)
|
|
146
|
+
assert len(list(app.get_route_traversals("/x", GET))) == 1
|
|
147
|
+
assert len(list(app.get_route_traversals("/x", POST))) == 1
|
|
148
|
+
assert len(list(app.get_route_traversals("/x", OPTIONS))) == 0
|
|
149
|
+
|
|
117
150
|
|
|
118
151
|
class TestRouteBeforeHooks(object):
|
|
119
152
|
def test_hook_is_called(self):
|
|
@@ -330,36 +363,36 @@ class TestPredicates(object):
|
|
|
330
363
|
|
|
331
364
|
class TestRouteNames(object):
|
|
332
365
|
def test_name_present_in_route_keys(self):
|
|
333
|
-
r = Route("/", GET, None, name="foo")
|
|
366
|
+
r = Route("/", GET, lambda: None, name="foo")
|
|
334
367
|
assert "foo" in list(r.route_keys())
|
|
335
368
|
|
|
336
369
|
def test_name_with_other_kwargs(self):
|
|
337
|
-
r = Route("/", GET, None, name="foo", x="bar")
|
|
370
|
+
r = Route("/", GET, lambda: None, name="foo", x="bar")
|
|
338
371
|
assert "foo" in list(r.route_keys())
|
|
339
372
|
|
|
340
373
|
def test_name_cannot_contain_colon(self):
|
|
341
374
|
with pytest.raises(ValueError):
|
|
342
|
-
Route("/", GET, None, name="foo:bar")
|
|
375
|
+
Route("/", GET, lambda: None, name="foo:bar")
|
|
343
376
|
|
|
344
377
|
|
|
345
378
|
class TestRouteCollection(object):
|
|
346
379
|
def test_it_adds_routes_from_constructor(self):
|
|
347
|
-
r1 = Route("/1", GET,
|
|
348
|
-
r2 = Route("/2", POST,
|
|
380
|
+
r1 = Route("/1", GET, dummyview, name="1")
|
|
381
|
+
r2 = Route("/2", POST, dummyview, name="2")
|
|
349
382
|
rc = RouteCollection([r1, r2])
|
|
350
383
|
assert [r.name for r in rc] == ["1", "2"]
|
|
351
384
|
|
|
352
385
|
def test_it_adds_routecollections_from_constructor(self):
|
|
353
|
-
r1 = Route("/", GET,
|
|
354
|
-
r2 = Route("/", POST,
|
|
355
|
-
r3 = Route("/", POST,
|
|
386
|
+
r1 = Route("/", GET, dummyview, name="1")
|
|
387
|
+
r2 = Route("/", POST, dummyview, name="2")
|
|
388
|
+
r3 = Route("/", POST, dummyview, name="3")
|
|
356
389
|
rc = RouteCollection([r1, RouteCollection([r2, r3])])
|
|
357
390
|
assert [r.name for r in rc] == ["1", "2", "3"]
|
|
358
391
|
|
|
359
392
|
def test_it_adds_dunderroutes_from_constructor(self):
|
|
360
|
-
r1 = Route("/", GET,
|
|
361
|
-
r2 = Route("/", POST,
|
|
362
|
-
r3 = Route("/", POST,
|
|
393
|
+
r1 = Route("/", GET, dummyview, name="1")
|
|
394
|
+
r2 = Route("/", POST, dummyview, name="2")
|
|
395
|
+
r3 = Route("/", POST, dummyview, name="3")
|
|
363
396
|
|
|
364
397
|
class A:
|
|
365
398
|
__routes__ = [r2, r3]
|
|
@@ -368,8 +401,8 @@ class TestRouteCollection(object):
|
|
|
368
401
|
assert [r.name for r in rc] == ["1", "2", "3"]
|
|
369
402
|
|
|
370
403
|
def test_get_routes_matches_on_method(self):
|
|
371
|
-
r_get = Route("/", GET,
|
|
372
|
-
r_post = Route("/", POST,
|
|
404
|
+
r_get = Route("/", GET, dummyview)
|
|
405
|
+
r_post = Route("/", POST, dummyview)
|
|
373
406
|
|
|
374
407
|
rc = RouteCollection([r_post, r_get])
|
|
375
408
|
|
|
@@ -377,8 +410,8 @@ class TestRouteCollection(object):
|
|
|
377
410
|
assert [r.route for r in rc.get_route_traversals("/", POST)] == [r_post]
|
|
378
411
|
|
|
379
412
|
def test_get_routes_matches_on_path(self):
|
|
380
|
-
r1 = Route("/1", GET,
|
|
381
|
-
r2 = Route("/2", GET,
|
|
413
|
+
r1 = Route("/1", GET, dummyview)
|
|
414
|
+
r2 = Route("/2", GET, dummyview)
|
|
382
415
|
|
|
383
416
|
rc = RouteCollection([r1, r2])
|
|
384
417
|
|
|
@@ -386,8 +419,8 @@ class TestRouteCollection(object):
|
|
|
386
419
|
assert [r.route for r in rc.get_route_traversals("/2", GET)] == [r2]
|
|
387
420
|
|
|
388
421
|
def test_get_routes_can_match_all_methods(self):
|
|
389
|
-
r1 = Route("/1", GET,
|
|
390
|
-
r2 = Route("/1", POST,
|
|
422
|
+
r1 = Route("/1", GET, dummyview)
|
|
423
|
+
r2 = Route("/1", POST, dummyview)
|
|
391
424
|
|
|
392
425
|
rc = RouteCollection([r1, r2])
|
|
393
426
|
assert [r.route for r in rc.get_route_traversals("/1", None)] == [
|
|
@@ -399,8 +432,8 @@ class TestRouteCollection(object):
|
|
|
399
432
|
a = RouteCollection()
|
|
400
433
|
b = RouteCollection()
|
|
401
434
|
|
|
402
|
-
a_route = Route("/harvey", GET,
|
|
403
|
-
b_route = Route("/harvey", GET,
|
|
435
|
+
a_route = Route("/harvey", GET, dummyview)
|
|
436
|
+
b_route = Route("/harvey", GET, dummyview)
|
|
404
437
|
|
|
405
438
|
a.add_route(a_route)
|
|
406
439
|
b.add_route(b_route)
|
|
@@ -411,7 +444,7 @@ class TestRouteCollection(object):
|
|
|
411
444
|
a.add_route(a_delegate_route)
|
|
412
445
|
b.add_route(b_delegate_route)
|
|
413
446
|
|
|
414
|
-
r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey",
|
|
447
|
+
r = next(a.get_route_traversals("/rabbit/hole/rabbit/harvey", "GET"))
|
|
415
448
|
|
|
416
449
|
assert r.collections_traversed == [
|
|
417
450
|
(a, "", a_delegate_route, (), {}, (), {}),
|
|
@@ -508,7 +541,9 @@ class TestRouteCollection(object):
|
|
|
508
541
|
]
|
|
509
542
|
|
|
510
543
|
def test_add_prefix_returns_prefixed_collection(self):
|
|
511
|
-
rc = RouteCollection(
|
|
544
|
+
rc = RouteCollection(
|
|
545
|
+
[Route("/fish", GET, dummyview), Route("/beans", GET, dummyview)]
|
|
546
|
+
)
|
|
512
547
|
prefixed = rc.add_prefix("/jelly")
|
|
513
548
|
assert [str(r.pattern) for r in prefixed] == [
|
|
514
549
|
"/jelly/fish",
|
|
@@ -611,15 +646,14 @@ class TestRouteCollection(object):
|
|
|
611
646
|
assert list(response.content_iterator) == [b"ok"]
|
|
612
647
|
|
|
613
648
|
def test_it_isolates_request_for_wsgi_app(self):
|
|
614
|
-
|
|
615
649
|
params: t.Dict[str, t.Any] = {}
|
|
616
650
|
outerapp = FrescoApp()
|
|
617
651
|
innerapp = FrescoApp()
|
|
618
652
|
innerapp.route("/y", GET=Response)
|
|
619
653
|
outerapp.route_wsgi("/x", innerapp)
|
|
620
654
|
|
|
621
|
-
innerapp.process_request(partial(params.__setitem__,
|
|
622
|
-
outerapp.process_request(partial(params.__setitem__,
|
|
655
|
+
innerapp.process_request(partial(params.__setitem__, "inner_request"))
|
|
656
|
+
outerapp.process_request(partial(params.__setitem__, "outer_request"))
|
|
623
657
|
|
|
624
658
|
with outerapp.requestcontext("/x/y"):
|
|
625
659
|
outerapp.view()
|
|
@@ -894,25 +928,25 @@ class TestRRoute:
|
|
|
894
928
|
|
|
895
929
|
class TestViewArgs(object):
|
|
896
930
|
def test_it_uses_args(self):
|
|
897
|
-
routes = RouteCollection([Route("/", GET,
|
|
931
|
+
routes = RouteCollection([Route("/", GET, dummyview, args=(1, 2))])
|
|
898
932
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
899
933
|
tms.InstanceOf(RouteTraversal, args=(1, 2))
|
|
900
934
|
]
|
|
901
935
|
|
|
902
936
|
def test_it_uses_view_args(self):
|
|
903
|
-
routes = RouteCollection([Route("/", GET,
|
|
937
|
+
routes = RouteCollection([Route("/", GET, dummyview, view_args=(1, 2))])
|
|
904
938
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
905
939
|
tms.InstanceOf(RouteTraversal, args=(1, 2))
|
|
906
940
|
]
|
|
907
941
|
|
|
908
942
|
def test_it_appends_args_extracted_from_path(self):
|
|
909
|
-
routes = RouteCollection([Route("/<:int>", GET,
|
|
943
|
+
routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1, 2))])
|
|
910
944
|
assert list(routes.get_route_traversals("/3", GET)) == [
|
|
911
945
|
tms.InstanceOf(RouteTraversal, args=(1, 2, 3))
|
|
912
946
|
]
|
|
913
947
|
|
|
914
948
|
def test_it_keeps_traversal_args_separate(self):
|
|
915
|
-
routes = RouteCollection([Route("/<:int>", GET,
|
|
949
|
+
routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1,))])
|
|
916
950
|
assert list(routes.get_route_traversals("/2", GET)) == [
|
|
917
951
|
tms.InstanceOf(
|
|
918
952
|
RouteTraversal,
|
|
@@ -928,25 +962,27 @@ class TestViewArgs(object):
|
|
|
928
962
|
|
|
929
963
|
class TestViewKwargs(object):
|
|
930
964
|
def test_it_reads_from_route_kwargs(self):
|
|
931
|
-
routes = RouteCollection([Route("/", GET,
|
|
965
|
+
routes = RouteCollection([Route("/", GET, dummyview, x=1)])
|
|
932
966
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
933
967
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
934
968
|
]
|
|
935
969
|
|
|
936
970
|
def test_it_reads_from_kwargs(self):
|
|
937
|
-
routes = RouteCollection([Route("/", GET,
|
|
971
|
+
routes = RouteCollection([Route("/", GET, dummyview, kwargs={"x": 1})])
|
|
938
972
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
939
973
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
940
974
|
]
|
|
941
975
|
|
|
942
976
|
def test_it_reads_from_view_kwargs(self):
|
|
943
|
-
routes = RouteCollection([Route("/", GET,
|
|
977
|
+
routes = RouteCollection([Route("/", GET, dummyview, view_kwargs={"x": 1})])
|
|
944
978
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
945
979
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
946
980
|
]
|
|
947
981
|
|
|
948
982
|
def test_it_keeps_traversal_kwargs_separate(self):
|
|
949
|
-
routes = RouteCollection(
|
|
983
|
+
routes = RouteCollection(
|
|
984
|
+
[Route("/<x:int>", GET, dummyview, view_kwargs={"y": 1})]
|
|
985
|
+
)
|
|
950
986
|
assert list(routes.get_route_traversals("/2", GET)) == [
|
|
951
987
|
tms.InstanceOf(
|
|
952
988
|
RouteTraversal,
|
|
@@ -1113,7 +1149,6 @@ class TestRouteCache(object):
|
|
|
1113
1149
|
|
|
1114
1150
|
|
|
1115
1151
|
class TestRouteAll:
|
|
1116
|
-
|
|
1117
1152
|
def test_route_all_matches_on_separator(self):
|
|
1118
1153
|
def view():
|
|
1119
1154
|
return Response()
|
|
@@ -1124,3 +1159,48 @@ class TestRouteAll:
|
|
|
1124
1159
|
assert len(list(app.get_route_traversals("/x", GET))) == 1
|
|
1125
1160
|
assert len(list(app.get_route_traversals("/x/y", GET))) == 1
|
|
1126
1161
|
assert len(list(app.get_route_traversals("/xy", GET))) == 0
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
class TestRequestParameter:
|
|
1165
|
+
def test_has_request_parameter(self):
|
|
1166
|
+
def a(request: Request):
|
|
1167
|
+
pass
|
|
1168
|
+
|
|
1169
|
+
def b(request: t.Optional[Request]):
|
|
1170
|
+
pass
|
|
1171
|
+
|
|
1172
|
+
def c(request: t.Union[Request, None]):
|
|
1173
|
+
pass
|
|
1174
|
+
|
|
1175
|
+
def d(request: t.Union[None, Request]):
|
|
1176
|
+
pass
|
|
1177
|
+
|
|
1178
|
+
def e(request: int):
|
|
1179
|
+
pass
|
|
1180
|
+
|
|
1181
|
+
def f(request):
|
|
1182
|
+
pass
|
|
1183
|
+
|
|
1184
|
+
assert _has_request_parameter(a) is True
|
|
1185
|
+
assert _has_request_parameter(b) is True
|
|
1186
|
+
assert _has_request_parameter(c) is True
|
|
1187
|
+
assert _has_request_parameter(d) is True
|
|
1188
|
+
assert _has_request_parameter(e) is False
|
|
1189
|
+
assert _has_request_parameter(f) is False
|
|
1190
|
+
|
|
1191
|
+
def test_request_is_provided_automatically(self):
|
|
1192
|
+
def a(request: Request):
|
|
1193
|
+
return Response("a")
|
|
1194
|
+
|
|
1195
|
+
def b():
|
|
1196
|
+
return Response("b")
|
|
1197
|
+
|
|
1198
|
+
app = FrescoApp()
|
|
1199
|
+
app.route("/a", GET=a)
|
|
1200
|
+
app.route("/b", GET=b)
|
|
1201
|
+
|
|
1202
|
+
with app.requestcontext("/a"):
|
|
1203
|
+
assert app.view().content == "a"
|
|
1204
|
+
|
|
1205
|
+
with app.requestcontext("/b"):
|
|
1206
|
+
assert app.view().content == "b"
|
fresco/tests/util/test_http.py
CHANGED
|
@@ -42,7 +42,7 @@ class TestParseQueryString(object):
|
|
|
42
42
|
return list(parse_querystring(value))
|
|
43
43
|
|
|
44
44
|
def test_empty(self):
|
|
45
|
-
self.p("") == []
|
|
45
|
+
assert self.p("") == []
|
|
46
46
|
|
|
47
47
|
def test_simple_key_value(self):
|
|
48
48
|
assert self.p("a=b") == [("a", "b")]
|
|
@@ -276,12 +276,10 @@ class TestParseFormEncodedData(object):
|
|
|
276
276
|
class TestEncodeMultipart(object):
|
|
277
277
|
def test_it_encodes_a_data_dict(self):
|
|
278
278
|
data, headers = encode_multipart([("foo", "bar baf")])
|
|
279
|
-
data = data.getvalue()
|
|
280
279
|
assert b'Content-Disposition: form-data; name="foo"\r\n\r\nbar baf' in data
|
|
281
280
|
|
|
282
281
|
def test_it_encodes_a_file_tuple(self):
|
|
283
282
|
data, headers = encode_multipart(files=[("foo", "foo.txt", "ascii", "bar")])
|
|
284
|
-
data = data.getvalue()
|
|
285
283
|
expected = (
|
|
286
284
|
b"Content-Disposition: form-data; "
|
|
287
285
|
b'name="foo"; filename="foo.txt"\r\n'
|
fresco/tests/util/test_urls.py
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from fresco.core import FrescoApp
|
|
16
16
|
from fresco.util.urls import normpath, make_query
|
|
17
17
|
from fresco.util.urls import is_safe_url
|
|
18
|
+
from fresco.util.urls import add_query
|
|
18
19
|
|
|
19
20
|
# Greek letters as unicode strings (require multi-byte representation in UTF-8)
|
|
20
21
|
alpha = b"\xce\xb1".decode("utf8")
|
|
@@ -174,3 +175,22 @@ class TestSafeURL(object):
|
|
|
174
175
|
]
|
|
175
176
|
for u in unsafe:
|
|
176
177
|
assert is_safe_url(u, allowed_hosts={"good.example.org"}) is False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestAddQuery:
|
|
181
|
+
|
|
182
|
+
def test_it_adds_query(self):
|
|
183
|
+
url = add_query("http://localhost", {"foo": "bar"})
|
|
184
|
+
assert url == "http://localhost?foo=bar"
|
|
185
|
+
|
|
186
|
+
def test_it_adds_query_from_list(self):
|
|
187
|
+
url = add_query("http://localhost", [("foo", "bar")])
|
|
188
|
+
assert url == "http://localhost?foo=bar"
|
|
189
|
+
|
|
190
|
+
def test_it_adds_query_from_keywords(self):
|
|
191
|
+
url = add_query("http://localhost", foo="bar")
|
|
192
|
+
assert url == "http://localhost?foo=bar"
|
|
193
|
+
|
|
194
|
+
def test_it_appends_query(self):
|
|
195
|
+
url = add_query("http://localhost?foo=bar", {"foo": "baz"})
|
|
196
|
+
assert url == "http://localhost?foo=bar&foo=baz"
|
fresco/types.py
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Optional
|
|
1
7
|
import typing as t
|
|
2
8
|
|
|
9
|
+
import fresco.response # noqa
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
QuerySpec = t.Union[
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
Mapping[str, Any],
|
|
14
|
+
Iterable[tuple[str, Any]]
|
|
15
|
+
]
|
|
16
|
+
ViewCallable = Callable[..., "fresco.response.Response"]
|
|
17
|
+
ExcInfo = tuple[type[BaseException], BaseException, TracebackType]
|
|
18
|
+
OptionalExcInfo = Optional[ExcInfo]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HeaderList = list[tuple[str, str]]
|
|
22
|
+
HeadersList = HeaderList
|
|
23
|
+
WSGIEnviron = dict[str, Any]
|
|
24
|
+
WriteCallable = Callable[[bytes], object]
|
|
25
|
+
StartResponse = Callable[[str, HeaderList, OptionalExcInfo], WriteCallable]
|
|
26
|
+
WSGIApplication = Callable[
|
|
27
|
+
[
|
|
28
|
+
WSGIEnviron,
|
|
29
|
+
StartResponse,
|
|
30
|
+
],
|
|
31
|
+
Iterable[bytes]
|
|
6
32
|
]
|
fresco/util/cache.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
+
from typing import Callable
|
|
2
3
|
from typing import Dict
|
|
3
4
|
from typing import List
|
|
4
5
|
import sys
|
|
@@ -46,7 +47,7 @@ def cache_generator(original_function, maxsize):
|
|
|
46
47
|
link[NEXT] = root
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def make_cache(original_function, maxsize=100):
|
|
50
|
+
def make_cache(original_function, maxsize=100) -> Callable[[Any], Any]:
|
|
50
51
|
"Create a cache around a function that takes a single argument"
|
|
51
52
|
c = cache_generator(original_function, maxsize)
|
|
52
53
|
next(c)
|