Project

Profile

Help

How to connect?
Download (10.4 KB) Statistics View on GitHub Reload from mirrored respository
| Branch: | Tag: | Revision:

github / src / config.py @ 05b173a7

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, ParseError
38
from validate import Validator
39

    
40
from log import main as logger
41
from world import World
42

    
43
class Configuration:
44

    
45
    """Class describing CocoMUD's configuration.
46

47
    Each configuration file is loaded here.  A configuration file can
48
    be either YAML or respecting the ConfigObj syntax (that is,
49
    basically a .ini file).  If everything goes smoothly, the ConfigObj
50
    objects can be found in __getitem__-ing these values, specifying
51
    the directory structure.  Additionally, results from the argument parser
52
    can also be found there.
53

54
    Example:
55

56
        >>> config = Configuration()
57
        >>> config.load()
58
        >>> # In the settings/global.conf file is the following line:
59
        >>> #     name = CocoMUD
60
        >>> # You can access this configuration through:
61
        >>> configuration["options"]["global"]["name"]
62
        >>> # Or, more readable
63
        >>> configuration["options.global.name"]
64

65
    """
66

    
67
    def __init__(self, root_dir, engine):
68
        self.root_dir = root_dir
69
        self.engine = engine
70
        self.values = {}
71

    
72
    def __getitem__(self, key):
73
        """Return the configured value at the specified key.
74

75
        The key is a string.  It can be an identifier without periods
76
        (.).  In which case, the top-level data at this key is returned
77
        or a KeyError exception is raised.
78
        The 'key' can also be a list of identifiers in a string separated
79
        by a period.  In this case, the data is looked for in the
80
        directory/file/ConfigObj hierarchy.  A KeyError exception
81
        is raised if the expected key cannot be found.
82

83
        Thus, the two following lines are identical:
84
            >>> configuration["settings"]["global"]["name"]
85
            >>> configuration["settings.global.name"]
86

87
        """
88
        if "." in key:
89
            keys = key.split(".")
90
            value = self.values
91
            for sub_key in keys:
92
                if sub_key not in value:
93
                    raise KeyError("the key {} cannot be found, cannot " \
94
                            "find {}".format(repr(key), repr(sub_key)))
95

    
96
                value = value[sub_key]
97

    
98
        else:
99
            value = self.values[key]
100

    
101
        return value
102

    
103
    def __setitem__(self, key, value):
104
        """Change the value of 'key'.
105

106
        'key' is a string, and can be specified in the two ways supported
107
        by '__getitem__'.
108

109
        """
110
        if "." in key:
111
            keys = key.split(".")
112
            last = keys[-1]
113
            del keys[-1]
114
            dictionary = self.values
115
            for sub_key in keys:
116
                if sub_key not in dictionary:
117
                    raise KeyError("the key {} cannot be found, cannot " \
118
                            "find {}".format(repr(key), repr(sub_key)))
119

    
120
                dictionary = dictionary[sub_key]
121

    
122
            dictionary[last] = value
123
        else:
124
            self.values[key] = value
125

    
126
    def load(self):
127
        """Load the configuration."""
128
        raise NotImplementedError
129

    
130
    def load_config_file(self, filename, spec, root_dir=None):
131
        """Load the specified file using ConfigObj."""
132
        root_dir = root_dir or self.root_dir
133
        fullpath = os.path.join(root_dir, filename)
134

    
135
        # Create the directory structure if necessary
136
        directories = os.path.dirname(fullpath).split("/")
137
        base = directories[0]
138
        if not os.path.exists(base):
139
            os.mkdir(base)
140

    
141
        for directory in directories[1:]:
142
            base += os.path.sep + directory
143
            if not os.path.exists(base):
144
                os.mkdir(base)
145

    
146
        # Create the ConfigObj
147
        try:
148
            config = ConfigObj(fullpath + ".conf", encoding="UTF8",
149
                    configspec=spec.split("\n"))
150
            config.backup_encoding = "utf-8"
151
        except (ParseError, UnicodeError):
152
            logger.warning("Unable to parse {}, try in latin-1 encoding".format(
153
                    repr(fullpath)))
154
            config = ConfigObj(fullpath + ".conf", configspec=spec.split("\n"), encoding="latin-1")
155
            config.encoding = "UTF8"
156
            config.backup_encoding = "utf-8"
157
            config.write()
158

    
159
        # Validates the configuration
160
        validator = Validator()
161
        result = config.validate(validator)
162

    
163
        # Saves the ConfigObj
164
        values = self.values
165
        for path in os.path.dirname(filename).split("/")[1:]:
166
            if path not in values:
167
                values[path] = {}
168
            values = values[path]
169

    
170
        values[os.path.basename(fullpath)] = config
171

    
172
    def load_YAML_file(self, filename):
173
        """Load the YAML file."""
174
        fullpath = self.root_dir + os.sep + filename
175
        if os.path.exists(fullpath + ".yml"):
176
            file = open(fullpath + ".yml", "r")
177
            datas = safe_load(file.read())
178
        else:
179
            datas = {}
180

    
181
        values = self.values
182
        for path in os.path.dirname(fullpath).split("/")[1:]:
183
            if path not in values:
184
                values[path] = {}
185
            values = values[path]
186

    
187
        values[os.path.basename(fullpath)] = datas
188

    
189
    def write_YAML_file(self, filename, data):
190
        """Write the YAML associated with the data.
191

192
        Arguments:
193
            filename: the filename relative to the rootdir without extension
194
            data: the data as a dictionary.
195

196
        """
197
        fullpath = self.root_dir + os.sep + filename + ".yml"
198
        file = open(fullpath, "w")
199
        try:
200
            safe_dump(data, file, default_flow_style=False)
201
        finally:
202
            file.close()
203

    
204

    
205
class Settings(Configuration):
206

    
207
    """Special configuration in the 'settings' directory."""
208

    
209
    LANGUAGES = (
210
        ("en", "English"),
211
        ("es", "Spanish"),
212
        ("fr", "French"),
213
    )
214

    
215
    def __init__(self, engine, config_dir):
216
        Configuration.__init__(self, os.path.join(config_dir, "settings"), engine)
217
        self.config_dir = config_dir
218

    
219
    def get_language(self):
220
        """Return the configured language.
221

222
        If the configuration hasn't been loaded, or the configured
223
        language isn't valid, return "en" (English).
224

225
        """
226
        default = "en"
227
        codes = [c[0] for c in type(self).LANGUAGES]
228
        try:
229
            lang = self["options.general.language"]
230
            assert lang in codes
231
        except (KeyError, AssertionError):
232
            return default
233

    
234
        return lang
235

    
236
    def load(self):
237
        """Load all the files."""
238
        self.load_options()
239

    
240
        # Load the world configuration
241
        for directory in os.listdir(os.path.join(self.engine.config_dir, "worlds")):
242
            world = World(location=directory)
243
            world.engine = self.engine
244
            settings = GameSettings(self.engine, world)
245
            settings.load()
246
            self.engine.worlds[world.name] = world
247
            world.load_characters()
248

    
249
    def load_options(self):
250
        """Load the file containing the options."""
251
        lang = locale.getdefaultlocale()[0].split("_")[0]
252
        spec = dedent(u"""
253
            [general]
254
                language = option('en', 'fr', default='{lang}')
255
                encoding = string(default="iso8859_15")
256
                screenreader = boolean(default=True)
257

258
            [input]
259
                command_stacking = string(default=";")
260
                auto_send_paste = boolean(default=True)
261

262
            [output]
263
                richtext = boolean(default=True)
264

265
            [TTS]
266
                on = boolean(default=True)
267
                outside = boolean(default=True)
268
                interrupt = boolean(default=True)
269
        """.format(lang=lang).strip("\n"))
270
        self.load_config_file("options", spec)
271

    
272
    def write_macros(self):
273
        """Write the YAML data file."""
274
        macros = {}
275
        for macro in self.engine.macros.values():
276
            macros[macro.shortcut] = macro.action
277

    
278
        self.write_YAML_file("macros", macros)
279

    
280
class GameSettings(Configuration):
281

    
282
    """Game settings (specific to a game/world)."""
283

    
284
    def __init__(self, engine, world):
285
        Configuration.__init__(self, "settings", engine)
286
        self.world = world
287

    
288
    def load(self):
289
        """Load all the files."""
290
        self.load_options()
291

    
292
    def load_options(self):
293
        """Load the file containing the options."""
294
        world = self.world
295
        spec = dedent(u"""
296
            [connection]
297
                name = string
298
                hostname = string
299
                port = integer
300
                protocol = string(default="telnet")
301
        """).strip("\n")
302
        self.load_config_file("options", spec, world.path)
303
        world.name = self["options.connection.name"]
304
        world.hostname = self["options.connection.hostname"]
305
        world.port = self["options.connection.port"]
306
        world.protocol = self["options.connection.protocol"]
307
        world.settings = self["options"]
(7-7/20)