config2py 0.1.33__py3-none-any.whl → 0.1.35__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/base.py +15 -17
- config2py/tests/test_tools.py +44 -0
- config2py/tests/{util.py → test_util.py} +1 -0
- config2py/tests/utils_for_testing.py +5 -0
- config2py/tools.py +2 -2
- config2py/util.py +21 -4
- {config2py-0.1.33.dist-info → config2py-0.1.35.dist-info}/METADATA +1 -1
- config2py-0.1.35.dist-info/RECORD +16 -0
- config2py-0.1.33.dist-info/RECORD +0 -15
- {config2py-0.1.33.dist-info → config2py-0.1.35.dist-info}/LICENSE +0 -0
- {config2py-0.1.33.dist-info → config2py-0.1.35.dist-info}/WHEEL +0 -0
- {config2py-0.1.33.dist-info → config2py-0.1.35.dist-info}/top_level.txt +0 -0
config2py/base.py
CHANGED
|
@@ -19,14 +19,10 @@ from typing import (
|
|
|
19
19
|
)
|
|
20
20
|
from dataclasses import dataclass
|
|
21
21
|
from functools import lru_cache, partial
|
|
22
|
-
from i2 import mk_sentinel # TODO: Only i2 dependency. Consider replacing.
|
|
23
22
|
|
|
24
|
-
from config2py.util import always_true, ask_user_for_input
|
|
23
|
+
from config2py.util import always_true, ask_user_for_input, no_default, not_found
|
|
25
24
|
from config2py.errors import ConfigNotFound
|
|
26
25
|
|
|
27
|
-
# def mk_sentinel(name): # TODO: Only i2 dependency. Here's replacement, but not picklable
|
|
28
|
-
# return type(name, (), {'__repr__': lambda self: name})()
|
|
29
|
-
|
|
30
26
|
Exceptions = Tuple[Type[Exception], ...]
|
|
31
27
|
|
|
32
28
|
|
|
@@ -95,9 +91,6 @@ GetConfigEgress = Callable[[KT, VT], VT]
|
|
|
95
91
|
# )
|
|
96
92
|
# openai.api_key = _api_key
|
|
97
93
|
|
|
98
|
-
config_not_found = mk_sentinel('config_not_found')
|
|
99
|
-
no_default = mk_sentinel('no_default')
|
|
100
|
-
|
|
101
94
|
|
|
102
95
|
def is_not_none_nor_empty(x):
|
|
103
96
|
if isinstance(x, str):
|
|
@@ -111,8 +104,8 @@ def get_config(
|
|
|
111
104
|
sources: Sources = None,
|
|
112
105
|
*,
|
|
113
106
|
default: VT = no_default,
|
|
114
|
-
egress: GetConfigEgress = None,
|
|
115
|
-
val_is_valid: Callable[[VT], bool] = always_true,
|
|
107
|
+
egress: Optional[GetConfigEgress] = None,
|
|
108
|
+
val_is_valid: Optional[Callable[[VT], bool]] = always_true,
|
|
116
109
|
config_not_found_exceptions: Exceptions = (Exception,),
|
|
117
110
|
):
|
|
118
111
|
"""Get a config value from a list of sources
|
|
@@ -215,8 +208,8 @@ def get_config(
|
|
|
215
208
|
get_config_.sources = sources
|
|
216
209
|
return get_config_
|
|
217
210
|
chain_map = sources_chainmap(sources, val_is_valid, config_not_found_exceptions)
|
|
218
|
-
value = chain_map.get(key,
|
|
219
|
-
if value is
|
|
211
|
+
value = chain_map.get(key, not_found)
|
|
212
|
+
if value is not_found:
|
|
220
213
|
if default is no_default:
|
|
221
214
|
raise ConfigNotFound(f'Could not find config for key: {key}')
|
|
222
215
|
else:
|
|
@@ -290,12 +283,16 @@ class FuncBasedGettableContainer:
|
|
|
290
283
|
getter: Callable[[KT], VT]
|
|
291
284
|
val_is_valid: Callable[[VT], bool] = always_true
|
|
292
285
|
config_not_found_exceptions: Exceptions = (Exception,)
|
|
286
|
+
cache_getter = False
|
|
293
287
|
|
|
294
288
|
def __post_init__(self):
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
if self.cache_getter:
|
|
290
|
+
# Note: The only purpose of the cache is to avoid calling the getter function
|
|
291
|
+
# twice when doing a ``in`` check before doing a ``[]`` lookup, for instance,
|
|
292
|
+
# in a``collections.ChainMap``.
|
|
293
|
+
# But this caching can lead to unexpected behavior if the getter function
|
|
294
|
+
# has side effects, or if the value it returns changes over time.
|
|
295
|
+
self.getter = lru_cache(maxsize=1)(self.getter)
|
|
299
296
|
|
|
300
297
|
def __getitem__(self, k: KT) -> VT:
|
|
301
298
|
try:
|
|
@@ -384,6 +381,7 @@ def ask_user_for_key(
|
|
|
384
381
|
ask_user_for_key,
|
|
385
382
|
prompt_template=prompt_template,
|
|
386
383
|
save_to=save_to,
|
|
384
|
+
save_condition=save_condition,
|
|
387
385
|
user_asker=user_asker,
|
|
388
386
|
egress=egress,
|
|
389
387
|
)
|
|
@@ -403,7 +401,7 @@ def user_gettable(
|
|
|
403
401
|
prompt_template='Enter a value for {}: ',
|
|
404
402
|
egress: Optional[Callable] = None,
|
|
405
403
|
user_asker=ask_user_for_input,
|
|
406
|
-
val_is_valid: Callable[[VT], bool] =
|
|
404
|
+
val_is_valid: Callable[[VT], bool] = is_not_empty,
|
|
407
405
|
config_not_found_exceptions: Exceptions = (Exception,),
|
|
408
406
|
):
|
|
409
407
|
"""
|
config2py/tests/test_tools.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import os
|
|
4
4
|
from unittest.mock import patch
|
|
5
5
|
|
|
6
|
+
import tempfile
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
8
9
|
from config2py.tools import simple_config_getter, source_config_params
|
|
10
|
+
from config2py.tests.utils_for_testing import user_input_patch
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@pytest.fixture
|
|
@@ -41,6 +43,48 @@ def test_simple_config_getter(mock_config_store_factory):
|
|
|
41
43
|
# assert config_getter.configs is mock_config_store
|
|
42
44
|
|
|
43
45
|
|
|
46
|
+
from functools import partial
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_simple_config_getter_with_user_input(monkeypatch):
|
|
50
|
+
user_inputs = partial(user_input_patch, monkeypatch)
|
|
51
|
+
local_config_dir = tempfile.mkdtemp()
|
|
52
|
+
|
|
53
|
+
# Note, at the time of writing this, the default is ask_user_if_key_not_found=None,
|
|
54
|
+
# which has the effect of NOT asking the user for a config value if we're not in
|
|
55
|
+
# a REPL (interactive mode). Therefore, the test worked in the notebook, but not here.
|
|
56
|
+
# So now, we're forcing ask_user_if_key_not_found=True
|
|
57
|
+
my_get_config = simple_config_getter(
|
|
58
|
+
local_config_dir, ask_user_if_key_not_found=True
|
|
59
|
+
)
|
|
60
|
+
config_name = 'SOME_CONFIG_NAME'
|
|
61
|
+
|
|
62
|
+
# make sure config_name not in environment or local_config_dir
|
|
63
|
+
assert config_name not in os.environ
|
|
64
|
+
assert config_name not in os.listdir(local_config_dir)
|
|
65
|
+
|
|
66
|
+
# since config_name doesn't exist, the following attempt to get this config
|
|
67
|
+
# should try to get it from the user
|
|
68
|
+
|
|
69
|
+
# Use monkeypatch to replace the input function with the mock_input function
|
|
70
|
+
user_inputs('') # user enters nothing
|
|
71
|
+
val = my_get_config(config_name, default='default_value')
|
|
72
|
+
|
|
73
|
+
# This the user didn't enter anything, the default value should be returned:
|
|
74
|
+
assert val == 'default_value'
|
|
75
|
+
|
|
76
|
+
# Still no config_name in the local_config_dir
|
|
77
|
+
assert config_name not in os.listdir(local_config_dir)
|
|
78
|
+
|
|
79
|
+
user_inputs('user_value') # user enters user_value
|
|
80
|
+
val = my_get_config(config_name, default='default_value')
|
|
81
|
+
# Now the user entered a value, so that value should be returned:
|
|
82
|
+
assert val == 'user_value'
|
|
83
|
+
|
|
84
|
+
# And now there's a config_name in the local_config_dir
|
|
85
|
+
assert config_name in os.listdir(local_config_dir)
|
|
86
|
+
|
|
87
|
+
|
|
44
88
|
def test_source_config_params():
|
|
45
89
|
@source_config_params('a', 'b')
|
|
46
90
|
def foo(a, b, c):
|
config2py/tools.py
CHANGED
|
@@ -27,7 +27,7 @@ def get_configs_local_store(
|
|
|
27
27
|
If it's a file, it's assumed to be an ini or cfg file.
|
|
28
28
|
If it's a string, it's assumed to be an app name, from which to create a folder
|
|
29
29
|
"""
|
|
30
|
-
if os.path.isdir(config_src):
|
|
30
|
+
if os.path.sep in config_src and os.path.isdir(config_src):
|
|
31
31
|
# TODO: This was a quick fix to avoid unknowingly making directories in the
|
|
32
32
|
# wrong place. Broke stuff so leaving this for later.
|
|
33
33
|
# if os.path.sep not in config_src:
|
|
@@ -76,7 +76,7 @@ def simple_config_getter(
|
|
|
76
76
|
default) asks the user for the value and stores it in the central config store.
|
|
77
77
|
|
|
78
78
|
:param configs_src: A specification of the central config store. By default:
|
|
79
|
-
If it's a directory, it's assumed to be a folder of text files.
|
|
79
|
+
If it's a directory (with at least a slash), it's assumed to be a folder of text files.
|
|
80
80
|
If it's a file, it's assumed to be an ini or cfg file.
|
|
81
81
|
If it's a string, it's assumed to be an app name, from which to create a folder
|
|
82
82
|
:param first_look_in_env_vars: Whether to look in environment variables first
|
config2py/util.py
CHANGED
|
@@ -7,9 +7,17 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Optional, Union, Any, Callable, Set, Iterable
|
|
8
8
|
import getpass
|
|
9
9
|
|
|
10
|
+
from i2 import mk_sentinel # TODO: Only i2 dependency. Consider replacing.
|
|
11
|
+
|
|
12
|
+
# def mk_sentinel(name): # TODO: Only i2 dependency. Here's replacement, but not picklable
|
|
13
|
+
# return type(name, (), {'__repr__': lambda self: name})()
|
|
14
|
+
|
|
10
15
|
DFLT_APP_NAME = 'config2py'
|
|
11
16
|
DFLT_MASKING_INPUT = False
|
|
12
17
|
|
|
18
|
+
not_found = mk_sentinel('not_found')
|
|
19
|
+
no_default = mk_sentinel('no_default')
|
|
20
|
+
|
|
13
21
|
|
|
14
22
|
def always_true(x: Any) -> bool:
|
|
15
23
|
"""Function that just returns True."""
|
|
@@ -29,7 +37,7 @@ def is_not_empty(x: Any) -> bool:
|
|
|
29
37
|
# TODO: Make this into an open-closed mini-framework
|
|
30
38
|
def ask_user_for_input(
|
|
31
39
|
prompt: str,
|
|
32
|
-
default: str =
|
|
40
|
+
default: str = '',
|
|
33
41
|
*,
|
|
34
42
|
mask_input=DFLT_MASKING_INPUT,
|
|
35
43
|
masking_toggle_str: str = None,
|
|
@@ -57,7 +65,7 @@ def ask_user_for_input(
|
|
|
57
65
|
f" (Input masking is {'ENABLED' if mask_input else 'DISABLED'}. "
|
|
58
66
|
f"Enter '{masking_toggle_str}' (without quotes) to toggle input masking)\n"
|
|
59
67
|
)
|
|
60
|
-
if default:
|
|
68
|
+
if default not in {''}:
|
|
61
69
|
prompt = prompt + f' [{default}]: '
|
|
62
70
|
if mask_input:
|
|
63
71
|
_prompt_func = getpass.getpass
|
|
@@ -182,6 +190,8 @@ def process_path(
|
|
|
182
190
|
ensure_endswith_slash=False,
|
|
183
191
|
ensure_does_not_end_with_slash=False,
|
|
184
192
|
expanduser=True,
|
|
193
|
+
expandvars=True,
|
|
194
|
+
abspath=True,
|
|
185
195
|
rootdir: str = '',
|
|
186
196
|
) -> str:
|
|
187
197
|
"""
|
|
@@ -194,12 +204,15 @@ def process_path(
|
|
|
194
204
|
ensure_endswith_slash (bool): Whether to ensure the path ends with a slash.
|
|
195
205
|
ensure_does_not_end_with_slash (bool): Whether to ensure the path does not end with a slash.
|
|
196
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.
|
|
197
210
|
|
|
198
211
|
Returns:
|
|
199
212
|
str: The processed path.
|
|
200
213
|
|
|
201
|
-
>>> process_path('a', 'b', 'c')
|
|
202
|
-
'a/b/c'
|
|
214
|
+
>>> process_path('a', 'b', 'c') # doctest: +ELLIPSIS
|
|
215
|
+
'...a/b/c'
|
|
203
216
|
>>> from functools import partial
|
|
204
217
|
>>> process_path('a', 'b', 'c', rootdir='/root/dir/', ensure_endswith_slash=True)
|
|
205
218
|
'/root/dir/a/b/c/'
|
|
@@ -214,6 +227,10 @@ def process_path(
|
|
|
214
227
|
path = os.path.join(rootdir, path)
|
|
215
228
|
if expanduser:
|
|
216
229
|
path = os.path.expanduser(path)
|
|
230
|
+
if expandvars:
|
|
231
|
+
path = os.path.expandvars(path)
|
|
232
|
+
if abspath:
|
|
233
|
+
path = os.path.abspath(path)
|
|
217
234
|
if ensure_endswith_slash:
|
|
218
235
|
if not path.endswith('/'):
|
|
219
236
|
path = path + '/'
|
|
@@ -0,0 +1,16 @@
|
|
|
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,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
config2py/__init__.py,sha256=ru-bUQk5EuOLzHmNAcElSN8P2saSUEb5KFRkfTkzsUo,770
|
|
2
|
-
config2py/base.py,sha256=saq1YVbeRg8jBoXcmc0y-maNJ5vHHeWwu5tc_EbOetE,15895
|
|
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=6wPAdnossxMPeQ6T0eqUilP3VnHAfbrJcFsg4xmaahA,9137
|
|
6
|
-
config2py/util.py,sha256=mzIsU2ozkatWx61NphsbD502IrwuI2Vi0V8NEjpNRXQ,15745
|
|
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=T0rBy8s6wHgQXnnr7Z1xkF1so3XkdGVASerEQ27ByxE,1950
|
|
10
|
-
config2py/tests/util.py,sha256=vO1VIepbH6vY2e-VHP7HX6jnVzzIDyFsp6md_uBnIXw,1351
|
|
11
|
-
config2py-0.1.33.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
-
config2py-0.1.33.dist-info/METADATA,sha256=kg26LOTcSczZ0pjFwn6sHUtnJDPI6UO-Bhf8qDO0-2Q,14559
|
|
13
|
-
config2py-0.1.33.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
14
|
-
config2py-0.1.33.dist-info/top_level.txt,sha256=DFnlOIKMIGWQRROr3voJFhWFViHaWgTTeWZjC5YC9QQ,10
|
|
15
|
-
config2py-0.1.33.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|