1
# Copyright 2004-2007 Canonical Ltd. All rights reserved.
3
"""Implementation classes for config."""
12
'ImplicitTypeSection',
17
'as_username_groupname',
28
from ConfigParser import NoSectionError, RawConfigParser
29
from os.path import abspath, basename, dirname
30
from textwrap import dedent
32
from zope.interface import implements
34
from canonical.lazr.interfaces import (
35
ConfigErrors, ICategory, IConfigData, IConfigLoader, IConfigSchema,
36
InvalidSectionNameError, ISection, ISectionSchema, IStackableConfig,
37
NoCategoryError, NoConfigError, RedefinedSectionError, UnknownKeyError,
39
from canonical.lazr.decorates import decorates
42
def read_content(filename):
43
"""Return the content of a file at filename as a string."""
44
source_file = open(filename, 'r')
46
raw_data = source_file.read()
53
"""See `ISectionSchema`."""
54
implements(ISectionSchema)
56
def __init__(self, name, options, is_optional=False):
57
"""Create an `ISectionSchema` from the name and options.
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.
64
# This method should raise RedefinedKeyError if the schema file
65
# redefines a key, but SafeConfigParser swallows redefined keys.
67
self._options = options
68
self.optional = is_optional
71
"""See `ISectionSchema`"""
72
return self._options.iterkeys()
74
def __contains__(self, name):
75
"""See `ISectionSchema`"""
76
return name in self._options
78
def __getitem__(self, key):
79
"""See `ISectionSchema`"""
80
return self._options[key]
83
def category_and_section_names(self):
84
"""See `ISectionSchema`."""
86
return tuple(self.name.split('.'))
88
return (None, self.name)
94
decorates(ISectionSchema, context='schema')
96
def __init__(self, schema, _options=None):
97
"""Create an `ISection` from schema.
99
:param schema: The ISectionSchema that defines this ISection.
101
# Use __dict__ because __getattr__ limits access to self.options.
102
self.__dict__['schema'] = schema
104
_options = dict([(key, schema[key]) for key in schema])
105
self.__dict__['_options'] = _options
107
def __getitem__(self, key):
109
return self._options[key]
111
def __getattr__(self, name):
112
"""See `ISection`."""
113
if name in self._options:
114
return self._options[name]
116
raise AttributeError(
117
"No section key named %s." % name)
119
def __setattr__(self, name, value):
120
"""Callsites cannot mutate the config by direct manipulation."""
121
raise AttributeError("Config options cannot be set directly.")
124
def category_and_section_names(self):
125
"""See `ISection`."""
126
return self.schema.category_and_section_names
128
def update(self, items):
129
"""Update the keys with new values.
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.
135
for key, value in items:
136
if key in self._options:
137
self._options[key] = value
139
msg = "%s does not have a %s key." % (self.name, key)
140
errors.append(UnknownKeyError(msg))
144
"""Return a copy of this section.
146
The extension mechanism requires a copy of a section to prevent
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']))
159
class ImplicitTypeSection(Section):
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.
166
re_types = re.compile(r'''
167
(?P<false> ^false$) |
170
(?P<int> ^[+-]?\d+$) |
172
''', re.IGNORECASE | re.VERBOSE)
174
def _convert(self, value):
175
"""Return the value as the datatype the str appears to be.
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.
183
match = self.re_types.match(value)
184
if match.group('false'):
186
elif match.group('true'):
188
elif match.group('none'):
190
elif match.group('int'):
193
# match.group('str'); just return the sripped value.
196
def __getitem__(self, key):
197
"""See `ISection`."""
198
value = super(ImplicitTypeSection, self).__getitem__(key)
199
return self._convert(value)
201
def __getattr__(self, name):
202
"""See `ISection`."""
203
value = super(ImplicitTypeSection, self).__getattr__(name)
204
return self._convert(value)
208
"""See `IConfigSchema`."""
209
implements(IConfigSchema, IConfigLoader)
211
_section_factory = Section
213
def __init__(self, filename):
214
"""Load a configuration schema from the provided filename.
216
:raise `UnicodeDecodeError`: if the string contains non-ascii
218
:raise `RedefinedSectionError`: if a SectionSchema name is redefined.
219
:raise `InvalidSectionNameError`: if a SectionSchema name is
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)
235
def _getRawSchema(self, filename):
236
"""Return the contents of the schema at filename as a StringIO.
238
This method verifies that the file is ascii encoded and that no
239
section name is redefined.
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.
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)
250
section_names.append(section_name)
251
return StringIO.StringIO(raw_schema)
253
def _setSectionSchemasAndCategoryNames(self, parser):
254
"""Set the SectionSchemas and category_names from the config."""
255
category_names = set()
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)
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)
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)
277
_section_name_pattern = re.compile(r'\w[\w.-]+\w')
279
def _parseSectionName(self, name):
280
"""Return a 4-tuple of names and kinds embedded in the name.
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'.
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]
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]
303
section_name = name_parts[0]
305
# Example: [category.name]
306
category_name = name_parts[0]
307
section_name = '.'.join(name_parts)
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)
316
def section_factory(self):
317
"""See `IConfigSchema`."""
318
return self._section_factory
321
def category_names(self):
322
"""See `IConfigSchema`."""
323
return self._category_names
326
"""See `IConfigSchema`."""
327
return self._section_schemas.itervalues()
329
def __contains__(self, name):
330
"""See `IConfigSchema`."""
331
return name in self._section_schemas.keys()
333
def __getitem__(self, name):
334
"""See `IConfigSchema`."""
336
return self._section_schemas[name]
338
raise NoSectionError(name)
340
def getByCategory(self, name):
341
"""See `IConfigSchema`."""
342
if name not in self.category_names:
343
raise NoCategoryError(name)
345
for key in self._section_schemas:
346
section = self._section_schemas[key]
347
category, dummy = section.category_and_section_names
349
section_schemas.append(section)
350
return section_schemas
352
def _getRequiredSections(self):
353
"""return a dict of `Section`s from the required `SectionSchemas`."""
355
for section_schema in self:
356
if not section_schema.optional:
357
sections[section_schema.name] = self.section_factory(
361
def load(self, filename):
362
"""See `IConfigLoader`."""
363
conf_data = read_content(filename)
364
return self._load(filename, conf_data)
366
def loadFile(self, source_file, filename=None):
367
"""See `IConfigLoader`."""
368
conf_data = source_file.read()
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)
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)
383
class ImplicitTypeSchema(ConfigSchema):
384
"""See `IConfigSchema`.
386
ImplicitTypeSchema creates a config that supports implicit datatyping
387
of section key values.
390
_section_factory = ImplicitTypeSection
394
"""See `IConfigData`."""
395
implements(IConfigData)
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
407
self._errors = errors
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)
420
def category_names(self):
421
"""See `IConfigData`."""
422
return self._category_names
425
"""See `IConfigData`."""
426
return self._sections.itervalues()
428
def __contains__(self, name):
429
"""See `IConfigData`."""
430
return name in self._sections.keys()
432
def __getitem__(self, name):
433
"""See `IConfigData`."""
435
return self._sections[name]
437
raise NoSectionError(name)
439
def getByCategory(self, name):
440
"""See `IConfigData`."""
441
if name not in self.category_names:
442
raise NoCategoryError(name)
444
for key in self._sections:
445
section = self._sections[key]
446
category, dummy = section.category_and_section_names
448
sections.append(section)
453
"""See `IStackableConfig`."""
454
# LAZR config classes may access ConfigData private data.
455
# pylint: disable-msg=W0212
456
implements(IStackableConfig)
457
decorates(IConfigData, context='data')
459
def __init__(self, schema):
460
"""Set the schema and configuration."""
462
ConfigData(schema.filename, schema._getRequiredSections()), )
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)
475
"""See `IStackableConfig`."""
476
return self.overlays[0]
480
"""See `IStackableConfig`."""
481
if len(self.overlays) == 1:
482
# The ConfigData made from the schema defaults extends nothing.
485
return self.overlays[1]
489
"""See `IStackableConfig`."""
490
return self._overlays
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)
499
def push(self, conf_name, conf_data):
500
"""See `IStackableConfig`.
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.
506
conf_data = dedent(conf_data)
507
confs = self._getExtendedConfs(conf_name, conf_data)
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.
513
config_data = self._createConfigData(
514
conf_name, parser, encoding_errors)
515
self._overlays = (config_data, ) + self._overlays
517
def _getExtendedConfs(self, conf_filename, conf_data, confs=None):
518
"""Return a list of 3-tuple(conf_name, parser, encoding_errors).
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.
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.
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)
545
def _createConfigData(self, conf_name, parser, encoding_errors):
546
"""Return a new ConfigData object created from a parsed conf file.
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.
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
559
for section in self.data:
560
sections[section.name] = section.clone()
561
errors = list(self.data._errors)
562
errors.extend(encoding_errors)
564
for section_name in parser.sections():
565
if section_name == 'meta':
566
extends, meta_errors = self._loadMetaData(parser)
567
errors.extend(meta_errors)
569
if (section_name.endswith('.template')
570
or section_name.endswith('.optional')):
571
# This section is a schema directive.
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))
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(
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)
590
def _verifyEncoding(self, config_data):
591
"""Verify that the data is ASCII encoded.
593
:return: a list of UnicodeDecodeError errors. If there are no
594
errors, return an empty list.
598
config_data.encode('ascii', 'ignore')
599
except UnicodeDecodeError, error:
603
def _loadMetaData(self, parser):
604
"""Load the config meta data from the ConfigParser.
606
The meta section is reserved for the LAZR config parser.
608
:return: a list of errors if there are errors, or an empty list.
612
for key in parser.options('meta'):
614
extends = parser.get('meta', 'extends')
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)
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
628
def _getIndexOfOverlay(self, conf_name):
629
"""Return the index of the config named conf_name.
631
The bottom of the stack cannot never be returned because it was
632
made from the schema.
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:
640
# The config data was not found in the overlays.
641
raise NoConfigError('No config with name: %s.' % conf_name)
645
"""See `ICategory`."""
646
implements(ICategory)
648
def __init__(self, name, sections):
649
"""Initialize the Category its name and a list of sections."""
652
for section in sections:
653
self._sections[section.name] = section
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)
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'.
666
:param value: The configuration value.
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)
678
host, port = value.split(':')
688
def as_username_groupname(value=None):
689
"""Turn a string of the form user:group into the user and group names.
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)
698
user, group = value.split(':', 1)
700
user = pwd.getpwuid(os.getuid()).pw_name
701
group = grp.getgrgid(os.getgid()).gr_name
705
def _sort_order(a, b):
706
"""Sort timedelta suffixes from greatest to least."""
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:
722
return cmp(suffix_a, suffix_b)
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),
732
# Complain if the components are out of order.
733
if ''.join(components) != value:
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:
742
keyword = keywords.get(interval[-1].lower())
745
if keyword in keyword_arguments:
747
if '.' in interval[:-1]:
748
converted = float(interval[:-1])
750
converted = int(interval[:-1])
751
keyword_arguments[keyword] = converted
752
if len(keyword_arguments) == 0:
754
return datetime.timedelta(**keyword_arguments)