~barry/lazr.config/megamerge

« back to all changes in this revision

Viewing changes to src/lazr/config/__init__.py

  • Committer: Gary Poster
  • Date: 2008-12-15 22:22:12 UTC
  • Revision ID: gary.poster@canonical.com-20081215222212-4we644wqwmv8lsa3
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2004-2007 Canonical Ltd.  All rights reserved.
 
2
 
 
3
"""Implementation classes for config."""
 
4
 
 
5
__metaclass__ = type
 
6
 
 
7
__all__ = [
 
8
    'Config',
 
9
    'ConfigData',
 
10
    'ConfigSchema',
 
11
    'ImplicitTypeSchema',
 
12
    'ImplicitTypeSection',
 
13
    'Section',
 
14
    'SectionSchema',
 
15
    'as_host_port',
 
16
    'as_timedelta',
 
17
    'as_username_groupname',
 
18
    ]
 
19
 
 
20
 
 
21
import StringIO
 
22
import datetime
 
23
import grp
 
24
import os
 
25
import pwd
 
26
import re
 
27
 
 
28
from ConfigParser import NoSectionError, RawConfigParser
 
29
from os.path import abspath, basename, dirname
 
30
from textwrap import dedent
 
31
 
 
32
from zope.interface import implements
 
33
 
 
34
from lazr.config.interfaces import (
 
35
    ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,
 
36
    InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,
 
37
    NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,
 
38
    UnknownSectionError)
 
39
from lazr.delegates import delegates
 
40
 
 
41
 
 
42
def read_content(filename):
 
43
    """Return the content of a file at filename as a string."""
 
44
    source_file = open(filename, 'r')
 
45
    try:
 
46
        raw_data = source_file.read()
 
47
    finally:
 
48
        source_file.close()
 
49
    return raw_data
 
50
 
 
51
 
 
52
class SectionSchema:
 
53
    """See `ISectionSchema`."""
 
54
    implements(ISectionSchema)
 
55
 
 
56
    def __init__(self, name, options, is_optional=False):
 
57
        """Create an `ISectionSchema` from the name and options.
 
58
 
 
59
        :param name: A string. The name of the ISectionSchema.
 
60
        :param options: A dict of the key-value pairs in the ISectionSchema.
 
61
        :param is_optional: A boolean. Is this section schema optional?
 
62
        :raise `RedefinedKeyError`: if a keys is redefined in SectionSchema.
 
63
        """
 
64
        # This method should raise RedefinedKeyError if the schema file
 
65
        # redefines a key, but SafeConfigParser swallows redefined keys.
 
66
        self.name = name
 
67
        self._options = options
 
68
        self.optional = is_optional
 
69
 
 
70
    def __iter__(self):
 
71
        """See `ISectionSchema`"""
 
72
        return self._options.iterkeys()
 
73
 
 
74
    def __contains__(self, name):
 
75
        """See `ISectionSchema`"""
 
76
        return name in self._options
 
77
 
 
78
    def __getitem__(self, key):
 
79
        """See `ISectionSchema`"""
 
80
        return self._options[key]
 
81
 
 
82
    @property
 
83
    def category_and_section_names(self):
 
84
        """See `ISectionSchema`."""
 
85
        if '.' in self.name:
 
86
            return tuple(self.name.split('.'))
 
87
        else:
 
88
            return (None, self.name)
 
89
 
 
90
 
 
91
class Section:
 
92
    """See `ISection`."""
 
93
    implements(ISection)
 
94
    delegates(ISectionSchema, context='schema')
 
95
 
 
96
    def __init__(self, schema, _options=None):
 
97
        """Create an `ISection` from schema.
 
98
 
 
99
        :param schema: The ISectionSchema that defines this ISection.
 
100
        """
 
101
        # Use __dict__ because __getattr__ limits access to self.options.
 
102
        self.__dict__['schema'] = schema
 
103
        if _options is None:
 
104
            _options = dict([(key, schema[key]) for key in schema])
 
105
        self.__dict__['_options'] = _options
 
106
 
 
107
    def __getitem__(self, key):
 
108
        """See `ISection`"""
 
109
        return self._options[key]
 
110
 
 
111
    def __getattr__(self, name):
 
112
        """See `ISection`."""
 
113
        if name in self._options:
 
114
            return self._options[name]
 
115
        else:
 
116
            raise AttributeError(
 
117
                "No section key named %s." % name)
 
118
 
 
119
    def __setattr__(self, name, value):
 
120
        """Callsites cannot mutate the config by direct manipulation."""
 
121
        raise AttributeError("Config options cannot be set directly.")
 
122
 
 
123
    @property
 
124
    def category_and_section_names(self):
 
125
        """See `ISection`."""
 
126
        return self.schema.category_and_section_names
 
127
 
 
128
    def update(self, items):
 
129
        """Update the keys with new values.
 
130
 
 
131
        :return: A list of `UnknownKeyError`s if the section does not have
 
132
            the key. An empty list is returned if there are no errors.
 
133
        """
 
134
        errors = []
 
135
        for key, value in items:
 
136
            if key in self._options:
 
137
                self._options[key] = value
 
138
            else:
 
139
                msg = "%s does not have a %s key." % (self.name, key)
 
140
                errors.append(UnknownKeyError(msg))
 
141
        return errors
 
142
 
 
143
    def clone(self):
 
144
        """Return a copy of this section.
 
145
 
 
146
        The extension mechanism requires a copy of a section to prevent
 
147
        mutation.
 
148
        """
 
149
        new_section = self.__class__(self.schema, self._options.copy())
 
150
        # XXX 2008-06-10 jamesh bug=237827:
 
151
        # Evil legacy code sometimes assigns directly to the config
 
152
        # section objects.  Copy those attributes over.
 
153
        new_section.__dict__.update(
 
154
            dict((key, value) for (key, value) in self.__dict__.iteritems()
 
155
                 if key not in ['schema', '_options']))
 
156
        return new_section
 
157
 
 
158
 
 
159
class ImplicitTypeSection(Section):
 
160
    """See `ISection`.
 
161
 
 
162
    ImplicitTypeSection supports implicit conversion of key values to
 
163
    simple datatypes. It accepts the same section data as Section; the
 
164
    datatype information is not embedded in the schema or the config file.
 
165
    """
 
166
    re_types = re.compile(r'''
 
167
        (?P<false> ^false$) |
 
168
        (?P<true> ^true$) |
 
169
        (?P<none> ^none$) |
 
170
        (?P<int> ^[+-]?\d+$) |
 
171
        (?P<str> ^.*)
 
172
        ''', re.IGNORECASE | re.VERBOSE)
 
173
 
 
174
    def _convert(self, value):
 
175
        """Return the value as the datatype the str appears to be.
 
176
 
 
177
        Conversion rules:
 
178
        * bool: a single word, 'true' or 'false', case insensitive.
 
179
        * int: a single word that is a number. Signed is supported,
 
180
            hex and octal numbers are not.
 
181
        * str: anything else.
 
182
        """
 
183
        match = self.re_types.match(value)
 
184
        if match.group('false'):
 
185
            return False
 
186
        elif match.group('true'):
 
187
            return True
 
188
        elif match.group('none'):
 
189
            return None
 
190
        elif match.group('int'):
 
191
            return int(value)
 
192
        else:
 
193
            # match.group('str'); just return the sripped value.
 
194
            return value.strip()
 
195
 
 
196
    def __getitem__(self, key):
 
197
        """See `ISection`."""
 
198
        value = super(ImplicitTypeSection, self).__getitem__(key)
 
199
        return self._convert(value)
 
200
 
 
201
    def __getattr__(self, name):
 
202
        """See `ISection`."""
 
203
        value = super(ImplicitTypeSection, self).__getattr__(name)
 
204
        return self._convert(value)
 
205
 
 
206
 
 
207
class ConfigSchema:
 
208
    """See `IConfigSchema`."""
 
209
    implements(IConfigSchema, IConfigLoader)
 
210
 
 
211
    _section_factory = Section
 
212
 
 
213
    def __init__(self, filename):
 
214
        """Load a configuration schema from the provided filename.
 
215
 
 
216
        :raise `UnicodeDecodeError`: if the string contains non-ascii
 
217
            characters.
 
218
        :raise `RedefinedSectionError`: if a SectionSchema name is redefined.
 
219
        :raise `InvalidSectionNameError`: if a SectionSchema name is
 
220
            ill-formed.
 
221
        """
 
222
        # XXX sinzui 2007-12-13:
 
223
        # RawConfigParser permits redefinition and non-ascii characters.
 
224
        # The raw schema data is examined before creating a config.
 
225
        self.filename = filename
 
226
        self.name = basename(filename)
 
227
        self._section_schemas = {}
 
228
        self._category_names = []
 
229
        raw_schema = self._getRawSchema(filename)
 
230
        parser = RawConfigParser()
 
231
        parser.readfp(raw_schema, filename)
 
232
        self._setSectionSchemasAndCategoryNames(parser)
 
233
 
 
234
 
 
235
    def _getRawSchema(self, filename):
 
236
        """Return the contents of the schema at filename as a StringIO.
 
237
 
 
238
        This method verifies that the file is ascii encoded and that no
 
239
        section name is redefined.
 
240
        """
 
241
        raw_schema = read_content(filename)
 
242
        # Verify that the string is ascii.
 
243
        raw_schema.encode('ascii', 'ignore')
 
244
        # Verify that no sections are redefined.
 
245
        section_names = []
 
246
        for section_name in re.findall(r'^\s*\[[^\]]+\]', raw_schema, re.M):
 
247
            if section_name in section_names:
 
248
                raise RedefinedSectionError(section_name)
 
249
            else:
 
250
                section_names.append(section_name)
 
251
        return StringIO.StringIO(raw_schema)
 
252
 
 
253
    def _setSectionSchemasAndCategoryNames(self, parser):
 
254
        """Set the SectionSchemas and category_names from the config."""
 
255
        category_names = set()
 
256
        templates = {}
 
257
        # Retrieve all the templates first because section() does not
 
258
        # follow the order of the conf file.
 
259
        for name in parser.sections():
 
260
            (section_name, category_name,
 
261
             is_template, is_optional) = self._parseSectionName(name)
 
262
            if is_template:
 
263
                templates[category_name] = dict(parser.items(name))
 
264
        for name in parser.sections():
 
265
            (section_name, category_name,
 
266
             is_template, is_optional) = self._parseSectionName(name)
 
267
            if is_template:
 
268
                continue
 
269
            options = dict(templates.get(category_name, {}))
 
270
            options.update(parser.items(name))
 
271
            self._section_schemas[section_name] = SectionSchema(
 
272
                section_name, options, is_optional)
 
273
            if category_name is not None:
 
274
                category_names.add(category_name)
 
275
        self._category_names = list(category_names)
 
276
 
 
277
    _section_name_pattern = re.compile(r'\w[\w.-]+\w')
 
278
 
 
279
    def _parseSectionName(self, name):
 
280
        """Return a 4-tuple of names and kinds embedded in the name.
 
281
 
 
282
        :return: (section_name, category_name, is_template, is_optional).
 
283
            section_name is always a string. category_name is a string or
 
284
            None if there is no prefix. is_template and is_optional
 
285
            are False by default, but will be true if the name's suffix
 
286
            ends in '.template' or '.optional'.
 
287
        """
 
288
        name_parts = name.split('.')
 
289
        is_template = name_parts[-1] == 'template'
 
290
        is_optional = name_parts[-1] == 'optional'
 
291
        if is_template or is_optional:
 
292
            # The suffix is not a part of the section name.
 
293
            # Example: [name.optional] or [category.template]
 
294
            del name_parts[-1]
 
295
        count = len(name_parts)
 
296
        if count == 1 and is_template:
 
297
            # Example: [category.template]
 
298
            category_name = name_parts[0]
 
299
            section_name = name_parts[0]
 
300
        elif count == 1:
 
301
            # Example: [name]
 
302
            category_name = None
 
303
            section_name = name_parts[0]
 
304
        elif count == 2:
 
305
            # Example: [category.name]
 
306
            category_name = name_parts[0]
 
307
            section_name = '.'.join(name_parts)
 
308
        else:
 
309
            raise InvalidSectionNameError('[%s] has too many parts.' % name)
 
310
        if self._section_name_pattern.match(section_name) is None:
 
311
            raise InvalidSectionNameError(
 
312
                '[%s] name does not match [\w.-]+.' % name)
 
313
        return (section_name, category_name,  is_template, is_optional)
 
314
 
 
315
    @property
 
316
    def section_factory(self):
 
317
        """See `IConfigSchema`."""
 
318
        return self._section_factory
 
319
 
 
320
    @property
 
321
    def category_names(self):
 
322
        """See `IConfigSchema`."""
 
323
        return self._category_names
 
324
 
 
325
    def __iter__(self):
 
326
        """See `IConfigSchema`."""
 
327
        return self._section_schemas.itervalues()
 
328
 
 
329
    def __contains__(self, name):
 
330
        """See `IConfigSchema`."""
 
331
        return name in self._section_schemas.keys()
 
332
 
 
333
    def __getitem__(self, name):
 
334
        """See `IConfigSchema`."""
 
335
        try:
 
336
            return self._section_schemas[name]
 
337
        except KeyError:
 
338
            raise NoSectionError(name)
 
339
 
 
340
    def getByCategory(self, name):
 
341
        """See `IConfigSchema`."""
 
342
        if name not in self.category_names:
 
343
            raise NoCategoryError(name)
 
344
        section_schemas = []
 
345
        for key in self._section_schemas:
 
346
            section = self._section_schemas[key]
 
347
            category, dummy = section.category_and_section_names
 
348
            if name == category:
 
349
                section_schemas.append(section)
 
350
        return section_schemas
 
351
 
 
352
    def _getRequiredSections(self):
 
353
        """return a dict of `Section`s from the required `SectionSchemas`."""
 
354
        sections = {}
 
355
        for section_schema in self:
 
356
            if not section_schema.optional:
 
357
                sections[section_schema.name] = self.section_factory(
 
358
                    section_schema)
 
359
        return sections
 
360
 
 
361
    def load(self, filename):
 
362
        """See `IConfigLoader`."""
 
363
        conf_data = read_content(filename)
 
364
        return self._load(filename, conf_data)
 
365
 
 
366
    def loadFile(self, source_file, filename=None):
 
367
        """See `IConfigLoader`."""
 
368
        conf_data = source_file.read()
 
369
        if filename is None:
 
370
            filename = getattr(source_file, 'name')
 
371
            assert filename is not None, (
 
372
                'filename must be provided if the file-like object '
 
373
                'does not have a name attribute.')
 
374
        return self._load(filename, conf_data)
 
375
 
 
376
    def _load(self, filename, conf_data):
 
377
        """Return a Config parsed from conf_data."""
 
378
        config = Config(self)
 
379
        config.push(filename, conf_data)
 
380
        return config
 
381
 
 
382
 
 
383
class ImplicitTypeSchema(ConfigSchema):
 
384
    """See `IConfigSchema`.
 
385
 
 
386
    ImplicitTypeSchema creates a config that supports implicit datatyping
 
387
    of section key values.
 
388
    """
 
389
 
 
390
    _section_factory = ImplicitTypeSection
 
391
 
 
392
 
 
393
class ConfigData:
 
394
    """See `IConfigData`."""
 
395
    implements(IConfigData)
 
396
 
 
397
    def __init__(self, filename, sections, extends=None, errors=None):
 
398
        """Set the configuration data."""
 
399
        self.filename = filename
 
400
        self.name = basename(filename)
 
401
        self._sections = sections
 
402
        self._category_names = self._getCategoryNames()
 
403
        self._extends = extends
 
404
        if errors is None:
 
405
            self._errors = []
 
406
        else:
 
407
            self._errors = errors
 
408
 
 
409
    def _getCategoryNames(self):
 
410
        """Return a tuple of category names that the `Section`s belong to."""
 
411
        category_names = set()
 
412
        for section_name in self._sections:
 
413
            section = self._sections[section_name]
 
414
            category, dummy = section.category_and_section_names
 
415
            if category is not None:
 
416
                category_names.add(category)
 
417
        return tuple(category_names)
 
418
 
 
419
    @property
 
420
    def category_names(self):
 
421
        """See `IConfigData`."""
 
422
        return self._category_names
 
423
 
 
424
    def __iter__(self):
 
425
        """See `IConfigData`."""
 
426
        return self._sections.itervalues()
 
427
 
 
428
    def __contains__(self, name):
 
429
        """See `IConfigData`."""
 
430
        return name in self._sections.keys()
 
431
 
 
432
    def __getitem__(self, name):
 
433
        """See `IConfigData`."""
 
434
        try:
 
435
            return self._sections[name]
 
436
        except KeyError:
 
437
            raise NoSectionError(name)
 
438
 
 
439
    def getByCategory(self, name):
 
440
        """See `IConfigData`."""
 
441
        if name not in self.category_names:
 
442
            raise NoCategoryError(name)
 
443
        sections = []
 
444
        for key in self._sections:
 
445
            section = self._sections[key]
 
446
            category, dummy = section.category_and_section_names
 
447
            if name == category:
 
448
                sections.append(section)
 
449
        return sections
 
450
 
 
451
 
 
452
class Config:
 
453
    """See `IStackableConfig`."""
 
454
    # LAZR config classes may access ConfigData private data.
 
455
    # pylint: disable-msg=W0212
 
456
    implements(IStackableConfig)
 
457
    delegates(IConfigData, context='data')
 
458
 
 
459
    def __init__(self, schema):
 
460
        """Set the schema and configuration."""
 
461
        self._overlays = (
 
462
            ConfigData(schema.filename, schema._getRequiredSections()), )
 
463
        self.schema = schema
 
464
 
 
465
    def __getattr__(self, name):
 
466
        """See `IStackableConfig`."""
 
467
        if name in self.data._sections:
 
468
            return self.data._sections[name]
 
469
        elif name in self.data._category_names:
 
470
            return Category(name, self.data.getByCategory(name))
 
471
        raise AttributeError("No section or category named %s." % name)
 
472
 
 
473
    @property
 
474
    def data(self):
 
475
        """See `IStackableConfig`."""
 
476
        return self.overlays[0]
 
477
 
 
478
    @property
 
479
    def extends(self):
 
480
        """See `IStackableConfig`."""
 
481
        if len(self.overlays) == 1:
 
482
            # The ConfigData made from the schema defaults extends nothing.
 
483
            return None
 
484
        else:
 
485
            return self.overlays[1]
 
486
 
 
487
    @property
 
488
    def overlays(self):
 
489
        """See `IStackableConfig`."""
 
490
        return self._overlays
 
491
 
 
492
    def validate(self):
 
493
        """See `IConfigData`."""
 
494
        if len(self.data._errors) > 0:
 
495
            message = "%s is not valid." % self.name
 
496
            raise ConfigErrors(message, errors=self.data._errors)
 
497
        return True
 
498
 
 
499
    def push(self, conf_name, conf_data):
 
500
        """See `IStackableConfig`.
 
501
 
 
502
        Create a new ConfigData object from the raw conf_data, and
 
503
        place it on top of the overlay stack. If the conf_data extends
 
504
        another conf, a ConfigData object will be created for that first.
 
505
        """
 
506
        conf_data = dedent(conf_data)
 
507
        confs = self._getExtendedConfs(conf_name, conf_data)
 
508
        confs.reverse()
 
509
        for conf_name, parser, encoding_errors in confs:
 
510
            if self.data.filename == self.schema.filename == conf_name:
 
511
                # Do not parse the schema file twice in a row.
 
512
                continue
 
513
            config_data = self._createConfigData(
 
514
                conf_name, parser, encoding_errors)
 
515
            self._overlays = (config_data, ) + self._overlays
 
516
 
 
517
    def _getExtendedConfs(self, conf_filename, conf_data, confs=None):
 
518
        """Return a list of 3-tuple(conf_name, parser, encoding_errors).
 
519
 
 
520
        :param conf_filename: The path and name the conf file.
 
521
        :param conf_data: Unparsed config data.
 
522
        :param confs: A list of confs that extend filename.
 
523
        :return: A list of confs ordered from extender to extendee.
 
524
        :raises IOError: If filename cannot be read.
 
525
 
 
526
        This method parses the config data and checks for encoding errors.
 
527
        It checks parsed config data for the extends key in the meta section.
 
528
        It reads the unparsed config_data from the extended filename.
 
529
        It passes filename, data, and the working list to itself.
 
530
        """
 
531
        if confs is None:
 
532
            confs = []
 
533
        encoding_errors = self._verifyEncoding(conf_data)
 
534
        parser = RawConfigParser()
 
535
        parser.readfp(StringIO.StringIO(conf_data), conf_filename)
 
536
        confs.append((conf_filename, parser, encoding_errors))
 
537
        if parser.has_option('meta', 'extends'):
 
538
            base_path = dirname(conf_filename)
 
539
            extends_name = parser.get('meta', 'extends')
 
540
            extends_filename = abspath('%s/%s' % (base_path, extends_name))
 
541
            extends_data = read_content(extends_filename)
 
542
            self._getExtendedConfs(extends_filename, extends_data, confs)
 
543
        return confs
 
544
 
 
545
    def _createConfigData(self, conf_name, parser, encoding_errors):
 
546
        """Return a new ConfigData object created from a parsed conf file.
 
547
 
 
548
        :param conf_name: the full name of the config file, may be a filepath.
 
549
        :param parser: the parsed config file; an instance of ConfigParser.
 
550
        :param encoding_errors: a list of encoding error in the config file.
 
551
        :return: a new ConfigData object.
 
552
 
 
553
        This method extracts the sections, keys, and values from the parser
 
554
        to construct a new ConfigData object The list of encoding errors are
 
555
        incorporated into the the list of data-related errors for the
 
556
        ConfigData.
 
557
        """
 
558
        sections = {}
 
559
        for section in self.data:
 
560
            sections[section.name] = section.clone()
 
561
        errors = list(self.data._errors)
 
562
        errors.extend(encoding_errors)
 
563
        extends = None
 
564
        for section_name in parser.sections():
 
565
            if section_name == 'meta':
 
566
                extends, meta_errors = self._loadMetaData(parser)
 
567
                errors.extend(meta_errors)
 
568
                continue
 
569
            if (section_name.endswith('.template')
 
570
                or section_name.endswith('.optional')):
 
571
                # This section is a schema directive.
 
572
                continue
 
573
            if section_name not in self.schema:
 
574
                # Any section not in the the schema is an error.
 
575
                msg = "%s does not have a %s section." % (
 
576
                    self.schema.name, section_name)
 
577
                errors.append(UnknownSectionError(msg))
 
578
                continue
 
579
            if section_name not in self.data:
 
580
                # Create the optional section from the schema.
 
581
                section_schema = self.schema[section_name]
 
582
                sections[section_name] = self.schema.section_factory(
 
583
                    section_schema)
 
584
            # Update the section with the parser options.
 
585
            items = parser.items(section_name)
 
586
            section_errors = sections[section_name].update(items)
 
587
            errors.extend(section_errors)
 
588
        return ConfigData(conf_name, sections, extends, errors)
 
589
 
 
590
    def _verifyEncoding(self, config_data):
 
591
        """Verify that the data is ASCII encoded.
 
592
 
 
593
        :return: a list of UnicodeDecodeError errors. If there are no
 
594
            errors, return an empty list.
 
595
        """
 
596
        errors = []
 
597
        try:
 
598
            config_data.encode('ascii', 'ignore')
 
599
        except UnicodeDecodeError, error:
 
600
            errors.append(error)
 
601
        return errors
 
602
 
 
603
    def _loadMetaData(self, parser):
 
604
        """Load the config meta data from the ConfigParser.
 
605
 
 
606
        The meta section is reserved for the LAZR config parser.
 
607
 
 
608
        :return: a list of errors if there are errors, or an empty list.
 
609
        """
 
610
        extends = None
 
611
        errors = []
 
612
        for key in parser.options('meta'):
 
613
            if key == "extends":
 
614
                extends = parser.get('meta', 'extends')
 
615
            else:
 
616
                # Any other key is an error.
 
617
                msg = "The meta section does not have a %s key." % key
 
618
                errors.append(UnknownKeyError(msg))
 
619
        return (extends, errors)
 
620
 
 
621
    def pop(self, conf_name):
 
622
        """See `IStackableConfig`."""
 
623
        index = self._getIndexOfOverlay(conf_name)
 
624
        removed_overlays = self.overlays[:index]
 
625
        self._overlays = self.overlays[index:]
 
626
        return removed_overlays
 
627
 
 
628
    def _getIndexOfOverlay(self, conf_name):
 
629
        """Return the index of the config named conf_name.
 
630
 
 
631
        The bottom of the stack cannot never be returned because it was
 
632
        made from the schema.
 
633
        """
 
634
        schema_index = len(self.overlays) - 1
 
635
        for index, config_data in enumerate(self.overlays):
 
636
            if index == schema_index and config_data.name == conf_name:
 
637
                raise NoConfigError("Cannot pop the schema's default config.")
 
638
            if config_data.name == conf_name:
 
639
                return index + 1
 
640
        # The config data was not found in the overlays.
 
641
        raise NoConfigError('No config with name: %s.' % conf_name)
 
642
 
 
643
 
 
644
class Category:
 
645
    """See `ICategory`."""
 
646
    implements(ICategory)
 
647
 
 
648
    def __init__(self, name, sections):
 
649
        """Initialize the Category its name and a list of sections."""
 
650
        self.name = name
 
651
        self._sections = {}
 
652
        for section in sections:
 
653
            self._sections[section.name] = section
 
654
 
 
655
    def __getattr__(self, name):
 
656
        """See `ICategory`."""
 
657
        full_name = "%s.%s" % (self.name, name)
 
658
        if full_name in self._sections:
 
659
            return self._sections[full_name]
 
660
        raise AttributeError("No section named %s." % name)
 
661
 
 
662
 
 
663
def as_host_port(value, default_host='localhost', default_port=25):
 
664
    """Return a 2-tuple of (host, port) from a value like 'host:port'.
 
665
 
 
666
    :param value: The configuration value.
 
667
    :type value: string
 
668
    :param default_host: Optional host name to use if the configuration value
 
669
        is missing the host name.
 
670
    :type default_host: string
 
671
    :param default_port: Optional port number to use if the configuration
 
672
        value is missing the port number.
 
673
    :type default_port: integer
 
674
    :return: a 2-tuple of the form (host, port)
 
675
    :rtype: 2-tuple of (string, integer)
 
676
    """
 
677
    if ':' in value:
 
678
        host, port = value.split(':')
 
679
        if host == '':
 
680
            host = default_host
 
681
        port = int(port)
 
682
    else:
 
683
        host = value
 
684
        port = default_port
 
685
    return host, port
 
686
 
 
687
 
 
688
def as_username_groupname(value=None):
 
689
    """Turn a string of the form user:group into the user and group names.
 
690
 
 
691
    :param value: The configuration value.
 
692
    :type value: a string containing exactly one colon, or None
 
693
    :return: a 2-tuple of (username, groupname).  If `value` was None, then
 
694
        the current user and group names are returned.
 
695
    :rtype: 2-tuple of type (string, string)
 
696
    """
 
697
    if value:
 
698
        user, group = value.split(':', 1)
 
699
    else:
 
700
        user  = pwd.getpwuid(os.getuid()).pw_name
 
701
        group = grp.getgrgid(os.getgid()).gr_name
 
702
    return user, group
 
703
 
 
704
 
 
705
def _sort_order(a, b):
 
706
    """Sort timedelta suffixes from greatest to least."""
 
707
    if len(a) == 0:
 
708
        return -1
 
709
    if len(b) == 0:
 
710
        return 1
 
711
    order = dict(
 
712
        w=0,    # weeks
 
713
        d=1,    # days
 
714
        h=2,    # hours
 
715
        m=3,    # minutes
 
716
        s=4,    # seconds
 
717
        )
 
718
    suffix_a = order.get(a[-1])
 
719
    suffix_b = order.get(b[-1])
 
720
    if suffix_a is None or suffix_b is None:
 
721
        raise ValueError
 
722
    return cmp(suffix_a, suffix_b)
 
723
 
 
724
 
 
725
def as_timedelta(value):
 
726
    """Convert a value string to the equivalent timedeta."""
 
727
    # Technically, the regex will match multiple decimal points in the
 
728
    # left-hand side, but that's okay because the float/int conversion below
 
729
    # will properly complain if there's more than one dot.
 
730
    components = sorted(re.findall(r'([\d.]+[smhdw])', value),
 
731
                        cmp=_sort_order)
 
732
    # Complain if the components are out of order.
 
733
    if ''.join(components) != value:
 
734
        raise ValueError
 
735
    keywords = dict((interval[0].lower(), interval)
 
736
                    for interval in ('weeks', 'days', 'hours',
 
737
                                     'minutes', 'seconds'))
 
738
    keyword_arguments = {}
 
739
    for interval in components:
 
740
        if len(interval) == 0:
 
741
            raise ValueError
 
742
        keyword = keywords.get(interval[-1].lower())
 
743
        if keyword is None:
 
744
            raise ValueError
 
745
        if keyword in keyword_arguments:
 
746
            raise ValueError
 
747
        if '.' in interval[:-1]:
 
748
            converted = float(interval[:-1])
 
749
        else:
 
750
            converted = int(interval[:-1])
 
751
        keyword_arguments[keyword] = converted
 
752
    if len(keyword_arguments) == 0:
 
753
        raise ValueError
 
754
    return datetime.timedelta(**keyword_arguments)