-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathconfiguration.py
More file actions
244 lines (182 loc) · 7.31 KB
/
configuration.py
File metadata and controls
244 lines (182 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
"""
Attributes:
CONFIG_FILE (str, constant): The full path to the MyPyTutor config file.
SPECIAL_FORMATS ({(str, str): function(str) -> type}): Special formats for
specific configuration keys. Keys are identified by the pair (section,
option). The corresponding value is the special type used for the
configuration key (eg int, float, list).
"""
import configparser
import os
from tutorlib.config.namespaces import Namespace
from tutorlib.config.shared import CONFIG_FILE
from tutorlib.gui.dialogs.config import TutorialDirectoryPrompt
SPECIAL_FORMATS = {
('online', 'store_credentials'): bool,
('resolution', 'height'): int,
('resolution', 'width'): int,
('tutorials', 'names'): list,
}
def config_exists():
return os.path.exists(CONFIG_FILE)
def load_config():
"""
Load the MyPyTutor configuration file.
If the file does not exist, cannot be opened, or cannot be parsed, then
revert to using default configuration values.
If the config file can be opened and parsed, but is missing any default
configuration value, revert to using that value for the relevant key.
All values will be unwrapped (and so converted to the appropriate format,
according to the SPECIAL_FORMATS module variable).
Returns:
A Namespace mapping configuration sections to a Namespace mapping
section options to values.
For example, if the configuration file looks like this:
[section_name]
key1 = value
Then the attribute `result.section_name.key1` will equal `value`.
"""
# parse the config file
parser = configparser.ConfigParser()
try:
with open(CONFIG_FILE, 'rU') as f:
parser.read_file(f)
except (IOError, FileNotFoundError, configparser.ParsingError):
# ignore parsing errors - we will just revert to defaults
pass
# transform this to a more useful format
# this involves hard-coding the keys, but that would have to happen in some
# place to *use* them anyway
defaults = {
'online': {
'store_credentials': '1',
'username': '',
},
'resolution': {
'height': '800',
'width': '600',
},
'tutorials': {
'names': '',
'default': '',
},
}
cfg_dict = defaults
# add in all parsed config values
for section in parser.sections():
if section not in defaults:
defaults[section] = {}
for option in parser.options(section):
cfg_dict[section][option] = parser.get(section, option)
# our final step is 'unwrapping' values
# this handles non-standard config formats, such as lists
# (side note: this is why it would have been better to use json)
cfg_dict = {
section: {
option: unwrap_value(section, option, value)
for option, value in options.items()
} for section, options in cfg_dict.items()
}
# Overwrite the store credentials
cfg_dict['online']['store_credentials'] = False
return Namespace(**cfg_dict)
def save_config(config):
"""
Save the given config data to disk.
All values will be wrapped before saving (and so converted back to strings,
which is necessary for the ConfigParser to play nice).
Args:
config (Namespace): The configuration data to save.
"""
# build up the config parser
parser = configparser.ConfigParser()
for section, options in config:
parser.add_section(section)
for option, value in options:
value = wrap_value(section, option, value)
parser.set(section, option, value)
# write the config to file
with open(CONFIG_FILE, 'w') as f:
parser.write(f)
def unwrap_value(section, option, value):
"""
Return the unwrapped value corresponding to the given config key.
Unwrapping values involves converting them to the appropriate Python data
type (from strs, which is all the ConfigParser can understand). The module
variable SPECIAL_FORMATS is used to determine the types to convert to.
Args:
section (str): The configuration section corresponding to the value.
option (str): The configuration option corresponding to the value.
value: (str): The value to convert.
Returns:
The value, converted to the relevant special format.
If no special format applies, return the value as a string.
"""
special_type = SPECIAL_FORMATS.get((section, option))
if special_type is None:
return value
# TODO: I vaguely remember a bug using is on builtins, should check
if special_type is list:
return [elem for elem in value.split(',') if elem]
elif special_type is int:
return int(value)
elif special_type is bool:
return value != '0'
raise AssertionError('Unknown special type {}'.format(special_type))
def wrap_value(section, option, value):
"""
Return the wrapped value corresponding to the given config key.
Wrapping values involves converting them back from the appropriate Python
data type to strs, which is all the ConfigParser can understand. The
module variable SPECIAL_FORMATS is used to determine the types to convert
from, and it is assumed that the value is in fact of the correct type.
Args:
section (str): The configuration section corresponding to the value.
option (str): The configuration option corresponding to the value.
value: (object): The value to convert.
Returns:
The value, converted to a string.
"""
special_type = SPECIAL_FORMATS.get((section, option))
if special_type is None:
return str(value)
assert isinstance(value, special_type)
# TODO: I vaguely remember a bug using is on builtins, should check
if special_type is list:
assert all(',' not in elem for elem in value), \
'Cannot create comma-separated list; one or more list ' \
'elements contain commas: {}'.format(value)
return ','.join(value)
elif special_type is int:
return str(value)
elif special_type is bool:
return '1' if value else '0'
raise AssertionError('Unknown special type {}'.format(special_type))
def add_tutorial(config, window=None, as_default=True):
"""
Prompt the user to add a tutorial to the given configuration datta.
Args:
config (Namespace): The configuration data to work with.
window (tk.Wm, optional): The base window of the prompt to show the user.
Defaults to None (which, in tk, is eqivalent to the root window).
as_default (bool, optional): Whether the added tutorial should be set as
the new default tutorial. Defaults to True.
Returns:
None, if the tutorial was successfully added.
An error message as a string, otherwise.
"""
# prompt for a tutorial directory to add
prompt = TutorialDirectoryPrompt(window)
if prompt.result is None:
return 'Cancelled'
tut_dir, ans_dir, name = prompt.result
if name in config.tutorials.names:
return 'The tutorial name {} already exists'.format(name)
config.tutorials.names.append(name)
if as_default:
config.tutorials.default = name
options = {
'tut_dir': tut_dir,
'ans_dir': ans_dir,
}
setattr(config, name, options)