migrate to Python3 (from alejeune)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2001, 2002, 2003 Python Software Foundation
|
||||
# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
|
||||
# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
|
||||
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
|
||||
|
||||
from .ini import INIConfig, change_comment_syntax
|
||||
from .config import BasicConfig, ConfigNamespace
|
||||
from .compat import RawConfigParser, ConfigParser, SafeConfigParser
|
||||
from .utils import tidy
|
||||
|
||||
from .configparser import DuplicateSectionError, \
|
||||
NoSectionError, NoOptionError, \
|
||||
InterpolationMissingOptionError, \
|
||||
InterpolationDepthError, \
|
||||
InterpolationSyntaxError, \
|
||||
DEFAULTSECT, MAX_INTERPOLATION_DEPTH
|
||||
|
||||
__all__ = [
|
||||
'BasicConfig', 'ConfigNamespace',
|
||||
'INIConfig', 'tidy', 'change_comment_syntax',
|
||||
'RawConfigParser', 'ConfigParser', 'SafeConfigParser',
|
||||
'DuplicateSectionError', 'NoSectionError', 'NoOptionError',
|
||||
'InterpolationMissingOptionError', 'InterpolationDepthError',
|
||||
'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH',
|
||||
]
|
||||
@@ -0,0 +1,343 @@
|
||||
# Copyright (c) 2001, 2002, 2003 Python Software Foundation
|
||||
# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
|
||||
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
|
||||
|
||||
"""Compatibility interfaces for ConfigParser
|
||||
|
||||
Interfaces of ConfigParser, RawConfigParser and SafeConfigParser
|
||||
should be completely identical to the Python standard library
|
||||
versions. Tested with the unit tests included with Python-2.3.4
|
||||
|
||||
The underlying INIConfig object can be accessed as cfg.data
|
||||
"""
|
||||
|
||||
import re
|
||||
from .configparser import DuplicateSectionError, \
|
||||
NoSectionError, NoOptionError, \
|
||||
InterpolationMissingOptionError, \
|
||||
InterpolationDepthError, \
|
||||
InterpolationSyntaxError, \
|
||||
DEFAULTSECT, MAX_INTERPOLATION_DEPTH
|
||||
|
||||
# These are imported only for compatiability.
|
||||
# The code below does not reference them directly.
|
||||
from .configparser import Error, InterpolationError, \
|
||||
MissingSectionHeaderError, ParsingError
|
||||
|
||||
import six
|
||||
|
||||
from . import ini
|
||||
|
||||
|
||||
class RawConfigParser(object):
|
||||
def __init__(self, defaults=None, dict_type=dict):
|
||||
if dict_type != dict:
|
||||
raise ValueError('Custom dict types not supported')
|
||||
self.data = ini.INIConfig(defaults=defaults, optionxformsource=self)
|
||||
|
||||
def optionxform(self, optionstr):
|
||||
return optionstr.lower()
|
||||
|
||||
def defaults(self):
|
||||
d = {}
|
||||
secobj = self.data._defaults
|
||||
for name in secobj._options:
|
||||
d[name] = secobj._compat_get(name)
|
||||
return d
|
||||
|
||||
def sections(self):
|
||||
"""Return a list of section names, excluding [DEFAULT]"""
|
||||
return list(self.data)
|
||||
|
||||
def add_section(self, section):
|
||||
"""Create a new section in the configuration.
|
||||
|
||||
Raise DuplicateSectionError if a section by the specified name
|
||||
already exists. Raise ValueError if name is DEFAULT or any of
|
||||
its case-insensitive variants.
|
||||
"""
|
||||
# The default section is the only one that gets the case-insensitive
|
||||
# treatment - so it is special-cased here.
|
||||
if section.lower() == "default":
|
||||
raise ValueError('Invalid section name: %s' % section)
|
||||
|
||||
if self.has_section(section):
|
||||
raise DuplicateSectionError(section)
|
||||
else:
|
||||
self.data._new_namespace(section)
|
||||
|
||||
def has_section(self, section):
|
||||
"""Indicate whether the named section is present in the configuration.
|
||||
|
||||
The DEFAULT section is not acknowledged.
|
||||
"""
|
||||
return section in self.data
|
||||
|
||||
def options(self, section):
|
||||
"""Return a list of option names for the given section name."""
|
||||
if section in self.data:
|
||||
return list(self.data[section])
|
||||
else:
|
||||
raise NoSectionError(section)
|
||||
|
||||
def read(self, filenames):
|
||||
"""Read and parse a filename or a list of filenames.
|
||||
|
||||
Files that cannot be opened are silently ignored; this is
|
||||
designed so that you can specify a list of potential
|
||||
configuration file locations (e.g. current directory, user's
|
||||
home directory, systemwide directory), and all existing
|
||||
configuration files in the list will be read. A single
|
||||
filename may also be given.
|
||||
"""
|
||||
files_read = []
|
||||
if isinstance(filenames, six.string_types):
|
||||
filenames = [filenames]
|
||||
for filename in filenames:
|
||||
try:
|
||||
fp = open(filename)
|
||||
except IOError:
|
||||
continue
|
||||
files_read.append(filename)
|
||||
self.data._readfp(fp)
|
||||
fp.close()
|
||||
return files_read
|
||||
|
||||
def readfp(self, fp, filename=None):
|
||||
"""Like read() but the argument must be a file-like object.
|
||||
|
||||
The `fp' argument must have a `readline' method. Optional
|
||||
second argument is the `filename', which if not given, is
|
||||
taken from fp.name. If fp has no `name' attribute, `<???>' is
|
||||
used.
|
||||
"""
|
||||
self.data._readfp(fp)
|
||||
|
||||
def get(self, section, option, vars=None):
|
||||
if not self.has_section(section):
|
||||
raise NoSectionError(section)
|
||||
|
||||
sec = self.data[section]
|
||||
if option in sec:
|
||||
return sec._compat_get(option)
|
||||
else:
|
||||
raise NoOptionError(option, section)
|
||||
|
||||
def items(self, section):
|
||||
if section in self.data:
|
||||
ans = []
|
||||
for opt in self.data[section]:
|
||||
ans.append((opt, self.get(section, opt)))
|
||||
return ans
|
||||
else:
|
||||
raise NoSectionError(section)
|
||||
|
||||
def getint(self, section, option):
|
||||
return int(self.get(section, option))
|
||||
|
||||
def getfloat(self, section, option):
|
||||
return float(self.get(section, option))
|
||||
|
||||
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
'0': False, 'no': False, 'false': False, 'off': False}
|
||||
|
||||
def getboolean(self, section, option):
|
||||
v = self.get(section, option)
|
||||
if v.lower() not in self._boolean_states:
|
||||
raise ValueError('Not a boolean: %s' % v)
|
||||
return self._boolean_states[v.lower()]
|
||||
|
||||
def has_option(self, section, option):
|
||||
"""Check for the existence of a given option in a given section."""
|
||||
if section in self.data:
|
||||
sec = self.data[section]
|
||||
else:
|
||||
raise NoSectionError(section)
|
||||
return (option in sec)
|
||||
|
||||
def set(self, section, option, value):
|
||||
"""Set an option."""
|
||||
if section in self.data:
|
||||
self.data[section][option] = value
|
||||
else:
|
||||
raise NoSectionError(section)
|
||||
|
||||
def write(self, fp):
|
||||
"""Write an .ini-format representation of the configuration state."""
|
||||
fp.write(str(self.data))
|
||||
|
||||
def remove_option(self, section, option):
|
||||
"""Remove an option."""
|
||||
if section in self.data:
|
||||
sec = self.data[section]
|
||||
else:
|
||||
raise NoSectionError(section)
|
||||
if option in sec:
|
||||
del sec[option]
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def remove_section(self, section):
|
||||
"""Remove a file section."""
|
||||
if not self.has_section(section):
|
||||
return False
|
||||
del self.data[section]
|
||||
return True
|
||||
|
||||
|
||||
class ConfigDict(object):
|
||||
"""Present a dict interface to a ini section."""
|
||||
|
||||
def __init__(self, cfg, section, vars):
|
||||
self.cfg = cfg
|
||||
self.section = section
|
||||
self.vars = vars
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return RawConfigParser.get(self.cfg, self.section, key, self.vars)
|
||||
except (NoOptionError, NoSectionError):
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class ConfigParser(RawConfigParser):
|
||||
|
||||
def get(self, section, option, raw=False, vars=None):
|
||||
"""Get an option value for a given section.
|
||||
|
||||
All % interpolations are expanded in the return values, based on the
|
||||
defaults passed into the constructor, unless the optional argument
|
||||
`raw' is true. Additional substitutions may be provided using the
|
||||
`vars' argument, which must be a dictionary whose contents overrides
|
||||
any pre-existing defaults.
|
||||
|
||||
The section DEFAULT is special.
|
||||
"""
|
||||
if section != DEFAULTSECT and not self.has_section(section):
|
||||
raise NoSectionError(section)
|
||||
|
||||
option = self.optionxform(option)
|
||||
value = RawConfigParser.get(self, section, option, vars)
|
||||
|
||||
if raw:
|
||||
return value
|
||||
else:
|
||||
d = ConfigDict(self, section, vars)
|
||||
return self._interpolate(section, option, value, d)
|
||||
|
||||
def _interpolate(self, section, option, rawval, vars):
|
||||
# do the string interpolation
|
||||
value = rawval
|
||||
depth = MAX_INTERPOLATION_DEPTH
|
||||
while depth: # Loop through this until it's done
|
||||
depth -= 1
|
||||
if "%(" in value:
|
||||
try:
|
||||
value = value % vars
|
||||
except KeyError as e:
|
||||
raise InterpolationMissingOptionError(
|
||||
option, section, rawval, e.args[0])
|
||||
else:
|
||||
break
|
||||
if value.find("%(") != -1:
|
||||
raise InterpolationDepthError(option, section, rawval)
|
||||
return value
|
||||
|
||||
def items(self, section, raw=False, vars=None):
|
||||
"""Return a list of tuples with (name, value) for each option
|
||||
in the section.
|
||||
|
||||
All % interpolations are expanded in the return values, based on the
|
||||
defaults passed into the constructor, unless the optional argument
|
||||
`raw' is true. Additional substitutions may be provided using the
|
||||
`vars' argument, which must be a dictionary whose contents overrides
|
||||
any pre-existing defaults.
|
||||
|
||||
The section DEFAULT is special.
|
||||
"""
|
||||
if section != DEFAULTSECT and not self.has_section(section):
|
||||
raise NoSectionError(section)
|
||||
if vars is None:
|
||||
options = list(self.data[section])
|
||||
else:
|
||||
options = []
|
||||
for x in self.data[section]:
|
||||
if x not in vars:
|
||||
options.append(x)
|
||||
options.extend(vars.keys())
|
||||
|
||||
if "__name__" in options:
|
||||
options.remove("__name__")
|
||||
|
||||
d = ConfigDict(self, section, vars)
|
||||
if raw:
|
||||
return [(option, d[option])
|
||||
for option in options]
|
||||
else:
|
||||
return [(option, self._interpolate(section, option, d[option], d))
|
||||
for option in options]
|
||||
|
||||
|
||||
class SafeConfigParser(ConfigParser):
|
||||
_interpvar_re = re.compile(r"%\(([^)]+)\)s")
|
||||
_badpercent_re = re.compile(r"%[^%]|%$")
|
||||
|
||||
def set(self, section, option, value):
|
||||
if not isinstance(value, six.string_types):
|
||||
raise TypeError("option values must be strings")
|
||||
# check for bad percent signs:
|
||||
# first, replace all "good" interpolations
|
||||
tmp_value = self._interpvar_re.sub('', value)
|
||||
# then, check if there's a lone percent sign left
|
||||
m = self._badpercent_re.search(tmp_value)
|
||||
if m:
|
||||
raise ValueError("invalid interpolation syntax in %r at "
|
||||
"position %d" % (value, m.start()))
|
||||
|
||||
ConfigParser.set(self, section, option, value)
|
||||
|
||||
def _interpolate(self, section, option, rawval, vars):
|
||||
# do the string interpolation
|
||||
L = []
|
||||
self._interpolate_some(option, L, rawval, section, vars, 1)
|
||||
return ''.join(L)
|
||||
|
||||
_interpvar_match = re.compile(r"%\(([^)]+)\)s").match
|
||||
|
||||
def _interpolate_some(self, option, accum, rest, section, map, depth):
|
||||
if depth > MAX_INTERPOLATION_DEPTH:
|
||||
raise InterpolationDepthError(option, section, rest)
|
||||
while rest:
|
||||
p = rest.find("%")
|
||||
if p < 0:
|
||||
accum.append(rest)
|
||||
return
|
||||
if p > 0:
|
||||
accum.append(rest[:p])
|
||||
rest = rest[p:]
|
||||
# p is no longer used
|
||||
c = rest[1:2]
|
||||
if c == "%":
|
||||
accum.append("%")
|
||||
rest = rest[2:]
|
||||
elif c == "(":
|
||||
m = self._interpvar_match(rest)
|
||||
if m is None:
|
||||
raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest)
|
||||
var = m.group(1)
|
||||
rest = rest[m.end():]
|
||||
try:
|
||||
v = map[var]
|
||||
except KeyError:
|
||||
raise InterpolationMissingOptionError(
|
||||
option, section, rest, var)
|
||||
if "%" in v:
|
||||
self._interpolate_some(option, accum, v,
|
||||
section, map, depth + 1)
|
||||
else:
|
||||
accum.append(v)
|
||||
else:
|
||||
raise InterpolationSyntaxError(
|
||||
option, section,
|
||||
"'%' must be followed by '%' or '(', found: " + repr(rest))
|
||||
@@ -0,0 +1,292 @@
|
||||
class ConfigNamespace(object):
|
||||
"""Abstract class representing the interface of Config objects.
|
||||
|
||||
A ConfigNamespace is a collection of names mapped to values, where
|
||||
the values may be nested namespaces. Values can be accessed via
|
||||
container notation - obj[key] - or via dotted notation - obj.key.
|
||||
Both these access methods are equivalent.
|
||||
|
||||
To minimize name conflicts between namespace keys and class members,
|
||||
the number of class members should be minimized, and the names of
|
||||
all class members should start with an underscore.
|
||||
|
||||
Subclasses must implement the methods for container-like access,
|
||||
and this class will automatically provide dotted access.
|
||||
|
||||
"""
|
||||
|
||||
# Methods that must be implemented by subclasses
|
||||
|
||||
def _getitem(self, key):
|
||||
return NotImplementedError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
raise NotImplementedError(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise NotImplementedError(key)
|
||||
|
||||
def __iter__(self):
|
||||
return NotImplementedError()
|
||||
|
||||
def _new_namespace(self, name):
|
||||
raise NotImplementedError(name)
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
self._getitem(key)
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Machinery for converting dotted access into container access,
|
||||
# and automatically creating new sections/namespaces.
|
||||
#
|
||||
# To distinguish between accesses of class members and namespace
|
||||
# keys, we first call object.__getattribute__(). If that succeeds,
|
||||
# the name is assumed to be a class member. Otherwise it is
|
||||
# treated as a namespace key.
|
||||
#
|
||||
# Therefore, member variables should be defined in the class,
|
||||
# not just in the __init__() function. See BasicNamespace for
|
||||
# an example.
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._getitem(key)
|
||||
except KeyError:
|
||||
return Undefined(key, self)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self._getitem(name)
|
||||
except KeyError:
|
||||
if name.startswith('__') and name.endswith('__'):
|
||||
raise AttributeError
|
||||
return Undefined(name, self)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
try:
|
||||
object.__getattribute__(self, name)
|
||||
object.__setattr__(self, name, value)
|
||||
except AttributeError:
|
||||
self.__setitem__(name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
try:
|
||||
object.__getattribute__(self, name)
|
||||
object.__delattr__(self, name)
|
||||
except AttributeError:
|
||||
self.__delitem__(name)
|
||||
|
||||
# During unpickling, Python checks if the class has a __setstate__
|
||||
# method. But, the data dicts have not been initialised yet, which
|
||||
# leads to _getitem and hence __getattr__ raising an exception. So
|
||||
# we explicitly impement default __setstate__ behavior.
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
|
||||
|
||||
class Undefined(object):
|
||||
"""Helper class used to hold undefined names until assignment.
|
||||
|
||||
This class helps create any undefined subsections when an
|
||||
assignment is made to a nested value. For example, if the
|
||||
statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet.
|
||||
"""
|
||||
|
||||
def __init__(self, name, namespace):
|
||||
object.__setattr__(self, 'name', name)
|
||||
object.__setattr__(self, 'namespace', namespace)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
obj = self.namespace._new_namespace(self.name)
|
||||
obj[name] = value
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
obj = self.namespace._new_namespace(self.name)
|
||||
obj[name] = value
|
||||
|
||||
|
||||
# ---- Basic implementation of a ConfigNamespace
|
||||
|
||||
class BasicConfig(ConfigNamespace):
|
||||
"""Represents a hierarchical collection of named values.
|
||||
|
||||
Values are added using dotted notation:
|
||||
|
||||
>>> n = BasicConfig()
|
||||
>>> n.x = 7
|
||||
>>> n.name.first = 'paramjit'
|
||||
>>> n.name.last = 'oberoi'
|
||||
|
||||
...and accessed the same way, or with [...]:
|
||||
|
||||
>>> n.x
|
||||
7
|
||||
>>> n.name.first
|
||||
'paramjit'
|
||||
>>> n.name.last
|
||||
'oberoi'
|
||||
>>> n['x']
|
||||
7
|
||||
>>> n['name']['first']
|
||||
'paramjit'
|
||||
|
||||
Iterating over the namespace object returns the keys:
|
||||
|
||||
>>> l = list(n)
|
||||
>>> l.sort()
|
||||
>>> l
|
||||
['name', 'x']
|
||||
|
||||
Values can be deleted using 'del' and printed using 'print'.
|
||||
|
||||
>>> n.aaa = 42
|
||||
>>> del n.x
|
||||
>>> print(n)
|
||||
aaa = 42
|
||||
name.first = paramjit
|
||||
name.last = oberoi
|
||||
|
||||
Nested namespaces are also namespaces:
|
||||
|
||||
>>> isinstance(n.name, ConfigNamespace)
|
||||
True
|
||||
>>> print(n.name)
|
||||
first = paramjit
|
||||
last = oberoi
|
||||
>>> sorted(list(n.name))
|
||||
['first', 'last']
|
||||
|
||||
Finally, values can be read from a file as follows:
|
||||
|
||||
>>> from six import StringIO
|
||||
>>> sio = StringIO('''
|
||||
... # comment
|
||||
... ui.height = 100
|
||||
... ui.width = 150
|
||||
... complexity = medium
|
||||
... have_python
|
||||
... data.secret.password = goodness=gracious me
|
||||
... ''')
|
||||
>>> n = BasicConfig()
|
||||
>>> n._readfp(sio)
|
||||
>>> print(n)
|
||||
complexity = medium
|
||||
data.secret.password = goodness=gracious me
|
||||
have_python
|
||||
ui.height = 100
|
||||
ui.width = 150
|
||||
"""
|
||||
|
||||
# this makes sure that __setattr__ knows this is not a namespace key
|
||||
_data = None
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
|
||||
def _getitem(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._data[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._data)
|
||||
|
||||
def __str__(self, prefix=''):
|
||||
lines = []
|
||||
keys = list(self._data.keys())
|
||||
keys.sort()
|
||||
for name in keys:
|
||||
value = self._data[name]
|
||||
if isinstance(value, ConfigNamespace):
|
||||
lines.append(value.__str__(prefix='%s%s.' % (prefix,name)))
|
||||
else:
|
||||
if value is None:
|
||||
lines.append('%s%s' % (prefix, name))
|
||||
else:
|
||||
lines.append('%s%s = %s' % (prefix, name, value))
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _new_namespace(self, name):
|
||||
obj = BasicConfig()
|
||||
self._data[name] = obj
|
||||
return obj
|
||||
|
||||
def _readfp(self, fp):
|
||||
while True:
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
if line[0] == '#': continue
|
||||
data = line.split('=', 1)
|
||||
if len(data) == 1:
|
||||
name = line
|
||||
value = None
|
||||
else:
|
||||
name = data[0].strip()
|
||||
value = data[1].strip()
|
||||
name_components = name.split('.')
|
||||
ns = self
|
||||
for n in name_components[:-1]:
|
||||
if n in ns:
|
||||
ns = ns[n]
|
||||
if not isinstance(ns, ConfigNamespace):
|
||||
raise TypeError('value-namespace conflict', n)
|
||||
else:
|
||||
ns = ns._new_namespace(n)
|
||||
ns[name_components[-1]] = value
|
||||
|
||||
|
||||
# ---- Utility functions
|
||||
|
||||
def update_config(target, source):
|
||||
"""Imports values from source into target.
|
||||
|
||||
Recursively walks the <source> ConfigNamespace and inserts values
|
||||
into the <target> ConfigNamespace. For example:
|
||||
|
||||
>>> n = BasicConfig()
|
||||
>>> n.playlist.expand_playlist = True
|
||||
>>> n.ui.display_clock = True
|
||||
>>> n.ui.display_qlength = True
|
||||
>>> n.ui.width = 150
|
||||
>>> print(n)
|
||||
playlist.expand_playlist = True
|
||||
ui.display_clock = True
|
||||
ui.display_qlength = True
|
||||
ui.width = 150
|
||||
|
||||
>>> from iniparse import ini
|
||||
>>> i = ini.INIConfig()
|
||||
>>> update_config(i, n)
|
||||
>>> print(i)
|
||||
[playlist]
|
||||
expand_playlist = True
|
||||
<BLANKLINE>
|
||||
[ui]
|
||||
display_clock = True
|
||||
display_qlength = True
|
||||
width = 150
|
||||
|
||||
"""
|
||||
for name in sorted(source):
|
||||
value = source[name]
|
||||
if isinstance(value, ConfigNamespace):
|
||||
if name in target:
|
||||
myns = target[name]
|
||||
if not isinstance(myns, ConfigNamespace):
|
||||
raise TypeError('value-namespace conflict')
|
||||
else:
|
||||
myns = target._new_namespace(name)
|
||||
update_config(myns, value)
|
||||
else:
|
||||
target[name] = value
|
||||
@@ -0,0 +1,7 @@
|
||||
try:
|
||||
from ConfigParser import *
|
||||
# not all objects get imported with __all__
|
||||
from ConfigParser import Error, InterpolationMissingOptionError
|
||||
except ImportError:
|
||||
from configparser import *
|
||||
from configparser import Error, InterpolationMissingOptionError
|
||||
@@ -0,0 +1,652 @@
|
||||
"""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>[;#])'
|
||||
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
|
||||
@@ -0,0 +1,48 @@
|
||||
from . import compat
|
||||
from .ini import LineContainer, EmptyLine
|
||||
|
||||
|
||||
def tidy(cfg):
|
||||
"""Clean up blank lines.
|
||||
|
||||
This functions makes the configuration look clean and
|
||||
handwritten - consecutive empty lines and empty lines at
|
||||
the start of the file are removed, and one is guaranteed
|
||||
to be at the end of the file.
|
||||
"""
|
||||
|
||||
if isinstance(cfg, compat.RawConfigParser):
|
||||
cfg = cfg.data
|
||||
cont = cfg._data.contents
|
||||
i = 1
|
||||
while i < len(cont):
|
||||
if isinstance(cont[i], LineContainer):
|
||||
tidy_section(cont[i])
|
||||
i += 1
|
||||
elif (isinstance(cont[i-1], EmptyLine) and
|
||||
isinstance(cont[i], EmptyLine)):
|
||||
del cont[i]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Remove empty first line
|
||||
if cont and isinstance(cont[0], EmptyLine):
|
||||
del cont[0]
|
||||
|
||||
# Ensure a last line
|
||||
if cont and not isinstance(cont[-1], EmptyLine):
|
||||
cont.append(EmptyLine())
|
||||
|
||||
|
||||
def tidy_section(lc):
|
||||
cont = lc.contents
|
||||
i = 1
|
||||
while i < len(cont):
|
||||
if isinstance(cont[i-1], EmptyLine) and isinstance(cont[i], EmptyLine):
|
||||
del cont[i]
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Remove empty first line
|
||||
if len(cont) > 1 and isinstance(cont[1], EmptyLine):
|
||||
del cont[1]
|
||||
Reference in New Issue
Block a user