Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / cxconfig.py

# (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