github / src / config.py @ 13e86181
1 |
# Copyright (c) 2016, LE GOFF Vincent |
---|---|
2 |
# All rights reserved.
|
3 |
|
4 |
# Redistribution and use in source and binary forms, with or without
|
5 |
# modification, are permitted provided that the following conditions are met:
|
6 |
|
7 |
# * Redistributions of source code must retain the above copyright notice, this
|
8 |
# list of conditions and the following disclaimer.
|
9 |
|
10 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
11 |
# this list of conditions and the following disclaimer in the documentation
|
12 |
# and/or other materials provided with the distribution.
|
13 |
|
14 |
# * Neither the name of ytranslate nor the names of its
|
15 |
# contributors may be used to endorse or promote products derived from
|
16 |
# this software without specific prior written permission.
|
17 |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28 |
|
29 |
"""This module defines the default configuration."""
|
30 |
|
31 |
import locale |
32 |
import os |
33 |
import os.path |
34 |
from textwrap import dedent |
35 |
|
36 |
from yaml import safe_dump, safe_load |
37 |
from configobj import ConfigObj |
38 |
from validate import Validator |
39 |
|
40 |
from world import World |
41 |
|
42 |
class Configuration(object): |
43 |
|
44 |
"""Class describing CocoMUD's configuration.
|
45 |
|
46 |
Each configuration file is loaded here. A configuration file can
|
47 |
be either YAML or respecting the ConfigObj syntax (that is,
|
48 |
basically a .ini file). If everything goes smoothly, the ConfigObj
|
49 |
objects can be found in __getitem__-ing these values, specifying
|
50 |
the directory structure.
|
51 |
|
52 |
Example:
|
53 |
|
54 |
>>> config = Configuration()
|
55 |
>>> config.load()
|
56 |
>>> # In the settings/global.conf file is the following line:
|
57 |
>>> # name = CocoMUD
|
58 |
>>> # You can access this configuration through:
|
59 |
>>> configuration["options"]["global"]["name"]
|
60 |
>>> # Or, more readable
|
61 |
>>> configuration["options.global.name"]
|
62 |
|
63 |
"""
|
64 |
|
65 |
def __init__(self, root_dir, engine): |
66 |
self.root_dir = root_dir
|
67 |
self.engine = engine
|
68 |
self.values = {}
|
69 |
|
70 |
def __getitem__(self, key): |
71 |
"""Return the configured value at the specified key.
|
72 |
|
73 |
The key is a string. It can be an identifier without periods
|
74 |
(.). In which case, the top-level data at this key is returned
|
75 |
or a KeyError exception is raised.
|
76 |
The 'key' can also be a list of identifiers in a string separated
|
77 |
by a period. In this case, the data is looked for in the
|
78 |
directory/file/ConfigObj hierarchy. A KeyError exception
|
79 |
is raised if the expected key cannot be found.
|
80 |
|
81 |
Thus, the two following lines are identical:
|
82 |
>>> configuration["settings"]["global"]["name"]
|
83 |
>>> configuration["settings.global.name"]
|
84 |
|
85 |
"""
|
86 |
if "." in key: |
87 |
keys = key.split(".")
|
88 |
value = self.values
|
89 |
for sub_key in keys: |
90 |
if sub_key not in value: |
91 |
raise KeyError("the key {} cannot be found, cannot " \ |
92 |
"find {}".format(repr(key), repr(sub_key))) |
93 |
|
94 |
value = value[sub_key] |
95 |
|
96 |
else:
|
97 |
value = self.values[key]
|
98 |
|
99 |
return value
|
100 |
|
101 |
def __setitem__(self, key, value): |
102 |
"""Change the value of 'key'.
|
103 |
|
104 |
'key' is a string, and can be specified in the two ways supported
|
105 |
by '__getitem__'.
|
106 |
|
107 |
"""
|
108 |
if "." in key: |
109 |
keys = key.split(".")
|
110 |
last = keys[-1]
|
111 |
del keys[-1] |
112 |
dictionary = self.values
|
113 |
for sub_key in keys: |
114 |
if sub_key not in dictionary: |
115 |
raise KeyError("the key {} cannot be found, cannot " \ |
116 |
"find {}".format(repr(key), repr(sub_key))) |
117 |
|
118 |
dictionary = dictionary[sub_key] |
119 |
|
120 |
dictionary[last] = value |
121 |
else:
|
122 |
self.values[key] = value
|
123 |
|
124 |
def load(self): |
125 |
"""Load the configuration."""
|
126 |
raise NotImplementedError |
127 |
|
128 |
def load_config_file(self, filename, spec, root_dir=None): |
129 |
"""Load the specified file using ConfigObj."""
|
130 |
root_dir = root_dir or self.root_dir |
131 |
fullpath = os.path.join(root_dir, filename) |
132 |
|
133 |
# Create the directory structure if necessary
|
134 |
directories = os.path.dirname(fullpath).split("/")
|
135 |
base = directories[0]
|
136 |
if not os.path.exists(base): |
137 |
os.mkdir(base) |
138 |
|
139 |
for directory in directories[1:]: |
140 |
base += os.path.sep + directory |
141 |
if not os.path.exists(base): |
142 |
os.mkdir(base) |
143 |
|
144 |
# Create the ConfigObj
|
145 |
config = ConfigObj(fullpath + ".conf", configspec=spec.split("\n")) |
146 |
|
147 |
# Validates the configuration
|
148 |
validator = Validator() |
149 |
result = config.validate(validator) |
150 |
|
151 |
# Saves the ConfigObj
|
152 |
values = self.values
|
153 |
for path in os.path.dirname(filename).split("/")[1:]: |
154 |
if path not in values: |
155 |
values[path] = {} |
156 |
values = values[path] |
157 |
|
158 |
values[os.path.basename(fullpath)] = config |
159 |
|
160 |
def load_YAML_file(self, filename): |
161 |
"""Load the YAML file."""
|
162 |
fullpath = self.root_dir + os.sep + filename
|
163 |
if os.path.exists(fullpath + ".yml"): |
164 |
file = open(fullpath + ".yml", "r") |
165 |
datas = safe_load(file.read())
|
166 |
else:
|
167 |
datas = {} |
168 |
|
169 |
values = self.values
|
170 |
for path in os.path.dirname(fullpath).split("/")[1:]: |
171 |
if path not in values: |
172 |
values[path] = {} |
173 |
values = values[path] |
174 |
|
175 |
values[os.path.basename(fullpath)] = datas |
176 |
|
177 |
def write_YAML_file(self, filename, data): |
178 |
"""Write the YAML associated with the data.
|
179 |
|
180 |
Arguments:
|
181 |
filename: the filename relative to the rootdir without extension
|
182 |
data: the data as a dictionary.
|
183 |
|
184 |
"""
|
185 |
fullpath = self.root_dir + os.sep + filename + ".yml" |
186 |
file = open(fullpath, "w") |
187 |
try:
|
188 |
safe_dump(data, file, default_flow_style=False) |
189 |
finally:
|
190 |
file.close()
|
191 |
|
192 |
|
193 |
class Settings(Configuration): |
194 |
|
195 |
"""Special configuration in the 'settings' directory."""
|
196 |
|
197 |
LANGUAGES = ( |
198 |
("en", "English"), |
199 |
("fr", "French"), |
200 |
) |
201 |
|
202 |
def __init__(self, engine): |
203 |
Configuration.__init__(self, "settings", engine) |
204 |
|
205 |
def get_language(self): |
206 |
"""Return the configured language.
|
207 |
|
208 |
If the configuration hasn't been loaded, or the configured
|
209 |
language isn't valid, return "en" (English).
|
210 |
|
211 |
"""
|
212 |
default = "en"
|
213 |
codes = [c[0] for c in type(self).LANGUAGES] |
214 |
try:
|
215 |
lang = self["options.general.language"] |
216 |
assert lang in codes |
217 |
except (KeyError, AssertionError): |
218 |
return default
|
219 |
|
220 |
return lang
|
221 |
|
222 |
def load(self): |
223 |
"""Load all the files."""
|
224 |
self.load_options()
|
225 |
|
226 |
# Load the world configuration
|
227 |
for directory in os.listdir("worlds"): |
228 |
world = World(location=directory) |
229 |
settings = GameSettings(self.engine, world)
|
230 |
settings.load() |
231 |
self.engine.worlds[world.name] = world
|
232 |
|
233 |
self.load_YAML_file("macros") |
234 |
|
235 |
def load_options(self): |
236 |
"""Load the file containing the options."""
|
237 |
lang = locale.getdefaultlocale()[0].split("_")[0] |
238 |
spec = dedent("""
|
239 |
[general]
|
240 |
language = option('en', 'fr', default='{lang}')
|
241 |
encoding = string(default="iso8859_15")
|
242 |
|
243 |
[accessibility]
|
244 |
tab_end = boolean(default=True)
|
245 |
nl_end = boolean(default=True)
|
246 |
|
247 |
[TTS]
|
248 |
on = boolean(default=True)
|
249 |
outside = boolean(default=True)
|
250 |
""".format(lang=lang).strip("\n")) |
251 |
self.load_config_file("options", spec) |
252 |
|
253 |
def write_macros(self): |
254 |
"""Write the YAML data file."""
|
255 |
macros = {} |
256 |
for macro in self.engine.macros.values(): |
257 |
macros[macro.shortcut] = macro.action |
258 |
|
259 |
self.write_YAML_file("macros", macros) |
260 |
|
261 |
class GameSettings(Configuration): |
262 |
|
263 |
"""Game settings (specific to a game/world)."""
|
264 |
|
265 |
def __init__(self, engine, world): |
266 |
Configuration.__init__(self, "settings", engine) |
267 |
self.world = world
|
268 |
|
269 |
def load(self): |
270 |
"""Load all the files."""
|
271 |
self.load_options()
|
272 |
|
273 |
def load_options(self): |
274 |
"""Load the file containing the options."""
|
275 |
world = self.world
|
276 |
spec = dedent("""
|
277 |
[connection]
|
278 |
name = string
|
279 |
hostname = string
|
280 |
port = integer
|
281 |
""").strip("\n") |
282 |
self.load_config_file("options", spec, world.path) |
283 |
world.name = self["options.connection.name"] |
284 |
world.hostname = self["options.connection.hostname"] |
285 |
world.port = self["options.connection.port"] |
286 |
world.settings = self["options"] |