Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit a684050f71665d44239e11ef5de8e1d187267e07 @klen committed Dec 28, 2012
Showing with 495 additions and 0 deletions.
  1. +8 −0 .gitignore
  2. +4 −0 Changelog
  3. +1 −0 DESCRIPTION
  4. +32 −0 LICENSE
  5. +2 −0 MANIFEST.in
  6. +53 −0 Makefile
  7. +85 −0 README.rst
  8. +192 −0 inirama.py
  9. +1 −0 requirements.txt
  10. +45 −0 setup.py
  11. +48 −0 tests/__init__.py
  12. +2 −0 tests/append.ini
  13. +3 −0 tests/invalid.ini
  14. +16 −0 tests/test.ini
  15. +3 −0 tests/vars.ini
@@ -0,0 +1,8 @@
+_build
+dist
+*.egg*
+.env
+*.pyc
+.ropeproject
+.tags
+.vimrc
@@ -0,0 +1,4 @@
+2012-12-21 klen
+
+ * Version 0.1.0
+ * Initial release
@@ -0,0 +1 @@
+Inirama -- Simple parser for INI files
@@ -0,0 +1,32 @@
+Copyright (c) 2012 by Kirill Klenov.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms of the software as well
+as documentation, with or without modification, are permitted provided
+that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+* The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
@@ -0,0 +1,2 @@
+recursive-include inirama *
+exclude *.pyc *.orig
@@ -0,0 +1,53 @@
+MODULE=inirama
+SPHINXBUILD=sphinx-build
+ALLSPHINXOPTS= -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+BUILDDIR=_build
+
+all: .env
+
+.PHONY: help
+# target: help - Display callable targets
+help:
+ @egrep "^# target:" [Mm]akefile
+
+.PHONY: clean
+# target: clean - Display callable targets
+clean:
+ @rm -rf build dist docs/_build
+ @rm -f *.py[co]
+ @rm -f *.orig
+ @rm -f */*.py[co]
+ @rm -f */*.orig
+
+.PHONY: register
+# target: register - Register module on PyPi
+register:
+ @python setup.py register
+
+.PHONY: upload
+# target: upload - Upload module on PyPi
+upload:
+ @python setup.py sdist upload || echo 'Upload already'
+
+.PHONY: test
+# target: test - Runs tests
+test: clean
+ @python setup.py test
+
+.PHONY: audit
+# target: audit - Audit code
+audit:
+ @pylama $(MODULE) -i E501
+
+.PHONY: docs
+docs:
+ python setup.py build_sphinx --source-dir=docs/ --build-dir=docs/_build --all-files
+ python setup.py upload_sphinx --upload-dir=docs/_build/html
+
+.PHONY: pep8
+pep8:
+ find $(MODULE) -name "*.py" | xargs -n 1 autopep8 -i
+
+.env: requirements.txt
+ virtualenv --no-site-packages .env
+ .env/bin/pip install -M -r requirements.txt
@@ -0,0 +1,85 @@
+Inirama
+#######
+
+Inirama -- Simple parser for INI files
+
+.. image:: https://secure.travis-ci.org/klen/inirama.png?branch=develop
+ :target: http://travis-ci.org/klen/inirama
+ :alt: Build Status
+
+.. contents::
+
+Requirements
+=============
+
+- python >= 2.6
+
+
+Installation
+=============
+
+**Inirama** should be installed using pip: ::
+
+ pip install inirama
+
+
+Usage
+=====
+
+::
+
+ from inirama import Namespace
+
+ parser = Namespace()
+ parser.read('config.ini')
+
+ print Parser['section']['key']
+
+ parser['other']['new'] = 'value'
+ parser.write('new_config.ini')
+
+
+Interpolation
+-------------
+::
+
+ from inirama import Namespace
+
+ parser = Namespace()
+ parser.parse("""
+ [main]
+ test = value
+ foo = bar {test}
+ more_deep = wow {foo}
+ """)
+ print parser['main']['more_deep'] # wow bar value
+
+
+Bug tracker
+===========
+
+If you have any suggestions, bug reports or
+annoyances please report them to the issue tracker
+at https://github.com/klen/inirama/issues
+
+
+Contributing
+============
+
+Development of inirama happens at github: https://github.com/klen/inirama
+
+
+Contributors
+=============
+
+* klen_ (Kirill Klenov)
+
+
+License
+=======
+
+Licensed under a `BSD license`_.
+
+
+.. _BSD license: http://www.linfo.org/bsdlicense.html
+.. _klen: http://klen.github.com/
@@ -0,0 +1,192 @@
+"""
+ Parse INI files.
+
+"""
+
+__version__ = '0.1.0'
+__project__ = 'Inirama'
+__author__ = "Kirill Klenov <horneds@gmail.com>"
+__license__ = "BSD"
+
+
+import re
+from collections import OrderedDict
+
+
+class Scanner(object):
+
+ 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.
+
+ :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 'Scanner: ' + ','.join("{0}({2}:{3})".format(*t) for t in self.tokens[-5:])
+
+
+class INIScanner(Scanner):
+ patterns = [
+ ('SECTION', re.compile(r'\[\w+\]')),
+ ('IGNORE', re.compile(r'[ \r\t\n]+')),
+ ('COMMENT', re.compile(r'[;#].*')),
+ ('KEY', re.compile(r'[\w_]+\s*[:=].*'))]
+
+ ignore = ['IGNORE']
+
+ def pre_scan(self):
+ self.source = re.sub(r'\\\n\s*', '', self.source)
+
+
+undefined = object()
+
+
+class Section(dict):
+
+ depth = 10
+
+ @property
+ def context(self):
+ return dict(iter(self))
+
+ def __setitem__(self, name, value):
+ super(Section, self).__setitem__(name, str(value))
+
+ def __getitem__(self, name):
+ value = super(Section, self).__getitem__(name)
+ sample = undefined
+ cnt = 0
+ while sample != value:
+ if cnt > self.depth:
+ raise ValueError("Reached maximum depth of interpretation by key: {0}".format(name))
+ sample, value, cnt = value, value.format(**self), cnt + 1
+ return value
+
+ def __iter__(self):
+ for key in super(Section, self).keys():
+ yield key, self[key]
+
+
+class Namespace:
+
+ default_section = 'default'
+ silent_read = True
+ section_type = Section
+
+ def __init__(self, **default_items):
+ self.sections = OrderedDict()
+ if default_items:
+ self[self.default_section] = self.section_type(**default_items)
+
+ def read(self, *files):
+ """ Read and parse INI files.
+ """
+ for f in files:
+ try:
+ with open(f, 'r') as ff:
+ self.parse(ff.read())
+ except (IOError, TypeError, SyntaxError):
+ if not self.silent_read:
+ raise
+
+ def write(self, f):
+ if isinstance(f, str):
+ f = open(f, 'w')
+
+ if not isinstance(f, file):
+ raise AttributeError("Wrong type of file: {0}".format(type(f)))
+
+ for section in self.sections.keys():
+ f.write('[{0}]\n'.format(section))
+ for k, v in iter(self[section]):
+ f.write('{0:15}= {1}\n'.format(k, v))
+ f.write('\n')
+ f.close()
+
+ def parse(self, source):
+ """ Parse INI source.
+ """
+ scanner = INIScanner(source)
+ scanner.scan()
+
+ section = self.default_section
+
+ for token in scanner.tokens:
+ if token[0] == 'KEY':
+ name, value = re.split('[=:]', token[1], 1)
+ self[section][name.strip()] = value.strip()
+
+ if token[0] == 'SECTION':
+ section = token[1].strip('[]')
+
+ def __getitem__(self, name):
+ """ Look name in self sections.
+ """
+ if not name in self.sections:
+ self.sections[name] = self.section_type()
+ return self.sections[name]
@@ -0,0 +1 @@
+pyPEG2==2.9.0
Oops, something went wrong. Retry.

0 comments on commit a684050

Please sign in to comment.