text: """Access and/or modify INI files
  
  * Compatiable with ConfigParser
  * Preserves order of sections & options
  * Preserves comments/blank lines/etc
  * More conveninet access to data
  
  Example:
  
      >>> from six import StringIO
      >>> sio = StringIO('''# configure foo-application
      ... [foo]
      ... bar1 = qualia
      ... bar2 = 1977
      ... [foo-ext]
      ... special = 1''')
  
      >>> cfg = INIConfig(sio)
      >>> print(cfg.foo.bar1)
      qualia
      >>> print(cfg['foo-ext'].special)
      1
      >>> cfg.foo.newopt = 'hi!'
      >>> cfg.baz.enabled = 0
  
      >>> print(cfg)
      # configure foo-application
      [foo]
      bar1 = qualia
      bar2 = 1977
      newopt = hi!
      [foo-ext]
      special = 1
      <BLANKLINE>
      [baz]
      enabled = 0
  
  """
  
  # An ini parser that supports ordered sections/options
  # Also supports updates, while preserving structure
  # Backward-compatiable with ConfigParser
  
  import re
  from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
  
  import six
  
  from . import config
  
  
  class LineType(object):
      line = None
  
      def __init__(self, line=None):
          if line is not None:
              self.line = line.strip('\n')
  
      # Return the original line for unmodified objects
      # Otherwise construct using the current attribute values
      def __str__(self):
          if self.line is not None:
              return self.line
          else:
              return self.to_string()
  
      # If an attribute is modified after initialization
      # set line to None since it is no longer accurate.
      def __setattr__(self, name, value):
          if hasattr(self,name):
              self.__dict__['line'] = None
          self.__dict__[name] = value
  
      def to_string(self):
          raise Exception('This method must be overridden in derived classes')
  
  
  class SectionLine(LineType):
      regex = re.compile(r'^\['
                         r'(?P<name>[^]]+)'
                         r'\]\s*'
                         r'((?P<csep>;|#)(?P<comment>.*))?$')
  
      def __init__(self, name, comment=None, comment_separator=None,
                               comment_offset=-1, line=None):
          super(SectionLine, self).__init__(line)
          self.name = name
          self.comment = comment
          self.comment_separator = comment_separator
          self.comment_offset = comment_offset
  
      def to_string(self):
          out = '[' + self.name + ']'
          if self.comment is not None:
              # try to preserve indentation of comments
              out = (out+' ').ljust(self.comment_offset)
              out = out + self.comment_separator + self.comment
          return out
  
      def parse(cls, line):
          m = cls.regex.match(line.rstrip())
          if m is None:
              return None
          return cls(m.group('name'), m.group('comment'),
                     m.group('csep'), m.start('csep'),
                     line)
      parse = classmethod(parse)
  
  
  class OptionLine(LineType):
      def __init__(self, name, value, separator=' = ', comment=None,
                   comment_separator=None, comment_offset=-1, line=None):
          super(OptionLine, self).__init__(line)
          self.name = name
          self.value = value
          self.separator = separator
          self.comment = comment
          self.comment_separator = comment_separator
          self.comment_offset = comment_offset
  
      def to_string(self):
          out = '%s%s%s' % (self.name, self.separator, self.value)
          if self.comment is not None:
              # try to preserve indentation of comments
              out = (out+' ').ljust(self.comment_offset)
              out = out + self.comment_separator + self.comment
          return out
  
      regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)'
                         r'(?P<sep>[:=]\s*)'
                         r'(?P<value>.*)$')
  
      def parse(cls, line):
          m = cls.regex.match(line.rstrip())
          if m is None:
              return None
  
          name = m.group('name').rstrip()
          value = m.group('value')
          sep = m.group('name')[len(name):] + m.group('sep')
  
          # comments are not detected in the regex because
          # ensuring total compatibility with ConfigParser
          # requires that:
          #     option = value    ;comment   // value=='value'
          #     option = value;1  ;comment   // value=='value;1  ;comment'
          #
          # Doing this in a regex would be complicated.  I
          # think this is a bug.  The whole issue of how to
          # include ';' in the value needs to be addressed.
          # Also, '#' doesn't mark comments in options...
  
          coff = value.find(';')
          if coff != -1 and value[coff-1].isspace():
              comment = value[coff+1:]
              csep = value[coff]
              value = value[:coff].rstrip()
              coff = m.start('value') + coff
          else:
              comment = None
              csep = None
              coff = -1
  
          return cls(name, value, sep, comment, csep, coff, line)
      parse = classmethod(parse)
  
  
  def change_comment_syntax(comment_chars='%;#', allow_rem=False):
      comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars)
      regex = r'^(?P<csep>[%s]' % comment_chars
      if allow_rem:
          regex += '|[rR][eE][mM]'
      regex += r')(?P<comment>.*)$'
      CommentLine.regex = re.compile(regex)
  
  
  class CommentLine(LineType):
      regex = re.compile(r'^(?P<csep>[;#]|[rR][eE][mM])'
                         r'(?P<comment>.*)$')
  
      def __init__(self, comment='', separator='#', line=None):
          super(CommentLine, self).__init__(line)
          self.comment = comment
          self.separator = separator
  
      def to_string(self):
          return self.separator + self.comment
  
      def parse(cls, line):
          m = cls.regex.match(line.rstrip())
          if m is None:
              return None
          return cls(m.group('comment'), m.group('csep'), line)
  
      parse = classmethod(parse)
  
  
  class EmptyLine(LineType):
      # could make this a singleton
      def to_string(self):
          return ''
  
      value = property(lambda self: '')
  
      def parse(cls, line):
          if line.strip():
              return None
          return cls(line)
  
      parse = classmethod(parse)
  
  
  class ContinuationLine(LineType):
      regex = re.compile(r'^\s+(?P<value>.*)$')
  
      def __init__(self, value, value_offset=None, line=None):
          super(ContinuationLine, self).__init__(line)
          self.value = value
          if value_offset is None:
              value_offset = 8
          self.value_offset = value_offset
  
      def to_string(self):
          return ' '*self.value_offset + self.value
  
      def parse(cls, line):
          m = cls.regex.match(line.rstrip())
          if m is None:
              return None
          return cls(m.group('value'), m.start('value'), line)
  
      parse = classmethod(parse)
  
  
  class LineContainer(object):
      def __init__(self, d=None):
          self.contents = []
          self.orgvalue = None
          if d:
              if isinstance(d, list): self.extend(d)
              else: self.add(d)
  
      def add(self, x):
          self.contents.append(x)
  
      def extend(self, x):
          for i in x: self.add(i)
  
      def get_name(self):
          return self.contents[0].name
  
      def set_name(self, data):
          self.contents[0].name = data
  
      def get_value(self):
          if self.orgvalue is not None:
              return self.orgvalue
          elif len(self.contents) == 1:
              return self.contents[0].value
          else:
              return '\n'.join([('%s' % x.value) for x in self.contents
                                if not isinstance(x, CommentLine)])
  
      def set_value(self, data):
          self.orgvalue = data
          lines = ('%s' % data).split('\n')
  
          # If there is an existing ContinuationLine, use its offset
          value_offset = None
          for v in self.contents:
              if isinstance(v, ContinuationLine):
                  value_offset = v.value_offset
                  break
  
          # Rebuild contents list, preserving initial OptionLine
          self.contents = self.contents[0:1]
          self.contents[0].value = lines[0]
          del lines[0]
          for line in lines:
              if line.strip():
                  self.add(ContinuationLine(line, value_offset))
              else:
                  self.add(EmptyLine())
  
      name = property(get_name, set_name)
  
      value = property(get_value, set_value)
  
      def __str__(self):
          s = [x.__str__() for x in self.contents]
          return '\n'.join(s)
  
      def finditer(self, key):
          for x in self.contents[::-1]:
              if hasattr(x, 'name') and x.name==key:
                  yield x
  
      def find(self, key):
          for x in self.finditer(key):
              return x
          raise KeyError(key)
  
  
  def _make_xform_property(myattrname, srcattrname=None):
      private_attrname = myattrname + 'value'
      private_srcname = myattrname + 'source'
      if srcattrname is None:
          srcattrname = myattrname
  
      def getfn(self):
          srcobj = getattr(self, private_srcname)
          if srcobj is not None:
              return getattr(srcobj, srcattrname)
          else:
              return getattr(self, private_attrname)
  
      def setfn(self, value):
          srcobj = getattr(self, private_srcname)
          if srcobj is not None:
              setattr(srcobj, srcattrname, value)
          else:
              setattr(self, private_attrname, value)
  
      return property(getfn, setfn)
  
  
  class INISection(config.ConfigNamespace):
      _lines = None
      _options = None
      _defaults = None
      _optionxformvalue = None
      _optionxformsource = None
      _compat_skip_empty_lines = set()
  
      def __init__(self, lineobj, defaults=None, optionxformvalue=None, optionxformsource=None):
          self._lines = [lineobj]
          self._defaults = defaults
          self._optionxformvalue = optionxformvalue
          self._optionxformsource = optionxformsource
          self._options = {}
  
      _optionxform = _make_xform_property('_optionxform')
  
      def _compat_get(self, key):
          # identical to __getitem__ except that _compat_XXX
          # is checked for backward-compatible handling
          if key == '__name__':
              return self._lines[-1].name
          if self._optionxform: key = self._optionxform(key)
          try:
              value = self._options[key].value
              del_empty = key in self._compat_skip_empty_lines
          except KeyError:
              if self._defaults and key in self._defaults._options:
                  value = self._defaults._options[key].value
                  del_empty = key in self._defaults._compat_skip_empty_lines
              else:
                  raise
          if del_empty:
              value = re.sub('\n+', '\n', value)
          return value
  
      def _getitem(self, key):
          if key == '__name__':
              return self._lines[-1].name
          if self._optionxform: key = self._optionxform(key)
          try:
              return self._options[key].value
          except KeyError:
              if self._defaults and key in self._defaults._options:
                  return self._defaults._options[key].value
              else:
                  raise
  
      def __setitem__(self, key, value):
          if self._optionxform: xkey = self._optionxform(key)
          else: xkey = key
          if xkey in self._compat_skip_empty_lines:
              self._compat_skip_empty_lines.remove(xkey)
          if xkey not in self._options:
              # create a dummy object - value may have multiple lines
              obj = LineContainer(OptionLine(key, ''))
              self._lines[-1].add(obj)
              self._options[xkey] = obj
          # the set_value() function in LineContainer
          # automatically handles multi-line values
          self._options[xkey].value = value
  
      def __delitem__(self, key):
          if self._optionxform: key = self._optionxform(key)
          if key in self._compat_skip_empty_lines:
              self._compat_skip_empty_lines.remove(key)
          for l in self._lines:
              remaining = []
              for o in l.contents:
                  if isinstance(o, LineContainer):
                      n = o.name
                      if self._optionxform: n = self._optionxform(n)
                      if key != n: remaining.append(o)
                  else:
                      remaining.append(o)
              l.contents = remaining
          del self._options[key]
  
      def __iter__(self):
          d = set()
          for l in self._lines:
              for x in l.contents:
                  if isinstance(x, LineContainer):
                      if self._optionxform:
                          ans = self._optionxform(x.name)
                      else:
                          ans = x.name
                      if ans not in d:
                          yield ans
                          d.add(ans)
          if self._defaults:
              for x in self._defaults:
                  if x not in d:
                      yield x
                      d.add(x)
  
      def _new_namespace(self, name):
          raise Exception('No sub-sections allowed', name)
  
  
  def make_comment(line):
      return CommentLine(line.rstrip('\n'))
  
  
  def readline_iterator(f):
      """iterate over a file by only using the file object's readline method"""
  
      have_newline = False
      while True:
          line = f.readline()
  
          if not line:
              if have_newline:
                  yield ""
              return
  
          if line.endswith('\n'):
              have_newline = True
          else:
              have_newline = False
  
          yield line
  
  
  def lower(x):
      return x.lower()
  
  
  class INIConfig(config.ConfigNamespace):
      _data = None
      _sections = None
      _defaults = None
      _optionxformvalue = None
      _optionxformsource = None
      _sectionxformvalue = None
      _sectionxformsource = None
      _parse_exc = None
      _bom = False
  
      def __init__(self, fp=None, defaults=None, parse_exc=True,
                   optionxformvalue=lower, optionxformsource=None,
                   sectionxformvalue=None, sectionxformsource=None):
          self._data = LineContainer()
          self._parse_exc = parse_exc
          self._optionxformvalue = optionxformvalue
          self._optionxformsource = optionxformsource
          self._sectionxformvalue = sectionxformvalue
          self._sectionxformsource = sectionxformsource
          self._sections = {}
          if defaults is None: defaults = {}
          self._defaults = INISection(LineContainer(), optionxformsource=self)
          for name, value in defaults.items():
              self._defaults[name] = value
          if fp is not None:
              self._readfp(fp)
  
      _optionxform = _make_xform_property('_optionxform', 'optionxform')
      _sectionxform = _make_xform_property('_sectionxform', 'optionxform')
  
      def _getitem(self, key):
          if key == DEFAULTSECT:
              return self._defaults
          if self._sectionxform: key = self._sectionxform(key)
          return self._sections[key]
  
      def __setitem__(self, key, value):
          raise Exception('Values must be inside sections', key, value)
  
      def __delitem__(self, key):
          if self._sectionxform: key = self._sectionxform(key)
          for line in self._sections[key]._lines:
              self._data.contents.remove(line)
          del self._sections[key]
  
      def __iter__(self):
          d = set()
          d.add(DEFAULTSECT)
          for x in self._data.contents:
              if isinstance(x, LineContainer):
                  if x.name not in d:
                      yield x.name
                      d.add(x.name)
  
      def _new_namespace(self, name):
          if self._data.contents:
              self._data.add(EmptyLine())
          obj = LineContainer(SectionLine(name))
          self._data.add(obj)
          if self._sectionxform: name = self._sectionxform(name)
          if name in self._sections:
              ns = self._sections[name]
              ns._lines.append(obj)
          else:
              ns = INISection(obj, defaults=self._defaults,
                              optionxformsource=self)
              self._sections[name] = ns
          return ns
  
      def __str__(self):
          if self._bom:
              fmt = u'\ufeff%s'
          else:
              fmt = '%s'
          return fmt % self._data.__str__()
  
      __unicode__ = __str__
  
      _line_types = [EmptyLine, CommentLine,
                     SectionLine, OptionLine,
                     ContinuationLine]
  
      def _parse(self, line):
          for linetype in self._line_types:
              lineobj = linetype.parse(line)
              if lineobj:
                  return lineobj
          else:
              # can't parse line
              return None
  
      def _readfp(self, fp):
          cur_section = None
          cur_option = None
          cur_section_name = None
          cur_option_name = None
          pending_lines = []
          pending_empty_lines = False
          try:
              fname = fp.name
          except AttributeError:
              fname = '<???>'
          line_count = 0
          exc = None
          line = None
  
          for line in readline_iterator(fp):
              # Check for BOM on first line
              if line_count == 0 and isinstance(line, six.text_type):
                  if line[0] == u'\ufeff':
                      line = line[1:]
                      self._bom = True
  
              line_obj = self._parse(line)
              line_count += 1
  
              if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)):
                  if self._parse_exc:
                      raise MissingSectionHeaderError(fname, line_count, line)
                  else:
                      line_obj = make_comment(line)
  
              if line_obj is None:
                  if self._parse_exc:
                      if exc is None:
                          exc = ParsingError(fname)
                      exc.append(line_count, line)
                  line_obj = make_comment(line)
  
              if isinstance(line_obj, ContinuationLine):
                  if cur_option:
                      if pending_lines:
                          cur_option.extend(pending_lines)
                          pending_lines = []
                          if pending_empty_lines:
                              optobj._compat_skip_empty_lines.add(cur_option_name)
                              pending_empty_lines = False
                      cur_option.add(line_obj)
                  else:
                      # illegal continuation line - convert to comment
                      if self._parse_exc:
                          if exc is None:
                              exc = ParsingError(fname)
                          exc.append(line_count, line)
                      line_obj = make_comment(line)
  
              if isinstance(line_obj, OptionLine):
                  if pending_lines:
                      cur_section.extend(pending_lines)
                      pending_lines = []
                      pending_empty_lines = False
                  cur_option = LineContainer(line_obj)
                  cur_section.add(cur_option)
                  if self._optionxform:
                      cur_option_name = self._optionxform(cur_option.name)
                  else:
                      cur_option_name = cur_option.name
                  if cur_section_name == DEFAULTSECT:
                      optobj = self._defaults
                  else:
                      optobj = self._sections[cur_section_name]
                  optobj._options[cur_option_name] = cur_option
  
              if isinstance(line_obj, SectionLine):
                  self._data.extend(pending_lines)
                  pending_lines = []
                  pending_empty_lines = False
                  cur_section = LineContainer(line_obj)
                  self._data.add(cur_section)
                  cur_option = None
                  cur_option_name = None
                  if cur_section.name == DEFAULTSECT:
                      self._defaults._lines.append(cur_section)
                      cur_section_name = DEFAULTSECT
                  else:
                      if self._sectionxform:
                          cur_section_name = self._sectionxform(cur_section.name)
                      else:
                          cur_section_name = cur_section.name
                      if cur_section_name not in self._sections:
                          self._sections[cur_section_name] = \
                                  INISection(cur_section, defaults=self._defaults,
                                             optionxformsource=self)
                      else:
                          self._sections[cur_section_name]._lines.append(cur_section)
  
              if isinstance(line_obj, (CommentLine, EmptyLine)):
                  pending_lines.append(line_obj)
                  if isinstance(line_obj, EmptyLine):
                      pending_empty_lines = True
  
          self._data.extend(pending_lines)
          if line and line[-1] == '\n':
              self._data.add(EmptyLine())
  
          if exc:
              raise exc
  