Project

Profile

Help

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

github / src / config.py @ eb9f2e41

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(object):
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.
52

53
    Example:
54

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

64
    """
65

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

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

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

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

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

    
95
                value = value[sub_key]
96

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

    
100
        return value
101

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

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

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

    
119
                dictionary = dictionary[sub_key]
120

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
203

    
204
class Settings(Configuration):
205

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

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

    
214
    def __init__(self, engine):
215
        Configuration.__init__(self, "settings", engine)
216

    
217
    def get_language(self):
218
        """Return the configured language.
219

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

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

    
232
        return lang
233

    
234
    def load(self):
235
        """Load all the files."""
236
        self.load_options()
237

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

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

255
            [input]
256
                command_stacking = string(default=";")
257
                auto_send_paste = boolean(default=True)
258

259
            [output]
260
                richtext = boolean(default=True)
261

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

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

    
275
        self.write_YAML_file("macros", macros)
276

    
277
class GameSettings(Configuration):
278

    
279
    """Game settings (specific to a game/world)."""
280

    
281
    def __init__(self, engine, world):
282
        Configuration.__init__(self, "settings", engine)
283
        self.world = world
284

    
285
    def load(self):
286
        """Load all the files."""
287
        self.load_options()
288

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