# (c) Copyright 2009-2011. CodeWeavers, Inc.
"""Provides support for reading and modifying configuration files."""
import os
import re
import sys
import threading
import time
try:
# pylint: disable=E0611
from collections import MutableMapping
from collections import Mapping
except ImportError:
from UserDict import DictMixin as MutableMapping
from UserDict import DictMixin as Mapping
import weakref
import cxfsnotifier
import cxlog
import cxobservable
import cxutils
# pylint: disable=R0901
#####
#
# Raw configuration data parsing and manipulation
#
#####
# These are the observable events
RELOADED = 'reloaded'
# Matches empty lines
_EMPTY_RE = re.compile(r'^\s*$')
class Section(MutableMapping):
"""Stores the data corresponding to a given configuration file section."""
#
# Initialization functions
#
def __init__(self, parent, name): # pylint: disable=W0231
# The configuration object this section belongs to.
self._parent = parent
# The section name as it was in the file (i.e. with the proper case).
self._name = name
# The blocks of lines composing this section.
self._blocks = []
# Maps the fields to the lines they appear in. If the field appears
# multiple times in this section, field_lines[0] will always be the
# last uncommented one.
self._field_lines = {}
# The field values. Only the uncommented fields are present obviously.
self._values = {}
#
# Regular expressions for parsing a section. See read_lines().
#
# Matches a section
_section_re = re.compile(r'^\[(?P<section>.*)\]\s*(?:;[^\]]*)?$')
# This is a field in the following format:
# "Name"="Value"
# or "Name" = "Value" ; comment
# or ;"Name" = "Value"
# where Name and Value are escaped strings which can
# contain backslashes and quotes.
_quoted_field_re = re.compile(r'^\s*(?P<comment>;+\s*)*"(?P<name>(?:[^\\"]|\\.)*)"(?P<equal>\s*=\s*)\"(?P<value>(?:[^\\"]|\\.)*)"\s*(?:;.*)?$')
# This is a field in the following format:
# Name=Value
# or Name = Value ; also part of the value
# or ;Name = Value
# Note that this intentionally also matches
# Name="Value"
# where the quotes are part of the value.
_field_re = re.compile(r'^\s*(?P<comment>;+\s*)*(?P<name>[^[=;][^=]*?)(?P<equal>\s*=\s*)(?P<value>.*?)\s*$')
def _add_field(self, match, blockline, quoted):
"""A helper for read_lines() which records the specified field."""
name = match.group('name')
if quoted:
name = cxutils.unescape_string(name)
key = name.lower()
if key not in self._field_lines:
self._field_lines[key] = []
if match.group('comment'):
if key in self._values:
self._field_lines[key].append(blockline)
else:
self._field_lines[key].insert(0, blockline)
else:
self._field_lines[key].insert(0, blockline)
value = match.group('value')
if quoted:
value = cxutils.unescape_string(value)
self._values[key] = value
def _add_block(self, block):
"""Adds a block of lines to this section and the configuration file."""
if block:
self._blocks.append(block)
self._parent.blocks.append(block)
def read_lines(self, lines, first_lineno, start_lineno):
"""Parses the specified configuration lines until the start of the next
section. Any encountered field is stored in the current section.
This function should really only be called by Raw.read_lines().
"""
if self._parent.read_only:
raise ValueError('This object is currently read-only')
# next_lineno tracks the first line of the next section, that's either
# the [section] line, or the first line of the comments that precede
# it.
# insert_lineno tracks where to insert new fields, that is right after
# the last non-empty line of the section.
# Case 1: "Var 1" = "Value 1"
#
# ; comment 1
# "Var 2" = "Value 2"
# ; comment 2 <-- insert line = next line
# ; comment 3
# [Next Section]
#
# Case 2: "Var 1" = "Value 1"
# ; "Var 2" = "Value 2"
# ; comment 1
# <-- insert line
#
#
# ; comment 2 <-- next line
# [Next Section]
next_lineno = insert_lineno = lineno = start_lineno
next_section = None
status = 'in-section'
# we need a reference to this lines block before we know exactly what
# to put in it
block = []
while True:
lineno += 1
if lineno == len(lines):
# Just in case we reached the end of file
next_lineno = lineno
if status == 'in-section':
insert_lineno = lineno
break
line = lines[lineno]
# Do a quick test to see if this could be a field before spending
# time doing a regular expression match
if '=' in line:
match = self._quoted_field_re.match(line)
if match:
self._add_field(match, (block, lineno - first_lineno), True)
status = 'in-section'
continue
match = self._field_re.match(line)
if match:
self._add_field(match, (block, lineno - first_lineno), False)
status = 'in-section'
continue
if line and line[0] != ';':
# These lines are quite rare so it's not worth trying too hard
# to optimize for speed here
match = _EMPTY_RE.match(line)
if match:
if status == 'in-section' or status == 'other':
insert_lineno = lineno
status = 'empty'
continue
if '[' in line:
match = self._section_re.match(line)
if match:
# This is the start line for the next section
next_section = match.group('section')
if status == 'empty':
next_lineno = lineno
elif status == 'in-section':
next_lineno = insert_lineno = lineno
break
# This should be a comment line but in reality we don't know
# anything except that it's not empty. We'll treat either the
# same way.
if status == 'in-section':
next_lineno = insert_lineno = lineno
elif status == 'empty':
next_lineno = lineno
status = 'other'
block.extend(lines[first_lineno:insert_lineno])
self._add_block(block)
self._add_block(lines[insert_lineno:next_lineno])
return next_lineno, lineno, next_section
#
# Other section methods
#
def _getname(self):
"""Returns the section name as it appears in the configuration file."""
return self._name
# pylint: disable=W1001
name = property(_getname)
def fieldname(self, name):
"""Given a field key, returns the field's original name.
Since the configuration files are case insensitive, the section
iterators do not preserve the case and may return all lowercase names.
This method lets callers that care recover the original field name.
"""
block, lineno = self._field_lines[name.lower()][0]
match = self._quoted_field_re.match(block[lineno])
if match:
return cxutils.unescape_string(match.group('name'))
return self._field_re.match(block[lineno]).group('name')
def iterblocks(self):
"""Returns an iterator over the section's blocks of lines."""
return iter(self._blocks)
def sortedkeys(self):
"""Returns a list containing the section's field names in the order in
which they appear in the file.
Note that only uncommented fields are returned. Also the case of the
field names may not match the case they have in the file.
"""
self._parent.lock()
try:
keys = self._values.keys()
def keys_cmp(key1, key2):
block1, lineno1 = self._field_lines[key1][0]
block2, lineno2 = self._field_lines[key2][0]
if block1 is not block2:
for block in self._blocks:
if block is block1:
return -1
if block is block2:
return 1
return lineno1 - lineno2
keys.sort(keys_cmp)
return keys
finally:
self._parent.unlock()
#
# Methods for MutableMapping()
#
def __contains__(self, name):
"""Returns True if this section contains a field by the specified name
and False otherwise. Note that field names are case insensitive."""
# This method is optional and is provided for efficiency
return name.lower() in self._values
def __getitem__(self, name):
"""Returns the value of the specified field if present and raises a
KeyError exception otherwise. Note that field names are case
insensitive."""
return self._values[name.lower()]
def __iter__(self):
"""Returns an iterator over the section's field names.
Note that only uncommented fields are returned. Also the case of the
field names may not match the case they have in the file.
"""
# This method is optional and is provided for efficiency
return self._values.iterkeys()
def iteritems(self):
"""Returns an iterator over the section's field name and value pairs.
Note that only uncommented fields are returned. Also the case of the
field names may not match the case they have in the file.
"""
return self._values.iteritems()
def keys(self):
"""Returns a list containing the section's field names.
Note that only uncommented fields are returned. Also the case of the
field names may not match the case they have in the file. Finally,
unlike for sortedkeys(), the order is random.
"""
return self._values.keys()
def __setitem__(self, name, value):
if self._parent.read_only:
raise ValueError('This object is currently read-only')
key = name.lower()
self._parent.lock()
try:
if self._values.get(key, None) == value:
# We won't rewrite the field for a mere case difference in
# its name so there is nothing to do.
return
if not self._blocks:
# This is a stub section which means it is not present in the
# file yet. Now is the time to add lines for it.
self._parent.add_section(self._name)
self._values[key] = value
if key in self._field_lines:
block, lineno = self._field_lines[key][0]
match = self._quoted_field_re.match(block[lineno])
if match:
block[lineno] = '"%s"%s"%s"\n' % (cxutils.escape_string(name), match.group('equal'), cxutils.escape_string(value))
else:
match = self._field_re.match(block[lineno])
# If name or value have leading or trailing spaces then they
# will be lost but I'm not going to care
block[lineno] = name + match.group('equal') + value + '\n'
else:
# This is a new field, add a line for it
block = self._blocks[0]
self._field_lines[key] = [(block, len(block))]
block.append('"%s" = "%s"\n' % (cxutils.escape_string(name), cxutils.escape_string(value)))
self._parent.modified = True
finally:
self._parent.unlock()
def __delitem__(self, name):
if self._parent.read_only:
raise ValueError('This object is currently read-only')
key = name.lower()
self._parent.lock()
try:
# Raise a KeyError exception if there is no such field
del self._values[key]
self._parent.modified = True
# 'Erase' the corresponding lines from the file
for block, lineno in self._field_lines.pop(key):
block[lineno] = None
finally:
self._parent.unlock()
def __len__(self):
return len(self._values)
class Raw(MutableMapping, cxobservable.Object):
"""A class for reading, writing and modifying configuration strings or
files.
"""
# This is the list of observable events
observable_events = frozenset((RELOADED, ))
read_only = False
def __init__(self): # pylint: disable=W0231
cxobservable.Object.__init__(self)
# _real_sections only references the sections that are or will be
# present in the file, while _all_sections also contains the stub
# sections created for __getitem__().
self._real_sections = {}
self._all_sections = {}
self._obj_lock = threading.RLock()
# This is really only meant for use by the Section class
self.modified = False
self.blocks = []
def lock(self):
"""Locks the object so its state can be modified in a thread-safe way.
Note that this does not lock the underlying file (if any). Also all
individual methods are already thread-safe (that is they take this
lock) so calling this method from outside cxconfig should rarely be
needed.
"""
self._obj_lock.acquire()
def unlock(self):
"""Unlocks the object. See lock() for details."""
self._obj_lock.release()
def read_lines(self, lines):
"""Parses the specified configuration lines.
Note that this function considers the lines preceding the first section
to not to belong to any section even if some data has been read
already. So it should at a minimum be given complete sections.
"""
if self.read_only:
raise ValueError('This object is currently read-only')
first_lineno = 0
start_lineno = -1
section = Section(self, None)
self.lock()
try:
while True:
first_lineno, start_lineno, name = \
section.read_lines(lines, first_lineno, start_lineno)
if name is None:
break
key = name.lower()
# Use setdefault() for thread safety
section = self._all_sections.setdefault(key, Section(self, name))
if key in self._real_sections:
cxlog.log("found a duplicate section on line %d" % (start_lineno + 1))
self._real_sections[key] = section
finally:
self.unlock()
def read_string(self, string):
"""Reads configuration data from the given string and merges it with the
settings that have already been read.
Note that this function considers the lines preceding the first section
to not to belong to any section even if some data has been read
already. So it should at a minimum be given complete sections.
"""
self.read_lines(string.splitlines())
def read(self, filename):
"""Reads the specified configuration file and merges it with the
settings that have already been read.
Note that this function considers the lines preceding the first section
to not to belong to any section even if some data has been read
already. So it should at a minimum be given complete sections.
"""
if self.read_only:
raise ValueError('This object is currently read-only')
thisfile = open(filename, 'r')
lines = list(thisfile.readlines())
thisfile.close()
if lines and lines[0].startswith('\xef\xbb\xbf'): #utf8 bom(b)
lines[0] = lines[0][3:]
self.read_lines(lines)
def dump(self):
"""Prints the configuration file's blocks of lines to stdout for
debugging.
"""
for block in self.blocks:
sys.stdout.write('---\n')
for line in block:
if line is None:
sys.stdout.write('<None>\n')
else:
sys.stdout.write('| ' + line)
sys.stdout.write('---\n')
def write(self, out):
"""Writes the in-memory settings to the specified file.
The configuration file is replaced atomically so that another process
or thread should never see an incomplete configuration file.
"""
temp_file = None
if hasattr(out, 'write'):
# file-like object
file_obj = out
elif isinstance(out, int):
# file handle
file_obj = os.fdopen(out, 'w')
else:
# filename, make sure we have a directory to write to
dirname = os.path.dirname(out)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
file_obj = open(out, 'w', 0)
temp_file = out + '.tmp'
file_obj = open(temp_file, 'w')
self.lock()
try:
try:
for block in self.blocks:
for line in block:
if line is not None:
file_obj.write(line)
finally:
file_obj.close()
if temp_file:
# Now atomically overwrite the old configuration file with the
# new one, and do so before unlocking.
os.rename(temp_file, out)
finally:
self.unlock()
def add_section(self, name):
"""Adds a new empty section of the specified name and returns it.
If the section already exists, then it is returned.
"""
if self.read_only:
raise ValueError('This object is currently read-only')
key = name.lower()
self.lock()
try:
if key in self._real_sections:
return self._real_sections[key]
section = self._all_sections.setdefault(key, Section(self, name))
header = '[%s]\n' % name
if self.blocks and self.blocks[-1][-1] is not None and \
not _EMPTY_RE.match(self.blocks[-1][-1]):
section.read_lines(('\n', header), 0, 1)
else:
section.read_lines((header, ), 0, 0)
self._real_sections[key] = section
return section
finally:
self.unlock()
#
# Methods for MutableMapping()
#
def __contains__(self, name):
"""Returns True if this configuration object contains a section by the
specified name and False otherwise. Note that section names are case
insensitive."""
# This method is optional and is provided for efficiency
return name.lower() in self._real_sections
def __getitem__(self, name):
"""Returns the specified section.
Note that for convenience the following syntax works even if the
specified section does not exist:
var = config['Nonexistent']['FieldName']
This means __getitem__() returns stub section objects for nonexistent
sections. However these sections will not be seen by __contains__() or
the iterator methods, and will not be saved when the configuration is
written to a file. Further note that section names are case
insensitive."""
try:
key = name.lower()
return self._real_sections[key]
except KeyError:
# Create a 'stub' section which we add to _all_sections but not
# _real_sections so __contains__ & co don't see it. Also, as long
# as it does not have blocks it will be ignored by write(). It is
# the section's __setitem__() which will be responsible for
# turning it into a real section when we first create a field.
# Use setdefault() so we don't have to recheck:
# if key not in _all_sections
return self._all_sections.setdefault(key, Section(self, name))
def __iter__(self):
"""Note that this method iterates over the lowercased section names.
Since the section names are case insensitive this should not bother
the caller or he should be able to deal with case differences anyway."""
# This method is optional and is provided for efficiency
return self._real_sections.iterkeys()
def iteritems(self):
"""Returns an iterator over the configuration's section name and object
pairs.
Note that because section names are case insensitive, the case of the
section names returned by the iterator may not match the case they have
in the file (to get the unaltered section name, query the section's
name attribute).
"""
# This method is optional and is provided for efficiency
return self._real_sections.iteritems()
def keys(self):
"""Returns a list containing the configuration's section names.
Note that because section names are case insensitive, the returned
section names may not have the same case as in the file (to get the
unaltered section name, query the section's name attribute).
"""
return self._real_sections.keys()
def __setitem__(self, _name, _value):
raise ValueError("Cannot assign to sections directly")
def __delitem__(self, name):
"""Deletes the specified section."""
if self.read_only:
raise ValueError('This object is currently read-only')
self.lock()
try:
key = name.lower()
section = self._real_sections.pop(key)
del self._all_sections[key]
self.modified = True
for block in section.iterblocks():
for i in xrange(len(block)):
block[i] = None
finally:
self.unlock()
def __len__(self):
return len(self._real_sections)
#####
#
# Reading / writing configuration files
#
#####
class File(Raw):
"""Loads a configuration file and automatically keeps it up to date.
Note that this means the object is read-only until the on-disk file has
been locked with lock_file(). The changes can then be committed to disk
with save_and_unlock_file(), at which point the object is read-only again.
"""
def __init__(self, filename):
"""Creates an object corresponding to the specified configuration
file.
Note that it is ok to specify a nonexistent filename. Also note that
the File object will automatically read the configuration file if some
other process creates eventually.
"""
Raw.__init__(self)
self._read_only = True
self._filename = filename
self._mtime = None
self._file_lock = None
self.refresh()
cxfsnotifier.add_observer(self._filename, self._fs_observer)
def _getread_only(self):
"""False if the configuration can be modified, True otherwise.
The object is in read-only mode at all times except between
lock_file() and save_and_unlock_file() calls.
"""
return self._read_only
# pylint: disable=W1001
read_only = property(_getread_only)
def _getfilename(self):
"""Returns the path of the configuration file this object corresponds
to."""
return self._filename
filename = property(_getfilename)
#
# Reading the configuration file
#
def _fs_observer(self, _event, path, _data):
"""Called by cxfsnotifier whenever something happens to the
configuration file."""
# Ignore changes if _filename is a directory
if path == self._filename:
self.refresh()
def refresh(self):
"""Re-reads the file if it has been modified on disk.
Note that cxconfig.File objects refresh themselves automatically
although there may be a small delay introduced by the platform's
file change notification mechanism. So calling this method is only
needed if you know the file has been modified outside of this object's
control and want to make sure the object has been refreshed before
continuing.
However note that any on-disk change will be ignored if the in-memory
data has been modified. However that also means that lock_file() has
been called and thus the on-disk file should not have been changed in
the first place.
"""
if self.modified:
cxlog.warn("Not re-reading %s because it has been modified in memory\n" % self._filename)
return
start = time.time()
self.lock()
try:
try:
mtime = os.path.getmtime(self._filename)
except OSError:
mtime = None
if self._mtime == mtime:
return
self._mtime = mtime
try:
self._read_only = False
self._real_sections = {}
self._all_sections = {}
self.blocks = []
self.read(self._filename)
except IOError:
# If we cannot read a file, leave the configuration empty
pass
self.emit_event(RELOADED)
finally:
self._read_only = True
self.unlock()
cxlog.log("reading '%s' took %0.4fs" % (cxlog.to_str(self._filename), time.time() - start))
#
# File locking and modification
#
def lock_file(self, lock_name=None):
"""Creates and takes a write lock for the specified configuration file,
refreshes the in-memory settings and makes them read-write (see the
read_only attribute).
Note that the OS-level lock is not taken on the configuration file
itself so we can lock nonexistent files and also so we allow
unfettered read access. The latter means using atomic renames to put
the new file in place when saving which would interfer with locks
taken on that file.
"""
self.lock()
try:
# First prevent updates to the configuration file
try:
if not lock_name:
lock_name = os.path.basename(self._filename)
self._file_lock = cxutils.FLock(lock_name)
except: # pylint: disable=W0702
self._file_lock = None
# Then make sure we have the latest version in memory
self.refresh()
self._read_only = False
self.modified = False
finally:
self.unlock()
def file_is_locked(self):
return self._file_lock is not None
def save_and_unlock_file(self, rm_if_empty=False):
"""Marks the in-memory settings as read-only, saves them if they have
been modified and then unlocks the configuration file."""
if not self._file_lock:
raise ValueError('The configuration file must be locked first')
self.lock()
try:
self._read_only = True
# First save the configuration file
if self.modified:
if rm_if_empty and not self._real_sections:
try:
cxlog.log("'%s' is empty -> deleting it" % cxlog.to_str(self._filename))
os.unlink(self._filename)
except OSError:
# Try to save to it instead
cxlog.warn("could not delete '%s', saving it instead\n" % cxlog.to_str(self._filename))
self.write(self._filename)
else:
cxlog.log("saving '%s'\n" % cxlog.to_str(self._filename))
self.write(self._filename)
self.modified = False
# Update _mtime so we don't immediately and needlessly reload it
try:
self._mtime = os.path.getmtime(self._filename)
except OSError:
self._mtime = None
else:
cxlog.log("'%s' not modified -> no need to save\n" % cxlog.to_str(self._filename))
# And finally unlock it
try:
self._file_lock.release()
finally:
self._file_lock = None
finally:
self.unlock()
def __del__(self):
if self._file_lock:
raise ValueError('The file must be unlocked before being thrown away!')
#####
#
# The configuration file cache
#
#####
_CACHE = weakref.WeakValueDictionary()
def get(filename):
"""Returns a cxconfig.File object containing the requested file.
Note that the resulting object is cached based on the specified filename.
This is done so all callers see the same data at all times even across
threads. To ensure this, the path is normalized a bit so a doubled path
separator does not throw it off. However symbolic links, parent directory
references, and absolute vs. relative paths result in different objects
because they may refer to different files in the future.
"""
# We don't want to use os.path.normpath(). See the above docstring.
while filename.startswith('./'):
filename = filename[2:]
while True:
normalized = filename.replace('/./', '/')
if normalized == filename:
break
filename = normalized
while True:
normalized = filename.replace('//', '/')
if normalized == filename:
break
filename = normalized
if filename in _CACHE:
config = _CACHE[filename]
# Callers expect get() to return fresh information. Those who want to
# avoid the extra disk access should keep the object around.
config.refresh()
return config
return _CACHE.setdefault(filename, File(filename))
#####
#
# Providing a unified view of multiple configuration files
#
#####
class _StackSection(Mapping):
"""This is a proxy class that presents a unified view of the sections of
the configuration objects contained in a configuration Stack instance."""
def __init__(self, parent, key): # pylint: disable=W0231
"""Creates a proxy instance for the section whose key is key for the
specified parent Stack instance."""
self._parent = parent
self._key = key
#
# Other section methods
#
def _getname(self):
"""See Section._getname() for details."""
for config in self._parent.iterconfigs():
section = config.get(self._key, None)
if section is not None:
return section.name
return self._key
# pylint: disable=W1001
name = property(_getname)
def fieldname(self, name):
"""See Section.fieldname() for details."""
for config in self._parent.iterconfigs():
section = config.get(self._key, None)
if section is not None:
try:
return section.fieldname(name)
except KeyError:
# Must be in the next section
pass
raise KeyError(name)
# No sortedkeys() method because its semantics would be hard to define
#
# Methods for Mapping()
#
def __contains__(self, name):
"""See Section.__contains__() for details."""
# This method is optional and is provided for efficiency
for config in self._parent.iterconfigs():
if self._key in config and name in config[self._key]:
return True
return False
def __getitem__(self, name):
"""See Section.__getitem__() for details."""
for config in self._parent.iterconfigs():
if self._key in config:
section = config[self._key]
if name in section:
return section[name]
raise KeyError(name)
def __iter__(self):
"""See Section.__iter__() for details."""
# This method is optional and is provided for efficiency
seen = set()
for config in self._parent.iterconfigs():
if self._key in config:
section = config[self._key]
for fieldname in section:
if fieldname not in seen:
seen.add(fieldname)
yield fieldname
# Let Mapping figure out how to provide iteritems()
def keys(self):
"""See Section.keys() for details."""
fields = set()
for config in self._parent.iterconfigs():
if self._key in config:
fields.update(config[self._key].keys())
return list(fields)
def __len__(self):
return len(self.keys())
# No __setitem__() and __delitem__() as this object is read-only
class Stack(Mapping, cxobservable.Object):
"""Provides a read-only seamless view of a stack of configuration files.
One faces a layered configuration file set whenever the configuration
settings are spread over multiple configuration files. This is typically
the case when the settings are split between a per-user file and a
system-wide file containing the defaults. For instance:
~/.cxoffice/cxoffice.conf
/opt/cxoffice/etc/cxoffice.conf
One can view this as a stack with the file containing the most specific
settings on top and the one containing the defaults at the bottom.
Furthermore the Stack implementation guarantees that any change made in
the underlying configuration objects will be immediately visible in the
Stack object. So the following code would work:
stack = cxconfig.Stack()
stack.addconfig('bottom.conf')
stack.addconfig('top.conf')
config = cxconfig.get('top.conf')
config['Section']['Setting'] = 'Changed'
assert stack['Section']['Setting'] == 'Changed'
"""
# This is the list of observable events
observable_events = frozenset((RELOADED, ))
def __init__(self): # pylint: disable=W0231
cxobservable.Object.__init__(self)
# The stack of configuration objects.
self._configs = []
# A cache for the proxy sections.
self._cache = {}
def copy(self):
"""Creates a copy of the Stack object.
One can then add more configuration files to the copy without
impacting the original object.
"""
stack = Stack()
# pylint: disable=W0212
stack._configs = self._configs[:]
return stack
def addconfig(self, filename):
"""Adds the specified configuration file on top of the stack.
The order in which the files are added is important: the settings
contained in the top-most file override those of the files below it.
The configuration file will be taken from the cxconfig cache to ensure
consistency with concurrent direct accesses to that file.
"""
config = get(filename)
self._configs.insert(0, config)
config.add_observer(RELOADED, self._event_forwarder)
def _event_forwarder(self, _source, event, *args):
"""Forwards selected events from the underlying configuration
objects."""
self.emit_event(event, *args)
def iterconfigs(self):
"""Returns an iterator over the configuration objects that have been
added to this Stack instance. The configuration objects are iterated
in top to bottom order."""
return iter(self._configs)
def configs(self):
"""Returns a list containing the configuration objects this Layered
instance is based on. See iterconfig() for the order of the objects in
the list."""
return self._configs[:]
def get_save_config(self):
""""Returns the top-most writable configuration file.
This is typically the file you would want to save modifications to.
Note that the underlying configuration file may not exist yet. It is
enough that it can be created and written to if needed.
"""
for config in self._configs:
path = config.filename
while True:
if os.path.lexists(path):
if path == config.filename:
if not os.path.isfile(path):
return None
elif not os.path.isdir(path):
return None
if os.access(path, os.W_OK):
return config
return None
if path == '' or path == '/':
return None
path = os.path.dirname(path)
return None
def refresh(self):
"""Synchronously refreshes each of the underlying files.
See File.refresh() for more details.
"""
for config in self._configs:
config.refresh()
#
# Methods for MutableMapping()
#
def __contains__(self, name):
"""See Raw.__contains__() for details."""
# This method is optional and is provided for efficiency
for config in self._configs:
if name in config:
return True
return False
def __getitem__(self, name):
"""See Raw.__getitem__() for details."""
key = name.lower()
if key in self._cache:
return self._cache[key]
# unlike an explicit assignment, setdefault is threadsafe
return self._cache.setdefault(key, _StackSection(self, name))
def __iter__(self):
"""See Raw.__iter__() for details."""
# This method is optional and is provided for efficiency
seen = set()
for config in self._configs:
for name in config:
if name not in seen:
seen.add(name)
yield name
# Let Mapping figure out how to provide iteritems()
def keys(self):
"""See Raw.keys() for details."""
names = set()
for config in self._configs:
names.update(config.keys())
return list(names)
def __len__(self):
return len(self.keys())
# No __setitem__() and __delitem__() as this object is read-only