|
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
|
|
|
|