fresco 3.5.0__py3-none-any.whl → 3.6.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 +55 -56
- fresco/core.py +33 -23
- fresco/decorators.py +6 -3
- fresco/defaults.py +1 -0
- fresco/middleware.py +4 -4
- fresco/multidict.py +35 -51
- fresco/options.py +146 -75
- fresco/request.py +155 -34
- fresco/requestcontext.py +3 -0
- fresco/response.py +12 -9
- fresco/routeargs.py +23 -9
- fresco/routing.py +74 -56
- fresco/static.py +1 -1
- fresco/subrequests.py +1 -1
- fresco/tests/test_core.py +4 -4
- fresco/tests/test_multidict.py +2 -2
- fresco/tests/test_options.py +40 -16
- fresco/tests/test_request.py +21 -10
- fresco/tests/test_routing.py +36 -33
- fresco/tests/util/test_http.py +1 -3
- fresco/types.py +28 -2
- fresco/util/cache.py +2 -1
- fresco/util/http.py +66 -46
- fresco/util/urls.py +13 -11
- fresco/util/wsgi.py +15 -14
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/METADATA +3 -2
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/RECORD +30 -30
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/WHEEL +1 -1
- fresco/typing.py +0 -17
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info/licenses}/LICENSE.txt +0 -0
- {fresco-3.5.0.dist-info → fresco-3.6.0.dist-info}/top_level.txt +0 -0
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_routing.py
CHANGED
|
@@ -47,6 +47,10 @@ from fresco.routing import (
|
|
|
47
47
|
from . import fixtures
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def dummyview() -> Response:
|
|
51
|
+
return Response()
|
|
52
|
+
|
|
53
|
+
|
|
50
54
|
def assert_method_bound_to(method, ob):
|
|
51
55
|
try:
|
|
52
56
|
assert method.__self__ is ob
|
|
@@ -363,22 +367,22 @@ class TestRouteNames(object):
|
|
|
363
367
|
|
|
364
368
|
class TestRouteCollection(object):
|
|
365
369
|
def test_it_adds_routes_from_constructor(self):
|
|
366
|
-
r1 = Route("/1", GET,
|
|
367
|
-
r2 = Route("/2", POST,
|
|
370
|
+
r1 = Route("/1", GET, dummyview, name="1")
|
|
371
|
+
r2 = Route("/2", POST, dummyview, name="2")
|
|
368
372
|
rc = RouteCollection([r1, r2])
|
|
369
373
|
assert [r.name for r in rc] == ["1", "2"]
|
|
370
374
|
|
|
371
375
|
def test_it_adds_routecollections_from_constructor(self):
|
|
372
|
-
r1 = Route("/", GET,
|
|
373
|
-
r2 = Route("/", POST,
|
|
374
|
-
r3 = Route("/", POST,
|
|
376
|
+
r1 = Route("/", GET, dummyview, name="1")
|
|
377
|
+
r2 = Route("/", POST, dummyview, name="2")
|
|
378
|
+
r3 = Route("/", POST, dummyview, name="3")
|
|
375
379
|
rc = RouteCollection([r1, RouteCollection([r2, r3])])
|
|
376
380
|
assert [r.name for r in rc] == ["1", "2", "3"]
|
|
377
381
|
|
|
378
382
|
def test_it_adds_dunderroutes_from_constructor(self):
|
|
379
|
-
r1 = Route("/", GET,
|
|
380
|
-
r2 = Route("/", POST,
|
|
381
|
-
r3 = Route("/", POST,
|
|
383
|
+
r1 = Route("/", GET, dummyview, name="1")
|
|
384
|
+
r2 = Route("/", POST, dummyview, name="2")
|
|
385
|
+
r3 = Route("/", POST, dummyview, name="3")
|
|
382
386
|
|
|
383
387
|
class A:
|
|
384
388
|
__routes__ = [r2, r3]
|
|
@@ -387,8 +391,8 @@ class TestRouteCollection(object):
|
|
|
387
391
|
assert [r.name for r in rc] == ["1", "2", "3"]
|
|
388
392
|
|
|
389
393
|
def test_get_routes_matches_on_method(self):
|
|
390
|
-
r_get = Route("/", GET,
|
|
391
|
-
r_post = Route("/", POST,
|
|
394
|
+
r_get = Route("/", GET, dummyview)
|
|
395
|
+
r_post = Route("/", POST, dummyview)
|
|
392
396
|
|
|
393
397
|
rc = RouteCollection([r_post, r_get])
|
|
394
398
|
|
|
@@ -396,8 +400,8 @@ class TestRouteCollection(object):
|
|
|
396
400
|
assert [r.route for r in rc.get_route_traversals("/", POST)] == [r_post]
|
|
397
401
|
|
|
398
402
|
def test_get_routes_matches_on_path(self):
|
|
399
|
-
r1 = Route("/1", GET,
|
|
400
|
-
r2 = Route("/2", GET,
|
|
403
|
+
r1 = Route("/1", GET, dummyview)
|
|
404
|
+
r2 = Route("/2", GET, dummyview)
|
|
401
405
|
|
|
402
406
|
rc = RouteCollection([r1, r2])
|
|
403
407
|
|
|
@@ -405,8 +409,8 @@ class TestRouteCollection(object):
|
|
|
405
409
|
assert [r.route for r in rc.get_route_traversals("/2", GET)] == [r2]
|
|
406
410
|
|
|
407
411
|
def test_get_routes_can_match_all_methods(self):
|
|
408
|
-
r1 = Route("/1", GET,
|
|
409
|
-
r2 = Route("/1", POST,
|
|
412
|
+
r1 = Route("/1", GET, dummyview)
|
|
413
|
+
r2 = Route("/1", POST, dummyview)
|
|
410
414
|
|
|
411
415
|
rc = RouteCollection([r1, r2])
|
|
412
416
|
assert [r.route for r in rc.get_route_traversals("/1", None)] == [
|
|
@@ -418,8 +422,8 @@ class TestRouteCollection(object):
|
|
|
418
422
|
a = RouteCollection()
|
|
419
423
|
b = RouteCollection()
|
|
420
424
|
|
|
421
|
-
a_route = Route("/harvey", GET,
|
|
422
|
-
b_route = Route("/harvey", GET,
|
|
425
|
+
a_route = Route("/harvey", GET, dummyview)
|
|
426
|
+
b_route = Route("/harvey", GET, dummyview)
|
|
423
427
|
|
|
424
428
|
a.add_route(a_route)
|
|
425
429
|
b.add_route(b_route)
|
|
@@ -527,7 +531,9 @@ class TestRouteCollection(object):
|
|
|
527
531
|
]
|
|
528
532
|
|
|
529
533
|
def test_add_prefix_returns_prefixed_collection(self):
|
|
530
|
-
rc = RouteCollection(
|
|
534
|
+
rc = RouteCollection(
|
|
535
|
+
[Route("/fish", GET, dummyview), Route("/beans", GET, dummyview)]
|
|
536
|
+
)
|
|
531
537
|
prefixed = rc.add_prefix("/jelly")
|
|
532
538
|
assert [str(r.pattern) for r in prefixed] == [
|
|
533
539
|
"/jelly/fish",
|
|
@@ -630,15 +636,14 @@ class TestRouteCollection(object):
|
|
|
630
636
|
assert list(response.content_iterator) == [b"ok"]
|
|
631
637
|
|
|
632
638
|
def test_it_isolates_request_for_wsgi_app(self):
|
|
633
|
-
|
|
634
639
|
params: t.Dict[str, t.Any] = {}
|
|
635
640
|
outerapp = FrescoApp()
|
|
636
641
|
innerapp = FrescoApp()
|
|
637
642
|
innerapp.route("/y", GET=Response)
|
|
638
643
|
outerapp.route_wsgi("/x", innerapp)
|
|
639
644
|
|
|
640
|
-
innerapp.process_request(partial(params.__setitem__,
|
|
641
|
-
outerapp.process_request(partial(params.__setitem__,
|
|
645
|
+
innerapp.process_request(partial(params.__setitem__, "inner_request"))
|
|
646
|
+
outerapp.process_request(partial(params.__setitem__, "outer_request"))
|
|
642
647
|
|
|
643
648
|
with outerapp.requestcontext("/x/y"):
|
|
644
649
|
outerapp.view()
|
|
@@ -913,25 +918,25 @@ class TestRRoute:
|
|
|
913
918
|
|
|
914
919
|
class TestViewArgs(object):
|
|
915
920
|
def test_it_uses_args(self):
|
|
916
|
-
routes = RouteCollection([Route("/", GET,
|
|
921
|
+
routes = RouteCollection([Route("/", GET, dummyview, args=(1, 2))])
|
|
917
922
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
918
923
|
tms.InstanceOf(RouteTraversal, args=(1, 2))
|
|
919
924
|
]
|
|
920
925
|
|
|
921
926
|
def test_it_uses_view_args(self):
|
|
922
|
-
routes = RouteCollection([Route("/", GET,
|
|
927
|
+
routes = RouteCollection([Route("/", GET, dummyview, view_args=(1, 2))])
|
|
923
928
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
924
929
|
tms.InstanceOf(RouteTraversal, args=(1, 2))
|
|
925
930
|
]
|
|
926
931
|
|
|
927
932
|
def test_it_appends_args_extracted_from_path(self):
|
|
928
|
-
routes = RouteCollection([Route("/<:int>", GET,
|
|
933
|
+
routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1, 2))])
|
|
929
934
|
assert list(routes.get_route_traversals("/3", GET)) == [
|
|
930
935
|
tms.InstanceOf(RouteTraversal, args=(1, 2, 3))
|
|
931
936
|
]
|
|
932
937
|
|
|
933
938
|
def test_it_keeps_traversal_args_separate(self):
|
|
934
|
-
routes = RouteCollection([Route("/<:int>", GET,
|
|
939
|
+
routes = RouteCollection([Route("/<:int>", GET, dummyview, view_args=(1,))])
|
|
935
940
|
assert list(routes.get_route_traversals("/2", GET)) == [
|
|
936
941
|
tms.InstanceOf(
|
|
937
942
|
RouteTraversal,
|
|
@@ -947,25 +952,27 @@ class TestViewArgs(object):
|
|
|
947
952
|
|
|
948
953
|
class TestViewKwargs(object):
|
|
949
954
|
def test_it_reads_from_route_kwargs(self):
|
|
950
|
-
routes = RouteCollection([Route("/", GET,
|
|
955
|
+
routes = RouteCollection([Route("/", GET, dummyview, x=1)])
|
|
951
956
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
952
957
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
953
958
|
]
|
|
954
959
|
|
|
955
960
|
def test_it_reads_from_kwargs(self):
|
|
956
|
-
routes = RouteCollection([Route("/", GET,
|
|
961
|
+
routes = RouteCollection([Route("/", GET, dummyview, kwargs={"x": 1})])
|
|
957
962
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
958
963
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
959
964
|
]
|
|
960
965
|
|
|
961
966
|
def test_it_reads_from_view_kwargs(self):
|
|
962
|
-
routes = RouteCollection([Route("/", GET,
|
|
967
|
+
routes = RouteCollection([Route("/", GET, dummyview, view_kwargs={"x": 1})])
|
|
963
968
|
assert list(routes.get_route_traversals("/", GET)) == [
|
|
964
969
|
tms.InstanceOf(RouteTraversal, kwargs={"x": 1})
|
|
965
970
|
]
|
|
966
971
|
|
|
967
972
|
def test_it_keeps_traversal_kwargs_separate(self):
|
|
968
|
-
routes = RouteCollection(
|
|
973
|
+
routes = RouteCollection(
|
|
974
|
+
[Route("/<x:int>", GET, dummyview, view_kwargs={"y": 1})]
|
|
975
|
+
)
|
|
969
976
|
assert list(routes.get_route_traversals("/2", GET)) == [
|
|
970
977
|
tms.InstanceOf(
|
|
971
978
|
RouteTraversal,
|
|
@@ -1132,7 +1139,6 @@ class TestRouteCache(object):
|
|
|
1132
1139
|
|
|
1133
1140
|
|
|
1134
1141
|
class TestRouteAll:
|
|
1135
|
-
|
|
1136
1142
|
def test_route_all_matches_on_separator(self):
|
|
1137
1143
|
def view():
|
|
1138
1144
|
return Response()
|
|
@@ -1145,10 +1151,8 @@ class TestRouteAll:
|
|
|
1145
1151
|
assert len(list(app.get_route_traversals("/xy", GET))) == 0
|
|
1146
1152
|
|
|
1147
1153
|
|
|
1148
|
-
class
|
|
1149
|
-
|
|
1154
|
+
class TestRequestParameter:
|
|
1150
1155
|
def test_has_request_parameter(self):
|
|
1151
|
-
|
|
1152
1156
|
def a(request: Request):
|
|
1153
1157
|
pass
|
|
1154
1158
|
|
|
@@ -1175,7 +1179,6 @@ class TestRequestParamter:
|
|
|
1175
1179
|
assert _has_request_parameter(f) is False
|
|
1176
1180
|
|
|
1177
1181
|
def test_request_is_provided_automatically(self):
|
|
1178
|
-
|
|
1179
1182
|
def a(request: Request):
|
|
1180
1183
|
return Response("a")
|
|
1181
1184
|
|
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/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)
|
fresco/util/http.py
CHANGED
|
@@ -20,6 +20,8 @@ Utilities for working with data on the HTTP level
|
|
|
20
20
|
from binascii import hexlify
|
|
21
21
|
from collections import namedtuple
|
|
22
22
|
from collections import deque
|
|
23
|
+
from collections.abc import Collection
|
|
24
|
+
from collections.abc import Mapping
|
|
23
25
|
from email.header import Header
|
|
24
26
|
from email.message import Message
|
|
25
27
|
from email.parser import BytesFeedParser
|
|
@@ -43,6 +45,7 @@ from urllib.parse import unquote_plus
|
|
|
43
45
|
from shutil import copyfileobj
|
|
44
46
|
|
|
45
47
|
import fresco
|
|
48
|
+
from fresco.defaults import DEFAULT_CHARSET
|
|
46
49
|
from fresco.exceptions import RequestParseError
|
|
47
50
|
from fresco.util.io import io_iterator
|
|
48
51
|
from fresco.util.io import ByteIterator
|
|
@@ -90,23 +93,23 @@ def get_content_type_info(
|
|
|
90
93
|
|
|
91
94
|
|
|
92
95
|
class TooBig(RequestParseError):
|
|
93
|
-
"""
|
|
96
|
+
"""
|
|
94
97
|
Request body is too big
|
|
95
98
|
"""
|
|
96
99
|
|
|
97
100
|
def __init__(self, *args, **kwargs):
|
|
98
101
|
super(TooBig, self).__init__(*args, **kwargs)
|
|
99
|
-
self.response = fresco.
|
|
102
|
+
self.response = fresco.Response.payload_too_large()
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
class MissingContentLength(RequestParseError):
|
|
103
|
-
"""
|
|
106
|
+
"""
|
|
104
107
|
No ``Content-Length`` header given
|
|
105
108
|
"""
|
|
106
109
|
|
|
107
110
|
def __init__(self, *args, **kwargs):
|
|
108
111
|
super(MissingContentLength, self).__init__(*args, **kwargs)
|
|
109
|
-
self.response = fresco.
|
|
112
|
+
self.response = fresco.Response.length_required()
|
|
110
113
|
|
|
111
114
|
|
|
112
115
|
def parse_parameters(s, preserve_backslashes=False) -> Dict[str, str]:
|
|
@@ -212,7 +215,7 @@ def parse_querystring(
|
|
|
212
215
|
|
|
213
216
|
:param data: The query string to parse.
|
|
214
217
|
:param charset: Character encoding used to decode values. If not specified,
|
|
215
|
-
``fresco.DEFAULT_CHARSET`` will be used.
|
|
218
|
+
``fresco.defaults.DEFAULT_CHARSET`` will be used.
|
|
216
219
|
|
|
217
220
|
:param keep_blank_values: if True, keys without associated values will be
|
|
218
221
|
returned as empty strings. if False, no key,
|
|
@@ -223,7 +226,7 @@ def parse_querystring(
|
|
|
223
226
|
"""
|
|
224
227
|
|
|
225
228
|
if charset is None:
|
|
226
|
-
charset =
|
|
229
|
+
charset = DEFAULT_CHARSET
|
|
227
230
|
|
|
228
231
|
result: List[Tuple[str, str]] = []
|
|
229
232
|
append = result.append
|
|
@@ -275,7 +278,7 @@ def parse_post(
|
|
|
275
278
|
ct, charset, ct_params = get_content_type_info(
|
|
276
279
|
environ,
|
|
277
280
|
"application/x-www-form-urlencoded",
|
|
278
|
-
default_charset or
|
|
281
|
+
default_charset or DEFAULT_CHARSET,
|
|
279
282
|
)
|
|
280
283
|
|
|
281
284
|
try:
|
|
@@ -454,14 +457,15 @@ def parse_multipart(
|
|
|
454
457
|
f.close()
|
|
455
458
|
raise
|
|
456
459
|
|
|
457
|
-
close: Optional[Callable]
|
|
460
|
+
close: Optional[Callable] # type: ignore
|
|
458
461
|
if open_files:
|
|
459
462
|
|
|
460
463
|
def close():
|
|
461
464
|
for f in open_files:
|
|
462
465
|
f.close()
|
|
466
|
+
|
|
463
467
|
else:
|
|
464
|
-
close = None
|
|
468
|
+
close = None # type: ignore
|
|
465
469
|
|
|
466
470
|
return fields, close
|
|
467
471
|
|
|
@@ -554,7 +558,6 @@ def read_until(
|
|
|
554
558
|
return
|
|
555
559
|
|
|
556
560
|
def remainder():
|
|
557
|
-
nonlocal buf
|
|
558
561
|
if buf:
|
|
559
562
|
yield buf
|
|
560
563
|
yield from stream
|
|
@@ -591,7 +594,21 @@ class FileUpload(object):
|
|
|
591
594
|
copyfileobj(self.file, fileob)
|
|
592
595
|
|
|
593
596
|
|
|
594
|
-
def encode_multipart(
|
|
597
|
+
def encode_multipart(
|
|
598
|
+
data: Optional[
|
|
599
|
+
Union[
|
|
600
|
+
Mapping[str, str],
|
|
601
|
+
Collection[tuple[str, str]],
|
|
602
|
+
]
|
|
603
|
+
] = None,
|
|
604
|
+
files: Optional[
|
|
605
|
+
Collection[
|
|
606
|
+
tuple[str, str, str, Union[bytes, t.Iterable[bytes], t.BinaryIO]]
|
|
607
|
+
]
|
|
608
|
+
] = None,
|
|
609
|
+
charset="UTF-8",
|
|
610
|
+
**kwargs
|
|
611
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
595
612
|
"""
|
|
596
613
|
Encode ``data`` using multipart/form-data encoding, returning a tuple
|
|
597
614
|
of ``(<encoded data>, <environ items>)``.
|
|
@@ -602,7 +619,7 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
|
|
|
602
619
|
:param charset: Encoding used for any string values encountered in
|
|
603
620
|
``data``
|
|
604
621
|
|
|
605
|
-
:param files:
|
|
622
|
+
:param files: collection of ``(name, filename, content_type, data)`` tuples.
|
|
606
623
|
``data`` may be either a byte string, iterator or
|
|
607
624
|
file-like object.
|
|
608
625
|
|
|
@@ -614,45 +631,22 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
|
|
|
614
631
|
dict.
|
|
615
632
|
"""
|
|
616
633
|
|
|
617
|
-
def header_block(name):
|
|
618
|
-
return [("Content-Disposition", 'form-data; name="%s"' % (name,))]
|
|
619
|
-
|
|
620
|
-
def file_header_block(name, filename, content_type):
|
|
621
|
-
return [
|
|
622
|
-
(
|
|
623
|
-
"Content-Disposition",
|
|
624
|
-
'form-data; name="%s"; filename="%s"' % (name, filename),
|
|
625
|
-
),
|
|
626
|
-
("Content-Type", content_type),
|
|
627
|
-
]
|
|
628
|
-
|
|
629
|
-
def write_payload(stream, data):
|
|
630
|
-
"Write ``data`` to ``stream``, encoding as required"
|
|
631
|
-
if hasattr(data, "read"):
|
|
632
|
-
copyfileobj(data, stream)
|
|
633
|
-
elif isinstance(data, bytes):
|
|
634
|
-
stream.write(data)
|
|
635
|
-
elif isinstance(data, str):
|
|
636
|
-
stream.write(data.encode(charset))
|
|
637
|
-
else:
|
|
638
|
-
raise ValueError(data)
|
|
639
|
-
|
|
640
|
-
if data is None:
|
|
641
|
-
data = {}
|
|
642
|
-
|
|
643
634
|
if files is None:
|
|
644
635
|
files = []
|
|
645
636
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
637
|
+
data_items: Iterable[tuple[str, str]]
|
|
638
|
+
if data is None:
|
|
639
|
+
data_items = []
|
|
640
|
+
elif isinstance(data, Mapping):
|
|
641
|
+
data_items = iter(data.items()) # type: ignore
|
|
642
|
+
else:
|
|
643
|
+
data_items = data
|
|
650
644
|
|
|
651
|
-
|
|
645
|
+
data_items = chain(data_items, kwargs.items())
|
|
652
646
|
|
|
653
647
|
boundary = b"-------" + hexlify(os.urandom(16))
|
|
654
648
|
alldata = chain(
|
|
655
|
-
((header_block(k), payload) for k, payload in
|
|
649
|
+
((header_block(k), payload) for k, payload in data_items),
|
|
656
650
|
((file_header_block(k, fn, ct), payload) for k, fn, ct, payload in files),
|
|
657
651
|
)
|
|
658
652
|
|
|
@@ -664,7 +658,7 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
|
|
|
664
658
|
for name, value in headers:
|
|
665
659
|
post_data.write("{0}: {1}\r\n".format(name, value).encode("ascii"))
|
|
666
660
|
post_data.write(CRLF)
|
|
667
|
-
write_payload(post_data, payload)
|
|
661
|
+
write_payload(post_data, payload, charset)
|
|
668
662
|
post_data.write(b"\r\n--" + boundary)
|
|
669
663
|
post_data.write(b"--\r\n")
|
|
670
664
|
length = post_data.tell()
|
|
@@ -676,4 +670,30 @@ def encode_multipart(data=None, files=None, charset="UTF-8", **kwargs):
|
|
|
676
670
|
),
|
|
677
671
|
}
|
|
678
672
|
|
|
679
|
-
return (post_data, wsgienv)
|
|
673
|
+
return (post_data.getvalue(), wsgienv)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def header_block(name):
|
|
677
|
+
return [("Content-Disposition", 'form-data; name="%s"' % (name,))]
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def file_header_block(name, filename, content_type):
|
|
681
|
+
return [
|
|
682
|
+
(
|
|
683
|
+
"Content-Disposition",
|
|
684
|
+
'form-data; name="%s"; filename="%s"' % (name, filename),
|
|
685
|
+
),
|
|
686
|
+
("Content-Type", content_type),
|
|
687
|
+
]
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def write_payload(stream, data, charset):
|
|
691
|
+
"Write ``data`` to ``stream``, encoding as required"
|
|
692
|
+
if hasattr(data, "read"):
|
|
693
|
+
copyfileobj(data, stream)
|
|
694
|
+
elif isinstance(data, bytes):
|
|
695
|
+
stream.write(data)
|
|
696
|
+
elif isinstance(data, str):
|
|
697
|
+
stream.write(data.encode(charset))
|
|
698
|
+
else:
|
|
699
|
+
raise ValueError(data)
|
fresco/util/urls.py
CHANGED
|
@@ -27,7 +27,7 @@ import posixpath
|
|
|
27
27
|
import re
|
|
28
28
|
import typing as t
|
|
29
29
|
|
|
30
|
-
import
|
|
30
|
+
from fresco.defaults import DEFAULT_CHARSET
|
|
31
31
|
from fresco.multidict import MultiDict
|
|
32
32
|
from fresco.types import QuerySpec
|
|
33
33
|
|
|
@@ -174,13 +174,13 @@ def make_query(
|
|
|
174
174
|
data: QuerySpec = [],
|
|
175
175
|
separator: str = "&",
|
|
176
176
|
charset: t.Optional[str] = None,
|
|
177
|
-
**kwargs,
|
|
177
|
+
**kwargs: t.Any,
|
|
178
178
|
) -> str:
|
|
179
179
|
"""
|
|
180
180
|
Return a query string formed from the given dictionary data.
|
|
181
181
|
|
|
182
182
|
If no encoding is given, unicode values are encoded using the character set
|
|
183
|
-
specified by ``fresco.DEFAULT_CHARSET``.
|
|
183
|
+
specified by ``fresco.defaults.DEFAULT_CHARSET``.
|
|
184
184
|
|
|
185
185
|
Basic usage::
|
|
186
186
|
|
|
@@ -210,46 +210,48 @@ def make_query(
|
|
|
210
210
|
:param charset: encoding to be used for unicode values
|
|
211
211
|
:rtype: str
|
|
212
212
|
"""
|
|
213
|
-
items: t.Iterable[
|
|
213
|
+
items: t.Iterable[tuple[str, t.Any]]
|
|
214
214
|
if isinstance(data, MultiDict):
|
|
215
215
|
items = data.allitems()
|
|
216
216
|
elif isinstance(data, Mapping):
|
|
217
|
-
items = data.items()
|
|
217
|
+
items = data.items() # type: ignore
|
|
218
218
|
else:
|
|
219
219
|
items = data
|
|
220
220
|
if kwargs:
|
|
221
221
|
items = chain(items, kwargs.items())
|
|
222
222
|
|
|
223
223
|
if charset is None:
|
|
224
|
-
charset =
|
|
224
|
+
charset = DEFAULT_CHARSET
|
|
225
225
|
|
|
226
|
-
pairs:
|
|
226
|
+
pairs: list[str] = []
|
|
227
227
|
append = pairs.append
|
|
228
228
|
for k, v in items:
|
|
229
229
|
if isinstance(v, (str, bytes)):
|
|
230
230
|
append(f"{quote_plus(k, charset)}={quote_plus(v, charset)}")
|
|
231
|
+
elif v is None:
|
|
232
|
+
pass
|
|
231
233
|
elif hasattr(v, "__iter__"):
|
|
232
234
|
for v in v:
|
|
233
235
|
append(f"{quote_plus(k, charset)}={quote_plus(str(v), charset)}")
|
|
234
|
-
|
|
236
|
+
else:
|
|
235
237
|
append(f"{quote_plus(k, charset)}={quote_plus(str(v), charset)}")
|
|
236
238
|
return separator.join(pairs)
|
|
237
239
|
|
|
238
240
|
|
|
239
241
|
def _qs_frag(key, value, charset=None):
|
|
240
|
-
"""
|
|
242
|
+
"""
|
|
241
243
|
Return a fragment of a query string in the format 'key=value'::
|
|
242
244
|
|
|
243
245
|
>>> _qs_frag('search-by', 'author, editor')
|
|
244
246
|
'search-by=author%2C+editor'
|
|
245
247
|
|
|
246
248
|
If no encoding is specified, unicode values are encoded using the character
|
|
247
|
-
set specified by ``fresco.DEFAULT_CHARSET``.
|
|
249
|
+
set specified by ``fresco.defaults.DEFAULT_CHARSET``.
|
|
248
250
|
|
|
249
251
|
:rtype: str
|
|
250
252
|
"""
|
|
251
253
|
if charset is None:
|
|
252
|
-
charset =
|
|
254
|
+
charset = DEFAULT_CHARSET
|
|
253
255
|
|
|
254
256
|
key = str(key).encode(charset)
|
|
255
257
|
value = str(value).encode(charset)
|