"vscode:/vscode.git/clone" did not exist on "7e0b0e35e16dc777270a3d5597d553d2d1921cbb"
config.py 26 KB
Newer Older
1
# Copyright (c) OpenMMLab. All rights reserved.
2
import ast
3
import copy
4
import os
Kai Chen's avatar
Kai Chen committed
5
import os.path as osp
6
import platform
lizz's avatar
lizz committed
7
import shutil
Kai Chen's avatar
Kai Chen committed
8
import sys
lizz's avatar
lizz committed
9
import tempfile
10
import uuid
11
import warnings
12
from argparse import Action, ArgumentParser
13
from collections import abc
Kai Chen's avatar
Kai Chen committed
14
15
16
from importlib import import_module

from addict import Dict
17
from yapf.yapflib.yapf_api import FormatCode
Kai Chen's avatar
Kai Chen committed
18

19
from .misc import import_modules_from_strings
20
21
from .path import check_file_exist

Jintao Lin's avatar
Jintao Lin committed
22
23
24
25
26
if platform.system() == 'Windows':
    import regex as re
else:
    import re

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
27
28
BASE_KEY = '_base_'
DELETE_KEY = '_delete_'
29
DEPRECATION_KEY = '_deprecation_'
30
RESERVED_KEYS = ['filename', 'text', 'pretty_text']
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
31

32
33
34
35
36
37
38
39
40
41

class ConfigDict(Dict):

    def __missing__(self, name):
        raise KeyError(name)

    def __getattr__(self, name):
        try:
            value = super(ConfigDict, self).__getattr__(name)
        except KeyError:
Cao Yuhang's avatar
Cao Yuhang committed
42
43
            ex = AttributeError(f"'{self.__class__.__name__}' object has no "
                                f"attribute '{name}'")
44
45
46
47
48
49
        except Exception as e:
            ex = e
        else:
            return value
        raise ex

Kai Chen's avatar
Kai Chen committed
50
51
52
53
54
55
56
57
58
59
60
61

def add_args(parser, cfg, prefix=''):
    for k, v in cfg.items():
        if isinstance(v, str):
            parser.add_argument('--' + prefix + k)
        elif isinstance(v, int):
            parser.add_argument('--' + prefix + k, type=int)
        elif isinstance(v, float):
            parser.add_argument('--' + prefix + k, type=float)
        elif isinstance(v, bool):
            parser.add_argument('--' + prefix + k, action='store_true')
        elif isinstance(v, dict):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
62
            add_args(parser, v, prefix + k + '.')
63
        elif isinstance(v, abc.Iterable):
Kai Chen's avatar
Kai Chen committed
64
65
            parser.add_argument('--' + prefix + k, type=type(v[0]), nargs='+')
        else:
Cao Yuhang's avatar
Cao Yuhang committed
66
            print(f'cannot parse key {prefix + k} of type {type(v)}')
Kai Chen's avatar
Kai Chen committed
67
68
69
    return parser


lizz's avatar
lizz committed
70
class Config:
Kai Chen's avatar
Kai Chen committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    """A facility for config and config files.

    It supports common file formats as configs: python/json/yaml. The interface
    is the same as a dict object and also allows access config values as
    attributes.

    Example:
        >>> cfg = Config(dict(a=1, b=dict(b1=[0, 1])))
        >>> cfg.a
        1
        >>> cfg.b
        {'b1': [0, 1]}
        >>> cfg.b.b1
        [0, 1]
        >>> cfg = Config.fromfile('tests/data/config/a.py')
        >>> cfg.filename
        "/home/kchen/projects/mmcv/tests/data/config/a.py"
        >>> cfg.item4
        'test'
        >>> cfg
        "Config [path: /home/kchen/projects/mmcv/tests/data/config/a.py]: "
        "{'item1': [1, 2], 'item2': {'a': 0}, 'item3': True, 'item4': 'test'}"
    """

95
96
    @staticmethod
    def _validate_py_syntax(filename):
WRH's avatar
WRH committed
97
98
        with open(filename, 'r', encoding='utf-8') as f:
            # Setting encoding explicitly to resolve coding issue on windows
99
100
101
            content = f.read()
        try:
            ast.parse(content)
102
        except SyntaxError as e:
103
            raise SyntaxError('There are syntax errors in config '
104
                              f'file {filename}: {e}')
105

Kai Chen's avatar
Kai Chen committed
106
    @staticmethod
107
108
109
110
111
112
113
114
115
116
    def _substitute_predefined_vars(filename, temp_config_name):
        file_dirname = osp.dirname(filename)
        file_basename = osp.basename(filename)
        file_basename_no_extension = osp.splitext(file_basename)[0]
        file_extname = osp.splitext(filename)[1]
        support_templates = dict(
            fileDirname=file_dirname,
            fileBasename=file_basename,
            fileBasenameNoExtension=file_basename_no_extension,
            fileExtname=file_extname)
WRH's avatar
WRH committed
117
118
        with open(filename, 'r', encoding='utf-8') as f:
            # Setting encoding explicitly to resolve coding issue on windows
119
            config_file = f.read()
120
121
        for key, value in support_templates.items():
            regexp = r'\{\{\s*' + str(key) + r'\s*\}\}'
122
            value = value.replace('\\', '/')
123
            config_file = re.sub(regexp, value, config_file)
124
        with open(temp_config_name, 'w', encoding='utf-8') as tmp_config_file:
125
126
            tmp_config_file.write(config_file)

127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
    @staticmethod
    def _pre_substitute_base_vars(filename, temp_config_name):
        """Substitute base variable placehoders to string, so that parsing
        would work."""
        with open(filename, 'r', encoding='utf-8') as f:
            # Setting encoding explicitly to resolve coding issue on windows
            config_file = f.read()
        base_var_dict = {}
        regexp = r'\{\{\s*' + BASE_KEY + r'\.([\w\.]+)\s*\}\}'
        base_vars = set(re.findall(regexp, config_file))
        for base_var in base_vars:
            randstr = f'_{base_var}_{uuid.uuid4().hex.lower()[:6]}'
            base_var_dict[randstr] = base_var
            regexp = r'\{\{\s*' + BASE_KEY + r'\.' + base_var + r'\s*\}\}'
            config_file = re.sub(regexp, f'"{randstr}"', config_file)
142
        with open(temp_config_name, 'w', encoding='utf-8') as tmp_config_file:
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
            tmp_config_file.write(config_file)
        return base_var_dict

    @staticmethod
    def _substitute_base_vars(cfg, base_var_dict, base_cfg):
        """Substitute variable strings to their actual values."""
        cfg = copy.deepcopy(cfg)

        if isinstance(cfg, dict):
            for k, v in cfg.items():
                if isinstance(v, str) and v in base_var_dict:
                    new_v = base_cfg
                    for new_k in base_var_dict[v].split('.'):
                        new_v = new_v[new_k]
                    cfg[k] = new_v
                elif isinstance(v, (list, tuple, dict)):
                    cfg[k] = Config._substitute_base_vars(
                        v, base_var_dict, base_cfg)
        elif isinstance(cfg, tuple):
            cfg = tuple(
                Config._substitute_base_vars(c, base_var_dict, base_cfg)
                for c in cfg)
        elif isinstance(cfg, list):
            cfg = [
                Config._substitute_base_vars(c, base_var_dict, base_cfg)
                for c in cfg
            ]
        elif isinstance(cfg, str) and cfg in base_var_dict:
            new_v = base_cfg
            for new_k in base_var_dict[cfg].split('.'):
                new_v = new_v[new_k]
            cfg = new_v

        return cfg

178
179
    @staticmethod
    def _file2dict(filename, use_predefined_variables=True):
Kai Chen's avatar
Kai Chen committed
180
        filename = osp.abspath(osp.expanduser(filename))
181
        check_file_exist(filename)
182
        fileExtname = osp.splitext(filename)[1]
Danil's avatar
Danil committed
183
        if fileExtname not in ['.py', '.json', '.yaml', '.yml']:
184
185
186
187
188
            raise IOError('Only py/yml/yaml/json type are supported now!')

        with tempfile.TemporaryDirectory() as temp_config_dir:
            temp_config_file = tempfile.NamedTemporaryFile(
                dir=temp_config_dir, suffix=fileExtname)
189
190
            if platform.system() == 'Windows':
                temp_config_file.close()
191
192
193
194
195
196
197
            temp_config_name = osp.basename(temp_config_file.name)
            # Substitute predefined variables
            if use_predefined_variables:
                Config._substitute_predefined_vars(filename,
                                                   temp_config_file.name)
            else:
                shutil.copyfile(filename, temp_config_file.name)
198
199
200
            # Substitute base variables from placeholders to strings
            base_var_dict = Config._pre_substitute_base_vars(
                temp_config_file.name, temp_config_file.name)
201
202

            if filename.endswith('.py'):
203
                temp_module_name = osp.splitext(temp_config_name)[0]
lizz's avatar
lizz committed
204
                sys.path.insert(0, temp_config_dir)
205
                Config._validate_py_syntax(filename)
206
                mod = import_module(temp_module_name)
lizz's avatar
lizz committed
207
208
209
210
211
212
                sys.path.pop(0)
                cfg_dict = {
                    name: value
                    for name, value in mod.__dict__.items()
                    if not name.startswith('__')
                }
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
213
                # delete imported module
214
                del sys.modules[temp_module_name]
215
216
217
218
219
            elif filename.endswith(('.yml', '.yaml', '.json')):
                import mmcv
                cfg_dict = mmcv.load(temp_config_file.name)
            # close temp file
            temp_config_file.close()
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
220

221
222
223
224
225
226
227
228
229
230
231
        # check deprecation information
        if DEPRECATION_KEY in cfg_dict:
            deprecation_info = cfg_dict.pop(DEPRECATION_KEY)
            warning_msg = f'The config file {filename} will be deprecated ' \
                'in the future.'
            if 'expected' in deprecation_info:
                warning_msg += f' Please use {deprecation_info["expected"]} ' \
                    'instead.'
            if 'reference' in deprecation_info:
                warning_msg += ' More information can be found at ' \
                    f'{deprecation_info["reference"]}'
232
            warnings.warn(warning_msg, DeprecationWarning)
233

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
234
        cfg_text = filename + '\n'
WRH's avatar
WRH committed
235
236
        with open(filename, 'r', encoding='utf-8') as f:
            # Setting encoding explicitly to resolve coding issue on windows
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
237
238
            cfg_text += f.read()

239
        if BASE_KEY in cfg_dict:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
240
            cfg_dir = osp.dirname(filename)
241
            base_filename = cfg_dict.pop(BASE_KEY)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
242
243
244
245
246
247
248
249
250
251
252
253
            base_filename = base_filename if isinstance(
                base_filename, list) else [base_filename]

            cfg_dict_list = list()
            cfg_text_list = list()
            for f in base_filename:
                _cfg_dict, _cfg_text = Config._file2dict(osp.join(cfg_dir, f))
                cfg_dict_list.append(_cfg_dict)
                cfg_text_list.append(_cfg_text)

            base_cfg_dict = dict()
            for c in cfg_dict_list:
254
255
256
257
                duplicate_keys = base_cfg_dict.keys() & c.keys()
                if len(duplicate_keys) > 0:
                    raise KeyError('Duplicate key is not allowed among bases. '
                                   f'Duplicate keys: {duplicate_keys}')
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
258
259
                base_cfg_dict.update(c)

260
            # Substitute base variables from strings to their actual values
261
262
263
            cfg_dict = Config._substitute_base_vars(cfg_dict, base_var_dict,
                                                    base_cfg_dict)

264
            base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
265
266
267
268
269
270
271
272
273
            cfg_dict = base_cfg_dict

            # merge cfg_text
            cfg_text_list.append(cfg_text)
            cfg_text = '\n'.join(cfg_text_list)

        return cfg_dict, cfg_text

    @staticmethod
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
    def _merge_a_into_b(a, b, allow_list_keys=False):
        """merge dict ``a`` into dict ``b`` (non-inplace).

        Values in ``a`` will overwrite ``b``. ``b`` is copied first to avoid
        in-place modifications.

        Args:
            a (dict): The source dict to be merged into ``b``.
            b (dict): The origin dict to be fetch keys from ``a``.
            allow_list_keys (bool): If True, int string keys (e.g. '0', '1')
              are allowed in source ``a`` and will replace the element of the
              corresponding index in b if b is a list. Default: False.

        Returns:
            dict: The modified dict of ``b`` using ``a``.

        Examples:
            # Normally merge a into b.
            >>> Config._merge_a_into_b(
            ...     dict(obj=dict(a=2)), dict(obj=dict(a=1)))
            {'obj': {'a': 2}}

            # Delete b first and merge a into b.
            >>> Config._merge_a_into_b(
            ...     dict(obj=dict(_delete_=True, a=2)), dict(obj=dict(a=1)))
            {'obj': {'a': 2}}

            # b is a list
            >>> Config._merge_a_into_b(
            ...     {'0': dict(a=2)}, [dict(a=1), dict(b=2)], True)
            [{'a': 2}, {'b': 2}]
        """
306
        b = b.copy()
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
307
        for k, v in a.items():
308
309
310
311
312
            if allow_list_keys and k.isdigit() and isinstance(b, list):
                k = int(k)
                if len(b) <= k:
                    raise KeyError(f'Index {k} exceeds the length of list {b}')
                b[k] = Config._merge_a_into_b(v, b[k], allow_list_keys)
313
314
315
316
317
318
319
320
321
322
323
324
325
            elif isinstance(v, dict):
                if k in b and not v.pop(DELETE_KEY, False):
                    allowed_types = (dict, list) if allow_list_keys else dict
                    if not isinstance(b[k], allowed_types):
                        raise TypeError(
                            f'{k}={v} in child config cannot inherit from '
                            f'base because {k} is a dict in the child config '
                            f'but is of type {type(b[k])} in base config. '
                            f'You may set `{DELETE_KEY}=True` to ignore the '
                            f'base config.')
                    b[k] = Config._merge_a_into_b(v, b[k], allow_list_keys)
                else:
                    b[k] = ConfigDict(v)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
326
327
            else:
                b[k] = v
328
        return b
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
329
330

    @staticmethod
331
332
333
    def fromfile(filename,
                 use_predefined_variables=True,
                 import_custom_modules=True):
334
335
        cfg_dict, cfg_text = Config._file2dict(filename,
                                               use_predefined_variables)
336
337
        if import_custom_modules and cfg_dict.get('custom_imports', None):
            import_modules_from_strings(**cfg_dict['custom_imports'])
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
338
        return Config(cfg_dict, cfg_text=cfg_text, filename=filename)
Kai Chen's avatar
Kai Chen committed
339

340
341
342
343
344
345
346
347
348
349
    @staticmethod
    def fromstring(cfg_str, file_format):
        """Generate config from config str.

        Args:
            cfg_str (str): Config str.
            file_format (str): Config file format corresponding to the
               config str. Only py/yml/yaml/json type are supported now!

        Returns:
350
            :obj:`Config`: Config obj.
351
352
353
354
355
356
357
        """
        if file_format not in ['.py', '.json', '.yaml', '.yml']:
            raise IOError('Only py/yml/yaml/json type are supported now!')
        if file_format != '.py' and 'dict(' in cfg_str:
            # check if users specify a wrong suffix for python
            warnings.warn(
                'Please check "file_format", the file format may be .py')
358
        with tempfile.NamedTemporaryFile(
359
360
                'w', encoding='utf-8', suffix=file_format,
                delete=False) as temp_file:
361
            temp_file.write(cfg_str)
362
363
364
365
            # on windows, previous implementation cause error
            # see PR 1077 for details
        cfg = Config.fromfile(temp_file.name)
        os.remove(temp_file.name)
366
367
        return cfg

Kai Chen's avatar
Kai Chen committed
368
369
    @staticmethod
    def auto_argparser(description=None):
Kai Chen's avatar
Kai Chen committed
370
        """Generate argparser from config file automatically (experimental)"""
Kai Chen's avatar
Kai Chen committed
371
372
373
        partial_parser = ArgumentParser(description=description)
        partial_parser.add_argument('config', help='config file path')
        cfg_file = partial_parser.parse_known_args()[0].config
374
        cfg = Config.fromfile(cfg_file)
Kai Chen's avatar
Kai Chen committed
375
376
377
378
379
        parser = ArgumentParser(description=description)
        parser.add_argument('config', help='config file path')
        add_args(parser, cfg)
        return parser, cfg

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
380
    def __init__(self, cfg_dict=None, cfg_text=None, filename=None):
Kai Chen's avatar
Kai Chen committed
381
382
383
        if cfg_dict is None:
            cfg_dict = dict()
        elif not isinstance(cfg_dict, dict):
Cao Yuhang's avatar
Cao Yuhang committed
384
385
            raise TypeError('cfg_dict must be a dict, but '
                            f'got {type(cfg_dict)}')
386
387
388
        for key in cfg_dict:
            if key in RESERVED_KEYS:
                raise KeyError(f'{key} is reserved for config file')
Kai Chen's avatar
Kai Chen committed
389

390
        super(Config, self).__setattr__('_cfg_dict', ConfigDict(cfg_dict))
Kai Chen's avatar
Kai Chen committed
391
        super(Config, self).__setattr__('_filename', filename)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
392
393
394
        if cfg_text:
            text = cfg_text
        elif filename:
Kai Chen's avatar
Kai Chen committed
395
            with open(filename, 'r') as f:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
396
                text = f.read()
Kai Chen's avatar
Kai Chen committed
397
        else:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
398
399
            text = ''
        super(Config, self).__setattr__('_text', text)
Kai Chen's avatar
Kai Chen committed
400
401
402
403
404
405
406
407
408

    @property
    def filename(self):
        return self._filename

    @property
    def text(self):
        return self._text

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
    @property
    def pretty_text(self):

        indent = 4

        def _indent(s_, num_spaces):
            s = s_.split('\n')
            if len(s) == 1:
                return s_
            first = s.pop(0)
            s = [(num_spaces * ' ') + line for line in s]
            s = '\n'.join(s)
            s = first + '\n' + s
            return s

424
        def _format_basic_types(k, v, use_mapping=False):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
425
            if isinstance(v, str):
Cao Yuhang's avatar
Cao Yuhang committed
426
                v_str = f"'{v}'"
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
427
428
            else:
                v_str = str(v)
429
430
431
432
433
434

            if use_mapping:
                k_str = f"'{k}'" if isinstance(k, str) else str(k)
                attr_str = f'{k_str}: {v_str}'
            else:
                attr_str = f'{str(k)}={v_str}'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
435
436
437
438
            attr_str = _indent(attr_str, indent)

            return attr_str

439
        def _format_list(k, v, use_mapping=False):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
440
441
442
443
            # check if all items in the list are dict
            if all(isinstance(_, dict) for _ in v):
                v_str = '[\n'
                v_str += '\n'.join(
Cao Yuhang's avatar
Cao Yuhang committed
444
                    f'dict({_indent(_format_dict(v_), indent)}),'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
445
                    for v_ in v).rstrip(',')
446
447
448
449
450
                if use_mapping:
                    k_str = f"'{k}'" if isinstance(k, str) else str(k)
                    attr_str = f'{k_str}: {v_str}'
                else:
                    attr_str = f'{str(k)}={v_str}'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
451
452
                attr_str = _indent(attr_str, indent) + ']'
            else:
453
                attr_str = _format_basic_types(k, v, use_mapping)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
454
455
            return attr_str

456
457
458
459
460
461
462
463
        def _contain_invalid_identifier(dict_str):
            contain_invalid_identifier = False
            for key_name in dict_str:
                contain_invalid_identifier |= \
                    (not str(key_name).isidentifier())
            return contain_invalid_identifier

        def _format_dict(input_dict, outest_level=False):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
464
465
            r = ''
            s = []
466
467
468
469
470
471

            use_mapping = _contain_invalid_identifier(input_dict)
            if use_mapping:
                r += '{'
            for idx, (k, v) in enumerate(input_dict.items()):
                is_last = idx >= len(input_dict) - 1
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
472
473
474
                end = '' if outest_level or is_last else ','
                if isinstance(v, dict):
                    v_str = '\n' + _format_dict(v)
475
476
477
478
479
                    if use_mapping:
                        k_str = f"'{k}'" if isinstance(k, str) else str(k)
                        attr_str = f'{k_str}: dict({v_str}'
                    else:
                        attr_str = f'{str(k)}=dict({v_str}'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
480
481
                    attr_str = _indent(attr_str, indent) + ')' + end
                elif isinstance(v, list):
482
                    attr_str = _format_list(k, v, use_mapping) + end
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
483
                else:
484
                    attr_str = _format_basic_types(k, v, use_mapping) + end
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
485
486
487

                s.append(attr_str)
            r += '\n'.join(s)
488
489
            if use_mapping:
                r += '}'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
490
491
492
493
            return r

        cfg_dict = self._cfg_dict.to_dict()
        text = _format_dict(cfg_dict, outest_level=True)
494
495
496
497
498
499
        # copied from setup.cfg
        yapf_style = dict(
            based_on_style='pep8',
            blank_line_before_nested_class_or_def=True,
            split_before_expression_after_opening_paren=True)
        text, _ = FormatCode(text, style_config=yapf_style, verify=True)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
500
501
502

        return text

Kai Chen's avatar
Kai Chen committed
503
    def __repr__(self):
Cao Yuhang's avatar
Cao Yuhang committed
504
        return f'Config (path: {self.filename}): {self._cfg_dict.__repr__()}'
Kai Chen's avatar
Kai Chen committed
505
506
507
508
509
510
511
512
513
514
515
516

    def __len__(self):
        return len(self._cfg_dict)

    def __getattr__(self, name):
        return getattr(self._cfg_dict, name)

    def __getitem__(self, name):
        return self._cfg_dict.__getitem__(name)

    def __setattr__(self, name, value):
        if isinstance(value, dict):
517
            value = ConfigDict(value)
Kai Chen's avatar
Kai Chen committed
518
519
520
521
        self._cfg_dict.__setattr__(name, value)

    def __setitem__(self, name, value):
        if isinstance(value, dict):
522
            value = ConfigDict(value)
Kai Chen's avatar
Kai Chen committed
523
524
525
526
        self._cfg_dict.__setitem__(name, value)

    def __iter__(self):
        return iter(self._cfg_dict)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
527

Kevin's avatar
Kevin committed
528
529
530
    def __getstate__(self):
        return (self._cfg_dict, self._filename, self._text)

Ma Zerun's avatar
Ma Zerun committed
531
532
533
534
535
536
537
538
539
540
    def __deepcopy__(self, memo):
        cls = self.__class__
        other = cls.__new__(cls)
        memo[id(self)] = other

        for key, value in self.__dict__.items():
            super(Config, other).__setattr__(key, copy.deepcopy(value, memo))

        return other

Kevin's avatar
Kevin committed
541
542
543
544
545
546
    def __setstate__(self, state):
        _cfg_dict, _filename, _text = state
        super(Config, self).__setattr__('_cfg_dict', _cfg_dict)
        super(Config, self).__setattr__('_filename', _filename)
        super(Config, self).__setattr__('_text', _text)

547
548
549
550
551
552
    def dump(self, file=None):
        cfg_dict = super(Config, self).__getattribute__('_cfg_dict').to_dict()
        if self.filename.endswith('.py'):
            if file is None:
                return self.pretty_text
            else:
553
                with open(file, 'w', encoding='utf-8') as f:
554
555
556
557
558
559
560
561
                    f.write(self.pretty_text)
        else:
            import mmcv
            if file is None:
                file_format = self.filename.split('.')[-1]
                return mmcv.dump(cfg_dict, file_format=file_format)
            else:
                mmcv.dump(cfg_dict, file)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
562

563
    def merge_from_dict(self, options, allow_list_keys=True):
Kai Chen's avatar
Kai Chen committed
564
        """Merge list into cfg_dict.
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
565
566

        Merge the dict parsed by MultipleKVAction into this cfg.
Kai Chen's avatar
Kai Chen committed
567
568

        Examples:
569
570
            >>> options = {'model.backbone.depth': 50,
            ...            'model.backbone.with_cp':True}
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
571
572
            >>> cfg = Config(dict(model=dict(backbone=dict(type='ResNet'))))
            >>> cfg.merge_from_dict(options)
573
574
575
            >>> cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
            >>> assert cfg_dict == dict(
            ...     model=dict(backbone=dict(depth=50, with_cp=True)))
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
576

577
            >>> # Merge list element
578
579
580
581
582
583
584
585
            >>> cfg = Config(dict(pipeline=[
            ...     dict(type='LoadImage'), dict(type='LoadAnnotations')]))
            >>> options = dict(pipeline={'0': dict(type='SelfLoadImage')})
            >>> cfg.merge_from_dict(options, allow_list_keys=True)
            >>> cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
            >>> assert cfg_dict == dict(pipeline=[
            ...     dict(type='SelfLoadImage'), dict(type='LoadAnnotations')])

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
586
587
        Args:
            options (dict): dict of configs to merge from.
588
589
590
591
            allow_list_keys (bool): If True, int string keys (e.g. '0', '1')
              are allowed in ``options`` and will replace the element of the
              corresponding index in the config if the config is a list.
              Default: True.
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
592
593
594
595
596
597
        """
        option_cfg_dict = {}
        for full_key, v in options.items():
            d = option_cfg_dict
            key_list = full_key.split('.')
            for subkey in key_list[:-1]:
598
                d.setdefault(subkey, ConfigDict())
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
599
600
601
602
603
                d = d[subkey]
            subkey = key_list[-1]
            d[subkey] = v

        cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
604
        super(Config, self).__setattr__(
605
606
607
            '_cfg_dict',
            Config._merge_a_into_b(
                option_cfg_dict, cfg_dict, allow_list_keys=allow_list_keys))
608
609
610
611
612


class DictAction(Action):
    """
    argparse action to split an argument into KEY=VALUE form
613
614
615
616
    on the first = and append to a dictionary. List options can
    be passed as comma separated values, i.e 'KEY=V1,V2,V3', or with explicit
    brackets, i.e. 'KEY=[V1,V2,V3]'. It also support nested brackets to build
    list/tuple values. e.g. 'KEY=[(V1,V2),(V3,V4)]'
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
    """

    @staticmethod
    def _parse_int_float_bool(val):
        try:
            return int(val)
        except ValueError:
            pass
        try:
            return float(val)
        except ValueError:
            pass
        if val.lower() in ['true', 'false']:
            return True if val.lower() == 'true' else False
        return val

633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
    @staticmethod
    def _parse_iterable(val):
        """Parse iterable values in the string.

        All elements inside '()' or '[]' are treated as iterable values.

        Args:
            val (str): Value string.

        Returns:
            list | tuple: The expanded list or tuple from the string.

        Examples:
            >>> DictAction._parse_iterable('1,2,3')
            [1, 2, 3]
            >>> DictAction._parse_iterable('[a, b, c]')
            ['a', 'b', 'c']
            >>> DictAction._parse_iterable('[(1, 2, 3), [a, b], c]')
Zaida Zhou's avatar
Zaida Zhou committed
651
            [(1, 2, 3), ['a', 'b'], 'c']
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
        """

        def find_next_comma(string):
            """Find the position of next comma in the string.

            If no ',' is found in the string, return the string length. All
            chars inside '()' and '[]' are treated as one element and thus ','
            inside these brackets are ignored.
            """
            assert (string.count('(') == string.count(')')) and (
                    string.count('[') == string.count(']')), \
                f'Imbalanced brackets exist in {string}'
            end = len(string)
            for idx, char in enumerate(string):
                pre = string[:idx]
                # The string before this ',' is balanced
                if ((char == ',') and (pre.count('(') == pre.count(')'))
                        and (pre.count('[') == pre.count(']'))):
                    end = idx
                    break
            return end

        # Strip ' and " characters and replace whitespace.
        val = val.strip('\'\"').replace(' ', '')
        is_tuple = False
        if val.startswith('(') and val.endswith(')'):
            is_tuple = True
            val = val[1:-1]
        elif val.startswith('[') and val.endswith(']'):
            val = val[1:-1]
        elif ',' not in val:
            # val is a single value
            return DictAction._parse_int_float_bool(val)

        values = []
        while len(val) > 0:
            comma_idx = find_next_comma(val)
            element = DictAction._parse_iterable(val[:comma_idx])
            values.append(element)
            val = val[comma_idx + 1:]
        if is_tuple:
            values = tuple(values)
        return values

696
697
698
699
    def __call__(self, parser, namespace, values, option_string=None):
        options = {}
        for kv in values:
            key, val = kv.split('=', maxsplit=1)
700
            options[key] = self._parse_iterable(val)
701
        setattr(namespace, self.dest, options)