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 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'Could not find config for key: {key}')
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'There was an exception when computing key: {k} with the function '
303
- f'{self.getter}. The exception was: {e}'
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'Value for key {k} is not valid: {v}')
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'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 '
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'Source must be a Gettable or a Callable, not {type(src)}'
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='Enter a value for {}: ',
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, '__setitem__'):
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='Enter a value for {}: ',
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,
@@ -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 = '''[Simple Values]
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__ = 'ConfigParserStore'
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 '\n' in source:
229
+ if "\n" in source:
228
230
  self.read_string(source)
229
- source_kind = 'string'
231
+ source_kind = "string"
230
232
  else:
231
233
  self.read(source)
232
- source_kind = 'filepath'
234
+ source_kind = "filepath"
233
235
  elif isinstance(source, bytes):
234
236
  self.read_string(source.decode())
235
- source_kind = 'bytes'
237
+ source_kind = "bytes"
236
238
  elif isinstance(source, dict):
237
239
  self.read_dict(source)
238
- source_kind = 'dict'
239
- elif hasattr(source, 'read'):
240
+ source_kind = "dict"
241
+ elif hasattr(source, "read"):
240
242
  self.read_file(source)
241
- source_kind = 'stream'
243
+ source_kind = "stream"
242
244
  else:
243
245
  self.read(source)
244
- source_kind = 'unknown'
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 == 'filepath':
268
- with open(self.source, 'w') as fp:
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 == 'stream':
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 {'string', 'bytes'}:
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 == 'string':
281
+ if self.target_kind == "string":
280
282
  return string_data
281
- elif self.target_kind == 'bytes':
283
+ elif self.target_kind == "bytes":
282
284
  return string_data.encode()
283
285
  else:
284
- raise ValueError(f'Unknown target_kind: {self.target_kind}')
285
- elif self.target_kind == 'dict':
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'Unknown target_kind: {self.target_kind}')
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('persist disabled for ConfigReader')
365
+ raise NotImplementedError("persist disabled for ConfigReader")
364
366
 
365
367
  def __setitem__(self, k, v):
366
- raise NotImplementedError('__setitem__ disabled for ConfigReader')
368
+ raise NotImplementedError("__setitem__ disabled for ConfigReader")
367
369
 
368
370
  def __delitem__(self, k):
369
- raise NotImplementedError('__delitem__ disabled for ConfigReader')
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('[\n\r\t]+')
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('\n'):
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 = '\n\t' + '\n\t'.join(v)
438
+ v = "\n\t" + "\n\t".join(v)
437
439
  yield k, v
@@ -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('config2py.tools.get_configs_local_store') as mock_factory:
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 = '_CONFIG2PY_SAFE_TO_DELETE_VAR_'
20
+ key = "_CONFIG2PY_SAFE_TO_DELETE_VAR_"
21
21
 
22
22
  # Set up mock config store
23
- mock_config_store = {key: 'from store'}
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] = 'from env var'
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) == 'from env var'
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('builtins.input', return_value='from user'):
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('new_key') == 'from user'
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 = 'SOME_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('') # user enters nothing
71
- val = my_get_config(config_name, default='default_value')
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 == 'default_value'
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('user_value') # user enters user_value
80
- val = my_get_config(config_name, default='default_value')
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 == 'user_value'
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('a', 'b')
89
+ @source_config_params("a", "b")
90
90
  def foo(a, b, c):
91
91
  return a, b, c
92
92
 
93
- config = {'a': 1, 'b': 2, 'c': 3}
94
- _v = foo(a='a', b='b', c=3, _config_getter=config.get)
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('a', 'b')
97
+ @source_config_params("a", "b")
98
98
  def bar(a, b, c, **kw):
99
- assert 'kw' not in kw, f'kw should be unpacked into **kw. Got: {kw=}'
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='a', b='b', c=3, d=4, e=5, _config_getter=config.get)
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__ == '__main__':
107
- pytest.main(['-v', __file__])
106
+ if __name__ == "__main__":
107
+ pytest.main(["-v", __file__])
@@ -2,4 +2,4 @@ from functools import partial
2
2
 
3
3
 
4
4
  def user_input_patch(monkeypatch, user_input_string: str):
5
- monkeypatch.setattr('builtins.input', lambda _: user_input_string)
5
+ monkeypatch.setattr("builtins.input", lambda _: user_input_string)
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 {'.ini', '.cfg'}:
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'config_src must be a directory, ini or cfg file, or app name. '
58
- f'Was: {config_src}'
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(os.environ)
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('export .+')
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('export ') :],
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 '\n' not in exports and Path(resolve_path(exports)).is_file():
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(['_config_getter'])
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 = 'config2py'
18
+ DFLT_APP_NAME = "config2py"
16
19
  DFLT_MASKING_INPUT = False
17
20
 
18
- not_found = mk_sentinel('not_found')
19
- no_default = mk_sentinel('no_default')
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] != ' ': # pragma: no cover
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'{prompt}\n'
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' [{default}]: '
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'^export\s+([A-Za-z0-9_]+)=(.*)$')
153
- lines = string.split('\n')
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'${key}', val)
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 == 'nt':
199
+ if os.name == "nt":
173
200
  # Windows
174
- app_data_folder = os.getenv('APPDATA')
201
+ app_data_folder = os.getenv("APPDATA")
175
202
  else:
176
203
  # macOS and Linux/Unix
177
- app_data_folder = os.path.expanduser('~/.config')
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
- 'CONFIG2PY_APP_DATA_FOLDER', _system_default_for_app_data_folder()
209
+ "CONFIG2PY_APP_DATA_FOLDER", _system_default_for_app_data_folder()
183
210
  )
184
211
 
185
212
 
186
- def process_path(
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
- Process a path string, ensuring it exists, and optionally expanding user.
215
+ Create directories up to a specified limit.
199
216
 
200
- Args:
201
- path (Iterable[str]): The path to process. Can be multiple components of a path.
202
- ensure_dir_exists (bool): Whether to ensure the path exists.
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
- str: The processed path.
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
- >>> process_path('a', 'b', 'c') # doctest: +ELLIPSIS
215
- '...a/b/c'
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
- path = os.path.join(*path)
222
- if ensure_endswith_slash and ensure_does_not_end_with_slash:
223
- raise ValueError(
224
- 'Cannot ensure both ends with slash and does not end with slash.'
225
- )
226
- if rootdir:
227
- path = os.path.join(rootdir, path)
228
- if expanduser:
229
- path = os.path.expanduser(path)
230
- if expandvars:
231
- path = os.path.expandvars(path)
232
- if abspath:
233
- path = os.path.abspath(path)
234
- if ensure_endswith_slash:
235
- if not path.endswith('/'):
236
- path = path + '/'
237
- if ensure_does_not_end_with_slash:
238
- if path.endswith('/'):
239
- path = path[:-1]
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) / '.config2py').write_text('Created by config2py.')
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 = 'configs'
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 'get_ipython' in globals()
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['__main__'], '__file__')
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.35
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 ; python_version < "3.9"
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: bdist_wheel (0.45.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,