dot_files/vim-plugins/python-mode/pymode/libs/pylama/libs/inirama.py
2017-07-31 00:00:01 +03:00

405 lines
10 KiB
Python

"""
Inirama is a python module that parses INI files.
.. _badges:
.. include:: ../README.rst
:start-after: .. _badges:
:end-before: .. _contents:
.. _description:
.. include:: ../README.rst
:start-after: .. _description:
:end-before: .. _badges:
:copyright: 2013 by Kirill Klenov.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals, print_function
__version__ = "0.8.0"
__project__ = "Inirama"
__author__ = "Kirill Klenov <horneds@gmail.com>"
__license__ = "BSD"
import io
import re
import logging
try:
from collections import OrderedDict
except ImportError:
from UserDict import DictMixin
class OrderedDict(dict, DictMixin):
null = object()
def __init__(self, *args, **kwargs):
self.clear()
self.update(*args, **kwargs)
def clear(self):
self.__map = dict()
self.__order = list()
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
self.__map[key] = len(self.__order)
self.__order.append(key)
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
self.__map.pop(key)
self.__order = self.null
def __iter__(self):
for key in self.__order:
if key is not self.null:
yield key
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
NS_LOGGER = logging.getLogger('inirama')
class Scanner(object):
""" Split a code string on tokens. """
def __init__(self, source, ignore=None, patterns=None):
""" Init Scanner instance.
:param patterns: List of token patterns [(token, regexp)]
:param ignore: List of ignored tokens
"""
self.reset(source)
if patterns:
self.patterns = []
for k, r in patterns:
self.patterns.append((k, re.compile(r)))
if ignore:
self.ignore = ignore
def reset(self, source):
""" Reset scanner's state.
:param source: Source for parsing
"""
self.tokens = []
self.source = source
self.pos = 0
def scan(self):
""" Scan source and grab tokens. """
self.pre_scan()
token = None
end = len(self.source)
while self.pos < end:
best_pat = None
best_pat_len = 0
# Check patterns
for p, regexp in self.patterns:
m = regexp.match(self.source, self.pos)
if m:
best_pat = p
best_pat_len = len(m.group(0))
break
if best_pat is None:
raise SyntaxError(
"SyntaxError[@char {0}: {1}]".format(
self.pos, "Bad token."))
# Ignore patterns
if best_pat in self.ignore:
self.pos += best_pat_len
continue
# Create token
token = (
best_pat,
self.source[self.pos:self.pos + best_pat_len],
self.pos,
self.pos + best_pat_len,
)
self.pos = token[-1]
self.tokens.append(token)
def pre_scan(self):
""" Prepare source. """
pass
def __repr__(self):
""" Print the last 5 tokens that have been scanned in.
:return str:
"""
return '<Scanner: ' + ','.join(
"{0}({2}:{3})".format(*t) for t in self.tokens[-5:]) + ">"
class INIScanner(Scanner):
""" Get tokens for INI. """
patterns = [
('SECTION', re.compile(r'\[[^]]+\]')),
('IGNORE', re.compile(r'[ \r\t\n]+')),
('COMMENT', re.compile(r'[;#].*')),
('KEY_VALUE', re.compile(r'[^=\s]+\s*[:=].*')),
('CONTINUATION', re.compile(r'.*'))
]
ignore = ['IGNORE']
def pre_scan(self):
""" Prepare string for scaning. """
escape_re = re.compile(r'\\\n[\t ]+')
self.source = escape_re.sub('', self.source)
undefined = object()
class Section(OrderedDict):
""" Representation of INI section. """
def __init__(self, namespace, *args, **kwargs):
super(Section, self).__init__(*args, **kwargs)
self.namespace = namespace
def __setitem__(self, name, value):
value = str(value)
if value.isdigit():
value = int(value)
super(Section, self).__setitem__(name, value)
class InterpolationSection(Section):
""" INI section with interpolation support. """
var_re = re.compile('{([^}]+)}')
def get(self, name, default=None):
""" Get item by name.
:return object: value or None if name not exists
"""
if name in self:
return self[name]
return default
def __interpolate__(self, math):
try:
key = math.group(1).strip()
return self.namespace.default.get(key) or self[key]
except KeyError:
return ''
def __getitem__(self, name, raw=False):
value = super(InterpolationSection, self).__getitem__(name)
if not raw:
sample = undefined
while sample != value:
try:
sample, value = value, self.var_re.sub(
self.__interpolate__, value)
except RuntimeError:
message = "Interpolation failed: {0}".format(name)
NS_LOGGER.error(message)
raise ValueError(message)
return value
def iteritems(self, raw=False):
""" Iterate self items. """
for key in self:
yield key, self.__getitem__(key, raw=raw)
items = iteritems
class Namespace(object):
""" Default class for parsing INI.
:param **default_items: Default items for default section.
Usage
-----
::
from inirama import Namespace
ns = Namespace()
ns.read('config.ini')
print ns['section']['key']
ns['other']['new'] = 'value'
ns.write('new_config.ini')
"""
#: Name of default section (:attr:`~inirama.Namespace.default`)
default_section = 'DEFAULT'
#: Dont raise any exception on file reading erorrs
silent_read = True
#: Class for generating sections
section_type = Section
def __init__(self, **default_items):
self.sections = OrderedDict()
for k, v in default_items.items():
self[self.default_section][k] = v
@property
def default(self):
""" Return default section or empty dict.
:return :class:`inirama.Section`: section
"""
return self.sections.get(self.default_section, dict())
def read(self, *files, **params):
""" Read and parse INI files.
:param *files: Files for reading
:param **params: Params for parsing
Set `update=False` for prevent values redefinition.
"""
for f in files:
try:
with io.open(f, encoding='utf-8') as ff:
NS_LOGGER.info('Read from `{0}`'.format(ff.name))
self.parse(ff.read(), **params)
except (IOError, TypeError, SyntaxError, io.UnsupportedOperation):
if not self.silent_read:
NS_LOGGER.error('Reading error `{0}`'.format(ff.name))
raise
def write(self, f):
""" Write namespace as INI file.
:param f: File object or path to file.
"""
if isinstance(f, str):
f = io.open(f, 'w', encoding='utf-8')
if not hasattr(f, 'read'):
raise AttributeError("Wrong type of file: {0}".format(type(f)))
NS_LOGGER.info('Write to `{0}`'.format(f.name))
for section in self.sections.keys():
f.write('[{0}]\n'.format(section))
for k, v in self[section].items():
f.write('{0:15}= {1}\n'.format(k, v))
f.write('\n')
f.close()
def parse(self, source, update=True, **params):
""" Parse INI source as string.
:param source: Source of INI
:param update: Replace alredy defined items
"""
scanner = INIScanner(source)
scanner.scan()
section = self.default_section
name = None
for token in scanner.tokens:
if token[0] == 'KEY_VALUE':
name, value = re.split('[=:]', token[1], 1)
name, value = name.strip(), value.strip()
if not update and name in self[section]:
continue
self[section][name] = value
elif token[0] == 'SECTION':
section = token[1].strip('[]')
elif token[0] == 'CONTINUATION':
if not name:
raise SyntaxError(
"SyntaxError[@char {0}: {1}]".format(
token[2], "Bad continuation."))
self[section][name] += '\n' + token[1].strip()
def __getitem__(self, name):
""" Look name in self sections.
:return :class:`inirama.Section`: section
"""
if name not in self.sections:
self.sections[name] = self.section_type(self)
return self.sections[name]
def __contains__(self, name):
return name in self.sections
def __repr__(self):
return "<Namespace: {0}>".format(self.sections)
class InterpolationNamespace(Namespace):
""" That implements the interpolation feature.
::
from inirama import InterpolationNamespace
ns = InterpolationNamespace()
ns.parse('''
[main]
test = value
foo = bar {test}
more_deep = wow {foo}
''')
print ns['main']['more_deep'] # wow bar value
"""
section_type = InterpolationSection
# pylama:ignore=D,W02,E731,W0621