inirama.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. """
  2. Inirama is a python module that parses INI files.
  3. .. _badges:
  4. .. include:: ../README.rst
  5. :start-after: .. _badges:
  6. :end-before: .. _contents:
  7. .. _description:
  8. .. include:: ../README.rst
  9. :start-after: .. _description:
  10. :end-before: .. _badges:
  11. :copyright: 2013 by Kirill Klenov.
  12. :license: BSD, see LICENSE for more details.
  13. """
  14. from __future__ import unicode_literals, print_function
  15. __version__ = "0.8.0"
  16. __project__ = "Inirama"
  17. __author__ = "Kirill Klenov <horneds@gmail.com>"
  18. __license__ = "BSD"
  19. import io
  20. import re
  21. import logging
  22. from collections import OrderedDict
  23. NS_LOGGER = logging.getLogger('inirama')
  24. class Scanner:
  25. """ Split a code string on tokens. """
  26. def __init__(self, source, ignore=None, patterns=None):
  27. """ Init Scanner instance.
  28. :param patterns: List of token patterns [(token, regexp)]
  29. :param ignore: List of ignored tokens
  30. """
  31. self.reset(source)
  32. if patterns:
  33. self.patterns = []
  34. for k, r in patterns:
  35. self.patterns.append((k, re.compile(r)))
  36. if ignore:
  37. self.ignore = ignore
  38. def reset(self, source):
  39. """ Reset scanner's state.
  40. :param source: Source for parsing
  41. """
  42. self.tokens = []
  43. self.source = source
  44. self.pos = 0
  45. def scan(self):
  46. """ Scan source and grab tokens. """
  47. self.pre_scan()
  48. token = None
  49. end = len(self.source)
  50. while self.pos < end:
  51. best_pat = None
  52. best_pat_len = 0
  53. # Check patterns
  54. for p, regexp in self.patterns:
  55. m = regexp.match(self.source, self.pos)
  56. if m:
  57. best_pat = p
  58. best_pat_len = len(m.group(0))
  59. break
  60. if best_pat is None:
  61. raise SyntaxError(
  62. "SyntaxError[@char {0}: {1}]".format(
  63. self.pos, "Bad token."))
  64. # Ignore patterns
  65. if best_pat in self.ignore:
  66. self.pos += best_pat_len
  67. continue
  68. # Create token
  69. token = (
  70. best_pat,
  71. self.source[self.pos:self.pos + best_pat_len],
  72. self.pos,
  73. self.pos + best_pat_len,
  74. )
  75. self.pos = token[-1]
  76. self.tokens.append(token)
  77. def pre_scan(self):
  78. """ Prepare source. """
  79. pass
  80. def __repr__(self):
  81. """ Print the last 5 tokens that have been scanned in.
  82. :return str:
  83. """
  84. return '<Scanner: ' + ','.join(
  85. "{0}({2}:{3})".format(*t) for t in self.tokens[-5:]) + ">"
  86. class INIScanner(Scanner):
  87. """ Get tokens for INI. """
  88. patterns = [
  89. ('SECTION', re.compile(r'\[[^]]+\]')),
  90. ('IGNORE', re.compile(r'[ \r\t\n]+')),
  91. ('COMMENT', re.compile(r'[;#].*')),
  92. ('KEY_VALUE', re.compile(r'[^=\s]+\s*[:=].*')),
  93. ('CONTINUATION', re.compile(r'.*'))
  94. ]
  95. ignore = ['IGNORE']
  96. def pre_scan(self):
  97. """ Prepare string for scanning. """
  98. escape_re = re.compile(r'\\\n[\t ]+')
  99. self.source = escape_re.sub('', self.source)
  100. undefined = object()
  101. class Section(OrderedDict):
  102. """ Representation of INI section. """
  103. def __init__(self, namespace, *args, **kwargs):
  104. super(Section, self).__init__(*args, **kwargs)
  105. self.namespace = namespace
  106. def __setitem__(self, name, value):
  107. value = str(value)
  108. if value.isdigit():
  109. value = int(value)
  110. super(Section, self).__setitem__(name, value)
  111. class InterpolationSection(Section):
  112. """ INI section with interpolation support. """
  113. var_re = re.compile('{([^}]+)}')
  114. def get(self, name, default=None):
  115. """ Get item by name.
  116. :return object: value or None if name not exists
  117. """
  118. if name in self:
  119. return self[name]
  120. return default
  121. def __interpolate__(self, math):
  122. try:
  123. key = math.group(1).strip()
  124. return self.namespace.default.get(key) or self[key]
  125. except KeyError:
  126. return ''
  127. def __getitem__(self, name, raw=False):
  128. value = super(InterpolationSection, self).__getitem__(name)
  129. if not raw:
  130. sample = undefined
  131. while sample != value:
  132. try:
  133. sample, value = value, self.var_re.sub(
  134. self.__interpolate__, value)
  135. except RuntimeError:
  136. message = "Interpolation failed: {0}".format(name)
  137. NS_LOGGER.error(message)
  138. raise ValueError(message)
  139. return value
  140. def iteritems(self, raw=False):
  141. """ Iterate self items. """
  142. for key in self:
  143. yield key, self.__getitem__(key, raw=raw)
  144. items = iteritems
  145. class Namespace(object):
  146. """ Default class for parsing INI.
  147. :param **default_items: Default items for default section.
  148. Usage
  149. -----
  150. ::
  151. from inirama import Namespace
  152. ns = Namespace()
  153. ns.read('config.ini')
  154. print ns['section']['key']
  155. ns['other']['new'] = 'value'
  156. ns.write('new_config.ini')
  157. """
  158. #: Name of default section (:attr:`~inirama.Namespace.default`)
  159. default_section = 'DEFAULT'
  160. #: Dont raise any exception on file reading errors
  161. silent_read = True
  162. #: Class for generating sections
  163. section_type = Section
  164. def __init__(self, **default_items):
  165. self.sections = OrderedDict()
  166. for k, v in default_items.items():
  167. self[self.default_section][k] = v
  168. @property
  169. def default(self):
  170. """ Return default section or empty dict.
  171. :return :class:`inirama.Section`: section
  172. """
  173. return self.sections.get(self.default_section, dict())
  174. def read(self, *files, **params):
  175. """ Read and parse INI files.
  176. :param *files: Files for reading
  177. :param **params: Params for parsing
  178. Set `update=False` for prevent values redefinition.
  179. """
  180. for f in files:
  181. try:
  182. with io.open(f, encoding='utf-8') as ff:
  183. NS_LOGGER.info('Read from `{0}`'.format(ff.name))
  184. self.parse(ff.read(), **params)
  185. except (IOError, TypeError, SyntaxError, io.UnsupportedOperation):
  186. if not self.silent_read:
  187. NS_LOGGER.error('Reading error `{0}`'.format(ff.name))
  188. raise
  189. def write(self, f):
  190. """ Write namespace as INI file.
  191. :param f: File object or path to file.
  192. """
  193. if isinstance(f, str):
  194. f = io.open(f, 'w', encoding='utf-8')
  195. if not hasattr(f, 'read'):
  196. raise AttributeError("Wrong type of file: {0}".format(type(f)))
  197. NS_LOGGER.info('Write to `{0}`'.format(f.name))
  198. for section in self.sections.keys():
  199. f.write('[{0}]\n'.format(section))
  200. for k, v in self[section].items():
  201. f.write('{0:15}= {1}\n'.format(k, v))
  202. f.write('\n')
  203. f.close()
  204. def parse(self, source, update=True, **params):
  205. """ Parse INI source as string.
  206. :param source: Source of INI
  207. :param update: Replace already defined items
  208. """
  209. scanner = INIScanner(source)
  210. scanner.scan()
  211. section = self.default_section
  212. name = None
  213. for token in scanner.tokens:
  214. if token[0] == 'KEY_VALUE':
  215. name, value = re.split('[=:]', token[1], 1)
  216. name, value = name.strip(), value.strip()
  217. if not update and name in self[section]:
  218. continue
  219. self[section][name] = value
  220. elif token[0] == 'SECTION':
  221. section = token[1].strip('[]')
  222. elif token[0] == 'CONTINUATION':
  223. if not name:
  224. raise SyntaxError(
  225. "SyntaxError[@char {0}: {1}]".format(
  226. token[2], "Bad continuation."))
  227. self[section][name] += '\n' + token[1].strip()
  228. def __getitem__(self, name):
  229. """ Look name in self sections.
  230. :return :class:`inirama.Section`: section
  231. """
  232. if name not in self.sections:
  233. self.sections[name] = self.section_type(self)
  234. return self.sections[name]
  235. def __contains__(self, name):
  236. return name in self.sections
  237. def __repr__(self):
  238. return "<Namespace: {0}>".format(self.sections)
  239. class InterpolationNamespace(Namespace):
  240. """ That implements the interpolation feature.
  241. ::
  242. from inirama import InterpolationNamespace
  243. ns = InterpolationNamespace()
  244. ns.parse('''
  245. [main]
  246. test = value
  247. foo = bar {test}
  248. more_deep = wow {foo}
  249. ''')
  250. print ns['main']['more_deep'] # wow bar value
  251. """
  252. section_type = InterpolationSection
  253. # pylama:ignore=D,W02,E731,W0621