Project

Profile

Help

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

github / src / config.py @ ebe2010d

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="latin-1",
148
                    configspec=spec.split("\n"))
149
        except ParseError:
150
            logger.warning("Unable to parse {}, try without encoding".format(
151
                    repr(fullpath)))
152
            config = ConfigObj(fullpath + ".conf", configspec=spec.split("\n"))
153
            config.encoding = "latin-1"
154
            config.write()
155

    
156
        # Validates the configuration
157
        validator = Validator()
158
        result = config.validate(validator)
159

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

    
167
        values[os.path.basename(fullpath)] = config
168

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

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

    
184
        values[os.path.basename(fullpath)] = datas
185

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

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

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

    
201

    
202
class Settings(Configuration):
203

    
204
    """Special configuration in the 'settings' directory."""
205

    
206
    LANGUAGES = (
207
        ("en", "English"),
208
        ("fr", "French"),
209
    )
210

    
211
    def __init__(self, engine):
212
        Configuration.__init__(self, "settings", engine)
213

    
214
    def get_language(self):
215
        """Return the configured language.
216

217
        If the configuration hasn't been loaded, or the configured
218
        language isn't valid, return "en" (English).
219

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

    
229
        return lang
230

    
231
    def load(self):
232
        """Load all the files."""
233
        self.load_options()
234

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

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

252
            [input]
253
                command_stacking = string(default=";")
254

255
            [output]
256
                richtext = boolean(default=True)
257

258
            [TTS]
259
                on = boolean(default=True)
260
                outside = boolean(default=True)
261
                interrupt = boolean(default=True)
262
        """.format(lang=lang).strip("\n"))
263
        self.load_config_file("options", spec)
264

    
265
    def write_macros(self):
266
        """Write the YAML data file."""
267
        macros = {}
268
        for macro in self.engine.macros.values():
269
            macros[macro.shortcut] = macro.action
270

    
271
        self.write_YAML_file("macros", macros)
272

    
273
class GameSettings(Configuration):
274

    
275
    """Game settings (specific to a game/world)."""
276

    
277
    def __init__(self, engine, world):
278
        Configuration.__init__(self, "settings", engine)
279
        self.world = world
280

    
281
    def load(self):
282
        """Load all the files."""
283
        self.load_options()
284

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