config2py 0.1.36__py3-none-any.whl → 0.1.38__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 +46 -21
- {config2py-0.1.36.dist-info → config2py-0.1.38.dist-info}/METADATA +2 -3
- config2py-0.1.38.dist-info/RECORD +15 -0
- {config2py-0.1.36.dist-info → config2py-0.1.38.dist-info}/WHEEL +1 -1
- config2py-0.1.36.dist-info/RECORD +0 -15
- {config2py-0.1.36.dist-info → config2py-0.1.38.dist-info}/LICENSE +0 -0
- {config2py-0.1.36.dist-info → config2py-0.1.38.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,6 +3,7 @@
|
|
|
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
|
|
@@ -14,11 +15,11 @@ from i2 import mk_sentinel # TODO: Only i2 dependency. Consider replacing.
|
|
|
14
15
|
# def mk_sentinel(name): # TODO: Only i2 dependency. Here's replacement, but not picklable
|
|
15
16
|
# return type(name, (), {'__repr__': lambda self: name})()
|
|
16
17
|
|
|
17
|
-
DFLT_APP_NAME =
|
|
18
|
+
DFLT_APP_NAME = "config2py"
|
|
18
19
|
DFLT_MASKING_INPUT = False
|
|
19
20
|
|
|
20
|
-
not_found = mk_sentinel(
|
|
21
|
-
no_default = mk_sentinel(
|
|
21
|
+
not_found = mk_sentinel("not_found")
|
|
22
|
+
no_default = mk_sentinel("no_default")
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def always_true(x: Any) -> bool:
|
|
@@ -36,10 +37,34 @@ def is_not_empty(x: Any) -> bool:
|
|
|
36
37
|
return bool(x)
|
|
37
38
|
|
|
38
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
|
+
|
|
39
64
|
# TODO: Make this into an open-closed mini-framework
|
|
40
65
|
def ask_user_for_input(
|
|
41
66
|
prompt: str,
|
|
42
|
-
default: str =
|
|
67
|
+
default: str = "",
|
|
43
68
|
*,
|
|
44
69
|
mask_input=DFLT_MASKING_INPUT,
|
|
45
70
|
masking_toggle_str: str = None,
|
|
@@ -59,16 +84,16 @@ def ask_user_for_input(
|
|
|
59
84
|
:return: The user's response (or the default value if the user entered nothing)
|
|
60
85
|
"""
|
|
61
86
|
_original_prompt = prompt
|
|
62
|
-
if prompt[-1] !=
|
|
63
|
-
prompt = prompt +
|
|
87
|
+
if prompt[-1] != " ": # pragma: no cover
|
|
88
|
+
prompt = prompt + " "
|
|
64
89
|
if masking_toggle_str is not None:
|
|
65
90
|
prompt = (
|
|
66
|
-
f
|
|
91
|
+
f"{prompt}\n"
|
|
67
92
|
f" (Input masking is {'ENABLED' if mask_input else 'DISABLED'}. "
|
|
68
93
|
f"Enter '{masking_toggle_str}' (without quotes) to toggle input masking)\n"
|
|
69
94
|
)
|
|
70
|
-
if default not in {
|
|
71
|
-
prompt = prompt + f
|
|
95
|
+
if default not in {""}:
|
|
96
|
+
prompt = prompt + f" [{default}]: "
|
|
72
97
|
if mask_input:
|
|
73
98
|
_prompt_func = getpass.getpass
|
|
74
99
|
else:
|
|
@@ -151,8 +176,8 @@ def extract_variable_declarations(
|
|
|
151
176
|
expand = None
|
|
152
177
|
|
|
153
178
|
env_vars = {}
|
|
154
|
-
pattern = re.compile(r
|
|
155
|
-
lines = string.split(
|
|
179
|
+
pattern = re.compile(r"^export\s+([A-Za-z0-9_]+)=(.*)$")
|
|
180
|
+
lines = string.split("\n")
|
|
156
181
|
for line in lines:
|
|
157
182
|
line = line.strip()
|
|
158
183
|
match = pattern.match(line)
|
|
@@ -161,7 +186,7 @@ def extract_variable_declarations(
|
|
|
161
186
|
value = match.group(2).strip('"')
|
|
162
187
|
if expand is not None:
|
|
163
188
|
for key, val in expand.items():
|
|
164
|
-
value = value.replace(f
|
|
189
|
+
value = value.replace(f"${key}", val)
|
|
165
190
|
env_vars[name] = value
|
|
166
191
|
expand = dict(expand, **env_vars)
|
|
167
192
|
else:
|
|
@@ -171,17 +196,17 @@ def extract_variable_declarations(
|
|
|
171
196
|
|
|
172
197
|
def _system_default_for_app_data_folder():
|
|
173
198
|
"""Get the system default for the app data folder."""
|
|
174
|
-
if os.name ==
|
|
199
|
+
if os.name == "nt":
|
|
175
200
|
# Windows
|
|
176
|
-
app_data_folder = os.getenv(
|
|
201
|
+
app_data_folder = os.getenv("APPDATA")
|
|
177
202
|
else:
|
|
178
203
|
# macOS and Linux/Unix
|
|
179
|
-
app_data_folder = os.path.expanduser(
|
|
204
|
+
app_data_folder = os.path.expanduser("~/.config")
|
|
180
205
|
return app_data_folder
|
|
181
206
|
|
|
182
207
|
|
|
183
208
|
DFLT_APP_DATA_FOLDER = os.getenv(
|
|
184
|
-
|
|
209
|
+
"CONFIG2PY_APP_DATA_FOLDER", _system_default_for_app_data_folder()
|
|
185
210
|
)
|
|
186
211
|
|
|
187
212
|
|
|
@@ -220,7 +245,7 @@ def create_directories(dirpath, max_dirs_to_make=None):
|
|
|
220
245
|
>>> shutil.rmtree(temp_dir) # Cleanup
|
|
221
246
|
"""
|
|
222
247
|
if max_dirs_to_make is not None and max_dirs_to_make < 0:
|
|
223
|
-
raise ValueError(
|
|
248
|
+
raise ValueError("max_dirs_to_make must be non-negative or None")
|
|
224
249
|
|
|
225
250
|
if os.path.exists(dirpath):
|
|
226
251
|
return True
|
|
@@ -294,7 +319,7 @@ def _default_folder_setup(directory_path: str) -> None:
|
|
|
294
319
|
# Add a hidden file to annotate the directory as one managed by config2py.
|
|
295
320
|
# This helps distinguish it from directories created by other programs
|
|
296
321
|
# (this can be useful to avoid conflicts).
|
|
297
|
-
(Path(directory_path) /
|
|
322
|
+
(Path(directory_path) / ".config2py").write_text("Created by config2py.")
|
|
298
323
|
|
|
299
324
|
|
|
300
325
|
def get_app_data_folder(
|
|
@@ -347,7 +372,7 @@ def get_app_data_folder(
|
|
|
347
372
|
return app_data_path
|
|
348
373
|
|
|
349
374
|
|
|
350
|
-
DFLT_CONFIGS_NAME =
|
|
375
|
+
DFLT_CONFIGS_NAME = "configs"
|
|
351
376
|
|
|
352
377
|
|
|
353
378
|
# TODO: is "get" the right word, since it makes the folder too?
|
|
@@ -387,11 +412,11 @@ import sys
|
|
|
387
412
|
|
|
388
413
|
|
|
389
414
|
def _get_ipython_in_globals():
|
|
390
|
-
return
|
|
415
|
+
return "get_ipython" in globals()
|
|
391
416
|
|
|
392
417
|
|
|
393
418
|
def _main_does_not_have_file_attribute():
|
|
394
|
-
return not hasattr(sys.modules[
|
|
419
|
+
return not hasattr(sys.modules["__main__"], "__file__")
|
|
395
420
|
|
|
396
421
|
|
|
397
422
|
_repl_conditions = {_get_ipython_in_globals, _main_does_not_have_file_attribute}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: config2py
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.38
|
|
4
4
|
Summary: Simplified reading and writing configurations from various sources and formats
|
|
5
5
|
Home-page: https://github.com/i2mint/config2py
|
|
6
|
-
Author: OtoSense
|
|
7
6
|
License: apache-2.0
|
|
8
7
|
Platform: any
|
|
9
8
|
Description-Content-Type: text/markdown
|
|
10
9
|
License-File: LICENSE
|
|
11
10
|
Requires-Dist: dol
|
|
12
11
|
Requires-Dist: i2
|
|
13
|
-
Requires-Dist: importlib-resources
|
|
12
|
+
Requires-Dist: importlib-resources; python_version < "3.9"
|
|
14
13
|
|
|
15
14
|
# config2py
|
|
16
15
|
|
|
@@ -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.38.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
+
config2py-0.1.38.dist-info/METADATA,sha256=cj4bH2tshyDKLOGqttbOXrNWV7zB_AjNr-1-dEYtfVg,14541
|
|
13
|
+
config2py-0.1.38.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
14
|
+
config2py-0.1.38.dist-info/top_level.txt,sha256=DFnlOIKMIGWQRROr3voJFhWFViHaWgTTeWZjC5YC9QQ,10
|
|
15
|
+
config2py-0.1.38.dist-info/RECORD,,
|
|
@@ -1,15 +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=xVxfOduKsv1BdNIFVVdPMmA0wxBYyipCFYpPD-r6A6Y,16136
|
|
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/utils_for_testing.py,sha256=Vz6EDY27uy_RZCSceZ7jqXkp_CXe52KAZSXcYKivazM,162
|
|
11
|
-
config2py-0.1.36.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
-
config2py-0.1.36.dist-info/METADATA,sha256=VGU8KFs1Xt3w8HL2cp-895nDZhbmXxgpTjBMhc_2lR0,14559
|
|
13
|
-
config2py-0.1.36.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
14
|
-
config2py-0.1.36.dist-info/top_level.txt,sha256=DFnlOIKMIGWQRROr3voJFhWFViHaWgTTeWZjC5YC9QQ,10
|
|
15
|
-
config2py-0.1.36.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|