config2py 0.1.35__py3-none-any.whl → 0.1.37__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.
- config2py/__init__.py +1 -0
- config2py/base.py +14 -14
- config2py/s_configparser.py +29 -27
- config2py/tests/test_tools.py +22 -22
- config2py/tests/utils_for_testing.py +1 -1
- config2py/tools.py +10 -9
- config2py/util.py +101 -73
- {config2py-0.1.35.dist-info → config2py-0.1.37.dist-info}/METADATA +2 -2
- config2py-0.1.37.dist-info/RECORD +15 -0
- {config2py-0.1.35.dist-info → config2py-0.1.37.dist-info}/WHEEL +1 -1
- config2py/tests/test_util.py +0 -39
- config2py-0.1.35.dist-info/RECORD +0 -16
- {config2py-0.1.35.dist-info → config2py-0.1.37.dist-info}/LICENSE +0 -0
- {config2py-0.1.35.dist-info → config2py-0.1.37.dist-info}/top_level.txt +0 -0
config2py/__init__.py
CHANGED
|
@@ -13,6 +13,7 @@ from config2py.tools import (
|
|
|
13
13
|
)
|
|
14
14
|
from config2py.base import get_config, user_gettable, sources_chainmap
|
|
15
15
|
from config2py.util import (
|
|
16
|
+
envvar, # os.environ, but with dict display override to hide secrets
|
|
16
17
|
ask_user_for_input,
|
|
17
18
|
get_app_data_folder,
|
|
18
19
|
get_configs_folder_for_app,
|
config2py/base.py
CHANGED
|
@@ -94,7 +94,7 @@ GetConfigEgress = Callable[[KT, VT], VT]
|
|
|
94
94
|
|
|
95
95
|
def is_not_none_nor_empty(x):
|
|
96
96
|
if isinstance(x, str):
|
|
97
|
-
return x !=
|
|
97
|
+
return x != ""
|
|
98
98
|
else:
|
|
99
99
|
return x is not None
|
|
100
100
|
|
|
@@ -211,7 +211,7 @@ def get_config(
|
|
|
211
211
|
value = chain_map.get(key, not_found)
|
|
212
212
|
if value is not_found:
|
|
213
213
|
if default is no_default:
|
|
214
|
-
raise ConfigNotFound(f
|
|
214
|
+
raise ConfigNotFound(f"Could not find config for key: {key}")
|
|
215
215
|
else:
|
|
216
216
|
value = default
|
|
217
217
|
if egress is not None:
|
|
@@ -299,11 +299,11 @@ class FuncBasedGettableContainer:
|
|
|
299
299
|
v = self.getter(k)
|
|
300
300
|
except self.config_not_found_exceptions as e:
|
|
301
301
|
raise KeyError(
|
|
302
|
-
f
|
|
303
|
-
f
|
|
302
|
+
f"There was an exception when computing key: {k} with the function "
|
|
303
|
+
f"{self.getter}. The exception was: {e}"
|
|
304
304
|
)
|
|
305
305
|
if not self.val_is_valid(v):
|
|
306
|
-
raise KeyError(f
|
|
306
|
+
raise KeyError(f"Value for key {k} is not valid: {v}")
|
|
307
307
|
return v
|
|
308
308
|
|
|
309
309
|
# TODO: Is this used to indicate that the getter couldn't find a key.
|
|
@@ -317,10 +317,10 @@ class FuncBasedGettableContainer:
|
|
|
317
317
|
|
|
318
318
|
def __iter__(self):
|
|
319
319
|
raise TypeError(
|
|
320
|
-
f
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
f"Iteration is NOT ACTUALLY implemented for {type(self).__name__}. "
|
|
321
|
+
"The reason we still stuck it in the class is because python will "
|
|
322
|
+
"automatically try to use __getitem__ on 0, 1, ... to iterate over the "
|
|
323
|
+
"object, and we want to avoid that, since this would result in an obscure "
|
|
324
324
|
"error message saying that the getter function can't be called on 0"
|
|
325
325
|
)
|
|
326
326
|
|
|
@@ -342,7 +342,7 @@ def gettable_containers(
|
|
|
342
342
|
)
|
|
343
343
|
else:
|
|
344
344
|
raise AssertionError(
|
|
345
|
-
f
|
|
345
|
+
f"Source must be a Gettable or a Callable, not {type(src)}"
|
|
346
346
|
)
|
|
347
347
|
|
|
348
348
|
|
|
@@ -362,7 +362,7 @@ SaveTo = Optional[Union[MutableMapping, KTSaver]]
|
|
|
362
362
|
|
|
363
363
|
def is_not_empty(val) -> bool:
|
|
364
364
|
if isinstance(val, str):
|
|
365
|
-
return val !=
|
|
365
|
+
return val != ""
|
|
366
366
|
else:
|
|
367
367
|
return val is not None
|
|
368
368
|
|
|
@@ -370,7 +370,7 @@ def is_not_empty(val) -> bool:
|
|
|
370
370
|
def ask_user_for_key(
|
|
371
371
|
key=None,
|
|
372
372
|
*,
|
|
373
|
-
prompt_template=
|
|
373
|
+
prompt_template="Enter a value for {}: ",
|
|
374
374
|
save_to: SaveTo = None,
|
|
375
375
|
save_condition=is_not_empty,
|
|
376
376
|
user_asker=ask_user_for_input,
|
|
@@ -389,7 +389,7 @@ def ask_user_for_key(
|
|
|
389
389
|
if isinstance(egress, Callable):
|
|
390
390
|
val = egress(key, val)
|
|
391
391
|
if save_to is not None and save_condition(val):
|
|
392
|
-
if hasattr(save_to,
|
|
392
|
+
if hasattr(save_to, "__setitem__"):
|
|
393
393
|
save_to_func = save_to.__setitem__
|
|
394
394
|
save_to_func(key, val)
|
|
395
395
|
return val
|
|
@@ -398,7 +398,7 @@ def ask_user_for_key(
|
|
|
398
398
|
def user_gettable(
|
|
399
399
|
save_to: SaveTo = None,
|
|
400
400
|
*,
|
|
401
|
-
prompt_template=
|
|
401
|
+
prompt_template="Enter a value for {}: ",
|
|
402
402
|
egress: Optional[Callable] = None,
|
|
403
403
|
user_asker=ask_user_for_input,
|
|
404
404
|
val_is_valid: Callable[[VT], bool] = is_not_empty,
|
config2py/s_configparser.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Data Object Layer for configparser standard lib.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from configparser import ConfigParser
|
|
5
6
|
from configparser import BasicInterpolation, ExtendedInterpolation
|
|
6
7
|
from functools import wraps
|
|
@@ -10,7 +11,7 @@ from dol import Store
|
|
|
10
11
|
|
|
11
12
|
# from py2store.signatures import Sig
|
|
12
13
|
|
|
13
|
-
_test_config_str =
|
|
14
|
+
_test_config_str = """[Simple Values]
|
|
14
15
|
key=value
|
|
15
16
|
spaces in keys=allowed
|
|
16
17
|
spaces in values=allowed as well
|
|
@@ -51,7 +52,7 @@ empty string value here =
|
|
|
51
52
|
deeper than the first line
|
|
52
53
|
of a value
|
|
53
54
|
# Did I mention we can indent comments, too?
|
|
54
|
-
|
|
55
|
+
"""
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
def persist_after_operation(method_func):
|
|
@@ -87,7 +88,7 @@ def super_and_persist(super_cls, method_name):
|
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
ConfigParserStore = Store.wrap(ConfigParser)
|
|
90
|
-
ConfigParserStore.__name__ =
|
|
91
|
+
ConfigParserStore.__name__ = "ConfigParserStore"
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
# TODO: ConfigParser is already a mapping, but pros/cons of subclassing?
|
|
@@ -199,7 +200,8 @@ class ConfigStore(ConfigParserStore):
|
|
|
199
200
|
>>> ConfigReader(ini_filepath).to_dict()
|
|
200
201
|
{'DEFAULT': {}, 'nothing': {'like': 'that', 'ever': 'happened'}}
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
"""
|
|
204
|
+
|
|
203
205
|
space_around_delimiters = True
|
|
204
206
|
BasicInterpolation = BasicInterpolation
|
|
205
207
|
ExtendedInterpolation = ExtendedInterpolation
|
|
@@ -224,24 +226,24 @@ class ConfigStore(ConfigParserStore):
|
|
|
224
226
|
self._within_context_manager = False
|
|
225
227
|
|
|
226
228
|
if isinstance(source, str):
|
|
227
|
-
if
|
|
229
|
+
if "\n" in source:
|
|
228
230
|
self.read_string(source)
|
|
229
|
-
source_kind =
|
|
231
|
+
source_kind = "string"
|
|
230
232
|
else:
|
|
231
233
|
self.read(source)
|
|
232
|
-
source_kind =
|
|
234
|
+
source_kind = "filepath"
|
|
233
235
|
elif isinstance(source, bytes):
|
|
234
236
|
self.read_string(source.decode())
|
|
235
|
-
source_kind =
|
|
237
|
+
source_kind = "bytes"
|
|
236
238
|
elif isinstance(source, dict):
|
|
237
239
|
self.read_dict(source)
|
|
238
|
-
source_kind =
|
|
239
|
-
elif hasattr(source,
|
|
240
|
+
source_kind = "dict"
|
|
241
|
+
elif hasattr(source, "read"):
|
|
240
242
|
self.read_file(source)
|
|
241
|
-
source_kind =
|
|
243
|
+
source_kind = "stream"
|
|
242
244
|
else:
|
|
243
245
|
self.read(source)
|
|
244
|
-
source_kind =
|
|
246
|
+
source_kind = "unknown"
|
|
245
247
|
self.source = source
|
|
246
248
|
self.source_kind = source_kind
|
|
247
249
|
self.target_kind = target_kind or source_kind
|
|
@@ -264,28 +266,28 @@ class ConfigStore(ConfigParserStore):
|
|
|
264
266
|
Persists means to call
|
|
265
267
|
"""
|
|
266
268
|
if not self._within_context_manager:
|
|
267
|
-
if self.target_kind ==
|
|
268
|
-
with open(self.source,
|
|
269
|
+
if self.target_kind == "filepath":
|
|
270
|
+
with open(self.source, "w") as fp:
|
|
269
271
|
return self.write(fp, self.space_around_delimiters)
|
|
270
272
|
else:
|
|
271
|
-
if self.target_kind ==
|
|
273
|
+
if self.target_kind == "stream":
|
|
272
274
|
target = self.source
|
|
273
275
|
return self.write(target, self.space_around_delimiters)
|
|
274
|
-
elif self.target_kind in {
|
|
276
|
+
elif self.target_kind in {"string", "bytes"}:
|
|
275
277
|
string_target = StringIO()
|
|
276
278
|
self.write(string_target, self.space_around_delimiters)
|
|
277
279
|
string_target.seek(0)
|
|
278
280
|
string_data = string_target.read()
|
|
279
|
-
if self.target_kind ==
|
|
281
|
+
if self.target_kind == "string":
|
|
280
282
|
return string_data
|
|
281
|
-
elif self.target_kind ==
|
|
283
|
+
elif self.target_kind == "bytes":
|
|
282
284
|
return string_data.encode()
|
|
283
285
|
else:
|
|
284
|
-
raise ValueError(f
|
|
285
|
-
elif self.target_kind ==
|
|
286
|
+
raise ValueError(f"Unknown target_kind: {self.target_kind}")
|
|
287
|
+
elif self.target_kind == "dict":
|
|
286
288
|
return self.to_dict()
|
|
287
289
|
else:
|
|
288
|
-
raise ValueError(f
|
|
290
|
+
raise ValueError(f"Unknown target_kind: {self.target_kind}")
|
|
289
291
|
|
|
290
292
|
def __enter__(self):
|
|
291
293
|
self._within_context_manager = True
|
|
@@ -360,13 +362,13 @@ class ConfigReader(ConfigStore):
|
|
|
360
362
|
"""
|
|
361
363
|
|
|
362
364
|
def persist(self):
|
|
363
|
-
raise NotImplementedError(
|
|
365
|
+
raise NotImplementedError("persist disabled for ConfigReader")
|
|
364
366
|
|
|
365
367
|
def __setitem__(self, k, v):
|
|
366
|
-
raise NotImplementedError(
|
|
368
|
+
raise NotImplementedError("__setitem__ disabled for ConfigReader")
|
|
367
369
|
|
|
368
370
|
def __delitem__(self, k):
|
|
369
|
-
raise NotImplementedError(
|
|
371
|
+
raise NotImplementedError("__delitem__ disabled for ConfigReader")
|
|
370
372
|
|
|
371
373
|
|
|
372
374
|
# TODO: Need to wrap SectionProxy to make this work, since the obj and data here are
|
|
@@ -404,11 +406,11 @@ def postprocess_ini_section_items(items: Union[Mapping, Iterable]) -> Generator:
|
|
|
404
406
|
{'name': 'aspyre', 'keywords': ['documentation', 'packaging', 'publishing']}
|
|
405
407
|
|
|
406
408
|
"""
|
|
407
|
-
splitter_re = re.compile(
|
|
409
|
+
splitter_re = re.compile("[\n\r\t]+")
|
|
408
410
|
if isinstance(items, Mapping):
|
|
409
411
|
items = items.items()
|
|
410
412
|
for k, v in items:
|
|
411
|
-
if v.startswith(
|
|
413
|
+
if v.startswith("\n"):
|
|
412
414
|
v = splitter_re.split(v[1:])
|
|
413
415
|
v = [vv.strip() for vv in v if vv.strip()]
|
|
414
416
|
yield k, v
|
|
@@ -433,5 +435,5 @@ def preprocess_ini_section_items(items: Union[Mapping, Iterable]) -> Generator:
|
|
|
433
435
|
items = items.items()
|
|
434
436
|
for k, v in items:
|
|
435
437
|
if isinstance(v, list):
|
|
436
|
-
v =
|
|
438
|
+
v = "\n\t" + "\n\t".join(v)
|
|
437
439
|
yield k, v
|
config2py/tests/test_tools.py
CHANGED
|
@@ -12,21 +12,21 @@ from config2py.tests.utils_for_testing import user_input_patch
|
|
|
12
12
|
|
|
13
13
|
@pytest.fixture
|
|
14
14
|
def mock_config_store_factory():
|
|
15
|
-
with patch(
|
|
15
|
+
with patch("config2py.tools.get_configs_local_store") as mock_factory:
|
|
16
16
|
yield mock_factory
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_simple_config_getter(mock_config_store_factory):
|
|
20
|
-
key =
|
|
20
|
+
key = "_CONFIG2PY_SAFE_TO_DELETE_VAR_"
|
|
21
21
|
|
|
22
22
|
# Set up mock config store
|
|
23
|
-
mock_config_store = {key:
|
|
23
|
+
mock_config_store = {key: "from store"}
|
|
24
24
|
mock_config_store_factory.return_value = mock_config_store
|
|
25
25
|
|
|
26
26
|
# Test getting config from environment variable
|
|
27
|
-
os.environ[key] =
|
|
27
|
+
os.environ[key] = "from env var"
|
|
28
28
|
config_getter = simple_config_getter(first_look_in_env_vars=True)
|
|
29
|
-
assert config_getter(key) ==
|
|
29
|
+
assert config_getter(key) == "from env var"
|
|
30
30
|
|
|
31
31
|
# Test getting config from central config store
|
|
32
32
|
del os.environ[key] # delete env var to test central config store
|
|
@@ -34,9 +34,9 @@ def test_simple_config_getter(mock_config_store_factory):
|
|
|
34
34
|
# assert config_getter(key) == "from store"
|
|
35
35
|
|
|
36
36
|
# Test getting config with ask_user_if_key_not_found=True
|
|
37
|
-
with patch(
|
|
37
|
+
with patch("builtins.input", return_value="from user"):
|
|
38
38
|
config_getter = simple_config_getter(ask_user_if_key_not_found=True)
|
|
39
|
-
assert config_getter(
|
|
39
|
+
assert config_getter("new_key") == "from user"
|
|
40
40
|
|
|
41
41
|
# TODO: Make this test work
|
|
42
42
|
# Test that config_getter.configs is set correctly
|
|
@@ -57,7 +57,7 @@ def test_simple_config_getter_with_user_input(monkeypatch):
|
|
|
57
57
|
my_get_config = simple_config_getter(
|
|
58
58
|
local_config_dir, ask_user_if_key_not_found=True
|
|
59
59
|
)
|
|
60
|
-
config_name =
|
|
60
|
+
config_name = "SOME_CONFIG_NAME"
|
|
61
61
|
|
|
62
62
|
# make sure config_name not in environment or local_config_dir
|
|
63
63
|
assert config_name not in os.environ
|
|
@@ -67,41 +67,41 @@ def test_simple_config_getter_with_user_input(monkeypatch):
|
|
|
67
67
|
# should try to get it from the user
|
|
68
68
|
|
|
69
69
|
# Use monkeypatch to replace the input function with the mock_input function
|
|
70
|
-
user_inputs(
|
|
71
|
-
val = my_get_config(config_name, default=
|
|
70
|
+
user_inputs("") # user enters nothing
|
|
71
|
+
val = my_get_config(config_name, default="default_value")
|
|
72
72
|
|
|
73
73
|
# This the user didn't enter anything, the default value should be returned:
|
|
74
|
-
assert val ==
|
|
74
|
+
assert val == "default_value"
|
|
75
75
|
|
|
76
76
|
# Still no config_name in the local_config_dir
|
|
77
77
|
assert config_name not in os.listdir(local_config_dir)
|
|
78
78
|
|
|
79
|
-
user_inputs(
|
|
80
|
-
val = my_get_config(config_name, default=
|
|
79
|
+
user_inputs("user_value") # user enters user_value
|
|
80
|
+
val = my_get_config(config_name, default="default_value")
|
|
81
81
|
# Now the user entered a value, so that value should be returned:
|
|
82
|
-
assert val ==
|
|
82
|
+
assert val == "user_value"
|
|
83
83
|
|
|
84
84
|
# And now there's a config_name in the local_config_dir
|
|
85
85
|
assert config_name in os.listdir(local_config_dir)
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def test_source_config_params():
|
|
89
|
-
@source_config_params(
|
|
89
|
+
@source_config_params("a", "b")
|
|
90
90
|
def foo(a, b, c):
|
|
91
91
|
return a, b, c
|
|
92
92
|
|
|
93
|
-
config = {
|
|
94
|
-
_v = foo(a=
|
|
93
|
+
config = {"a": 1, "b": 2, "c": 3}
|
|
94
|
+
_v = foo(a="a", b="b", c=3, _config_getter=config.get)
|
|
95
95
|
assert _v == (1, 2, 3)
|
|
96
96
|
|
|
97
|
-
@source_config_params(
|
|
97
|
+
@source_config_params("a", "b")
|
|
98
98
|
def bar(a, b, c, **kw):
|
|
99
|
-
assert
|
|
99
|
+
assert "kw" not in kw, f"kw should be unpacked into **kw. Got: {kw=}"
|
|
100
100
|
return a + b + c + sum(kw.values())
|
|
101
101
|
|
|
102
|
-
_v = bar(a=
|
|
102
|
+
_v = bar(a="a", b="b", c=3, d=4, e=5, _config_getter=config.get)
|
|
103
103
|
assert _v == 15
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
if __name__ ==
|
|
107
|
-
pytest.main([
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
pytest.main(["-v", __file__])
|
config2py/tools.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
from i2 import Sig
|
|
9
9
|
|
|
10
10
|
from config2py.util import (
|
|
11
|
+
envvar,
|
|
11
12
|
get_configs_folder_for_app,
|
|
12
13
|
DFLT_CONFIG_FOLDER,
|
|
13
14
|
is_repl,
|
|
@@ -44,7 +45,7 @@ def get_configs_local_store(
|
|
|
44
45
|
# TODO: Not tested
|
|
45
46
|
# TODO: Make this open-closed plug-in via routing argument
|
|
46
47
|
_, extension = os.path.splitext(config_src)
|
|
47
|
-
if extension in {
|
|
48
|
+
if extension in {".ini", ".cfg"}:
|
|
48
49
|
from config2py.s_configparser import ConfigStore
|
|
49
50
|
|
|
50
51
|
return ConfigStore(config_src)
|
|
@@ -54,8 +55,8 @@ def get_configs_local_store(
|
|
|
54
55
|
return TextFiles(path)
|
|
55
56
|
else:
|
|
56
57
|
raise ValueError(
|
|
57
|
-
f
|
|
58
|
-
f
|
|
58
|
+
f"config_src must be a directory, ini or cfg file, or app name. "
|
|
59
|
+
f"Was: {config_src}"
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
|
|
@@ -89,7 +90,7 @@ def simple_config_getter(
|
|
|
89
90
|
central_configs = config_store_factory(configs_src)
|
|
90
91
|
sources = []
|
|
91
92
|
if first_look_in_env_vars:
|
|
92
|
-
sources.append(
|
|
93
|
+
sources.append(envvar)
|
|
93
94
|
sources.append(central_configs)
|
|
94
95
|
if ask_user_if_key_not_found is None:
|
|
95
96
|
# if the user didn't ask for anythin explicit (True or False), then
|
|
@@ -126,13 +127,13 @@ configs = local_configs # TODO: backwards compatibility alias
|
|
|
126
127
|
|
|
127
128
|
# --------------------------------------------------------------------
|
|
128
129
|
|
|
129
|
-
export_line_p = re.compile(
|
|
130
|
+
export_line_p = re.compile("export .+")
|
|
130
131
|
export_p = re.compile(r'(\w+)\s?\=\s?"(.+)"')
|
|
131
132
|
|
|
132
133
|
_extract_name_and_value_from_export_line = Pipe(
|
|
133
|
-
lambda x: x[len(
|
|
134
|
+
lambda x: x[len("export ") :],
|
|
134
135
|
lambda x: export_p.match(x),
|
|
135
|
-
lambda x: x.groups() if x else
|
|
136
|
+
lambda x: x.groups() if x else "",
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
|
|
@@ -155,7 +156,7 @@ def extract_exports(exports: str) -> dict:
|
|
|
155
156
|
then this simple parser can be useful.
|
|
156
157
|
|
|
157
158
|
"""
|
|
158
|
-
if
|
|
159
|
+
if "\n" not in exports and Path(resolve_path(exports)).is_file():
|
|
159
160
|
exports = Path(resolve_path(exports)).read_text()
|
|
160
161
|
return dict(
|
|
161
162
|
filter(
|
|
@@ -211,7 +212,7 @@ def source_config_params(*config_params):
|
|
|
211
212
|
|
|
212
213
|
sig = Sig(func)
|
|
213
214
|
|
|
214
|
-
@sig.add_params([
|
|
215
|
+
@sig.add_params(["_config_getter"])
|
|
215
216
|
def wrapped_func(*args, _config_getter, **kwargs):
|
|
216
217
|
def source(k, v):
|
|
217
218
|
if k == sig.var_keyword_name:
|
config2py/util.py
CHANGED
|
@@ -3,20 +3,23 @@
|
|
|
3
3
|
import re
|
|
4
4
|
import os
|
|
5
5
|
import ast
|
|
6
|
+
from collections import ChainMap
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Optional, Union, Any, Callable, Set, Iterable
|
|
8
9
|
import getpass
|
|
9
10
|
|
|
11
|
+
from dol import process_path
|
|
12
|
+
|
|
10
13
|
from i2 import mk_sentinel # TODO: Only i2 dependency. Consider replacing.
|
|
11
14
|
|
|
12
15
|
# def mk_sentinel(name): # TODO: Only i2 dependency. Here's replacement, but not picklable
|
|
13
16
|
# return type(name, (), {'__repr__': lambda self: name})()
|
|
14
17
|
|
|
15
|
-
DFLT_APP_NAME =
|
|
18
|
+
DFLT_APP_NAME = "config2py"
|
|
16
19
|
DFLT_MASKING_INPUT = False
|
|
17
20
|
|
|
18
|
-
not_found = mk_sentinel(
|
|
19
|
-
no_default = mk_sentinel(
|
|
21
|
+
not_found = mk_sentinel("not_found")
|
|
22
|
+
no_default = mk_sentinel("no_default")
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def always_true(x: Any) -> bool:
|
|
@@ -34,10 +37,34 @@ def is_not_empty(x: Any) -> bool:
|
|
|
34
37
|
return bool(x)
|
|
35
38
|
|
|
36
39
|
|
|
40
|
+
# Note: Why subclassing ChainMap works, but subclassing dict doesn't.
|
|
41
|
+
# The `EnvironmentVariables` class inherits from `collections.ChainMap` and wraps
|
|
42
|
+
# `os.environ`, providing a dynamic view of the environment variables without exposing
|
|
43
|
+
# sensitive data. Unlike a regular `dict` copy, which creates a static snapshot,
|
|
44
|
+
# `ChainMap` maintains a live reference to `os.environ`, so any changes to the
|
|
45
|
+
# environment—whether through `os.environ['KEY'] = 'value'` or external updates—are
|
|
46
|
+
# immediately reflected in `EnvironmentVariables`. Overriding `__repr__` ensures that
|
|
47
|
+
# printing the object (e.g., in a REPL or log) hides the actual contents,
|
|
48
|
+
# preserving confidentiality while retaining full read/write functionality.
|
|
49
|
+
class EnvironmentVariables(ChainMap):
|
|
50
|
+
"""
|
|
51
|
+
Class to wrap environment variables without revealing sensitive information.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(os.environ)
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
return "EnvironmentVariables"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
envvar = EnvironmentVariables()
|
|
62
|
+
|
|
63
|
+
|
|
37
64
|
# TODO: Make this into an open-closed mini-framework
|
|
38
65
|
def ask_user_for_input(
|
|
39
66
|
prompt: str,
|
|
40
|
-
default: str =
|
|
67
|
+
default: str = "",
|
|
41
68
|
*,
|
|
42
69
|
mask_input=DFLT_MASKING_INPUT,
|
|
43
70
|
masking_toggle_str: str = None,
|
|
@@ -57,16 +84,16 @@ def ask_user_for_input(
|
|
|
57
84
|
:return: The user's response (or the default value if the user entered nothing)
|
|
58
85
|
"""
|
|
59
86
|
_original_prompt = prompt
|
|
60
|
-
if prompt[-1] !=
|
|
61
|
-
prompt = prompt +
|
|
87
|
+
if prompt[-1] != " ": # pragma: no cover
|
|
88
|
+
prompt = prompt + " "
|
|
62
89
|
if masking_toggle_str is not None:
|
|
63
90
|
prompt = (
|
|
64
|
-
f
|
|
91
|
+
f"{prompt}\n"
|
|
65
92
|
f" (Input masking is {'ENABLED' if mask_input else 'DISABLED'}. "
|
|
66
93
|
f"Enter '{masking_toggle_str}' (without quotes) to toggle input masking)\n"
|
|
67
94
|
)
|
|
68
|
-
if default not in {
|
|
69
|
-
prompt = prompt + f
|
|
95
|
+
if default not in {""}:
|
|
96
|
+
prompt = prompt + f" [{default}]: "
|
|
70
97
|
if mask_input:
|
|
71
98
|
_prompt_func = getpass.getpass
|
|
72
99
|
else:
|
|
@@ -149,8 +176,8 @@ def extract_variable_declarations(
|
|
|
149
176
|
expand = None
|
|
150
177
|
|
|
151
178
|
env_vars = {}
|
|
152
|
-
pattern = re.compile(r
|
|
153
|
-
lines = string.split(
|
|
179
|
+
pattern = re.compile(r"^export\s+([A-Za-z0-9_]+)=(.*)$")
|
|
180
|
+
lines = string.split("\n")
|
|
154
181
|
for line in lines:
|
|
155
182
|
line = line.strip()
|
|
156
183
|
match = pattern.match(line)
|
|
@@ -159,7 +186,7 @@ def extract_variable_declarations(
|
|
|
159
186
|
value = match.group(2).strip('"')
|
|
160
187
|
if expand is not None:
|
|
161
188
|
for key, val in expand.items():
|
|
162
|
-
value = value.replace(f
|
|
189
|
+
value = value.replace(f"${key}", val)
|
|
163
190
|
env_vars[name] = value
|
|
164
191
|
expand = dict(expand, **env_vars)
|
|
165
192
|
else:
|
|
@@ -169,79 +196,80 @@ def extract_variable_declarations(
|
|
|
169
196
|
|
|
170
197
|
def _system_default_for_app_data_folder():
|
|
171
198
|
"""Get the system default for the app data folder."""
|
|
172
|
-
if os.name ==
|
|
199
|
+
if os.name == "nt":
|
|
173
200
|
# Windows
|
|
174
|
-
app_data_folder = os.getenv(
|
|
201
|
+
app_data_folder = os.getenv("APPDATA")
|
|
175
202
|
else:
|
|
176
203
|
# macOS and Linux/Unix
|
|
177
|
-
app_data_folder = os.path.expanduser(
|
|
204
|
+
app_data_folder = os.path.expanduser("~/.config")
|
|
178
205
|
return app_data_folder
|
|
179
206
|
|
|
180
207
|
|
|
181
208
|
DFLT_APP_DATA_FOLDER = os.getenv(
|
|
182
|
-
|
|
209
|
+
"CONFIG2PY_APP_DATA_FOLDER", _system_default_for_app_data_folder()
|
|
183
210
|
)
|
|
184
211
|
|
|
185
212
|
|
|
186
|
-
def
|
|
187
|
-
*path: Iterable[str],
|
|
188
|
-
ensure_dir_exists=False,
|
|
189
|
-
assert_exists=False,
|
|
190
|
-
ensure_endswith_slash=False,
|
|
191
|
-
ensure_does_not_end_with_slash=False,
|
|
192
|
-
expanduser=True,
|
|
193
|
-
expandvars=True,
|
|
194
|
-
abspath=True,
|
|
195
|
-
rootdir: str = '',
|
|
196
|
-
) -> str:
|
|
213
|
+
def create_directories(dirpath, max_dirs_to_make=None):
|
|
197
214
|
"""
|
|
198
|
-
|
|
215
|
+
Create directories up to a specified limit.
|
|
199
216
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
assert_exists (bool): Whether to assert that the path exists.
|
|
204
|
-
ensure_endswith_slash (bool): Whether to ensure the path ends with a slash.
|
|
205
|
-
ensure_does_not_end_with_slash (bool): Whether to ensure the path does not end with a slash.
|
|
206
|
-
expanduser (bool): Whether to expand the user in the path.
|
|
207
|
-
expandvars (bool): Whether to expand environment variables in the path.
|
|
208
|
-
abspath (bool): Whether to convert the path to an absolute path.
|
|
209
|
-
rootdir (str): The root directory to prepend to the path.
|
|
217
|
+
Parameters:
|
|
218
|
+
dirpath (str): The directory path to create.
|
|
219
|
+
max_dirs_to_make (int, optional): The maximum number of directories to create. If None, there's no limit.
|
|
210
220
|
|
|
211
221
|
Returns:
|
|
212
|
-
|
|
222
|
+
bool: True if the directory was created successfully, False otherwise.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If max_dirs_to_make is negative.
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
>>> import tempfile, shutil
|
|
229
|
+
>>> temp_dir = tempfile.mkdtemp()
|
|
230
|
+
>>> target_dir = os.path.join(temp_dir, 'a', 'b', 'c')
|
|
231
|
+
>>> create_directories(target_dir, max_dirs_to_make=2)
|
|
232
|
+
False
|
|
233
|
+
>>> create_directories(target_dir, max_dirs_to_make=3)
|
|
234
|
+
True
|
|
235
|
+
>>> os.path.isdir(target_dir)
|
|
236
|
+
True
|
|
237
|
+
>>> shutil.rmtree(temp_dir) # Cleanup
|
|
238
|
+
|
|
239
|
+
>>> temp_dir = tempfile.mkdtemp()
|
|
240
|
+
>>> target_dir = os.path.join(temp_dir, 'a', 'b', 'c', 'd')
|
|
241
|
+
>>> create_directories(target_dir)
|
|
242
|
+
True
|
|
243
|
+
>>> os.path.isdir(target_dir)
|
|
244
|
+
True
|
|
245
|
+
>>> shutil.rmtree(temp_dir) # Cleanup
|
|
246
|
+
"""
|
|
247
|
+
if max_dirs_to_make is not None and max_dirs_to_make < 0:
|
|
248
|
+
raise ValueError("max_dirs_to_make must be non-negative or None")
|
|
213
249
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
>>> from functools import partial
|
|
217
|
-
>>> process_path('a', 'b', 'c', rootdir='/root/dir/', ensure_endswith_slash=True)
|
|
218
|
-
'/root/dir/a/b/c/'
|
|
250
|
+
if os.path.exists(dirpath):
|
|
251
|
+
return True
|
|
219
252
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if ensure_dir_exists:
|
|
241
|
-
os.makedirs(path, exist_ok=True)
|
|
242
|
-
if assert_exists:
|
|
243
|
-
assert os.path.exists(path), f'Path does not exist: {path}'
|
|
244
|
-
return path
|
|
253
|
+
if max_dirs_to_make is None:
|
|
254
|
+
os.makedirs(dirpath, exist_ok=True)
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# Calculate the number of directories to create
|
|
258
|
+
dirs_to_make = []
|
|
259
|
+
current_path = dirpath
|
|
260
|
+
|
|
261
|
+
while not os.path.exists(current_path):
|
|
262
|
+
dirs_to_make.append(current_path)
|
|
263
|
+
current_path, _ = os.path.split(current_path)
|
|
264
|
+
|
|
265
|
+
if len(dirs_to_make) > max_dirs_to_make:
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
# Create directories from the top level down
|
|
269
|
+
for dir_to_make in reversed(dirs_to_make):
|
|
270
|
+
os.mkdir(dir_to_make)
|
|
271
|
+
|
|
272
|
+
return True
|
|
245
273
|
|
|
246
274
|
|
|
247
275
|
# Note: First possible i2 dependency -- vendoring for now
|
|
@@ -291,7 +319,7 @@ def _default_folder_setup(directory_path: str) -> None:
|
|
|
291
319
|
# Add a hidden file to annotate the directory as one managed by config2py.
|
|
292
320
|
# This helps distinguish it from directories created by other programs
|
|
293
321
|
# (this can be useful to avoid conflicts).
|
|
294
|
-
(Path(directory_path) /
|
|
322
|
+
(Path(directory_path) / ".config2py").write_text("Created by config2py.")
|
|
295
323
|
|
|
296
324
|
|
|
297
325
|
def get_app_data_folder(
|
|
@@ -344,7 +372,7 @@ def get_app_data_folder(
|
|
|
344
372
|
return app_data_path
|
|
345
373
|
|
|
346
374
|
|
|
347
|
-
DFLT_CONFIGS_NAME =
|
|
375
|
+
DFLT_CONFIGS_NAME = "configs"
|
|
348
376
|
|
|
349
377
|
|
|
350
378
|
# TODO: is "get" the right word, since it makes the folder too?
|
|
@@ -384,11 +412,11 @@ import sys
|
|
|
384
412
|
|
|
385
413
|
|
|
386
414
|
def _get_ipython_in_globals():
|
|
387
|
-
return
|
|
415
|
+
return "get_ipython" in globals()
|
|
388
416
|
|
|
389
417
|
|
|
390
418
|
def _main_does_not_have_file_attribute():
|
|
391
|
-
return not hasattr(sys.modules[
|
|
419
|
+
return not hasattr(sys.modules["__main__"], "__file__")
|
|
392
420
|
|
|
393
421
|
|
|
394
422
|
_repl_conditions = {_get_ipython_in_globals, _main_does_not_have_file_attribute}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: config2py
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.37
|
|
4
4
|
Summary: Simplified reading and writing configurations from various sources and formats
|
|
5
5
|
Home-page: https://github.com/i2mint/config2py
|
|
6
6
|
Author: OtoSense
|
|
@@ -10,7 +10,7 @@ Description-Content-Type: text/markdown
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: dol
|
|
12
12
|
Requires-Dist: i2
|
|
13
|
-
Requires-Dist: importlib-resources
|
|
13
|
+
Requires-Dist: importlib-resources; python_version < "3.9"
|
|
14
14
|
|
|
15
15
|
# config2py
|
|
16
16
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
config2py/__init__.py,sha256=GZ8lkm8TOKrV-zVd_Tc_8XVXDzFePECacjhOSGe2xcs,844
|
|
2
|
+
config2py/base.py,sha256=eQpRQjZYT-z6GhBemepaPUEyVVP8e_l04dghYeBBJdI,15880
|
|
3
|
+
config2py/errors.py,sha256=QdwGsoJhv6LHDHp-_yyz4oUg1Fgu4S-S7O2nuA0a5cw,203
|
|
4
|
+
config2py/s_configparser.py,sha256=-Sl2-J-QOLUiahwhCTiPsmjs4cKc79JuTbQ9gQcOiGY,15871
|
|
5
|
+
config2py/tools.py,sha256=goDuHHXKJzdFgmHzDnLBGMZEhp0kKU-aK47c1-MpJT8,9199
|
|
6
|
+
config2py/util.py,sha256=bchsJjaMnsfnplCJsg7i0Osyzuha3N-hExuc6gmS-7I,17188
|
|
7
|
+
config2py/scrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
config2py/tests/__init__.py,sha256=sk-yGJQOZES2z70M4xmZB57tsxSktX_84ybDuV8Cz5Q,297
|
|
9
|
+
config2py/tests/test_tools.py,sha256=sdiBNTavuzxW2AsqBRTO9U21iWig5DEyV38r6lmaZak,3728
|
|
10
|
+
config2py/tests/utils_for_testing.py,sha256=RcMiVtKK39rc8BsgIXQH3RCkd8qKo2o2MT7Rt0dJF2E,162
|
|
11
|
+
config2py-0.1.37.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
+
config2py-0.1.37.dist-info/METADATA,sha256=oVY9jkk7t4epX9WoV1luTW6z2E-enwqfm92p2M9xyQU,14558
|
|
13
|
+
config2py-0.1.37.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
14
|
+
config2py-0.1.37.dist-info/top_level.txt,sha256=DFnlOIKMIGWQRROr3voJFhWFViHaWgTTeWZjC5YC9QQ,10
|
|
15
|
+
config2py-0.1.37.dist-info/RECORD,,
|
config2py/tests/test_util.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import tempfile
|
|
2
|
-
import os
|
|
3
|
-
import pytest
|
|
4
|
-
from config2py.util import process_path
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_process_path():
|
|
8
|
-
# Create a temporary directory
|
|
9
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
10
|
-
temp_path = os.path.join(temp_dir, 'foo/bar')
|
|
11
|
-
|
|
12
|
-
output_path = process_path(temp_path)
|
|
13
|
-
assert output_path == temp_path
|
|
14
|
-
assert not os.path.exists(output_path)
|
|
15
|
-
|
|
16
|
-
output_path = process_path(temp_path, expanduser=False)
|
|
17
|
-
assert output_path == temp_path
|
|
18
|
-
assert not os.path.exists(output_path)
|
|
19
|
-
|
|
20
|
-
with pytest.raises(AssertionError):
|
|
21
|
-
output_path = process_path(temp_path, assert_exists=True)
|
|
22
|
-
|
|
23
|
-
output_path = process_path(temp_path, ensure_dir_exists=True)
|
|
24
|
-
assert output_path == temp_path
|
|
25
|
-
assert os.path.exists(output_path)
|
|
26
|
-
|
|
27
|
-
output_path = process_path(temp_path, assert_exists=True)
|
|
28
|
-
assert output_path == temp_path
|
|
29
|
-
assert os.path.exists(output_path)
|
|
30
|
-
|
|
31
|
-
# If path doesn't end with a (system file separator) slash, add one:
|
|
32
|
-
output_path = process_path(temp_path, ensure_endswith_slash=True)
|
|
33
|
-
assert output_path == temp_path + os.path.sep
|
|
34
|
-
|
|
35
|
-
# If path ends with a (system file separator) slash, remove it.
|
|
36
|
-
output_path = process_path(
|
|
37
|
-
temp_path + os.path.sep, ensure_does_not_end_with_slash=True
|
|
38
|
-
)
|
|
39
|
-
assert output_path == temp_path
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
config2py/__init__.py,sha256=ru-bUQk5EuOLzHmNAcElSN8P2saSUEb5KFRkfTkzsUo,770
|
|
2
|
-
config2py/base.py,sha256=fDKClGpgNecWFL9a7Y_FpqwuRxAAD_XCuByW68Yq5KE,15880
|
|
3
|
-
config2py/errors.py,sha256=QdwGsoJhv6LHDHp-_yyz4oUg1Fgu4S-S7O2nuA0a5cw,203
|
|
4
|
-
config2py/s_configparser.py,sha256=XhxFz6-PG4-QsecJfbjLFdBWHcPU6dwgqwkTZyY_y3E,15873
|
|
5
|
-
config2py/tools.py,sha256=W2YQm-PerKRN8prsxVfcBCChx75pZ9H8UqWH8pGxECE,9191
|
|
6
|
-
config2py/util.py,sha256=C9JiGlMcSQozq6H-nuaS02sh49i4irRzSVqofW9Bq74,16460
|
|
7
|
-
config2py/scrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
config2py/tests/__init__.py,sha256=sk-yGJQOZES2z70M4xmZB57tsxSktX_84ybDuV8Cz5Q,297
|
|
9
|
-
config2py/tests/test_tools.py,sha256=km9RNh-gDtSJNjYiLQdkWcRi9IB5MwpijD3U2XCyi1Y,3728
|
|
10
|
-
config2py/tests/test_util.py,sha256=DNJn60dvyr7xb86Spoz_VDS4cwU2mtcRo1_MgoCrKCY,1391
|
|
11
|
-
config2py/tests/utils_for_testing.py,sha256=Vz6EDY27uy_RZCSceZ7jqXkp_CXe52KAZSXcYKivazM,162
|
|
12
|
-
config2py-0.1.35.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
13
|
-
config2py-0.1.35.dist-info/METADATA,sha256=LN--FD2SE3BMV8vAfAq5CUSaksGY5DbXKw8qyZagzbs,14559
|
|
14
|
-
config2py-0.1.35.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
15
|
-
config2py-0.1.35.dist-info/top_level.txt,sha256=DFnlOIKMIGWQRROr3voJFhWFViHaWgTTeWZjC5YC9QQ,10
|
|
16
|
-
config2py-0.1.35.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|