Project

Profile

Help

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

github / src / config.py @ 96664dff

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", encoding="latin-1",
146
                configspec=spec.split("\n"))
147

    
148
        # Validates the configuration
149
        validator = Validator()
150
        result = config.validate(validator)
151

    
152
        # Saves the ConfigObj
153
        values = self.values
154
        for path in os.path.dirname(filename).split("/")[1:]:
155
            if path not in values:
156
                values[path] = {}
157
            values = values[path]
158

    
159
        values[os.path.basename(fullpath)] = config
160

    
161
    def load_YAML_file(self, filename):
162
        """Load the YAML file."""
163
        fullpath = self.root_dir + os.sep + filename
164
        if os.path.exists(fullpath + ".yml"):
165
            file = open(fullpath + ".yml", "r")
166
            datas = safe_load(file.read())
167
        else:
168
            datas = {}
169

    
170
        values = self.values
171
        for path in os.path.dirname(fullpath).split("/")[1:]:
172
            if path not in values:
173
                values[path] = {}
174
            values = values[path]
175

    
176
        values[os.path.basename(fullpath)] = datas
177

    
178
    def write_YAML_file(self, filename, data):
179
        """Write the YAML associated with the data.
180

181
        Arguments:
182
            filename: the filename relative to the rootdir without extension
183
            data: the data as a dictionary.
184

185
        """
186
        fullpath = self.root_dir + os.sep + filename + ".yml"
187
        file = open(fullpath, "w")
188
        try:
189
            safe_dump(data, file, default_flow_style=False)
190
        finally:
191
            file.close()
192

    
193

    
194
class Settings(Configuration):
195

    
196
    """Special configuration in the 'settings' directory."""
197

    
198
    LANGUAGES = (
199
        ("en", "English"),
200
        ("fr", "French"),
201
    )
202

    
203
    def __init__(self, engine):
204
        Configuration.__init__(self, "settings", engine)
205

    
206
    def get_language(self):
207
        """Return the configured language.
208

209
        If the configuration hasn't been loaded, or the configured
210
        language isn't valid, return "en" (English).
211

212
        """
213
        default = "en"
214
        codes = [c[0] for c in type(self).LANGUAGES]
215
        try:
216
            lang = self["options.general.language"]
217
            assert lang in codes
218
        except (KeyError, AssertionError):
219
            return default
220

    
221
        return lang
222

    
223
    def load(self):
224
        """Load all the files."""
225
        self.load_options()
226

    
227
        # Load the world configuration
228
        for directory in os.listdir("worlds"):
229
            world = World(location=directory)
230
            settings = GameSettings(self.engine, world)
231
            settings.load()
232
            self.engine.worlds[world.name] = world
233
            world.load_characters()
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
                screenreader = boolean(default=True)
243

244
            [input]
245
                command_stacking = string(default=";")
246

247
            [output]
248
                richtext = boolean(default=True)
249

250
            [TTS]
251
                on = boolean(default=True)
252
                outside = boolean(default=True)
253
                interrupt = boolean(default=True)
254
        """.format(lang=lang).strip("\n"))
255
        self.load_config_file("options", spec)
256

    
257
    def write_macros(self):
258
        """Write the YAML data file."""
259
        macros = {}
260
        for macro in self.engine.macros.values():
261
            macros[macro.shortcut] = macro.action
262

    
263
        self.write_YAML_file("macros", macros)
264

    
265
class GameSettings(Configuration):
266

    
267
    """Game settings (specific to a game/world)."""
268

    
269
    def __init__(self, engine, world):
270
        Configuration.__init__(self, "settings", engine)
271
        self.world = world
272

    
273
    def load(self):
274
        """Load all the files."""
275
        self.load_options()
276

    
277
    def load_options(self):
278
        """Load the file containing the options."""
279
        world = self.world
280
        spec = dedent("""
281
            [connection]
282
                name = string
283
                hostname = string
284
                port = integer
285
                protocol = string(default="telnet")
286
        """).strip("\n")
287
        self.load_config_file("options", spec, world.path)
288
        world.name = self["options.connection.name"]
289
        world.hostname = self["options.connection.hostname"]
290
        world.port = self["options.connection.port"]
291
        world.protocol = self["options.connection.protocol"]
292
        world.settings = self["options"]
(7-7/19)