fresco 3.3.2__py3-none-any.whl → 3.3.4__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.
@@ -0,0 +1,323 @@
1
+ # encoding=UTF-8
2
+ # Copyright 2015 Oliver Cope
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # See LICENSE.txt for terms of redistribution and use.
17
+
18
+ from io import BytesIO
19
+ from urllib.parse import quote
20
+ import re
21
+
22
+ from hypothesis import given, strategies as st
23
+ import pytest
24
+
25
+ from fresco import FrescoApp
26
+ from fresco import context
27
+ from fresco import Response
28
+ from fresco.exceptions import RequestParseError
29
+ from fresco.routing import POST
30
+ from fresco.util.http import FileUpload
31
+ from fresco.util.http import PostParser
32
+ from fresco.util.http import encode_multipart
33
+ from fresco.util.http import parse_parameters
34
+ from fresco.util.http import parse_post
35
+ from fresco.util.http import parse_querystring
36
+
37
+ from . import form_data
38
+
39
+
40
+ class TestParseQueryString(object):
41
+ def p(self, value):
42
+ return list(parse_querystring(value))
43
+
44
+ def test_empty(self):
45
+ self.p("") == []
46
+
47
+ def test_simple_key_value(self):
48
+ assert self.p("a=b") == [("a", "b")]
49
+
50
+ def test_key_with_space(self):
51
+ assert self.p("a+b=c") == [("a b", "c")]
52
+
53
+ def test_value_with_space(self):
54
+ assert self.p("a=b+c") == [("a", "b c")]
55
+
56
+ def test_double_equals(self):
57
+ assert self.p("a==b") == [("a", "=b")]
58
+
59
+ def test_escaped_chars(self):
60
+ assert self.p("%20==c%3D") == [(" ", "=c=")]
61
+
62
+ def test_charset(self):
63
+ assert self.p("a=el%20ni%C3%B1o") == [("a", "el niño")]
64
+
65
+
66
+ class TestParseMultipart(object):
67
+ def _make_env(self, data: bytes):
68
+ data = b"----XX\r\n" + data + b"----XX--\r\n"
69
+ return (
70
+ BytesIO(data),
71
+ {
72
+ "CONTENT_LENGTH": len(data),
73
+ "CONTENT_TYPE": "multipart/form-data; boundary=--XX",
74
+ },
75
+ )
76
+
77
+ def test_multipart(self):
78
+ for data in form_data.multipart_samples:
79
+ io = BytesIO(data["data"]) # type: ignore
80
+ io.seek(0)
81
+ environ = {
82
+ "CONTENT_LENGTH": data["content_length"],
83
+ "CONTENT_TYPE": data["content_type"],
84
+ }
85
+ with PostParser(environ, io, "UTF-8") as parsed:
86
+ parsed = sorted(list(parsed))
87
+
88
+ assert [name for name, value in parsed] == [
89
+ "empty-text-input",
90
+ "file-upload",
91
+ "text-input-ascii",
92
+ "text-input-unicode",
93
+ ]
94
+
95
+ assert parsed[0] == ("empty-text-input", "")
96
+ assert parsed[2] == ("text-input-ascii", "abcdef")
97
+ assert parsed[3] == (
98
+ "text-input-unicode",
99
+ b"\xce\xb1\xce\xb2\xce\xb3\xce\xb4".decode("utf8"),
100
+ )
101
+
102
+ fieldname, fileupload = parsed[1]
103
+ assert isinstance(fileupload, FileUpload)
104
+ assert fieldname == "file-upload"
105
+ assert fileupload.filename == "test.data"
106
+ assert fileupload.headers["content-type"] == "application/octet-stream"
107
+ assert fileupload.file.read() == form_data.FILE_UPLOAD_DATA
108
+
109
+ def test_non_ascii_headers(self):
110
+ """
111
+ Ensure that headers containing non-ascii characters are handled.
112
+ NB the stdlib email.parser module that we use for parsing replaces
113
+ any 8-bit characters with the unicode replacement character.
114
+ """
115
+ filename = "café.txt".encode("utf8")
116
+ data = (
117
+ b"----XXXXXX\r\n"
118
+ b'Content-Disposition: form-data; name="u"; '
119
+ b'filename="' + filename + b'"\r\n'
120
+ b"Content-Type: application/octet-stream\r\n\r\n"
121
+ b"1234567890\r\n"
122
+ b"----XXXXXX--\r\n"
123
+ )
124
+ environ = {
125
+ "CONTENT_LENGTH": len(data),
126
+ "CONTENT_TYPE": "multipart/form-data; boundary=--XXXXXX",
127
+ }
128
+ f = BytesIO(data)
129
+ with PostParser(environ, f, "UTF-8") as parsed:
130
+ fieldname, fileupload = list(parsed)[0]
131
+ assert isinstance(fileupload, FileUpload)
132
+ assert fileupload.filename == "caf\uFFFD\uFFFD.txt"
133
+
134
+ def test_malformed_headers_raise_exceptions(self):
135
+ # Missing '=' in parameter
136
+ data, env = self._make_env(
137
+ b"Content-Disposition: form-data; name; \r\n\r\n" b"1234567890\r\n"
138
+ )
139
+ with pytest.raises(RequestParseError):
140
+ list(parse_post(env, data))
141
+
142
+ def test_fileupload_too_big(self):
143
+ """\
144
+ Verify that multipart/form-data encoded POST data raises an exception
145
+ if the total data size exceeds request.MAX_SIZE bytes
146
+ """
147
+
148
+ def view():
149
+ request = context.request
150
+ request.MAX_MULTIPART_SIZE = 500
151
+ request.get("f1")
152
+ return Response(["ok"])
153
+
154
+ app = FrescoApp()
155
+ app.route("/", POST, view)
156
+
157
+ with app.requestcontext_post(
158
+ "/", files=[("f1", "filename.txt", "text/plain", "x" * 1000)]
159
+ ):
160
+ assert app.view().status == "413 Payload Too Large"
161
+
162
+ with app.requestcontext_post(
163
+ "/",
164
+ files=[("f1", "filename.txt", "text/plain", "x" * 400)],
165
+ data={"f2": "x" * 101},
166
+ ):
167
+ assert app.view().status == "413 Payload Too Large"
168
+
169
+ def test_fileupload_with_invalid_content_length(self):
170
+ def view():
171
+ request = context.request
172
+ request.get("f1")
173
+ return Response(["ok"])
174
+
175
+ app = FrescoApp()
176
+ app.route("/", POST, view)
177
+
178
+ with app.requestcontext_post(
179
+ "/", files=[("f1", "filename.txt", "text/plain", "x" * 1000)]
180
+ ) as c:
181
+ c.request.environ["CONTENT_LENGTH"] = str("500")
182
+ assert app.view().status == "400 Bad Request"
183
+
184
+ def test_multipart_field_too_big(self):
185
+ """
186
+ Verify that multipart/form-data encoded POST data raises an exception
187
+ if it contains a single field exceeding request.MAX_SIZE bytes
188
+ """
189
+
190
+ def view():
191
+ request = context.request
192
+ request.MAX_MULTIPART_SIZE = 500
193
+ request.MAX_SIZE = 100
194
+ request.get("f1")
195
+ return Response(["ok"])
196
+
197
+ app = FrescoApp()
198
+ app.route("/", POST, view)
199
+
200
+ with app.requestcontext_post("/", multipart=True, data=[("f1", "x" * 200)]):
201
+ assert app.view().status == "413 Payload Too Large"
202
+
203
+
204
+ class TestParseFormEncodedData(object):
205
+ char_latin1 = b"\xa3"
206
+ char_utf8 = b"\xc2\xa3"
207
+ char = char_latin1.decode("latin1")
208
+
209
+ assert char_latin1.decode("latin1") == char_utf8.decode("utf8") == char
210
+
211
+ def test_formencoded_data_too_big(self):
212
+ """
213
+ Verify that application/x-www-form-urlencoded POST data raises an
214
+ exception if it exceeds request.MAX_SIZE bytes
215
+ """
216
+
217
+ def view():
218
+ request = context.request
219
+ request.MAX_SIZE = 100
220
+ request.get("f1")
221
+ return Response(["ok"])
222
+
223
+ app = FrescoApp()
224
+ app.route("/", POST, view)
225
+
226
+ with app.requestcontext_post("/", data=[("f1", "x" * 200)]):
227
+ assert app.view().status == "413 Payload Too Large"
228
+
229
+ def test_posted_data_contains_non_ascii_chars(self):
230
+ """
231
+ Verify that it raises an exception if POST data contains invalid
232
+ characters.
233
+ """
234
+
235
+ def view():
236
+ context.request.get("f1")
237
+
238
+ app = FrescoApp()
239
+ app.route("/", POST, view)
240
+
241
+ data = "foo=bár".encode("utf8")
242
+
243
+ with app.requestcontext(
244
+ "/",
245
+ REQUEST_METHOD="POST",
246
+ CONTENT_LENGTH=str(len(data)),
247
+ CONTENT_TYPE="application/x-www-form-urlencoded",
248
+ wsgi_input=data,
249
+ ):
250
+ response = app.view()
251
+ assert response.status == "400 Bad Request"
252
+
253
+ def test_non_utf8_data_posted(self):
254
+ data = b"char=" + quote(self.char_latin1).encode("ascii")
255
+ env = {
256
+ "REQUEST_METHOD": "POST",
257
+ "CONTENT_LENGTH": str(len(data)),
258
+ "wsgi.input": BytesIO(data),
259
+ }
260
+
261
+ with FrescoApp().requestcontext(environ=env) as c:
262
+ request = c.request
263
+ request.charset = "latin1"
264
+ assert request.form["char"] == self.char
265
+
266
+ def test_non_utf8_data_getted(self):
267
+ data = "char=" + quote(self.char_latin1)
268
+ env = {"REQUEST_METHOD": "GET", "QUERY_STRING": data}
269
+
270
+ with FrescoApp().requestcontext(environ=env) as c:
271
+ request = c.request
272
+ request.charset = "latin1"
273
+ assert request.query["char"] == self.char
274
+
275
+
276
+ class TestEncodeMultipart(object):
277
+ def test_it_encodes_a_data_dict(self):
278
+ data, headers = encode_multipart([("foo", "bar baf")])
279
+ data = data.getvalue()
280
+ assert b'Content-Disposition: form-data; name="foo"\r\n\r\nbar baf' in data
281
+
282
+ def test_it_encodes_a_file_tuple(self):
283
+ data, headers = encode_multipart(files=[("foo", "foo.txt", "ascii", "bar")])
284
+ data = data.getvalue()
285
+ expected = (
286
+ b"Content-Disposition: form-data; "
287
+ b'name="foo"; filename="foo.txt"\r\n'
288
+ b"Content-Type: ascii\r\n"
289
+ b"\r\n"
290
+ b"bar"
291
+ )
292
+ assert expected in data
293
+
294
+
295
+ class TestParseParameters(object):
296
+ token = st.text(
297
+ alphabet="".join(
298
+ chr(c) for c in range(33, 127) if chr(c) not in '()<>@,;:\\/[]?="'
299
+ ),
300
+ min_size=1,
301
+ )
302
+
303
+ @given(items=st.lists(st.tuples(token, token)))
304
+ def test_parse_tokens(self, items):
305
+ s = ";".join("{}={}".format(k, v) for k, v in items)
306
+ assert parse_parameters(s) == dict(items)
307
+
308
+ @given(items=st.lists(st.tuples(token, st.text(min_size=1))))
309
+ def test_parse_parameters(self, items):
310
+ def escape(s):
311
+ return re.sub(r'(["\\\r])', r"\\\1", s)
312
+
313
+ s = ";".join('{}="{}"'.format(k, escape(v)) for k, v in items)
314
+ assert parse_parameters(s) == dict(items)
315
+
316
+ def test_semicolons_in_parameters(self):
317
+ assert parse_parameters('name=";"') == {"name": ";"}
318
+
319
+ def test_quoted_strings_in_parameters(self):
320
+ assert parse_parameters('name="\\""') == {"name": '"'}
321
+
322
+ def test_it_preserves_backslashes(self):
323
+ assert parse_parameters('f="C:\\a.txt"', True) == {"f": "C:\\a.txt"}
@@ -0,0 +1,34 @@
1
+ # Copyright 2015 Oliver Cope
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ import pytest
16
+
17
+ from fresco.util.security import check_equal_constant_time
18
+
19
+
20
+ class TestSecurity(object):
21
+ def test_check_equal_constant_time_returns_equality(self):
22
+ assert check_equal_constant_time("", "") is True
23
+ assert check_equal_constant_time("abcabcabc", "abcabcabc") is True
24
+
25
+ def test_check_equal_constant_time_returns_inequality(self):
26
+ assert check_equal_constant_time(" ", "") is False
27
+ assert check_equal_constant_time("abcabcabc", "abcabcabx") is False
28
+ assert check_equal_constant_time("abcabcabc", "abcabcabx") is False
29
+ assert check_equal_constant_time("abcabcabc", "abcabcabde") is False
30
+ assert check_equal_constant_time("abcabcabc", "abcabcab") is False
31
+
32
+ def test_check_equal_constant_time_raises_an_error_on_non_strings(self):
33
+ with pytest.raises(TypeError):
34
+ check_equal_constant_time(None, None)
@@ -0,0 +1,176 @@
1
+ # Copyright 2015 Oliver Cope
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ from fresco.core import FrescoApp
16
+ from fresco.util.urls import normpath, make_query
17
+ from fresco.util.urls import is_safe_url
18
+
19
+ # Greek letters as unicode strings (require multi-byte representation in UTF-8)
20
+ alpha = b"\xce\xb1".decode("utf8")
21
+ beta = b"\xce\xb2".decode("utf8")
22
+ gamma = b"\xce\xb3".decode("utf8")
23
+
24
+
25
+ class TestNormPath(object):
26
+ def test_empty_string(selfself):
27
+ assert normpath("") == ""
28
+
29
+ def test_root_url(self):
30
+ assert normpath("/") == "/"
31
+
32
+ def test_condenses_consecutive_slashes(self):
33
+ assert normpath("//") == "/"
34
+ assert normpath("///") == "/"
35
+
36
+ def test_remove_single_dot(self):
37
+ assert normpath("/./") == "/"
38
+
39
+ def test_double_dot_interpreted(self):
40
+ assert normpath("/../") == "/"
41
+ assert normpath("/foo/../") == "/"
42
+
43
+ def test_triple_dot_preserved(self):
44
+ assert normpath("/.../") == "/.../"
45
+
46
+ def test_combined_patterns(self):
47
+ assert normpath("/..//../") == "/"
48
+ assert normpath("/hello/.//dolly//") == "/hello/dolly/"
49
+ assert normpath("///hello/.//dolly//./..//.//sailor") == "/hello/sailor"
50
+
51
+ def test_trailing_slash_preserved(self):
52
+ assert normpath("/sliced/bread/") == "/sliced/bread/"
53
+
54
+
55
+ class TestMakeQuery(object):
56
+ def test_make_query(self):
57
+ assert sorted(make_query(a="1", b=2).split("&")) == ["a=1", "b=2"]
58
+ assert make_query(a="one two three") == "a=one+two+three"
59
+ assert make_query(a=["one", "two", "three"]) == "a=one&a=two&a=three"
60
+
61
+ def test_make_query_unicode(self):
62
+ assert (
63
+ make_query(a=[alpha, beta, gamma], charset="utf8")
64
+ == "a=%CE%B1&a=%CE%B2&a=%CE%B3"
65
+ )
66
+
67
+ def test_make_query_unicode_default_encoding(self):
68
+ assert make_query(a=[alpha, beta, gamma], charset="utf8") == make_query(
69
+ a=[alpha, beta, gamma]
70
+ )
71
+
72
+
73
+ class TestSafeURL(object):
74
+ def test_it_loads_defaults_from_context(self):
75
+ with FrescoApp().requestcontext(
76
+ **{"wsgi.url_scheme": "https", "HTTP_HOST": "foo.example.org"}
77
+ ):
78
+ assert is_safe_url("http://foo.example.org/") is True
79
+ assert is_safe_url("https://foo.example.org/") is True
80
+ assert is_safe_url("//foo.example.org/") is True
81
+ assert is_safe_url("http://example.org/") is False
82
+ assert is_safe_url("https://example.org/") is False
83
+ assert is_safe_url("//example.org/") is False
84
+
85
+ def test_it_passes_safe_urls(self):
86
+ safe = [
87
+ "/foo",
88
+ "http://good.example.org/x",
89
+ "https://good.example.org/",
90
+ "HTTPS://good.example.org/",
91
+ "//good.example.org/",
92
+ "/path?next_url=http://bad.example.org/",
93
+ ]
94
+ for u in safe:
95
+ assert is_safe_url(u, allowed_hosts={"good.example.org"}) is True
96
+
97
+ def test_it_handles_port_numbers_correctly(self):
98
+ def test(scheme, server_name="localhost", server_port="80", http_host=None):
99
+ env = {
100
+ "SERVER_NAME": server_name,
101
+ "SERVER_PORT": server_port,
102
+ "wsgi.url_scheme": scheme,
103
+ "HTTP_HOST": http_host,
104
+ }
105
+
106
+ def test(url, expected):
107
+ with FrescoApp().requestcontext(**env):
108
+ assert is_safe_url(url) is expected
109
+
110
+ return test
111
+
112
+ # server_name, server_port set, but no http_host
113
+ test("http", "example", "80")("http://example/", True)
114
+ test("http", "example", "80")("http://example:80/", True)
115
+ test("http", "example", "80")("https://example/", True)
116
+ test("http", "example", "80")("https://example:443/", True)
117
+ test("http", "example", "80")("http://example:443/", False)
118
+ test("http", "example", "80")("http://example:8080/", False)
119
+
120
+ test("https", "example", "443")("http://example/", True)
121
+ test("https", "example", "443")("http://example:80/", True)
122
+ test("https", "example", "443")("https://example/", True)
123
+ test("https", "example", "443")("https://example:443/", True)
124
+ test("https", "example", "443")("https://example:80/", False)
125
+
126
+ test("http", "example", "8080")("http://example:8080/", True)
127
+ test("http", "example", "8080")("https://example:8080/", True)
128
+ test("http", "example", "8080")("http://example/", False)
129
+ test("http", "example", "8080")("http://example:80/", False)
130
+
131
+ # http_host set - strict match on origin
132
+ test("http", http_host="example")("http://example/", True)
133
+ test("http", http_host="example")("http://example:80/", True)
134
+ test("http", http_host="example")("https://example/", True)
135
+ test("http", http_host="example")("https://example:443/", True)
136
+ test("http", http_host="example")("http://example:443/", False)
137
+ test("http", http_host="example")("http://example:8080/", False)
138
+
139
+ test("https", http_host="example")("http://example/", True)
140
+ test("https", http_host="example")("http://example:80/", True)
141
+ test("https", http_host="example")("https://example/", True)
142
+ test("https", http_host="example")("https://example:443/", True)
143
+ test("https", http_host="example")("http://example:443/", False)
144
+ test("https", http_host="example")("http://example:8080/", False)
145
+
146
+ test("http", http_host="example:80")("http://example/", True)
147
+ test("http", http_host="example:80")("http://example:80/", True)
148
+ test("http", http_host="example:80")("https://example/", True)
149
+ test("http", http_host="example:80")("https://example:443/", True)
150
+ test("http", http_host="example:80")("http://example:443/", False)
151
+ test("http", http_host="example:80")("http://example:8080/", False)
152
+
153
+ test("https", http_host="example:443")("http://example/", True)
154
+ test("https", http_host="example:443")("http://example:80/", True)
155
+ test("https", http_host="example:443")("https://example/", True)
156
+ test("https", http_host="example:443")("https://example:443/", True)
157
+ test("https", http_host="example:443")("http://example:443/", False)
158
+ test("https", http_host="example:443")("http://example:8080/", False)
159
+
160
+ test("http", http_host="example:8080")("http://example:8080/", True)
161
+ test("http", http_host="example:8080")("https://example:8080/", True)
162
+ test("http", http_host="example:8080")("http://example/", False)
163
+ test("http", http_host="example:8080")("http://example:80/", False)
164
+
165
+ def test_it_catches_unsafe_urls(self):
166
+ unsafe = [
167
+ "///foo",
168
+ "http://good.example.org@bad.example.org/",
169
+ "http://good.example.org:good.example.org@bad.example.org/",
170
+ "javascript:do_something_bad()",
171
+ "",
172
+ "\x00http://good.example.org",
173
+ "http://good.example.org\nHost: http://bad.example.org",
174
+ ]
175
+ for u in unsafe:
176
+ assert is_safe_url(u, allowed_hosts={"good.example.org"}) is False
@@ -0,0 +1,107 @@
1
+ # encoding=UTF-8
2
+ # Copyright 2015 Oliver Cope
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ from typing import Dict
17
+
18
+ from fresco import FrescoApp, Response
19
+ from fresco.util.wsgi import (
20
+ ClosingIterator,
21
+ StartResponseWrapper,
22
+ getenv,
23
+ setenv,
24
+ )
25
+
26
+
27
+ class Counter(object):
28
+ value = 0
29
+
30
+ def inc(self):
31
+ self.value += 1
32
+
33
+
34
+ def start_response(status, headers, exc_info=None):
35
+ pass
36
+
37
+
38
+ class _TestException(Exception):
39
+ pass
40
+
41
+
42
+ class TestClosingIterator(object):
43
+ def app(self, environ, start_response):
44
+ start_response("200 OK", [("Content-Type: text/plain")])
45
+ yield "Foo"
46
+ yield "Bar"
47
+
48
+ def test_close_called_after_iterator_finished(self):
49
+ count = Counter()
50
+ environ: Dict[str, str] = {}
51
+
52
+ result = self.app(environ, start_response)
53
+ result = ClosingIterator(result, count.inc)
54
+ assert count.value == 0
55
+ try:
56
+ list(result)
57
+ finally:
58
+ result.close()
59
+ assert count.value == 1
60
+
61
+ def test_multiple_close_functions_called(self):
62
+ count1 = Counter()
63
+ count2 = Counter()
64
+ environ: Dict[str, str] = {}
65
+
66
+ result = self.app(environ, start_response)
67
+ result = ClosingIterator(result, count1.inc, count2.inc)
68
+ assert count1.value == 0
69
+ assert count2.value == 0
70
+ try:
71
+ list(result)
72
+ finally:
73
+ result.close()
74
+ assert count1.value == 1
75
+ assert count2.value == 1
76
+
77
+
78
+ class TestStartResponseWrapper(object):
79
+ def test_write(self):
80
+ def wsgiapp(environ, start_response):
81
+ start_response = StartResponseWrapper(start_response)
82
+ write = start_response("200 OK", [("Content-Type", "text/plain")])
83
+
84
+ write(b"cat")
85
+ write(b"sat")
86
+ write2 = start_response.call_start_response()
87
+ write2(b"mat")
88
+ return []
89
+
90
+ with FrescoApp().requestcontext() as c:
91
+ r = Response.from_wsgi(
92
+ wsgiapp, c.request.environ, lambda status, headers: None
93
+ )
94
+ assert b"".join(r.content) == b"catsatmat"
95
+
96
+
97
+ class TestEnvironGetSet(object):
98
+ wsgibytes = "ø".encode("UTF-8")
99
+ wsgistr = wsgibytes.decode("ISO-8859-1")
100
+
101
+ def test_getenv_returns_bytes(self):
102
+ assert getenv({"x": self.wsgistr}, "x") == "ø".encode("UTF-8")
103
+
104
+ def test_setenv_sets_str(self):
105
+ env: Dict[str, str] = {}
106
+ setenv(env, "x", "ø".encode("UTF-8"))
107
+ assert env["x"] == self.wsgistr
@@ -1,6 +1,7 @@
1
1
  from operator import methodcaller
2
2
  import itertools
3
3
  import functools
4
+ import typing
4
5
 
5
6
  #: Allowed character encodings.
6
7
  #: This list is based on the list of standard encodings here:
@@ -312,7 +313,7 @@ ALLOWED_ENCODINGS = {
312
313
  # Spelling alternatives that only differ in case or use a hyphen
313
314
  # instead of an underscore are valid aliases; for example, 'utf-8' is an
314
315
  # alias for the 'utf_8' codec."
315
- transforms = [
316
+ transforms: list[list[typing.Callable]] = [
316
317
  [
317
318
  lambda s: s,
318
319
  methodcaller("lower"),
fresco/util/http.py CHANGED
@@ -454,12 +454,14 @@ def parse_multipart(
454
454
  f.close()
455
455
  raise
456
456
 
457
- close: Optional[Callable] = None
457
+ close: Optional[Callable]
458
458
  if open_files:
459
459
 
460
460
  def close():
461
461
  for f in open_files:
462
462
  f.close()
463
+ else:
464
+ close = None
463
465
 
464
466
  return fields, close
465
467
 
fresco/util/wsgi.py CHANGED
@@ -310,7 +310,7 @@ class ClosingIterator(object):
310
310
  func()
311
311
  except Exception:
312
312
  logger.exception(
313
- "ContentIterator close function {func!r} raised an exception"
313
+ f"ContentIterator close function {func!r} raised an exception"
314
314
  )
315
315
  self._closed = True
316
316