-
Notifications
You must be signed in to change notification settings - Fork 2
/
configparsebetter.py
1414 lines (1207 loc) · 68 KB
/
configparsebetter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
>>> UNFINISHED PROJECT, DO NOT EDIT <<<
This is an unfinished build of an unreleased project. It is very messy.
This will eventually be released as a fully-fledged Python library.
This version is included purely for compatability purposes and will
inevitably be removed (once it becomes a library).
>>> UNFINISHED PROJECT, DO NOT EDIT <<<
New for Instant-Replay-Suite:
-added `_ConfigParseBetter.comment` method
-added custom `RawConfigParser._write_section` method
-implemented `caseSensitive` parameter
-added `autosaveOnlyWhenFileDoesNotExist` parameter
-added `optionFormatCallback` parameter
-added `ifMissing` to `_ConfigParseBetter.write` method
-default `section` parameter is now None
-None now becomes 'general' instead of 'DEFAULTS'
-remade `_ConfigParseBetter.getSection` for speed
-simplified `_ConfigParseBetter.load` for speed/clarity
-added `BOOLEAN_STATES` for direct boolean conversion
-refactored `BetterSectionProxy.__init__` for speed
-used more efficient alias method in `_ConfigParseBetter`
-added aliases to `BetterSectionProxy` for speed
-`_ConfigParseBetter.loadAllFromSection` changes
-added `suffix` parameter
-added `comments` parameter (defaults to False)
-added alias for self.load for speed
-changed `returnKey` paramter to `valuesOnly`
-returning key and value is now the default
-edited configparser-related parameters for clarity
-deleted a lot of comments/commented out code
-remade/refactored most attribute-based magic methods
-added `KEY_ORDER` for strict ordering while writing
-added `noInterpolation` and `extendedInterpolation` parameters
New for chatGPT-vidgen:
-added `remove` load parameter
-improved consistency of loading and writing iterables
-`autosave` parameter is now saved to `__autosave`
-improved ConfigParseBetterQt
-added QFontComboBox support
-added `subclasses` dictionary parameter
-added `autosave` parameter to `write()` and `loadQt()`
-determines whether or not `__widgets` will be used
-if None, `__autosave` is used
TODO: % and $ interpolation do not actually need to be escaped. fix this in configparser's classes
TODO: main.cfg.lastdir (without load()) -> "AttributeError: 'NoneType' object has no attribute 'section'"
TODO: case sensitive attribute errors should be clearer
TODO: deal with endless name conflicts (remember, settings are part of __dict__)
TODO: add setting for deleting empty sections on write
TODO: __getattribute__ and __getattr__ should directly call configparser methods
TODO: load() should be able to take callbacks (like 'topleft' to Position.TopLeft in battery.py)
TODO: .load() -> 5.6 sec/million
TODO: option to print out key/value pairs while loading (use logging.debug)
TODO: ability for user to auto-raise error on missing values
TODO: alternate class that always reads from the file?
TODO: alternate class that always WRITES to the file? (_ConfigParseBetter.__setattr__)
TODO: add more filler functions like remove_section
TODO: work out sectionLock with dot notation + load() + etc.
TODO: low memory mode -> bettersectionproxy, attributes, etc.
TODO: errors with capitalization of options with dot notation?
TODO: base load()'s type on fallback or do try/excepts with typecasts?
TODO: function/attribute overhead stuff
TODO: typecasting multi-type lists (hello,500 -> ['hello', 500])
TODO: should "type(???) in ITER_TYPES" be a function with an isinstance for-loop?
TODO: change bsp to something readable
TODO: make sure you can do load('lastdir', '.') and cfg.lastdir = '.'
- these MUST do the same thing when you load for the first time
TODO: loading something for the first time and then writing immediately does not update the value
TODO: make more parameters like fallback_align/__strip global AND line-by-line
- will this significantly impact performance? probably not?
TODO: general performance. get rid of proxies? how efficient is saving/assigning?
TODO: finish lowMemoryMode or make subclass
TODO: type(x) in (types) VS. for type in types: isinstance(x)?
TODO: make ConfigparsebetterQt show args and kwargs correctly for __init__
TODO: .name vs .getName()
TODO: keep lower() but DON'T change case in file (very hard -> [OPTIONS] but .options works)
TODO: optional debug messages (search for #print)
TODO: WHOOPSIE --> cfg['general']['something'] = True --> LITERALLY CANNOT WORK
- cfg['something'] = True --> also wouldn't work
TODO: alternate-data-streams (ADS) -> https://www.daniweb.com/programming/tutorials/523626/creating-a-gui-wrapper-for-vlc-media-player-in-python-wxpython
TODO: add warning or fix for "stripvalues + delimiter ending in ' ' + ambiguous length" combo
TODO: way of performing action to each value, like "key" parameter in sort (like doing os.path.exists on every string in a list)
TODO: should this just... be configparser? like instead of wrapping it, just edit configparser.py itself?
From main.pyw:
- advanced radio button support (with min(max()))?
- method for saving those things with the sliders like in BDL
> POSSIBLE SOLUTIONS:
> bite the bullet and get rid of OptionProxies? people will just have to do x = cfg.x and then cfg.x = x.
- autosave involves just saving every attribute to its name with its own value
- use generators to map expected typings to their actual types?
- recreate 2 generators everytime new load()'s are called
- one generator is the names of all values
- the other is the expected type for each corresponding value
- add if-statement in __setattr__ that autosaves/writes edits
> use __getattribute__ to directly intervene and get/set .value for items that are OptionProxies
- this solves most problems, but introduces two new ones:
- Proxies are no longer mutable in the reasonable sense (UNFIXABLE)
- People who want to access the proxies themselves must use alternate methods
like .copy() or .proxy() or .get() or some other garbage
- val_type to just type?
- delimiter_type to list/tuple as default?
- what is delimiter_type in save()?
- better min_len and max_len -> use slicing to cut off unwanted parts with max_len?
- change __types to __listtypes or something
- align_with_fallback should also fill in default values based on index
- improve the way empty values for delimited lists are handled. should the fallback just take over immediately?
- is that how it already works?
'''
import os
import sys
import logging
#import traceback
import configparser
logger = logging.getLogger('CPB')
#from inspect import getframeinfo, stack
#def info(*args, sep=' '): # TODO remove this, temporary print() fixup
# caller = getframeinfo(stack()[1][0])
# log = sep.join(str(arg).lstrip() for arg in args)
# logger.info(f'{caller.function:<11} [{caller.lineno:<3}] - {log}')
GENERATOR_TYPE = type(_ for _ in ())
ITER_TYPES = (GENERATOR_TYPE, list, tuple, set)
CAST_TYPES = (int, float)
BOOLEAN_STATES = configparser.RawConfigParser.BOOLEAN_STATES
# TODO: global KEY_ORDER vs. property (global means order can break with multiple parsers running, but who does that?)
# TODO: should we ALWAYS assume users will only make one parser? maybe have a subclass for allowing multiple?
NEWLINES_BETWEEN_SECTIONS = 2
KEY_ORDER = []
_append_to_key_order = KEY_ORDER.append
def _write_section(self, fp, section_name, section_items, delimiter):
''' Modified version of `configparser.RawConfigParser._write_section`
that supports comments and strict ordering. '''
# TODO: do the "strict ordering" part
comment_prefixes = self._comment_prefixes
comment_prefix = comment_prefixes[0] if comment_prefixes else False
interpolate_before = self._interpolation.before_write
allow_no_value = self._allow_no_value
get_order_index = KEY_ORDER.index
fp.write(f'[{section_name}]\n')
#print('\nWRITING SECTION:', section_name, section_items)
def align_to_order(pair):
try: return get_order_index(section_name + pair[0])
except: return len(section_items)
for key, value in sorted(section_items, key=align_to_order):
value = interpolate_before(self, section_name, key, value)
try: is_comment = key.lstrip()[:len(comment_prefix)] == comment_prefix
except: is_comment = False
if is_comment: # if comment is blank, don't write the prefix
if key.strip() == comment_prefix: fp.write('\n')
else: fp.write(key + '\n')
else:
if value is not None or not allow_no_value:
value = delimiter + str(value).replace('\n', '\n\t')
else:
value = ''
fp.write(f'{key}{value}\n')
fp.write('\n' * NEWLINES_BETWEEN_SECTIONS)
configparser.RawConfigParser._write_section = _write_section
class LockedNameException(Exception): # TODO finish this
def __init__(self, name):
self.name = name
def __str__(self):
return f'Option name "{self.name}" is not allowed ' \
'(__parser, __section, __filepath, ' \
'__sectionLock, __defaultExtension).'
class SetSectionToValueError(Exception):
def __init__(self, name):
self.name = name
def __str__(self):
return f'Setting a section ("{self.name}") to a value is not allowed.'
class InvalidSectionError(Exception):
def __init__(self, name, caller):
self.name = name
self.caller = caller
def __str__(self):
return f'{self.caller}: "{self.name}" is not a valid section.'
class OptionProxy:
__slots__ = ('section', 'sectionDict', 'name') # OptionProxies do not dynamically store values like BSP and CPB do
def __init__(self, name, section_proxy, delimiter):
self.section = section_proxy
self.sectionDict = section_proxy.__dict__
#self.sectionDict = self.section.__dict__
self.name = name
#self.delimiter = delimiter # TODO use this? requires much more in-depth autosave
def set(self, value):
self.sectionDict[self.name] = value
@property
def value(self):
#print('OptionProxy.value:', self.name, self.section, self.sectionDict)
return self.sectionDict[self.name]
#def __getitem__(self, key): return self.value[key]
#def __setitem__(self, key, val): self.value[key] = val
#def __call__(self, *args, **kwargs): return self.value(*args, **kwargs)
#def __contains__(self, val): return val in self.value
#def __add__(self, other): return self.value + (other.value if isinstance(other, OptionProxy) else other)
def __str__(self): return str(self.value)
#def __repr__(self): return self.value # causes crashes
# FIXME: BSPs likely need to be manually handled when renaming/deleting sections
class BetterSectionProxy:
def __init__(self, parent, section): # this is idiotic
parser = parent._ConfigParseBetter__parser
self.__parent = parent
self.__parser = parser
self.__caseSensitive = parent._ConfigParseBetter__caseSensitive
self.__section = parser[section]
@property
def name(self):
''' Name may or may not be accessible on initialization. '''
return self.__section.name
def get(self, key): # TODO slow. should this even exist?
return self.__getattr__(key)
def getSettings(self):
return dict(self.__parser.items(self.name))
def __getitem__(self, key):
#try: return self.__parser[key] # this is commented out in twitch CPB (GOOD.)
#except: return None
try: return self.__dict__[key]
except:
try: return self.__parser[key]
except: return None
#try: return self.__getattr__(key) # TODO twitch CPB
#except:
# try: return self.__parser[key]
# except: return None
def __setitem__(self, key, value):
#self.__parser[key] = value
#print('\n\n\nSETTING ITEM', key, value, self.__section)
self.__setattr__(key, value) # TODO twitch CPB
def __setattr__(self, name, value):
if name[:19] != '_BetterSectionProxy':
if self.__caseSensitive: name = name.lower()
self.__parent.save(name, value, section=self.__section)
self.__dict__[name] = value
else: self.__dict__[name] = value
def __getattr__(self, name): # handles dot notation when `name` does not exist
# TODO this is still too different from the base __getattr__ implementations
#print('BSP GETATTR (irs version):', name)
if not self.__caseSensitive and not name.islower():
name = name.lower()
try: return self.__dict__[name]
except KeyError: pass
try: value = self.__parent.__dict__[name]
except KeyError: value = self.__parent.loadFrom(self.name, name)
self.__dict__[name] = value
return value
def __contains__(self, other): return other in self.__dict__ # used near the end of _ConfigParseBetter.load()
def __str__(self): return f'{self.name}: {self.getSettings()}'
#def __repr__(self): return str(self.value) # causes crashes
class _ConfigParseBetter:
def __init__(self, filepath=None, parser=None,
autoread=True, autosave=True, appdata=False,
section=None, stripValues=False,
autoUnpackLen1Lists=True, defaultExtension='.ini',
defaultDelimiter=',', defaultValType=None,
sectionLock=False, caseSensitive=False,
lowMemoryMode=False, autosaveCallback=True,
autosaveOnlyWhenFileDoesNotExist=False,
optionFormatCallback=None,
encoding=None, noInterpolation=False,
extendedInterpolation=True, **parserkwargs):
self.__autosave = autosave # currently only used in ConfigParseBetterQt
self.__sectionLock = sectionLock
self.__caseSensitive = caseSensitive
#self.__errorForMissingFallbacks = True # TODO unused
self.__stripValues = stripValues
self.__autoUnpackLen1Lists = autoUnpackLen1Lists
self.__defaultExtension = defaultExtension
self.__defaultDelimiter = defaultDelimiter
#self.__defaultValType = defaultValType # TODO unused
#self.__lowMemoryMode = lowMemoryMode # TODO unused
self.__encoding = encoding
#self.__locked = tuple(self.__dict__.keys()) # TODO unused (use soon)
#print(self.__locked, end='\n\n')
# add default parser if none is given, with specified interpolation
if parser is None:
if noInterpolation: interpolation = configparser.BasicInterpolation()
elif extendedInterpolation: interpolation = configparser.ExtendedInterpolation()
else: interpolation = configparser._UNSET
parser = configparser.ConfigParser(interpolation=interpolation, **parserkwargs)
self.__parser = parser
#parser.parent = self # allow parser to reference us
# `optionxform` is normally used for lowering option names before
# working on them. If `caseSensitive` is True, we don't want this.
# NOTE: We must set the function BEFORE we read the file
if caseSensitive or optionFormatCallback:
if optionFormatCallback:
parser.optionxform = optionFormatCallback
else: parser.optionxform = lambda option: option
if section: self.__section = section
else: self.__section = 'general' # TODO i need to understand the DEFAULTS section better
# TODO this needs to be improved (in general)
# TODO warn user if self.read returns 0 read files
# TODO override RawConfigParser.read() for better error handling?
self.__filepath = filepath
if not filepath:
if sys.argv[0]:
self.__filepath = sys.argv[0].split('\\')[-1][:-3]
else:
if appdata:
import inspect
caller = inspect.stack()[-1][0].f_code.co_filename
base = os.path.splitext(os.path.basename(caller))[0]
self.__filepath = os.path.join(base, 'config')
else:
self.__filepath = 'config'
self.__filepath = self.createConfigPath(appdata=appdata)
if autoread: self.read(self.__filepath, section, encoding=encoding)
# False -> no callback; True or None -> default callback
if (autosave or autosaveOnlyWhenFileDoesNotExist) and autosaveCallback is not False:
import atexit
if autosaveCallback is not True and autosaveCallback is not None: # custom callback specified
if type(autosaveCallback) in ITER_TYPES: # callback is an array with arguments
kwargs_index = -1
for index, arg in enumerate(autosaveCallback):
if isinstance(arg, dict):
kwargs_index = index
break
if kwargs_index != -1: # keyword arguments detected
if kwargs_index == 1: # only keyword arguments detected
atexit.register(autosaveCallback[0],
**autosaveCallback[1:])
else: # positional and keyword arguments detected
atexit.register(autosaveCallback[0],
*autosaveCallback[1:kwargs_index],
**autosaveCallback[kwargs_index:])
else: # no keyword arguments detected
atexit.register(autosaveCallback[0],
*autosaveCallback[1:])
else: atexit.register(autosaveCallback) # callback includes no arguments
else: atexit.register( # use default callback (self.write)
self.write,
appdata=appdata,
ifMissing=autosaveOnlyWhenFileDoesNotExist
)
# aliases, for speed TODO: some of these are TOO redundant
# I wanted to do multiple versions of __getattr__ here but...
# ...you can't actually have instance-specific magic methods.
self.__getRawParserSections = parser.sections
_ConfigParseBetter.deleteSection = self.removeSection
_ConfigParseBetter.delete_section = self.removeSection
_ConfigParseBetter.remove_section = self.removeSection
_ConfigParseBetter.read_dict = parser.read_dict
_ConfigParseBetter.read_file = parser.read_file
_ConfigParseBetter.read_string = parser.read_string
def createConfigPath(self, path=None, appdata=False):
path = path if path else self.__filepath
path = os.path.normpath(path)
if appdata:
appdatapath = os.path.expandvars('%LOCALAPPDATA%')
if not path.lower().startswith(appdatapath.lower()):
path = os.path.join(appdatapath, path)
dirs, name = os.path.split(path)
if dirs and not os.path.exists(dirs):
os.makedirs(dirs)
if name[-4:] not in ('.ini', '.cfg'):
name += self.__defaultExtension
return os.path.join(dirs, name)
def read(self, filepath=None, setSection=None, **kwargs):
if setSection is not None: self.setSection(setSection)
filepath = filepath or self.__filepath
encoding = kwargs.get('encoding', self.__encoding)
return self.__parser.read(filepath, encoding=encoding)
def write(self, filepath=None, mode='w',
appdata=False, ifMissing=False, **kwargs):
filepath = self.createConfigPath(filepath, appdata) if filepath else self.__filepath
if ifMissing and os.path.exists(filepath): return
encoding = kwargs.get('encoding', self.__encoding)
logger.debug('Writing ConfigParseBetter config.')
with open(filepath, mode, encoding=encoding) as configfile:
self.__parser.write(configfile)
# -- Outdated version --
#if (self.__autosave if autosave is None else autosave): # attempt to auto-save all sections and their options
# for section_name in self.sections():
# for option in self.__parser.options(section_name):
# try:
# values = self.__dict__[section_name].__dict__[option]
# if type(values) in ITER_TYPES: # unpack values
# self.save(
# option,
# *values,
# section=section_name
# )
# else: # leave values as is
# self.save(
# option,
# values,
# section=section_name
# )
# except KeyError: pass
# #section = self.__dict__[section_name]
# #section.__dict__[option] = section.__dict__[option]
# #for key, value in self.__dict__.items():
# # if (key[:18] != '_configparsebetter' and
# # not isinstance(value, BetterSectionProxy)):
# # self.save(key, value)
#with open(filepath, 'w') as configfile: self.__parser.write(configfile)
# -- Twitch CPB version --
#if (self.__autoSave if autoSave is None else autoSave):
# for section_name in self.sections():
# for option in self.__parser.options(section_name):
# try:
# info(section_name, option, self.__dict__[section_name].__dict__[option])
# self.save(
# option,
# self.__dict__[section_name].__dict__[option].value,
# section=section_name
# )
# except KeyError: pass
# #section = self.__dict__[section_name]
# #section.__dict__[option] = section.__dict__[option]
# #for key, value in self.__dict__.items():
# # if (key[:18] != '_configparsebetter' and
# # not isinstance(value, BetterSectionProxy)):
# # self.save(key, value)
#path = self.createConfigPath(filepath, appdata) if filepath else self.__filepath
#with open(path, 'w') as configfile: self.__parser.write(configfile)
#def refresh(self, newParser=None, autoread=True, encoding=None, **parserkwargs):
# ''' Deletes/replaces an old configparser object with a new one. '''
# oldSection = self.__dict__['_ConfigParseBetter__section'].name
# del self.__parser
# self.__parser = newParser or configparser.ConfigParser(**parserkwargs)
# if autoread:
# encoding = encoding or self.__encoding
# self.read(self.__filepath, encoding=encoding)
# self.setSection(oldSection) # restore previous section, if possible
#def reset(self, newParser=None, autoread=True, encoding=None, **parserkwargs):
def reset(self):
for key in self.__dict__:
if key[:18] != '_ConfigParseBetter':
del self.__dict__[key]
#self.refresh(newParser, autoread, encoding, **parserkwargs)
def comment(self, comment='', before='', after='', section=None):
''' Adds a `comment` to the config file. If `comment` is empty,
a blank line is added instead. Otherwise, the comment will
be prefixed by the first string specified in the parser's
`comment_prefixes` parameter (`('#', ';')` by default).
`before` and `after` define strings that should be added
before and after the comment, such as newlines. '''
section = self.getSection(section)
prefixes = self.__parser._comment_prefixes
if not prefixes and comment:
raise ValueError(
'Please specify `comment_prefixes` for '
'your parser object to use comments.'
)
# add prefix and a space to start of every line
prefix = prefixes[0] + ' '
multiline_prefix = '\n' + prefix
lines = comment.split('\n')
comment = before + prefix + multiline_prefix.join(lines) + after
self.__parser._sections[section.name][comment] = None
# add comment to our key order so we remember its position
order_key = section.name + comment
if order_key not in KEY_ORDER:
_append_to_key_order(order_key)
# TODO: using an iterable fallback with a custom delimiter should not be allowed (too complicated for now)
# - when implemented, it should detect/set appropriate params -> delimiter, val_type. but it's more involved than that
def load(self, key, fallback='', delimiter=None, # TODO should delimiter_type and val_type switch places?
val_type=None, delimiter_type=None,
fallback_align=True, force_delimiter_type=True,
min_len=None, max_len=None, fill_with_defaults=False,
fill_with_fallback=False, default=None,
aliases=None, remove='', section=None):
# get fallback's type and verify fallback's actual value
if isinstance(fallback, type): # check if fallback is a literal type, like "fallback=int"
fallback_type = fallback
#fallback = fallback() # get default value for that type (i.e. int() = 0)
fallback = '' # TODO why was this needed? _empty_fallback (below)?
if val_type is None:
val_type = fallback_type
else:
fallback_type = type(fallback)
fallback_is_iterable = fallback_type in ITER_TYPES
if fallback_is_iterable and delimiter is None:
delimiter = self.__defaultDelimiter
#section, value = self._load(key, fallback, fallback_type, delimiter, section)
if not self.__caseSensitive: key = key.lower()
# TODO This block here was originally a separate function called _load, but it's been moved here for optimization
if key[:3] == '__': raise LockedNameException(key)
section = self.getSection(section)
section_name = section.name
# add key to our key order so we remember its position
order_key = section_name + key
if order_key not in KEY_ORDER:
_append_to_key_order(order_key)
if section_name in self.__getRawParserSections():
try:
value = section.get(key, fallback=fallback)
if fallback_type == bool: # getboolean
value = BOOLEAN_STATES[value.lower()]
elif fallback_type in CAST_TYPES: # getint/getfloat
value = fallback_type(value)
except:
value = fallback
elif not self.__sectionLock:
value = self._loadFromAnywhere(key, fallback)
else:
value = fallback # TODO add elif for raising error here?
#print('Value before setting within parser:', key, value, type(value))
# TODO string conversion should probably happen when we do .get() above
# set the actual RawConfigParser section value (RawConfigParser.set())
# convert iterable values to string and convert back later to cover most edge-cases
if not fallback_is_iterable: parser_value = str(value)
else: parser_value = value = str(value).strip('()[]{}')
for char in remove: parser_value = value = value.replace(char, '')
#if strip: parser_value = value = value.strip(strip)
#elif lstrip: parser_value = value = value.lstrip(lstrip)
#elif rstrip: parser_value = value = value.rstrip(rstrip)
# TODO this eventually results in __parser.get(), right? why not just use that
#section[key] = parser_value.replace('%', '%%') # 1.0595 sec/million TODO these % signs are for a nightmare https://stackoverflow.com/questions/46156125/how-to-use-signs-in-configparser-python
section[key] = parser_value
logger.debug(f'Loading: key={key} -> value={value} ({type(value)}) | fallback={fallback} delim={delimiter} val_type={val_type} delim_type={delimiter_type}')
#try: # TODO throw more errors here
# TODO: what is the "aliases" parameter for?
if aliases: true_value = aliases.get(value, fallback) # TODO FIXME: should this use `default` instead?
else: true_value = value
#key = key.lower()
#print('true_value:', true_value)
# there is nothing for us to actually write -> cleanup and return early
if not true_value and not fallback and delimiter_type is None and true_value == fallback:
#print('not true_value and not fallback and true_value == fallback')
#key = key.lower()
bsp = self.getBetterSectionProxy(section)
option_proxy = OptionProxy(key, bsp, delimiter)
self.__dict__[key] = option_proxy
#self.__dict__[section.name].__dict__[key] = value
bsp.__dict__[key] = true_value # TODO should these all be value here and not true_value?
#return option_proxy
return true_value
# double check if we have a delimiter yet -> is so, value is going to be split into a list
if delimiter is None:
if val_type is not None:
true_value = val_type(true_value)
else:
#print(f'value={value} type={fallback_type} fallback={fallback} type={type(fallback)}')
if delimiter_type is None:
if fallback_is_iterable:
delimiter_type = fallback_type
#else:
# delimiter_type = list
fallback_is_empty = fallback == ''
try:
#if delimiter_type in ITER_TYPES: # we want each element to be an iterable
# print('here!', delimiter_type, delimiter, value)
# #joined_value = delimiter.join(str(v) for v in value)
# #print('joined_value:', joined_value)
#else:
# joined_value = str(value)
#true_value = joined_value.split(delimiter)
#true_value = (value if value else fallback).split(delimiter) # TODO add option for preserving empty lists for `value`?
if value != '': true_value = value.split(delimiter)
elif not fallback_is_empty: true_value = fallback.split(delimiter)
else: true_value = []
#print(f'ABOUT TO SPLIT VALUE: {value}', true_value)
if val_type:
if self.__stripValues: # strip string values
# strip and typecast values to val_type. if failed, fill in with default if specified, otherwise use val_type's default
for index, val in enumerate(true_value):
try: true_value[index] = val_type(val.strip())
except ValueError:
if val_type is int: # TODO test speed of this vs. "if val_type is int and '.' in val" / should this be separate entirely?
try: true_value[index] = int(float(val.strip())) # int() cannot convert float strings such as "398.0"
except ValueError: true_value[index] = default if default is not None else val_type() # example: int() = 0
else: # leave values as-is
#print('else', true_value)
# typecast values to val_type. if failed, fill in with default if specified, otherwise use val_type's default
for index, val in enumerate(true_value):
try: true_value[index] = val_type(val)
except ValueError:
if val_type is int: # TODO test speed of this vs. "if val_type is int and '.' in val" / should this be separate entirely?
try: true_value[index] = int(float(val)) # int() cannot convert float strings such as "398.0"
except ValueError: true_value[index] = default if default is not None else val_type() # example: int() = 0
#fallback_list = true_value # TODO why was I using this?
if fallback_is_empty: fallback_list = []
elif fallback_is_iterable: fallback_list = fallback
else: fallback_list = fallback.split(delimiter)
#print('before fallback corrections', true_value, fallback_list)
if fallback_align:
#print(1, true_value)
#print('fallback_align', min_len, max_len, value, fallback_list)
fill_with_fallback = not fill_with_defaults
max_len = len(fallback_list)
min_len = len(fallback_list) # TODO vvv test this with more iterables, with/without needing fallback
if val_type is None: # set each element to excepted type based on position
for index in range(len(true_value)):
true_value[index] = type(fallback[index])(true_value[index])
if min_len or max_len: # TODO these if-statements could be simplified
#print(2, true_value)
#print(f'min_len={min_len}, max_len={max_len}')
if max_len: # TODO need a way to only take the last ones
true_value = true_value[:max_len]
#print(2.1, true_value)
if min_len and len(true_value) < min_len:
#print(2.15, true_value)
if fill_with_defaults:
#print(2.2, true_value)
while len(true_value) < min_len:
true_value.append(default)
#print(2.25, true_value)
elif fill_with_fallback:
#print(2.3, true_value)
true_value += fallback_list[len(true_value):]
#print(2.35, true_value, fallback_list)
else:
#print(2.4, true_value)
true_value = fallback_list
#elif not min_len: # TODO does unpacking len1lists here really serve a purpose? also actually use unpacklen1lists here is so
# print(3, true_value)
# if len(true_value) == 1:
# true_value = true_value[0]
# #elif len(true_value) == 0: # TODO ??????
# # true_value = joined_value
#print('\nend of try', true_value)
except: # TODO this ALL needs more testing
#print(f'Inner-load error - {traceback.format_exc()}')
try: true_value = value.split(delimiter)
except: true_value = value
if delimiter_type in ITER_TYPES: # TODO should this be less strict?
try:
#print('delimiter_type being applied', true_value, delimiter_type)
true_value = delimiter_type(true_value)
if not isinstance(true_value, delimiter_type):
raise TypeError
except TypeError:
if delimiter_type == list: true_value = [true_value]
elif delimiter_type == tuple: true_value = (true_value,)
elif delimiter_type == set: true_value = {true_value}
#key = key.lower()
#section_proxy = self.__dict__[section.name]
bsp = self.getBetterSectionProxy(section) # TODO redundant? see below
if key in bsp: # TODO this vs. __dict__ performance
#option_proxy = bsp[key] # TODO this vs. __dict__ performance
#print('getting very real value from key', key, '->', bsp[key])
#bsp[key].set(true_value)
#print(key, self.__dict__)
#print(key, self.__dict__[key])
self.__dict__[key].set(true_value)
#option_proxy.set(true_value)
else:
#option_proxy = OptionProxy(self, bsp, key, true_value) # <- bad twitch CPB OptionProxies
option_proxy = OptionProxy(key, bsp, delimiter)
self.__dict__[key] = option_proxy
#print('adding very real value to key', key, '->', true_value)
bsp.__dict__[key] = true_value
#print(bsp[key], bsp)
#option_proxy = OptionProxy(key, bsp, delimiter)
#self.__dict__[key] = option_proxy # TODO is this line needed inside or outside the else-statement?
#self.__dict__[key] = section # 0.1276 sec/million
#bsp.__dict__[key] = true_value
#return option_proxy
#print(key, true_value)
return true_value
#except Exception as error: print(f'Outer-load error - {type(error)}: {error}')
def loadFrom(self, section, key, fallback='', *args, **kwargs):
return self.load(key, fallback, *args, section=section, **kwargs)
def loadAllFromSection(self, section=None, fallback='', prefix=None,
suffix=None, valuesOnly=False, comments=False):
''' Loads all options from a `section` through a generator. If
`returnKey` is True, both the key and value of each option are
yielded together, otherwise just the value is yielded. If `prefix`
and/or `suffix` is specified, only options with matching keys are
loaded. `fallback` is applied to every loaded option. '''
# TODO: is there any benefit to this over parser.items(section)?
# TODO: If load() is called before setSection(), this will start
# returning the settings loaded without a section, even
# though they should be loaded into the 'DEFAULT' section.
section = self.getSection(section)
load = self.load
comment_prefixes = self.__parser._comment_prefixes
comment_prefix = comment_prefixes[0] if comment_prefixes else False
if prefix or suffix:
# ensure prefix/suffix are valid strings
if not prefix: prefix = '' if prefix is None else str(prefix)
if not suffix: suffix = '' if suffix is None else str(suffix)
i_prefix = len(prefix)
i_suffix = -len(suffix)
for key in self.__parser.options(section.name):
if not comments:
if key.lstrip()[:len(comment_prefix)] == comment_prefix:
continue
prefix_matches = key[:i_prefix] == prefix
suffix_matches = i_suffix == 0 or key[i_suffix:] == suffix
if prefix_matches and suffix_matches:
if valuesOnly: yield load(key, fallback, section=section)
else: yield key, load(key, fallback, section=section)
else:
for key in self.__parser.options(section.name):
if not comments:
if key.lstrip()[:len(comment_prefix)] == comment_prefix:
continue
if valuesOnly: yield load(key, fallback, section=section)
else: yield key, load(key, fallback, section=section)
#def _load(self, key, fallback, fallback_type, delimiter, section=None, verifySection=True):
# if key[:3] == '__':
# raise LockedNameException(key)
# if verifySection:
# section = self.getSection(section)
# if section.name in self.__getRawParserSections():
# try:
# #if isinstance(fallback, type): # fallback is a literal type, like "fallback=int"
# # fallback_type = fallback
# # fallback = fallback() # get default value for that type (i.e. int() = 0)
# #else:
# # fallback_type = type(fallback)
#
# if fallback_type == bool:
# return section, section.getboolean(key, fallback=fallback)
# elif fallback_type == int:
# return section, section.getint(key, fallback=fallback)
# elif fallback_type == float:
# return section, section.getfloat(key, fallback=fallback)
# elif fallback_is_iterable: # TODO: converting to string and back (bad and stupid)
# delimiter = delimiter if delimiter is not None else ','
# fallback = delimiter.join(str(v) for v in fallback)
# #print(3, 'section', section, 'key', key, 'fallback', fallback, fallback_type)
# #print(4, section.get(key, fallback=fallback))
# return section, section.get(key, fallback=fallback)
# except:
# return section, fallback
# elif not self.__sectionLock:
# return section, self._loadFromAnywhere(key, fallback)
# else:
# return section, fallback # TODO add elif for raising error here?
def _loadFromAnywhere(self, key, fallback): # TODO this is probably bad
if not self.__caseSensitive: key = key.lower()
for section in self.__getRawParserSections():
for sectionKey in self.__parser.options(section):
if sectionKey == key:
return self.__parser[section][sectionKey]
return fallback
def save(self, key, *values, delimiter=None,
delimiter_type=None, section=None):
logger.debug(f'Saving: key={key} values={values}, delim={delimiter}')
if delimiter is None: delimiter = self.__defaultDelimiter
if isinstance(values, GENERATOR_TYPE): values = tuple(values) # turn generator into more flexible iterable
if self.__autoUnpackLen1Lists:
if len(values) == 1 and type(values[0]) in ITER_TYPES:
values = values[0]
section = self.getSection(section)
#valueStr = delimiter.join(str(v).replace('%', '%%') for v in values) # TODO more % stuff
valueStr = delimiter.join(str(v) for v in values) # <- original
#section[key] = valueStr # TODO do time test here
self.__parser.set(section.name, key, valueStr)
if delimiter_type is None:
if len(values) == 1: self.__dict__[key.lower()].set(values[0])
else: self.__dict__[key.lower()].set(values)
else: self.__dict__[key.lower()].set(delimiter_type(values))
#if delimiter_type is None and len(values) == 1: # TODO delete this if the above works as expected
# self.__dict__[key.lower()] = values[0]
#elif delimiter_type is not None:
# self.__dict__[key.lower()] = delimiter_type(values)
#else:
# self.__dict__[key.lower()] = values
# TODO twitch CPB. I think this is from before I redid stuff like __setattr__
#if delimiter_type is None and len(values) == 1:
# try: self.__dict__[key.lower()].set(values[0])
# except: self.__dict__[key.lower()] = values[0]
#elif delimiter_type is not None:
# try: self.__dict__[key.lower()].set(delimiter_type(values))
# except: self.__dict__[key.lower()] = delimiter_type(values)
#else:
# try: self.__dict__[key.lower()].set(values)
# except: self.__dict__[key.lower()] = values
def saveTo(self, section, key, *values,
delimiter=None, delimiter_type=None):
if not delimiter: delimiter = self.__defaultDelimiter
self.save(key, *values, delimiter=delimiter,
delimiter_type=delimiter_type, section=section)
def sections(self, setSection=False, prefix='', suffix='', strict=True):
# NOTE: prefixes and suffixes may be iterables
# NOTE: this is intentionally obtuse as an optimization. could be even more extreme with setSection
if prefix:
if suffix:
if strict:
for section in self.__getRawParserSections():
if section.startswith(prefix) and section.endswith(suffix):
if setSection: self.setSection(section)
yield section
else:
for section in self.__getRawParserSections():
if section.startswith(prefix) or section.endswith(suffix):
if setSection: self.setSection(section)
yield section
else:
for section in self.__getRawParserSections():
if section.startswith(prefix):
if setSection: self.setSection(section)
yield section
elif suffix:
for section in self.__getRawParserSections():
if section.endswith(suffix):
if setSection: self.setSection(section)
yield section
elif setSection: # NOTE: this is slow, but probably worth it for consistency
for section in self.__getRawParserSections():
self.setSection(section)
yield section
else: return self.__getRawParserSections()
def setSection(self, section, locked=False): # TODO finish `locked` parameter
self.__section = self.getSection(section)
def getSection(self, section=None) -> configparser.SectionProxy:
if section is None:
section = self.__section
if isinstance(section, configparser.SectionProxy):
# TODO verify this never results in missing self.__dict__ entry
return section
else:
# aliases, for speed
section_key = section if self.__caseSensitive else section.lower()
parser = self.__parser
_dict = self.__dict__
# BetterSectionProxy raises KeyError if `section` doesn't exist...
# ...in parser, so we can kill two birds with one stone here
try:
if section not in _dict:
_dict[section_key] = BetterSectionProxy(self, section)
except KeyError:
parser[section] = {}
_dict[section_key] = BetterSectionProxy(self, section)
return parser[section]
def removeSection(self, section): # TODO is shortening this to one faster/worth it?
section = self.getSection(section)
self.__parser.remove_section(section.name)
def copySection(self, section, newSection):
''' Copies `section` to `newSection` with
the same options and comments. '''
section = self.getSection(section)
newSection = self.getSection(newSection)
self.setSection(newSection)
parser_set = self.__parser.set
new_name = newSection.name
for key, value in section.items():
parser_set(new_name, key, value)
def renameSection(self, section, newSection):
''' Renames `section` to `newSection` and cleans
up any references to `section` afterwards.'''
section = self.getSection(section)
newSection = self.getSection(newSection)
self.setSection(newSection)
# sections are not stored in our own __dict__
parser = self.__parser
parser._sections[newSection.name] = parser._sections[section.name]
parser._proxies[newSection.name] = parser._proxies[section.name]
parser.remove_section(section.name)
def moveSetting(self, oldKey, newSection, newKey=None, oldSection=None, replace=True):
''' Moves an option from `oldSection.oldKey` to `newSection.newKey`,
overwriting `newSection.newKey` if it already exists and `replace`
is True. If `newKey` is not provided, `newSection.oldKey` is used.
If `oldSection` is not provided, the current section is used.
Returns the value of `newSection.newKey` after the move.
If `oldSection.oldKey` does not exist, None is returned. '''
old_section = self.getSection(oldSection)
new_section = self.getSection(newSection)
old_section_dict: dict = self.__parser._sections[old_section.name]
new_section_dict: dict = self.__parser._sections[new_section.name]
# replace the new setting if necessary & get value to return later
newKey = newKey or oldKey
already_exists = newKey in new_section_dict
if replace or not already_exists:
try: value = self.__dict__[old_section.name].__dict__[oldKey]
except KeyError: return None
new_section_dict[newKey] = old_section_dict[oldKey]
else: # `already_exists` must be True if we got this far
value = self.__dict__[new_section.name].__dict__[newKey]
del old_section_dict[oldKey]
try: # replace `old` with `new` within our order
index = KEY_ORDER.index(old_section.name + oldKey)
KEY_ORDER.pop(index)
if not already_exists:
KEY_ORDER.insert(index, new_section.name + newKey)
except:
pass
return value
def renameSetting(self, old, new, replace=True, section=None):
''' Renames an option from `section.old` to `section.new`, overwriting
the `new` key if it already exists and `replace` is True. Returns
the value of `section.new` after the rename. If `section.old`
does not exist, None is returned. '''