1
1
from __future__ import annotations
2
2
3
3
import configparser
4
+ import re
4
5
from functools import singledispatch
5
6
from typing import TYPE_CHECKING
6
7
7
8
from configupdater import ConfigUpdater as INIDocument
8
9
from configupdater import Option , Section
9
10
from pydantic import TypeAdapter
11
+ from typing_extensions import assert_never
10
12
11
13
from usethis ._integrations .file .ini .errors import (
12
14
INIDecodeError ,
27
29
)
28
30
29
31
if TYPE_CHECKING :
30
- from collections .abc import Sequence
32
+ from collections .abc import Iterable , Sequence
31
33
from pathlib import Path
32
34
from typing import Any , ClassVar
33
35
@@ -121,9 +123,21 @@ def __getitem__(self, item: Sequence[Key]) -> Any:
121
123
return _as_dict (root )
122
124
elif len (keys ) == 1 :
123
125
(section_key ,) = keys
126
+ if not isinstance (section_key , str ):
127
+ msg = (
128
+ f"Only hard-coded strings are supported as keys when "
129
+ f"accessing values, but a { type (section_key )} was provided."
130
+ )
131
+ raise NotImplementedError (msg )
124
132
return _as_dict (root [section_key ])
125
133
elif len (keys ) == 2 :
126
134
(section_key , option_key ) = keys
135
+ if not isinstance (section_key , str ) or not isinstance (option_key , str ):
136
+ msg = (
137
+ f"Only hard-coded strings are supported as keys when "
138
+ f"accessing values, but a { type (section_key )} was provided."
139
+ )
140
+ raise NotImplementedError (msg )
127
141
return root [section_key ][option_key ].value
128
142
else :
129
143
msg = (
@@ -210,7 +224,7 @@ def _set_value_in_root(
210
224
def _set_value_in_section (
211
225
* ,
212
226
root : INIDocument ,
213
- section_key : str ,
227
+ section_key : Key ,
214
228
value : dict [str , str | list [str ]],
215
229
exists_ok : bool ,
216
230
) -> None :
@@ -224,6 +238,13 @@ def _set_value_in_section(
224
238
msg = f"The INI file already has content at the section '{ section_key } '"
225
239
raise INIValueAlreadySetError (msg )
226
240
241
+ if not isinstance (section_key , str ):
242
+ msg = (
243
+ f"Only hard-coded strings are supported as section keys when "
244
+ f"setting values, but a { type (section_key )} was provided."
245
+ )
246
+ raise NotImplementedError (msg )
247
+
227
248
for option_key in root [section_key ]:
228
249
# We need to remove options that are not in the new dict
229
250
# We don't want to remove existing ones to keep their positions.
@@ -242,11 +263,18 @@ def _set_value_in_section(
242
263
def _set_value_in_option (
243
264
* ,
244
265
root : INIDocument ,
245
- section_key : str ,
246
- option_key : str ,
266
+ section_key : Key ,
267
+ option_key : Key ,
247
268
value : str ,
248
269
exists_ok : bool ,
249
270
) -> None :
271
+ if not isinstance (section_key , str ) or not isinstance (option_key , str ):
272
+ msg = (
273
+ f"Only hard-coded strings are supported as keys when "
274
+ f"setting values, but a { type (section_key )} was provided."
275
+ )
276
+ raise NotImplementedError (msg )
277
+
250
278
if root .has_option (section = section_key , option = option_key ) and not exists_ok :
251
279
msg = (
252
280
f"The INI file already has content at the section '{ section_key } ' "
@@ -260,7 +288,7 @@ def _set_value_in_option(
260
288
261
289
@staticmethod
262
290
def _validated_set (
263
- * , root : INIDocument , section_key : str , option_key : str , value : str | list [str ]
291
+ * , root : INIDocument , section_key : Key , option_key : Key , value : str | list [str ]
264
292
) -> None :
265
293
if not isinstance (value , str | list ):
266
294
msg = (
@@ -269,14 +297,21 @@ def _validated_set(
269
297
)
270
298
raise InvalidINITypeError (msg )
271
299
300
+ if not isinstance (section_key , str ) or not isinstance (option_key , str ):
301
+ msg = (
302
+ f"Only hard-coded strings are supported as keys when "
303
+ f"setting values, but a { type (section_key )} was provided."
304
+ )
305
+ raise NotImplementedError (msg )
306
+
272
307
if section_key not in root :
273
308
root .add_section (section_key )
274
309
275
310
root .set (section = section_key , option = option_key , value = value )
276
311
277
312
@staticmethod
278
313
def _validated_append (
279
- * , root : INIDocument , section_key : str , option_key : str , value : str
314
+ * , root : INIDocument , section_key : Key , option_key : Key , value : str
280
315
) -> None :
281
316
if not isinstance (value , str ):
282
317
msg = (
@@ -285,6 +320,13 @@ def _validated_append(
285
320
)
286
321
raise InvalidINITypeError (msg )
287
322
323
+ if not isinstance (section_key , str ) or not isinstance (option_key , str ):
324
+ msg = (
325
+ f"Only hard-coded strings are supported as keys when "
326
+ f"setting values, but a { type (section_key )} was provided."
327
+ )
328
+ raise NotImplementedError (msg )
329
+
288
330
if section_key not in root :
289
331
root .add_section (section_key )
290
332
@@ -299,17 +341,62 @@ def __delitem__(self, keys: Sequence[Key]) -> None:
299
341
300
342
An empty list of keys corresponds to the root of the document.
301
343
"""
302
- root = self .get ()
344
+ # We will iterate through keys and find all matches in the document
345
+ seqs : list [list [str ]] = []
303
346
304
347
if len (keys ) == 0 :
348
+ seqs .append ([])
349
+ elif len (keys ) == 1 :
350
+ (section_key ,) = keys
351
+
352
+ for seq in _itermatches (self .get ().sections (), key = section_key ):
353
+ seqs .append ([seq ])
354
+ elif len (keys ) == 2 :
355
+ (section_key , option_key ) = keys
356
+
357
+ section_strkeys = []
358
+ for section_strkey in _itermatches (self .get ().sections (), key = section_key ):
359
+ section_strkeys .append (section_strkey )
360
+
361
+ for section_strkey in section_strkeys :
362
+ for option_strkey in _itermatches (
363
+ self .get ()[section_strkey ].options (), key = option_key
364
+ ):
365
+ seqs .append ([section_strkey , option_strkey ])
366
+ else :
367
+ msg = (
368
+ f"INI files do not support nested config, whereas access to "
369
+ f"'{ self .name } ' was attempted at '{ print_keys (keys )} '"
370
+ )
371
+ raise ININestingError (msg )
372
+
373
+ if not seqs :
374
+ msg = (
375
+ f"INI file '{ self .name } ' does not contain the keys '{ print_keys (keys )} '"
376
+ )
377
+ raise INIValueMissingError (msg )
378
+
379
+ for seq in seqs :
380
+ self ._delete_strkeys (seq )
381
+
382
+ def _delete_strkeys (self , strkeys : Sequence [str ]) -> None :
383
+ """Delete a specific value in the INI file.
384
+
385
+ An empty list of strkeys corresponds to the root of the document.
386
+
387
+ Assumes that the keys exist in the file.
388
+ """
389
+ root = self .get ()
390
+
391
+ if len (strkeys ) == 0 :
305
392
removed = False
306
393
for section_key in root .sections ():
307
394
removed |= root .remove_section (name = section_key )
308
- elif len (keys ) == 1 :
309
- (section_key ,) = keys
395
+ elif len (strkeys ) == 1 :
396
+ (section_key ,) = strkeys
310
397
removed = root .remove_section (name = section_key )
311
- elif len (keys ) == 2 :
312
- section_key , option_key = keys
398
+ elif len (strkeys ) == 2 :
399
+ section_key , option_key = strkeys
313
400
removed = root .remove_option (section = section_key , option = option_key )
314
401
315
402
# Cleanup section if empty
@@ -318,14 +405,12 @@ def __delitem__(self, keys: Sequence[Key]) -> None:
318
405
else :
319
406
msg = (
320
407
f"INI files do not support nested config, whereas access to "
321
- f"'{ self .name } ' was attempted at '{ print_keys (keys )} '"
408
+ f"'{ self .name } ' was attempted at '{ print_keys (strkeys )} '"
322
409
)
323
- raise INIValueMissingError (msg )
410
+ raise ININestingError (msg )
324
411
325
412
if not removed :
326
- msg = (
327
- f"INI file '{ self .name } ' does not contain the keys '{ print_keys (keys )} '"
328
- )
413
+ msg = f"INI file '{ self .name } ' does not contain the keys '{ print_keys (strkeys )} '"
329
414
raise INIValueMissingError (msg )
330
415
331
416
self .commit (root )
@@ -365,7 +450,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[str]) -> None:
365
450
366
451
@staticmethod
367
452
def _extend_list_in_option (
368
- * , root : INIDocument , section_key : str , option_key : str , values : list [str ]
453
+ * , root : INIDocument , section_key : Key , option_key : Key , values : list [str ]
369
454
) -> None :
370
455
for value in values :
371
456
INIFileManager ._validated_append (
@@ -374,14 +459,21 @@ def _extend_list_in_option(
374
459
375
460
@staticmethod
376
461
def _remove_from_list_in_option (
377
- * , root : INIDocument , section_key : str , option_key : str , values : list [str ]
462
+ * , root : INIDocument , section_key : Key , option_key : Key , values : list [str ]
378
463
) -> None :
379
464
if section_key not in root :
380
465
return
381
466
382
467
if option_key not in root [section_key ]:
383
468
return
384
469
470
+ if not isinstance (section_key , str ) or not isinstance (option_key , str ):
471
+ msg = (
472
+ f"Only hard-coded strings are supported as keys when "
473
+ f"modifying values, but a { type (section_key )} was provided."
474
+ )
475
+ raise NotImplementedError (msg )
476
+
385
477
original_values = root [section_key ][option_key ].as_list ()
386
478
# If already not present, silently pass
387
479
new_values = [value for value in original_values if value not in values ]
@@ -449,3 +541,16 @@ def _(value: INIDocument) -> dict[str, dict[str, Any]]:
449
541
@_as_dict .register (Section )
450
542
def _ (value : Section ) -> dict [str , Any ]:
451
543
return {option .key : option .value for option in value .iter_options ()}
544
+
545
+
546
+ def _itermatches (values : Iterable [str ], / , * , key : Key ):
547
+ """Iterate through an iterable and find all matches for a key."""
548
+ for value in values :
549
+ if isinstance (key , str ):
550
+ if key == value :
551
+ yield value
552
+ elif isinstance (key , re .Pattern ):
553
+ if key .fullmatch (value ):
554
+ yield value
555
+ else :
556
+ assert_never (key )
0 commit comments