Completed
Push — master ( ae86a0...10fc77 )
by Makoto
01:02
created

tumdlr.write_user_config()   B

Complexity

Conditions 4

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 37
rs 8.5806
1
import logging
2
import os
3
import re
4
from configparser import ConfigParser
5
6
from tumdlr import DATA_DIR, USER_CONFIG_DIR, SITE_CONFIG_DIR
7
8
9
def load_config(name, container=None, default=True):
10
    """
11
    Load a configuration file and optionally merge it with another default configuration file
12
13
    Args:
14
        name(str): Name of the configuration file to load without any file extensions
15
        container(Optional[str]): An optional container for the configuration file
16
        default(Optional[bool or str]): Merge with a default configuration file. Valid values are False for
17
            no default, True to use an installed default configuration file, or a path to a config file to use as the
18
            default configuration
19
20
    Returns:
21
        ConfigParser
22
    """
23
    paths = []
24
    filename = _config_path(name, container)
25
26
    # Load the default configuration (if enabled)
27
    if default:
28
        paths.append(os.path.join(DATA_DIR, 'config', filename) if default is True else default)
29
30
    # Load the site configuration first, then the user configuration
31
    paths.append(os.path.join(SITE_CONFIG_DIR, filename))
32
    paths.append(os.path.join(USER_CONFIG_DIR, filename))
33
34
    config = ConfigParser()
35
    config.read(paths)
36
37
    return config
38
39
40
def write_user_config(name, container=None, **kwargs):
41
    """
42
    Save a users configuration without losing comments / documentation in the example configuration file
43
44
    Args:
45
        name(str): Name of the configuration file to use
46
        container(Optional[str]): An optional container for the configuration file
47
        **kwargs(dict[str, dict]): Configuration parameters
48
49
    Returns:
50
        str: Path to the user configuration file
51
    """
52
    log = logging.getLogger('tumdlr.config')
53
54
    filename = _config_path(name, container)
55
    log.debug('Rendering user configuration file: %s', filename)
56
57
    # Make sure the user config directory exists
58
    os.makedirs(USER_CONFIG_DIR, 0o755, True)
59
    user_config_path = os.path.join(USER_CONFIG_DIR, filename)
60
61
    # Generate regular expressions for section tags and key=value assignments
62
    sect_regexps = _compile_setting_comment_regexps(**kwargs)
63
64
    # Read the example configuration
65
    eg_cfg_path = os.path.join(DATA_DIR, 'config', filename + '.example')
66
    with open(eg_cfg_path, 'r') as eg_cfg:
67
        logging.debug('Example configuration opened: %s', eg_cfg.name)
68
69
        # Save the configuration
70
        with open(user_config_path, 'w') as user_cfg:
71
            logging.debug('User configuration opened: %s', user_cfg.name)
72
73
            for line in _parse_example_configuration(eg_cfg, sect_regexps):
74
                user_cfg.write(line)
75
76
    return user_config_path
77
78
79
def _compile_setting_comment_regexps(**kwargs):
80
    """
81
    Compile regex substitutions for removing comments and updating setting values from example configuration files
82
83
    Args:
84
        **kwargs(dict[str, dict]): Setting key=values pairs
85
86
    Returns:
87
        dict[str, list[tuple[re.__Regex, str]]]
88
    """
89
    log = logging.getLogger('tumdlr.config')
90
    sect_regexps = {}
91
92
    for section, settings in kwargs.items():
93
        log.debug('Generating regular expression for section `%s`', section)
94
        sect_regexps[section] = [
95
            (
96
                re.compile('^#\s*\[{sect}\]\s*$'.format(sect=re.escape(section)), re.IGNORECASE),
97
                None
98
            )
99
        ]
100
101
        for key, value in settings.items():
102
            log.debug('Generating regular expression for setting `%s` with the value `%s`', key, value)
103
            sect_regexps[section].append(
104
                (
105
                    re.compile('^#\s*{key}\s+=\s+'.format(key=re.escape(key)), re.IGNORECASE),
106
                    (key, value)
107
                )
108
            )
109
110
        log.debug('Done generating regular expressions for section `%s`', section)
111
112
    return sect_regexps
113
114
115
def _parse_example_configuration(config, regexps):
116
    """
117
    Parse configuration lines against a set of comment regexps
118
119
    Args:
120
        config(_io.TextIOWrapper): Example configuration file to parse
121
        regexps(dict[str, list[tuple[re.__Regex, str]]]):
122
123
    Yields:
124
        str: Parsed configuration lines
125
    """
126
    # What section are we currently in?
127
    in_section = None
128
129
    def iter_regexps(_line):
130
        nonlocal regexps
131
        nonlocal in_section
132
133
        for section, sect_cfg in regexps.items():
134
            sect_regex = sect_cfg[0][0]  # type: re.__Regex
135
136
            if sect_regex.match(line):
137
                in_section = section
138
                return line.lstrip('#')
139
140
        raise RuntimeError('No section opening regexps matched')
141
142
    def iter_section_regexps(_line):
143
        nonlocal regexps
144
        nonlocal in_section
145
146
        for key_regex, kv in regexps[in_section]:
147
            # Skip the section regex
148
            if kv is None:
149
                continue
150
151
            # Does the key match?
152
            if key_regex.match(_line):
153
                return "{key} = {value}\n".format(key=kv[0], value=kv[1])
154
155
        raise RuntimeError('No key=value regexps matched')
156
157
    for line in config:
158
        # Are we in a section yet?
159
        if in_section:
160
            try:
161
                yield iter_section_regexps(line)
162
            except RuntimeError:
163
                # No key=value match? Are we venturing into a new section?
164
                try:
165
                    yield iter_regexps(line)
166
                except RuntimeError:
167
                    # Still nothing? Return the unprocessed line then
168
                    yield line
169
        # Not in a section yet? Are we venturing into our first one then?
170
        else:
171
            try:
172
                yield iter_regexps(line)
173
            except RuntimeError:
174
                # Not yet? Return the unprocessed line then
175
                yield line
176
177
178
def _config_path(name, container=None):
179
    """
180
    Convert a config name to a safe format for file/dir names
181
182
    Args:
183
        name(str): Configuration filename without the .cfg extension
184
        container(Optional[str]): Configuration container
185
186
    Returns:
187
        str
188
    """
189
    def slugify(string):
190
        string = string.lower().strip()
191
        string = re.sub('[^\w\s]', '', string)  # Strip non-word characters
192
        return re.sub('\s+', '_', string)  # Replace space characters with underscores
193
194
    filename = slugify(name) + '.cfg'
195
196
    return os.path.join(slugify(container), filename) if container else filename
197