| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- """
- 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
- from collections import OrderedDict
- NS_LOGGER = logging.getLogger('inirama')
- class Scanner:
- """ 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 scanning. """
- 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 errors
- 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 already 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
|