config.py 13.9 KB
Newer Older
Kai Chen's avatar
Kai Chen committed
1
# Copyright (c) Open-MMLab. All rights reserved.
Kai Chen's avatar
Kai Chen committed
2
import os.path as osp
lizz's avatar
lizz committed
3
import shutil
Kai Chen's avatar
Kai Chen committed
4
import sys
lizz's avatar
lizz committed
5
import tempfile
6
from argparse import Action, ArgumentParser
7
from collections import abc
Kai Chen's avatar
Kai Chen committed
8
9
10
from importlib import import_module

from addict import Dict
11
from yapf.yapflib.yapf_api import FormatCode
Kai Chen's avatar
Kai Chen committed
12

13
14
from .path import check_file_exist

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
15
16
BASE_KEY = '_base_'
DELETE_KEY = '_delete_'
17
RESERVED_KEYS = ['filename', 'text', 'pretty_text']
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
18

19
20
21
22
23
24
25
26
27
28

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
29
30
            ex = AttributeError(f"'{self.__class__.__name__}' object has no "
                                f"attribute '{name}'")
31
32
33
34
35
36
        except Exception as e:
            ex = e
        else:
            return value
        raise ex

Kai Chen's avatar
Kai Chen committed
37
38
39
40
41
42
43
44
45
46
47
48

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
49
            add_args(parser, v, prefix + k + '.')
50
        elif isinstance(v, abc.Iterable):
Kai Chen's avatar
Kai Chen committed
51
52
            parser.add_argument('--' + prefix + k, type=type(v[0]), nargs='+')
        else:
Cao Yuhang's avatar
Cao Yuhang committed
53
            print(f'cannot parse key {prefix + k} of type {type(v)}')
Kai Chen's avatar
Kai Chen committed
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
    return parser


class Config(object):
    """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'}"

    """

    @staticmethod
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
84
    def _file2dict(filename):
Kai Chen's avatar
Kai Chen committed
85
        filename = osp.abspath(osp.expanduser(filename))
86
        check_file_exist(filename)
Kai Chen's avatar
Kai Chen committed
87
        if filename.endswith('.py'):
lizz's avatar
lizz committed
88
            with tempfile.TemporaryDirectory() as temp_config_dir:
89
90
91
                temp_config_file = tempfile.NamedTemporaryFile(
                    dir=temp_config_dir, suffix='.py')
                temp_config_name = osp.basename(temp_config_file.name)
lizz's avatar
lizz committed
92
                shutil.copyfile(filename,
93
94
                                osp.join(temp_config_dir, temp_config_name))
                temp_module_name = osp.splitext(temp_config_name)[0]
lizz's avatar
lizz committed
95
                sys.path.insert(0, temp_config_dir)
96
                mod = import_module(temp_module_name)
lizz's avatar
lizz committed
97
98
99
100
101
102
                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
103
                # delete imported module
104
105
106
                del sys.modules[temp_module_name]
                # close temp file
                temp_config_file.close()
wangg12's avatar
wangg12 committed
107
        elif filename.endswith(('.yml', '.yaml', '.json')):
Kai Chen's avatar
Kai Chen committed
108
109
110
            import mmcv
            cfg_dict = mmcv.load(filename)
        else:
wangg12's avatar
wangg12 committed
111
            raise IOError('Only py/yml/yaml/json type are supported now!')
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
112
113
114
115
116

        cfg_text = filename + '\n'
        with open(filename, 'r') as f:
            cfg_text += f.read()

117
        if BASE_KEY in cfg_dict:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
118
            cfg_dir = osp.dirname(filename)
119
            base_filename = cfg_dict.pop(BASE_KEY)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
            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:
                if len(base_cfg_dict.keys() & c.keys()) > 0:
                    raise KeyError('Duplicate key is not allowed among bases')
                base_cfg_dict.update(c)

136
            base_cfg_dict = Config._merge_a_into_b(cfg_dict, base_cfg_dict)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
137
138
139
140
141
142
143
144
145
146
            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
    def _merge_a_into_b(a, b):
147
148
149
150
        # merge dict `a` into dict `b` (non-inplace). values in `a` will
        # overwrite `b`.
        # copy first to avoid inplace modification
        b = b.copy()
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
151
152
153
154
        for k, v in a.items():
            if isinstance(v, dict) and k in b and not v.pop(DELETE_KEY, False):
                if not isinstance(b[k], dict):
                    raise TypeError(
155
156
157
158
                        f'{k}={v} in child config cannot inherit from base '
                        f'because {k} is a dict in the child config but is of '
                        f'type {type(b[k])} in base config. You may set '
                        f'`{DELETE_KEY}=True` to ignore the base config')
159
                b[k] = Config._merge_a_into_b(v, b[k])
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
160
161
            else:
                b[k] = v
162
        return b
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
163
164
165
166
167

    @staticmethod
    def fromfile(filename):
        cfg_dict, cfg_text = Config._file2dict(filename)
        return Config(cfg_dict, cfg_text=cfg_text, filename=filename)
Kai Chen's avatar
Kai Chen committed
168
169
170
171
172
173
174
175

    @staticmethod
    def auto_argparser(description=None):
        """Generate argparser from config file automatically (experimental)
        """
        partial_parser = ArgumentParser(description=description)
        partial_parser.add_argument('config', help='config file path')
        cfg_file = partial_parser.parse_known_args()[0].config
176
        cfg = Config.fromfile(cfg_file)
Kai Chen's avatar
Kai Chen committed
177
178
179
180
181
        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
182
    def __init__(self, cfg_dict=None, cfg_text=None, filename=None):
Kai Chen's avatar
Kai Chen committed
183
184
185
        if cfg_dict is None:
            cfg_dict = dict()
        elif not isinstance(cfg_dict, dict):
Cao Yuhang's avatar
Cao Yuhang committed
186
187
            raise TypeError('cfg_dict must be a dict, but '
                            f'got {type(cfg_dict)}')
188
189
190
        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
191

192
        super(Config, self).__setattr__('_cfg_dict', ConfigDict(cfg_dict))
Kai Chen's avatar
Kai Chen committed
193
        super(Config, self).__setattr__('_filename', filename)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
194
195
196
        if cfg_text:
            text = cfg_text
        elif filename:
Kai Chen's avatar
Kai Chen committed
197
            with open(filename, 'r') as f:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
198
                text = f.read()
Kai Chen's avatar
Kai Chen committed
199
        else:
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
200
201
            text = ''
        super(Config, self).__setattr__('_text', text)
Kai Chen's avatar
Kai Chen committed
202
203
204
205
206
207
208
209
210

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

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

Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
    @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

226
        def _format_basic_types(k, v, use_mapping=False):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
227
            if isinstance(v, str):
Cao Yuhang's avatar
Cao Yuhang committed
228
                v_str = f"'{v}'"
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
229
230
            else:
                v_str = str(v)
231
232
233
234
235
236

            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
237
238
239
240
            attr_str = _indent(attr_str, indent)

            return attr_str

241
        def _format_list(k, v, use_mapping=False):
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
242
243
244
245
            # 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
246
                    f'dict({_indent(_format_dict(v_), indent)}),'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
247
                    for v_ in v).rstrip(',')
248
249
250
251
252
                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
253
254
                attr_str = _indent(attr_str, indent) + ']'
            else:
255
                attr_str = _format_basic_types(k, v, use_mapping)
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
256
257
            return attr_str

258
259
260
261
262
263
264
265
        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
266
267
            r = ''
            s = []
268
269
270
271
272
273

            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
274
275
276
                end = '' if outest_level or is_last else ','
                if isinstance(v, dict):
                    v_str = '\n' + _format_dict(v)
277
278
279
280
281
                    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
282
283
                    attr_str = _indent(attr_str, indent) + ')' + end
                elif isinstance(v, list):
284
                    attr_str = _format_list(k, v, use_mapping) + end
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
285
                else:
286
                    attr_str = _format_basic_types(k, v, use_mapping) + end
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
287
288
289

                s.append(attr_str)
            r += '\n'.join(s)
290
291
            if use_mapping:
                r += '}'
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
292
293
294
295
            return r

        cfg_dict = self._cfg_dict.to_dict()
        text = _format_dict(cfg_dict, outest_level=True)
296
297
298
299
300
301
        # 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
302
303
304

        return text

Kai Chen's avatar
Kai Chen committed
305
    def __repr__(self):
Cao Yuhang's avatar
Cao Yuhang committed
306
        return f'Config (path: {self.filename}): {self._cfg_dict.__repr__()}'
Kai Chen's avatar
Kai Chen committed
307
308
309
310
311
312
313
314
315
316
317
318

    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):
319
            value = ConfigDict(value)
Kai Chen's avatar
Kai Chen committed
320
321
322
323
        self._cfg_dict.__setattr__(name, value)

    def __setitem__(self, name, value):
        if isinstance(value, dict):
324
            value = ConfigDict(value)
Kai Chen's avatar
Kai Chen committed
325
326
327
328
        self._cfg_dict.__setitem__(name, value)

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

330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
    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:
                with open(file, 'w') as f:
                    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
345
346

    def merge_from_dict(self, options):
Kai Chen's avatar
Kai Chen committed
347
        """Merge list into cfg_dict
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
348
349

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

        Examples:
352
353
            >>> options = {'model.backbone.depth': 50,
            ...            'model.backbone.with_cp':True}
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
354
355
            >>> cfg = Config(dict(model=dict(backbone=dict(type='ResNet'))))
            >>> cfg.merge_from_dict(options)
356
357
358
            >>> 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
359
360
361
362
363
364
365
366
367

        Args:
            options (dict): dict of configs to merge from.
        """
        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]:
368
                d.setdefault(subkey, ConfigDict())
Jerry Jiarui XU's avatar
Jerry Jiarui XU committed
369
370
371
372
373
                d = d[subkey]
            subkey = key_list[-1]
            d[subkey] = v

        cfg_dict = super(Config, self).__getattribute__('_cfg_dict')
374
375
        super(Config, self).__setattr__(
            '_cfg_dict', Config._merge_a_into_b(option_cfg_dict, cfg_dict))
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


class DictAction(Action):
    """
    argparse action to split an argument into KEY=VALUE form
    on the first = and append to a dictionary. List options should
    be passed as comma separated values, i.e KEY=V1,V2,V3
    """

    @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

    def __call__(self, parser, namespace, values, option_string=None):
        options = {}
        for kv in values:
            key, val = kv.split('=', maxsplit=1)
            val = [self._parse_int_float_bool(v) for v in val.split(',')]
            if len(val) == 1:
                val = val[0]
            options[key] = val
        setattr(namespace, self.dest, options)