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,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]