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) |
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
|
|
|
|
113
|
|
|
def _parse_example_configuration(config, regexps): |
114
|
|
|
""" |
115
|
|
|
Parse configuration lines against a set of comment regexps |
116
|
|
|
|
117
|
|
|
Args: |
118
|
|
|
config(_io.TextIOWrapper): Example configuration file to parse |
119
|
|
|
regexps(dict[str, list[tuple[re.__Regex, str]]]): |
120
|
|
|
|
121
|
|
|
Yields: |
122
|
|
|
str: Parsed configuration lines |
123
|
|
|
""" |
124
|
|
|
# What section are we currently in? |
125
|
|
|
in_section = None |
126
|
|
|
|
127
|
|
|
for line in config: |
128
|
|
|
if in_section: |
129
|
|
|
for key_regex, kv in regexps[in_section]: |
130
|
|
|
# Skip the section regex |
131
|
|
|
if kv is None: |
132
|
|
|
continue |
133
|
|
|
|
134
|
|
|
# Does the key match? |
135
|
|
|
if key_regex.match(line): |
136
|
|
|
yield "{key} = {value}\n".format(key=kv[0], value=kv[1]) |
137
|
|
|
|
138
|
|
|
for section, sect_cfg in regexps.items(): |
139
|
|
|
sect_regex = sect_cfg[0][0] # type: re.__Regex |
140
|
|
|
|
141
|
|
|
if sect_regex.match(line): |
142
|
|
|
in_section = section |
143
|
|
|
yield line.lstrip('#') |
144
|
|
|
|
145
|
|
|
|
146
|
|
|
def _config_path(name, container=None): |
147
|
|
|
""" |
148
|
|
|
Convert a config name to a safe format for file/dir names |
149
|
|
|
|
150
|
|
|
Args: |
151
|
|
|
name(str): Configuration filename without the .cfg extension |
152
|
|
|
container(Optional[str]): Configuration container |
153
|
|
|
|
154
|
|
|
Returns: |
155
|
|
|
str |
156
|
|
|
""" |
157
|
|
|
def slugify(string): |
158
|
|
|
string = string.lower().strip() |
159
|
|
|
string = re.sub('[^\w\s]', '', string) # Strip non-word characters |
160
|
|
|
return re.sub('\s+', '_', string) # Replace space characters with underscores |
161
|
|
|
|
162
|
|
|
filename = slugify(name) + '.cfg' |
163
|
|
|
|
164
|
|
|
return os.path.join(slugify(container), filename) if container else filename |
165
|
|
|
|