Skip to content

Commit 9b11558

Browse files
committed
update yacs.py
1 parent 2809de4 commit 9b11558

1 file changed

Lines changed: 79 additions & 59 deletions

File tree

utils/yacs.py

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# File: config.py
1+
# File: yacs.py
22
# Description: Core module for YACS
33
# Created: 2021/6/15 20:30
44
# Author: Qiu Jueqin (qiujueqin@gmail.com)
@@ -14,8 +14,8 @@
1414
import yaml
1515

1616
"""
17-
Yet Another Configuration System: a lightweight yet sufficiently powerful configuration system
18-
with no 3rd-party dependency
17+
Yet Another Configuration System: a lightweight yet sufficiently powerful
18+
configuration system with no 3rd-party dependency
1919
2020
You can instantiate a Config object from a yaml file:
2121
@@ -44,8 +44,8 @@
4444
4545
>>> bs = cfg['batch_size']
4646
47-
and the dotted-dict way (more recommended since it requires less keyboard hits and save your
48-
line width)
47+
and the dotted-dict way (more recommended since it requires less keyboard
48+
hits and save your line width)
4949
5050
>>> bs = cfg.batch_size
5151
@@ -72,8 +72,9 @@ def __init__(self, init=None):
7272
self.from_namespace(init)
7373
else:
7474
raise TypeError(
75-
'Config could only be instantiated from a dict, a yaml filepath, or an '
76-
'argparse.Namespace object, but given a {} object'.format(type(init))
75+
f'Config could only be instantiated from a dict, a yaml '
76+
f'filepath, or an argparse.Namespace object, but given a '
77+
f'{type(init)} object'
7778
)
7879

7980
# ---------------- Immutability ----------------
@@ -88,8 +89,8 @@ def freeze(self):
8889
@contextmanager
8990
def unfreeze(self):
9091
"""
91-
When a Config is frozen (a default action once it is instantiated), users have to use the
92-
unfreeze() context manager to modify it:
92+
When a Config is frozen (a default action once it is instantiated),
93+
users have to use the unfreeze() context manager to modify it:
9394
9495
>>> cfg = Config('default_config.yaml')
9596
>>> with cfg.unfreeze():
@@ -126,7 +127,9 @@ def __getattr__(self, key):
126127
if key in self:
127128
return self[key]
128129
else:
129-
raise AttributeError('attempted to access a non-existing attribute: {}'.format(key))
130+
raise AttributeError(
131+
f'attempted to access a non-existing attribute: {key}'
132+
)
130133

131134
# ---------------- Input ----------------
132135

@@ -137,7 +140,7 @@ def from_dict(self, dic):
137140
"""
138141

139142
if not isinstance(dic, dict):
140-
raise TypeError('expected a dict, but given a {} object'.format(type(dic)))
143+
raise TypeError(f'expected a dict, but given a {type(dic)}')
141144

142145
super().__init__(Config._from_dict(dic))
143146
self.freeze()
@@ -147,12 +150,12 @@ def from_yaml(self, yaml_path):
147150

148151
if not isinstance(yaml_path, (str, pathlib.Path)):
149152
raise TypeError(
150-
'expected a path string or a pathlib.Path object, but given a {} object'.format(
151-
type(yaml_path))
153+
f'expected a path string or a pathlib.Path object, but given '
154+
f'a {type(yaml_path)}'
152155
)
153156

154157
if not op.isfile(str(yaml_path)):
155-
raise FileNotFoundError('file {} does not exist'.format(yaml_path))
158+
raise FileNotFoundError(f'file {yaml_path} does not exist')
156159

157160
with open(yaml_path, 'r') as fp:
158161
dic = yaml.safe_load(fp)
@@ -164,68 +167,73 @@ def from_namespace(self, namespace):
164167
"""
165168
Instantiation from an argparse.Namespace object.
166169
167-
Since argparse doesn't support nested arguments, we treat dot separator in the arguments
168-
as a notation to recursively create a child Config object.
170+
Since argparse doesn't support nested arguments, we treat dot
171+
separator in the arguments as a notation to recursively create a
172+
child Config object.
169173
170-
For example, creating an argparse.ArgumentParser with '--foo.bar' argument:
174+
For example, creating an argparse.ArgumentParser with '--foo.bar'
175+
argument:
171176
172177
>>> import argparse
173178
>>> parser = argparse.ArgumentParser()
174179
>>> parser.add_argument('--foo.bar', type=int, default=42)
175180
>>> args = parser.parse_args() # an argparse.Namespace object
176181
177-
Given the returned argparse.Namespace object 'args', from_namespace() will create a
178-
Config object as if it was instantiated from a nested dict d = {'foo': {'bar': 42}}
182+
Given the returned argparse.Namespace object 'args', from_namespace()
183+
will create a Config object as if it was instantiated from a nested
184+
dict d = {'foo': {'bar': 42}}
179185
"""
180186

181187
if not isinstance(namespace, argparse.Namespace):
182188
raise TypeError(
183-
'expected an argparse.Namespace object, but given a {} object'.format(
184-
type(namespace))
189+
f'expected an argparse.Namespace object, but given a '
190+
f'{type(namespace)} '
185191
)
186192

187193
nested_dict = self._separator_dict_to_nested_dict(vars(namespace))
188194
super().__init__(Config._from_dict(nested_dict))
189195
self.freeze()
190196

191-
def merge(self, other, allow_new_attributes=False, keep_existed_attributes=True):
197+
def merge(self, other, allow_new_attr=False, keep_existed_attr=True):
192198
"""
193199
Recursively merge from other object
194200
195-
:param other: Config object | dict | yaml filepath | argparse.Namespace object
196-
:param allow_new_attributes: whether allow to add new attributes
201+
:param other: Config object | dict | yaml filepath |
202+
argparse.Namespace object
203+
:param allow_new_attr: whether allow to add new attributes
197204
198205
Example:
199206
200207
>>> cfg = Config({'optimizer': 'adam'})
201-
>>> cfg.merge({'lr': 0.001}, allow_new_attributes=True)
208+
>>> cfg.merge({'lr': 0.001}, allow_new_attr=True)
202209
>>> cfg.print()
203210
204211
optimizer: adam
205212
lr: 0.001
206213
207-
>>> cfg.merge({'weight_decay': 1E-7}, allow_new_attributes=False)
214+
>>> cfg.merge({'weight_decay': 1E-7}, allow_new_attr=False)
208215
209216
AttributeError: attempted to add a new attribute: weight_decay
210217
211-
:param keep_existed_attributes: whether keep those attributes that are not in 'other'.
212-
You may wish to trigger this if requires to completely replace a child Config object.
213-
See example/examples.py: Example 5 for a practical usage.
218+
:param keep_existed_attr: whether keep those attributes that are not
219+
in 'other'. You may wish to trigger this if requires to completely
220+
replace a child Config object. See example/examples.py: Example 5
221+
for a practical usage.
214222
215223
Example:
216224
217225
>>> cfg1 = Config({'foo': {'Alice': 0, 'Bob': 1}})
218-
>>> cfg2 = cfg1.clone()
219-
>>> other = {'foo': {'Carol': 42}}
220-
>>> cfg1.merge(other, allow_new_attributes=True)
226+
>>> cfg2 = cfg1.copy()
227+
>>> another = {'foo': {'Carol': 42}}
228+
>>> cfg1.merge(another, allow_new_attr=True)
221229
>>> cfg1.print()
222230
223231
foo:
224232
Alice: 0
225233
Bob: 1
226234
Carol: 42
227235
228-
>>> cfg2.merge(other, allow_new_attributes=True, keep_existed_attributes=False)
236+
>>> cfg2.merge(another, allow_new_attr=True, keep_existed_attr=False)
229237
>>> cfg2.print()
230238
231239
foo:
@@ -238,24 +246,24 @@ def merge(self, other, allow_new_attributes=False, keep_existed_attributes=True)
238246
other = Config(other)
239247
else:
240248
raise TypeError(
241-
'attempted to merge from an unsupported {} object'.format(type(other))
249+
f'attempted to merge from an unsupported {type(other)} object'
242250
)
243251

244-
def _merge(source_cfg, other_cfg, allow_add_new, keep_existed):
252+
def _merge(source_cfg, other_cfg, add_new, keep_existed):
245253
""" Recursively merge the new Config object into the source one """
246254

247255
with source_cfg.unfreeze():
248256
for k, v in other_cfg.items():
249-
if k not in source_cfg and not allow_add_new:
257+
if k not in source_cfg and not add_new:
250258
raise AttributeError(
251-
'attempted to add an attribute {} but it is not found in the source '
252-
'Config. Set \'allow_new_attributes\' to True if requires to add new '
253-
'attribute'.format(k)
259+
f'attempted to add an attribute {k} but it is not '
260+
f'found in the source Config. Set `allow_new_attr` '
261+
f'to True if requires to add new attributes'
254262
)
255263

256264
if isinstance(v, Config):
257-
if k in source_cfg:
258-
_merge(source_cfg[k], v, allow_add_new, keep_existed)
265+
if isinstance(source_cfg.get(k, None), Config):
266+
_merge(source_cfg[k], v, add_new, keep_existed)
259267
else:
260268
source_cfg[k] = v
261269
else:
@@ -264,20 +272,23 @@ def _merge(source_cfg, other_cfg, allow_add_new, keep_existed):
264272
if not keep_existed:
265273
source_keys = list(source_cfg.keys())
266274
for k in source_keys:
267-
if k not in other_cfg and not isinstance(source_cfg[k], Config):
275+
if k not in other_cfg and \
276+
not isinstance(source_cfg[k], Config):
268277
source_cfg.remove(k)
269278

270-
_merge(self, other, allow_new_attributes, keep_existed_attributes)
279+
_merge(self, other, allow_new_attr, keep_existed_attr)
271280

272281
# ---------------- Output ----------------
273282

274-
def to_dict(self):
283+
def to_dict(self, alphabetical=False):
275284
"""
276285
Convert a Config object to a (nested) regular dict.
277286
An inverse method to self.from_dict()
278287
"""
279288

280289
def _to_dict(config):
290+
if alphabetical:
291+
config = sorted(config.items(), key=lambda x: x[0])
281292
dic = dict(config)
282293
for k, v in dic.items():
283294
if isinstance(v, Config):
@@ -290,18 +301,19 @@ def to_parser(self):
290301
"""
291302
Create an argparse.ArgumentParser object with keys as arguments.
292303
293-
Since argparse doesn't support nested arguments, we concatenate keys over hierarchies
294-
into a *dotted* argument. For example, supposing a Config object is organized as following:
304+
Since argparse doesn't support nested arguments, we concatenate keys
305+
over hierarchies into a *dotted* argument. For example, supposing a
306+
Config object is organized as following:
295307
296308
>>> cfg = {
297309
>>> 'foo': {
298310
>>> 'bar': 42
299311
>>> }
300312
>>> }
301313
302-
Then keys 'foo' and 'bar' will be concatenated into a new argument '--foo.bar',
303-
so by calling cfg.to_parser(), it is equivalent to creating an ArgumentParser object as
304-
following:
314+
Then keys 'foo' and 'bar' will be concatenated into a new argument
315+
'--foo.bar', so by calling cfg.to_parser(), it is equivalent to
316+
creating an ArgumentParser object as following:
305317
306318
>>> parser = argparse.ArgumentParser()
307319
>>> parser.add_argument('--foo.bar', type=int, default=42)
@@ -313,16 +325,16 @@ def to_parser(self):
313325

314326
parser = argparse.ArgumentParser()
315327
for k, v in separator_dict.items():
316-
parser.add_argument('--{}'.format(k), type=type(v), default=v)
328+
parser.add_argument(f'--{k}', type=type(v), default=v)
317329

318330
return parser
319331

320332
def dump(self, save_path):
321333
""" Dump a Config object into a yaml file """
322334
with open(save_path, 'w') as fp:
323-
yaml.safe_dump(self.to_dict(), fp, sort_keys=False)
335+
yaml.safe_dump(self.to_dict(alphabetical=True), fp)
324336

325-
def clone(self):
337+
def copy(self):
326338
""" Create a deep copy of the Config object """
327339
return Config(copy.deepcopy(self.to_dict()))
328340

@@ -332,6 +344,9 @@ def __repr__(self):
332344
return self.to_dict().__repr__()
333345

334346
def __str__(self):
347+
return self.to_dict().__repr__()
348+
349+
def _format(self):
335350

336351
def _to_string(dic, indent=0):
337352
texts = []
@@ -345,15 +360,17 @@ def _to_string(dic, indent=0):
345360
return '\n'.join(_to_string(self))
346361

347362
def print(self, streamer=print):
348-
streamer(str(self))
363+
streamer(self._format())
349364

350365
def remove(self, key):
351366
""" Remove an attribute by its key. """
352367

353368
if self.is_frozen:
354369
raise AttributeError('attempted to modify an immutable Config')
355370
if key not in self:
356-
raise AttributeError('attempted to delete a non-existing attribute: {}'.format(key))
371+
raise AttributeError(
372+
f'attempted to delete a non-existing attribute: {key}'
373+
)
357374

358375
del self[key]
359376

@@ -375,7 +392,8 @@ def _separator_dict_to_nested_dict(separator_dict, separator='.'):
375392
"""
376393
Create a nested dict from a single-hierarchy dict.
377394
378-
For example, given a non-nested dict in which part of keys contains dots:
395+
For example, given a non-nested dict in which part of keys contains
396+
dots:
379397
380398
d = {
381399
'foo': 42,
@@ -384,7 +402,8 @@ def _separator_dict_to_nested_dict(separator_dict, separator='.'):
384402
'bar.baz.world': 'WORLD'
385403
}
386404
387-
_separator_dict_to_nested_dict(d, separator='.') converts it into a nested dict:
405+
_separator_dict_to_nested_dict(d, separator='.') converts it into a
406+
nested dict:
388407
389408
{
390409
'foo': 42,
@@ -440,8 +459,9 @@ def _nested_dict_to_separator_dict(nested_dict, separator='.'):
440459
}
441460
}
442461
443-
_nested_dict_to_separator_dict(d, separator='.') converts it into a non-nested dict in
444-
which each key is the concatenation of keys from different hierarchies:
462+
_nested_dict_to_separator_dict(d, separator='.') converts it into a
463+
non-nested dict in which each key is the concatenation of keys from
464+
different hierarchies:
445465
446466
{
447467
'foo': 42,
@@ -458,7 +478,7 @@ def _nested_dict_to_separator_dict(nested_dict, separator='.'):
458478
def _create_separator_dict(x, key='', separator_dict={}):
459479
if isinstance(x, dict):
460480
for k, v in x.items():
461-
kk = '{}{}{}'.format(key, separator, k) if key else k
481+
kk = f'{key}{separator}{k}' if key else k
462482
_create_separator_dict(x[k], kk)
463483
else:
464484
separator_dict[key] = x

0 commit comments

Comments
 (0)