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.
- fresco/__init__.py +1 -1
- fresco/options.py +11 -3
- fresco/request.py +2 -2
- fresco/response.py +1 -1
- fresco/routing.py +1 -1
- fresco/tests/__init__.py +0 -0
- fresco/tests/fixtures.py +67 -0
- fresco/tests/test_cookie.py +59 -0
- fresco/tests/test_core.py +1038 -0
- fresco/tests/test_decorators.py +40 -0
- fresco/tests/test_exceptions.py +30 -0
- fresco/tests/test_middleware.py +92 -0
- fresco/tests/test_multidict.py +234 -0
- fresco/tests/test_options.py +319 -0
- fresco/tests/test_request.py +448 -0
- fresco/tests/test_requestcontext.py +107 -0
- fresco/tests/test_response.py +224 -0
- fresco/tests/test_routeargs.py +223 -0
- fresco/tests/test_routing.py +1126 -0
- fresco/tests/test_static.py +124 -0
- fresco/tests/test_subrequests.py +236 -0
- fresco/tests/util/__init__.py +0 -0
- fresco/tests/util/form_data.py +79 -0
- fresco/tests/util/test_common.py +34 -0
- fresco/tests/util/test_http.py +323 -0
- fresco/tests/util/test_security.py +34 -0
- fresco/tests/util/test_urls.py +176 -0
- fresco/tests/util/test_wsgi.py +107 -0
- fresco/util/contentencodings.py +2 -1
- fresco/util/http.py +3 -1
- fresco/util/wsgi.py +1 -1
- {fresco-3.3.2.dist-info → fresco-3.3.4.dist-info}/METADATA +5 -6
- fresco-3.3.4.dist-info/RECORD +57 -0
- {fresco-3.3.2.dist-info → fresco-3.3.4.dist-info}/WHEEL +1 -1
- fresco-3.3.2.dist-info/RECORD +0 -34
- {fresco-3.3.2.dist-info → fresco-3.3.4.dist-info}/LICENSE.txt +0 -0
- {fresco-3.3.2.dist-info → fresco-3.3.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
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 decimal import Decimal
|
|
16
|
+
from tempfile import NamedTemporaryFile
|
|
17
|
+
from tempfile import TemporaryDirectory
|
|
18
|
+
from unittest.mock import Mock
|
|
19
|
+
import contextlib
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from fresco.options import Options
|
|
27
|
+
from fresco.options import parse_key_value_pairs
|
|
28
|
+
from fresco.options import dict_from_options
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestOptions(object):
|
|
32
|
+
def test_options_dictionary_access(self):
|
|
33
|
+
options = Options()
|
|
34
|
+
options["x"] = 1
|
|
35
|
+
assert options["x"] == 1
|
|
36
|
+
|
|
37
|
+
def test_options_attribute_access(self):
|
|
38
|
+
options = Options()
|
|
39
|
+
options.x = 1
|
|
40
|
+
assert options.x == 1
|
|
41
|
+
|
|
42
|
+
def test_options_raises_AttributeError(self):
|
|
43
|
+
with pytest.raises(AttributeError):
|
|
44
|
+
Options().x
|
|
45
|
+
|
|
46
|
+
def test_options_works_with_getattr(self):
|
|
47
|
+
assert getattr(Options(), "x", 1) == 1
|
|
48
|
+
assert getattr(Options({"x": 2}), "x", 1) == 2
|
|
49
|
+
with pytest.raises(AttributeError):
|
|
50
|
+
assert getattr(Options(), "x")
|
|
51
|
+
|
|
52
|
+
def test_options_update_from_object(self):
|
|
53
|
+
class Foo:
|
|
54
|
+
a = 1
|
|
55
|
+
b = 2
|
|
56
|
+
|
|
57
|
+
options = Options()
|
|
58
|
+
options.update_from_object(Foo())
|
|
59
|
+
assert options == {"a": 1, "b": 2}
|
|
60
|
+
|
|
61
|
+
def test_options_update_from_object_loads_underscore_names(self):
|
|
62
|
+
class Foo:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
options = Options()
|
|
66
|
+
options.update_from_object(Foo(), True)
|
|
67
|
+
assert "__module__" in options
|
|
68
|
+
|
|
69
|
+
def test_options_update_from_file(self):
|
|
70
|
+
with NamedTemporaryFile() as tmpfile:
|
|
71
|
+
tmpfile.write(b"a = 1\nb = 2\n")
|
|
72
|
+
tmpfile.flush()
|
|
73
|
+
|
|
74
|
+
options = Options()
|
|
75
|
+
options.update_from_file(tmpfile.name)
|
|
76
|
+
assert options == {"a": 1, "b": 2}
|
|
77
|
+
|
|
78
|
+
def test_options_update_from_file_has_dunder_file_global(self):
|
|
79
|
+
with NamedTemporaryFile() as tmpfile:
|
|
80
|
+
tmpfile.write(b"a = __file__")
|
|
81
|
+
tmpfile.flush()
|
|
82
|
+
|
|
83
|
+
options = Options()
|
|
84
|
+
options.update_from_file(tmpfile.name)
|
|
85
|
+
assert options == {"a": tmpfile.name}
|
|
86
|
+
|
|
87
|
+
def test_options_respects_all(self):
|
|
88
|
+
with NamedTemporaryFile() as tmpfile:
|
|
89
|
+
tmpfile.write(b"__all__ = ['a']\n" b"a = 1\n" b"b = 2\n")
|
|
90
|
+
tmpfile.flush()
|
|
91
|
+
|
|
92
|
+
options = Options()
|
|
93
|
+
options.update_from_file(tmpfile.name)
|
|
94
|
+
assert options == {"a": 1}
|
|
95
|
+
|
|
96
|
+
def test_update_from_file_doesnt_add_module(self):
|
|
97
|
+
with NamedTemporaryFile() as tmpfile:
|
|
98
|
+
options = Options()
|
|
99
|
+
saved_modules = list(sorted(sys.modules.keys()))
|
|
100
|
+
options.update_from_file(tmpfile.name)
|
|
101
|
+
assert list(sorted(sys.modules.keys())) == saved_modules
|
|
102
|
+
|
|
103
|
+
def test_options_copy_returns_options(self):
|
|
104
|
+
assert isinstance(Options().copy(), Options)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestLoadKeyValuePairs:
|
|
108
|
+
def test_it_loads_strings(self):
|
|
109
|
+
assert parse_key_value_pairs({}, ["a=b"]) == {"a": "b"}
|
|
110
|
+
|
|
111
|
+
def test_it_loads_ints(self):
|
|
112
|
+
assert parse_key_value_pairs({}, ["a=100"]) == {"a": 100}
|
|
113
|
+
assert parse_key_value_pairs({}, ["a=-100"]) == {"a": -100}
|
|
114
|
+
assert parse_key_value_pairs({}, ["a=+100"]) == {"a": 100}
|
|
115
|
+
# leading zero - not treated as an int
|
|
116
|
+
assert parse_key_value_pairs({}, ["a=01"]) == {"a": "01"}
|
|
117
|
+
|
|
118
|
+
def test_it_loads_bools(self):
|
|
119
|
+
assert parse_key_value_pairs({}, ["a=true"]) == {"a": True}
|
|
120
|
+
assert parse_key_value_pairs({}, ["a=True"]) == {"a": True}
|
|
121
|
+
assert parse_key_value_pairs({}, ["a=TRUE"]) == {"a": True}
|
|
122
|
+
assert parse_key_value_pairs({}, ["a=false"]) == {"a": False}
|
|
123
|
+
assert parse_key_value_pairs({}, ["a=False"]) == {"a": False}
|
|
124
|
+
assert parse_key_value_pairs({}, ["a=FALSE"]) == {"a": False}
|
|
125
|
+
|
|
126
|
+
def test_it_loads_decimals(self):
|
|
127
|
+
assert parse_key_value_pairs({}, ["a=1.2"]) == {"a": Decimal("1.2")}
|
|
128
|
+
assert parse_key_value_pairs({}, ["a=+1."]) == {"a": Decimal("1")}
|
|
129
|
+
assert parse_key_value_pairs({}, ["a=-.1"]) == {"a": Decimal("-0.1")}
|
|
130
|
+
|
|
131
|
+
def test_it_ignores_comments(self):
|
|
132
|
+
assert parse_key_value_pairs({}, ["a=1", "b=2 #comment", "#c=3"]) == {
|
|
133
|
+
"a": 1,
|
|
134
|
+
"b": 2,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def test_it_interpolates(self):
|
|
138
|
+
assert parse_key_value_pairs({}, ["a=${x}"]) == {"a": "${x}"}
|
|
139
|
+
assert parse_key_value_pairs({}, ["a=$x"]) == {"a": "$x"}
|
|
140
|
+
assert parse_key_value_pairs({"x": 1}, ["a=${x}"]) == {"a": 1}
|
|
141
|
+
assert parse_key_value_pairs({"x": 1}, ["a=$x"]) == {"a": 1}
|
|
142
|
+
assert parse_key_value_pairs({}, ["a=1", "b=$a"]) == {"a": 1, "b": 1}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestLoadOptions:
|
|
146
|
+
|
|
147
|
+
def check_loadoptions(self, tmpdir, files, sources="*", tags=[], expected={}):
|
|
148
|
+
"""
|
|
149
|
+
Write the files indicated in ``sources`` to the given temporary directory,
|
|
150
|
+
|
|
151
|
+
Create an Options object and populate it from the specified sources/tags.
|
|
152
|
+
|
|
153
|
+
Assert that the loaded options is equal to the value of ``expected``.
|
|
154
|
+
"""
|
|
155
|
+
t = pathlib.Path(tmpdir)
|
|
156
|
+
for fname in files:
|
|
157
|
+
with (t / fname).open("w", encoding="UTF-8") as f:
|
|
158
|
+
f.write(files[fname])
|
|
159
|
+
|
|
160
|
+
@contextlib.contextmanager
|
|
161
|
+
def optionsdir():
|
|
162
|
+
def loadopts(sources="*", tags=[], strict=False, **kw):
|
|
163
|
+
return Options().load(sources, tags, strict=False, **kw)
|
|
164
|
+
|
|
165
|
+
saved = os.getcwd()
|
|
166
|
+
os.chdir(t)
|
|
167
|
+
try:
|
|
168
|
+
yield loadopts
|
|
169
|
+
finally:
|
|
170
|
+
os.chdir(saved)
|
|
171
|
+
|
|
172
|
+
if expected:
|
|
173
|
+
with optionsdir() as loadopts:
|
|
174
|
+
assert loadopts(sources, tags) == expected
|
|
175
|
+
else:
|
|
176
|
+
return optionsdir()
|
|
177
|
+
|
|
178
|
+
def test_it_loads_kvp_files(self, tmpdir):
|
|
179
|
+
self.check_loadoptions(tmpdir, {"a": "x = 2"}, expected={"x": 2})
|
|
180
|
+
self.check_loadoptions(
|
|
181
|
+
tmpdir, {"a": "x = 2\ny = ${x}"}, expected={"x": 2, "y": 2}
|
|
182
|
+
)
|
|
183
|
+
with self.check_loadoptions(tmpdir, {"a": "x = $__FILE__\ny=${x}"}) as loadopts:
|
|
184
|
+
result = loadopts()
|
|
185
|
+
assert result["x"] == result["y"] == str(pathlib.Path(tmpdir) / "a")
|
|
186
|
+
|
|
187
|
+
def test_it_loads_json(self, tmpdir):
|
|
188
|
+
self.check_loadoptions(
|
|
189
|
+
tmpdir, {"a.json": '{"a": ["b"]}'}, expected={"a": ["b"]}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def test_it_loads_py_files(self, tmpdir):
|
|
193
|
+
self.check_loadoptions(tmpdir, {"a.py": "x = 2 * 2"}, expected={"x": 4})
|
|
194
|
+
|
|
195
|
+
def test_it_selects_by_tag(self, tmpdir):
|
|
196
|
+
with self.check_loadoptions(
|
|
197
|
+
tmpdir,
|
|
198
|
+
{
|
|
199
|
+
"a.dev.txt": "a = 1",
|
|
200
|
+
"a.staging.txt": "b = 1",
|
|
201
|
+
"a.staging.local.txt": "c = 1",
|
|
202
|
+
"a.dev.local.txt": "d = 1",
|
|
203
|
+
"a.local.txt": "e = 1",
|
|
204
|
+
},
|
|
205
|
+
) as loadopts:
|
|
206
|
+
assert loadopts("*", ["dev"]) == {"a": 1}
|
|
207
|
+
assert loadopts("*", ["dev", "local"]) == {"a": 1, "d": 1, "e": 1}
|
|
208
|
+
assert loadopts("*", ["staging"]) == {"b": 1}
|
|
209
|
+
assert loadopts("*", ["staging", "local"]) == {
|
|
210
|
+
"b": 1,
|
|
211
|
+
"c": 1,
|
|
212
|
+
"e": 1,
|
|
213
|
+
}
|
|
214
|
+
assert loadopts("*", ["local"]) == {"e": 1}
|
|
215
|
+
|
|
216
|
+
def test_it_loads_in_tag_order(self, tmpdir):
|
|
217
|
+
with self.check_loadoptions(
|
|
218
|
+
tmpdir,
|
|
219
|
+
{
|
|
220
|
+
"a": "a = 0",
|
|
221
|
+
"a.dev.txt": "a = ${a}-1",
|
|
222
|
+
"a.local.txt": "a = ${a}-2",
|
|
223
|
+
"b.dev.txt": "a = ${a}-3",
|
|
224
|
+
},
|
|
225
|
+
) as loadopts:
|
|
226
|
+
assert loadopts("*", ["dev", "local"]) == {"a": "0-1-3-2"}
|
|
227
|
+
assert loadopts("*", ["local", "dev"]) == {"a": "0-2-1-3"}
|
|
228
|
+
|
|
229
|
+
def test_it_loads_from_os_environ(self, tmpdir):
|
|
230
|
+
with setenv(a="2"):
|
|
231
|
+
with self.check_loadoptions(tmpdir, {"a.txt": "a = 1"}) as loadopts:
|
|
232
|
+
assert loadopts("*", [], use_environ=False) == {"a": 1}
|
|
233
|
+
assert loadopts("*", [], use_environ=True) == {"a": 2}
|
|
234
|
+
|
|
235
|
+
def test_it_calls_callbacks(self, tmpdir):
|
|
236
|
+
with self.check_loadoptions(tmpdir, {"a.txt": "a = 1"}):
|
|
237
|
+
options = Options()
|
|
238
|
+
mock = Mock()
|
|
239
|
+
options.onload(mock)
|
|
240
|
+
options.load(str(pathlib.Path(tmpdir) / "*"))
|
|
241
|
+
assert mock.called
|
|
242
|
+
|
|
243
|
+
def test_it_sets_directory(self, tmpdir):
|
|
244
|
+
with self.check_loadoptions(tmpdir, {"a.txt": "a = 1"}) as loadopts:
|
|
245
|
+
with TemporaryDirectory() as tmpdir2:
|
|
246
|
+
os.chdir(tmpdir2)
|
|
247
|
+
assert loadopts("*") == {}
|
|
248
|
+
assert loadopts("*", dir=tmpdir) == {"a": 1}
|
|
249
|
+
|
|
250
|
+
def test_it_accepts_a_list_of_filespecs(self, tmpdir):
|
|
251
|
+
self.check_loadoptions(
|
|
252
|
+
tmpdir,
|
|
253
|
+
{"a.txt": "a=1", "b.txt": "b=1"},
|
|
254
|
+
sources=["a.*", "b.*"],
|
|
255
|
+
expected={"a": 1, "b": 1}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def test_it_substitutes_from_environment_variables(self, tmpdir):
|
|
259
|
+
with setenv(FOO="bar"):
|
|
260
|
+
self.check_loadoptions(
|
|
261
|
+
tmpdir,
|
|
262
|
+
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
263
|
+
tags=["{FOO}"],
|
|
264
|
+
expected={"a": 2}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
with setenv(FOO="baz"):
|
|
268
|
+
self.check_loadoptions(
|
|
269
|
+
tmpdir,
|
|
270
|
+
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
271
|
+
tags=["{FOO}"],
|
|
272
|
+
expected={"a": 1}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def test_it_allows_missing_environment_variables(self, tmpdir):
|
|
276
|
+
assert "FOO" not in os.environ
|
|
277
|
+
self.check_loadoptions(
|
|
278
|
+
tmpdir,
|
|
279
|
+
{"a.txt": "a=1", "a.bar.txt": "a=2"},
|
|
280
|
+
tags=["{FOO}"],
|
|
281
|
+
expected={"a": 1}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestDictFromOptions:
|
|
286
|
+
|
|
287
|
+
def test_it_splits_on_prefix(self):
|
|
288
|
+
|
|
289
|
+
options = Options(FOO_BAR=1, FOO_BAZ=2, FOO_BAR_BAZ=3, BAR=4)
|
|
290
|
+
assert dict_from_options("FOO_", options) == {"BAR": 1, "BAZ": 2, "BAR_BAZ": 3}
|
|
291
|
+
|
|
292
|
+
def test_it_splits_recursively(self):
|
|
293
|
+
|
|
294
|
+
options = Options(
|
|
295
|
+
A_A=1,
|
|
296
|
+
A_B_C_D=2,
|
|
297
|
+
A_B_E=3,
|
|
298
|
+
A_F_G_H=4,
|
|
299
|
+
A_I=5,
|
|
300
|
+
J_A=6,
|
|
301
|
+
)
|
|
302
|
+
assert dict_from_options("A_", options, recursive=True) == {
|
|
303
|
+
"A": 1,
|
|
304
|
+
"B": {"C": {"D": 2}, "E": 3},
|
|
305
|
+
"F": {"G": {"H": 4}},
|
|
306
|
+
"I": 5
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@contextlib.contextmanager
|
|
311
|
+
def setenv(**kw):
|
|
312
|
+
saved = {k: os.environ[k] for k in kw if k in os.environ}
|
|
313
|
+
os.environ.update(kw)
|
|
314
|
+
yield os.environ
|
|
315
|
+
for k in kw:
|
|
316
|
+
if k in saved:
|
|
317
|
+
os.environ[k] = saved[k]
|
|
318
|
+
else:
|
|
319
|
+
del os.environ[k]
|