[Python-modules-commits] [ldif3] 01/02: Imported Upstream version 3.0.2

Michael Fladischer fladi at moszumanska.debian.org
Tue Jun 30 19:31:06 UTC 2015


This is an automated email from the git hooks/post-receive script.

fladi pushed a commit to branch master
in repository ldif3.

commit a4ccc1d0aaf66bd5d97f13b09a14672b051113bd
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date:   Tue Jun 30 21:02:24 2015 +0200

    Imported Upstream version 3.0.2
---
 .gitignore     |   6 ++
 .travis.yml    |  10 ++
 CHANGES.rst    |  31 ++++++
 LICENSE        |  23 ++++
 MANIFEST.in    |   2 +
 README.rst     |  46 ++++++++
 docs/Makefile  | 177 ++++++++++++++++++++++++++++++
 docs/conf.py   |  22 ++++
 docs/index.rst |  18 ++++
 ldif3.py       | 334 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 setup.cfg      |  12 +++
 setup.py       |  36 +++++++
 tests.py       | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 tox.ini        |  18 ++++
 14 files changed, 1053 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5119dfd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.pyc
+.env
+.tox
+.cover
+.coverage
+docs/_build
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..163d3eb
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: python
+python:
+  - "2.7"
+install:
+  - "pip install tox"
+script: tox
+os:
+  - linux
+notifications:
+  email: false
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..bd2f171
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,31 @@
+3.0.2 (2015-06-22)
+------------------
+
+-   Include documentation source and changelog in source distribution.
+    (Thanks to Michael Fladischer)
+-   Add LICENSE file
+
+3.0.1 (2015-05-22)
+------------------
+
+-   Use OrderedDict for entries.
+
+
+3.0.0 (2015-05-22)
+------------------
+
+This is the first version of a fork of the ``ldif`` module from `python-ldap
+<http://www.python-ldap.org/>`_.  For any changes before that, see the
+documentation over there.  The last version before the fork was 2.4.15.
+
+The changes introduced with this version are:
+
+-   Dropped support for python < 2.7.
+-   Added support for python 3, including unicode support.
+-   All deprecated functions (``CreateLDIF``, ``ParseLDIF``) were removed.
+-   ``LDIFCopy`` and ``LDIFRecordList`` were removed.
+-   ``LDIFParser.handle()`` was removed.  Instead, ``LDIFParser.parse()``
+    yields the records.
+-   ``LDIFParser`` has now a ``strict`` option that defaults to ``True``
+    for backwards-compatibility.  If set to ``False``, recoverable parse errors
+    will produce log warnings rather than exceptions.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..93aee0b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2015, Tobias Bengfort <tobias.bengfort at posteo.de>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1.  Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+2.  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.
+
+THIS SOFTWARE 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 HOLDER 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, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..e8b4458
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include CHANGES.rst
+recursive-include docs Makefile *.rst *.py
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..9e12265
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,46 @@
+ldif3 - generate and parse LDIF data (see `RFC 2849`_).
+
+This is a fork of the ``ldif`` module from `python-ldap`_ with python3/unicode
+support. See the first entry in CHANGES.rst for a more complete list of
+differences.
+
+Usage
+-----
+
+Parse LDIF from a file (or ``BytesIO``)::
+
+    from ldif3 import LDIFParser
+    from pprint import pprint
+
+    parser = LDIFParser(open('data.ldif', 'rb'))
+    for dn, changetype, record in parser.parse():
+        if dn is not None:
+            print('got entry record: %s' % dn)
+        else:
+            print('got change record: %s' % changetype)
+        pprint(record)
+
+
+Write LDIF to a file (or ``BytesIO``)::
+
+    from ldif3 import LDIFWriter
+
+    writer = LDIFWriter(open('data.ldif', 'wb'))
+    writer.unparse('mail=alice at example.com', {
+        'cn': ['Alice Alison'],
+        'mail': ['alice at example.com'],
+        'objectclass': ['top', 'person'],
+    })
+
+Unicode support
+---------------
+
+The stream object that is passed to parser or writer must be a byte
+stream. It must use UTF-8 encoding as described in the spec.
+
+The parsed objects (``dn``, ``changetype`` and the keys and values of
+``record``) on the other hand are unicode strings.
+
+
+.. _RFC 2849: https://tools.ietf.org/html/rfc2849
+.. _python-ldap: http://www.python-ldap.org/
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..8043f00
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  xml        to make Docutils-native XML files"
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/2.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/2.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/2"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/2"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through platex and dvipdfmx..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+	@echo
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+	@echo
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..be2b087
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+import sys
+import os
+import subprocess
+
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, os.path.dirname(current_dir))
+
+
+def get_meta(key):
+    cmd = ['python', '../setup.py', '--' + key]
+    return subprocess.check_output(cmd).rstrip()
+
+
+extensions = [
+    'sphinx.ext.autodoc',
+]
+master_doc = 'index'
+
+project = get_meta('name')
+copyright = u'2015, ' + get_meta('author')
+version = get_meta('version')
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..c4a80fd
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,18 @@
+ldif3
++++++
+
+Introduction
+============
+
+.. include:: ../README.rst
+
+API reference
+=============
+
+.. automodule:: ldif3
+    :members:
+
+Changelog
+=========
+
+.. include:: ../CHANGES.rst
diff --git a/ldif3.py b/ldif3.py
new file mode 100644
index 0000000..6f1b723
--- /dev/null
+++ b/ldif3.py
@@ -0,0 +1,334 @@
+"""ldif3 - generate and parse LDIF data (see RFC 2849)."""
+
+from __future__ import unicode_literals
+
+__version__ = '3.0.2'
+
+__all__ = [
+    # constants
+    'LDIF_PATTERN',
+    # classes
+    'LDIFWriter',
+    'LDIFParser',
+]
+
+import base64
+import re
+import logging
+from collections import OrderedDict
+
+try:  # pragma: nocover
+    from urlparse import urlparse
+    from urllib import urlopen
+except ImportError:  # pragma: nocover
+    from urllib.parse import urlparse
+    from urllib.request import urlopen
+
+log = logging.getLogger('ldif3')
+
+ATTRTYPE_PATTERN = r'[\w;.-]+(;[\w_-]+)*'
+ATTRVALUE_PATTERN = r'(([^,]|\\,)+|".*?")'
+ATTR_PATTERN = ATTRTYPE_PATTERN + r'[ ]*=[ ]*' + ATTRVALUE_PATTERN
+RDN_PATTERN = ATTR_PATTERN + r'([ ]*\+[ ]*' + ATTR_PATTERN + r')*[ ]*'
+DN_PATTERN = RDN_PATTERN + r'([ ]*,[ ]*' + RDN_PATTERN + r')*[ ]*'
+DN_REGEX = re.compile('^%s$' % DN_PATTERN)
+
+LDIF_PATTERN = ('^((dn(:|::) %(DN_PATTERN)s)|(%(ATTRTYPE_PATTERN)'
+    's(:|::) .*)$)+' % vars())
+
+MOD_OPS = ['add', 'delete', 'replace']
+CHANGE_TYPES = ['add', 'delete', 'modify', 'modrdn']
+
+
+def is_dn(s):
+    """Return True if s is a LDAP DN."""
+    if s == '':
+        return True
+    rm = DN_REGEX.match(s)
+    return rm is not None and rm.group(0) == s
+
+
+UNSAFE_STRING_PATTERN = '(^[ :<]|[\000\n\r\200-\377])'
+UNSAFE_STRING_RE = re.compile(UNSAFE_STRING_PATTERN)
+
+
+def lower(l):
+    """Return a list with the lowercased items of l."""
+    return [i.lower() for i in l or []]
+
+
+class LDIFWriter(object):
+    """Write LDIF entry or change records to file object.
+
+    :type output_file: file-like object in binary mode
+    :param output_file: File for output
+
+    :type base64_attrs: List[string]
+    :param base64_attrs: List of attribute types to be base64-encoded in any
+        case
+
+    :type cols: int
+    :param cols: Specifies how many columns a line may have before it is
+        folded into many lines
+
+    :type line_sep: bytearray
+    :param line_sep: line separator
+    """
+
+    def __init__(
+            self, output_file, base64_attrs=[], cols=76, line_sep=b'\n'):
+        self._output_file = output_file
+        self._base64_attrs = lower(base64_attrs)
+        self._cols = cols
+        self._line_sep = line_sep
+        self.records_written = 0
+
+    def _fold_line(self, line):
+        """Write string line as one or more folded lines."""
+        if len(line) <= self._cols:
+            self._output_file.write(line)
+            self._output_file.write(self._line_sep)
+        else:
+            pos = self._cols
+            self._output_file.write(line[0:self._cols])
+            self._output_file.write(self._line_sep)
+            while pos < len(line):
+                self._output_file.write(b' ')
+                end = min(len(line), pos + self._cols - 1)
+                self._output_file.write(line[pos:end])
+                self._output_file.write(self._line_sep)
+                pos = end
+
+    def _needs_base64_encoding(self, attr_type, attr_value):
+        """Return True if attr_value has to be base-64 encoded.
+
+        This is the case because of special chars or because attr_type is in
+        self._base64_attrs
+        """
+        return attr_type.lower() in self._base64_attrs or \
+            UNSAFE_STRING_RE.search(attr_value) is not None
+
+    def _unparse_attr(self, attr_type, attr_value):
+        """Write a single attribute type/value pair."""
+        if self._needs_base64_encoding(attr_type, attr_value):
+            encoded = base64.encodestring(attr_value.encode('utf8'))\
+                .replace(b'\n', b'')\
+                .decode('utf8')
+            line = ':: '.join([attr_type, encoded])
+        else:
+            line = ': '.join([attr_type, attr_value])
+        self._fold_line(line.encode('utf8'))
+
+    def _unparse_entry_record(self, entry):
+        """
+        :type entry: Dict[string, List[string]]
+        :param entry: Dictionary holding an entry
+        """
+        for attr_type in sorted(entry.keys()):
+            for attr_value in entry[attr_type]:
+                self._unparse_attr(attr_type, attr_value)
+
+    def _unparse_changetype(self, mod_len):
+        """Detect and write the changetype."""
+        if mod_len == 2:
+            changetype = 'add'
+        elif mod_len == 3:
+            changetype = 'modify'
+        else:
+            raise ValueError("modlist item of wrong length")
+
+        self._unparse_attr('changetype', changetype)
+
+    def _unparse_change_record(self, modlist):
+        """
+        :type modlist: List[Tuple]
+        :param modlist: List of additions (2-tuple) or modifications (3-tuple)
+        """
+        mod_len = len(modlist[0])
+        self._unparse_changetype(mod_len)
+
+        for mod in modlist:
+            if len(mod) != mod_len:
+                raise ValueError("Subsequent modlist item of wrong length")
+
+            if mod_len == 2:
+                mod_type, mod_vals = mod
+            elif mod_len == 3:
+                mod_op, mod_type, mod_vals = mod
+                self._unparse_attr(MOD_OPS[mod_op], mod_type)
+
+            for mod_val in mod_vals:
+                self._unparse_attr(mod_type, mod_val)
+
+            if mod_len == 3:
+                self._output_file.write(b'-' + self._line_sep)
+
+    def unparse(self, dn, record):
+        """Write an entry or change record to the output file.
+
+        :type dn: string
+        :param dn: distinguished name
+
+        :type record: Union[Dict[string, List[string]], List[Tuple]]
+        :param record: Either a dictionary holding  an entry or a list of
+            additions (2-tuple) or modifications (3-tuple).
+        """
+        self._unparse_attr('dn', dn)
+        if isinstance(record, dict):
+            self._unparse_entry_record(record)
+        elif isinstance(record, list):
+            self._unparse_change_record(record)
+        else:
+            raise ValueError("Argument record must be dictionary or list")
+        self._output_file.write(self._line_sep)
+        self.records_written += 1
+
+
+class LDIFParser(object):
+    """Read LDIF entry or change records from file object.
+
+    :type input_file: file-like object in binary mode
+    :param input_file: file to read the LDIF input from
+
+    :type ignored_attr_types: List[string]
+    :param ignored_attr_types: List of attribute types that will be ignored
+
+    :type process_url_schemes: List[bytearray]
+    :param process_url_schemes: List of URL schemes to process with urllib.
+        An empty list turns off all URL processing and the attribute is
+        ignored completely.
+
+    :type line_sep: bytearray
+    :param line_sep: line separator
+
+    :type strict: boolean
+    :param strict: If set to ``False``, recoverable parse errors will produce
+        log warnings rather than exceptions.
+    """
+
+    def _strip_line_sep(self, s):
+        """Strip trailing line separators from s, but no other whitespaces."""
+        if s[-2:] == b'\r\n':
+            return s[:-2]
+        elif s[-1:] == b'\n':
+            return s[:-1]
+        else:
+            return s
+
+    def __init__(
+            self,
+            input_file,
+            ignored_attr_types=[],
+            process_url_schemes=[],
+            line_sep=b'\n',
+            strict=True):
+        self._input_file = input_file
+        self._process_url_schemes = lower(process_url_schemes)
+        self._ignored_attr_types = lower(ignored_attr_types)
+        self._line_sep = line_sep
+        self._strict = strict
+
+    def _iter_unfolded_lines(self):
+        """Iter input unfoled lines. Skip comments."""
+        line = self._input_file.readline()
+        while line:
+            line = self._strip_line_sep(line)
+
+            nextline = self._input_file.readline()
+            while nextline and nextline[:1] == b' ':
+                line += self._strip_line_sep(nextline)[1:]
+                nextline = self._input_file.readline()
+
+            if not line.startswith(b'#'):
+                yield line
+            line = nextline
+
+    def _iter_blocks(self):
+        """Iter input lines in blocks separated by blank lines."""
+        lines = []
+        for line in self._iter_unfolded_lines():
+            if line:
+                lines.append(line)
+            else:
+                yield lines
+                lines = []
+        if lines:
+            yield lines
+
+    def _parse_attr(self, line):
+        """Parse a single attribute type/value pair."""
+        colon_pos = line.index(b':')
+        attr_type = line[0:colon_pos]
+        value_spec = line[colon_pos:colon_pos + 2]
+        if value_spec == b'::':
+            attr_value = base64.decodestring(line[colon_pos + 2:])
+        elif value_spec == b':<':
+            url = line[colon_pos + 2:].strip()
+            attr_value = b''
+            if self._process_url_schemes:
+                u = urlparse(url)
+                if u[0] in self._process_url_schemes:
+                    attr_value = urlopen(url.decode('ascii')).read()
+        elif value_spec == b':\r\n' or value_spec == b'\n':
+            attr_value = b''
+        else:
+            attr_value = line[colon_pos + 2:].lstrip()
+        return attr_type.decode('utf8'), attr_value.decode('utf8')
+
+    def _error(self, msg):
+        if self._strict:
+            raise ValueError(msg)
+        else:
+            log.warning(msg)
+
+    def _check_dn(self, dn, attr_value):
+        """Check dn attribute for issues."""
+        if dn is not None:
+            self._error('Two lines starting with dn: in one record.')
+        if not is_dn(attr_value):
+            self._error('No valid string-representation of '
+                'distinguished name %s.' % attr_value)
+
+    def _check_changetype(self, dn, changetype, attr_value):
+        """Check changetype attribute for issues."""
+        if dn is None:
+            self._error('Read changetype: before getting valid dn: line.')
+        if changetype is not None:
+            self._error('Two lines starting with changetype: in one record.')
+        if attr_value not in CHANGE_TYPES:
+            self._error('changetype value %s is invalid.' % attr_value)
+
+    def _parse_record(self, lines):
+        """Parse a single record from a list of lines."""
+        dn = None
+        changetype = None
+        entry = OrderedDict()
+
+        for line in lines:
+            attr_type, attr_value = self._parse_attr(line)
+
+            if attr_type == 'dn':
+                self._check_dn(dn, attr_value)
+                dn = attr_value
+            elif attr_type == 'version' and dn is None:
+                pass  # version = 1
+            elif attr_type == 'changetype':
+                self._check_changetype(dn, changetype, attr_value)
+                changetype = attr_value
+            elif attr_value is not None and \
+                     attr_type.lower() not in self._ignored_attr_types:
+                if attr_type in entry:
+                    entry[attr_type].append(attr_value)
+                else:
+                    entry[attr_type] = [attr_value]
+
+        return dn, changetype, entry
+
+    def parse(self):
+        """Iterate LDIF records.
+
+        :rtype: Iterator[Tuple[string, string, Dict]]
+        :return: (dn, changetype, entry)
+        """
+        for block in self._iter_blocks():
+            yield self._parse_record(block)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3e99b07
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,12 @@
+[nosetests]
+all-modules=1
+with-coverage=1
+cover-inclusive=1
+cover-erase=1
+cover-branches=1
+cover-html=1
+cover-html-dir=.cover
+
+[flake8]
+exclude=.git,.tox,.env,build,dist
+ignore=E127,E128
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..31ce9c8
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+import os
+
+from setuptools import setup
+
+curdir = os.path.dirname(os.path.abspath(__file__))
+
+
+with open(os.path.join(curdir, 'ldif3.py')) as fh:
+    for line in fh:
+        if line.startswith('"""'):
+            name, description = line.rstrip().strip('"').split(' - ')
+        elif line.startswith('__version__'):
+            version = line.split('\'')[1]
+            break
+
+setup(
+    name=name,
+    version=version,
+    description=description,
+    long_description=open(os.path.join(curdir, 'README.rst')).read(),
+    url='https://github.com/xi/ldif3',
+    author='Tobias Bengfort',
+    author_email='tobias.bengfort at posteo.de',
+    py_modules=['ldif3'],
+    license='BSD',
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'License :: OSI Approved :: BSD License',
+        'Intended Audience :: Developers',
+        'Topic :: System :: Systems Administration :: '
+            'Authentication/Directory :: LDAP',
+    ])
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..05dd98b
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,318 @@
+from __future__ import unicode_literals
+
+import unittest
+
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
+from io import BytesIO
+
+import ldif3
+
+
+BYTES = b"""version: 1
+dn: cn=Alice Alison,
+ mail=alicealison at example.com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+cn: Alison Alison
+mail: alicealison at example.com
+modifytimestamp: 4a463e9a
+
+# another person
+dn: mail=foobar at example.org
+objectclass: top
+objectclass:  person
+mail: foobar at example.org
+modifytimestamp: 4a463e9a
+"""
+
+BYTES_OUT = b"""dn: cn=Alice Alison,mail=alicealison at example.com
+cn: Alison Alison
+mail: alicealison at example.com
+modifytimestamp: 4a463e9a
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+
+dn: mail=foobar at example.org
+mail: foobar at example.org
+modifytimestamp: 4a463e9a
+objectclass: top
+objectclass: person
+
+"""
+
+LINES = [
+    b'version: 1',
+    b'dn: cn=Alice Alison,mail=alicealison at example.com',
+    b'objectclass: top',
+    b'objectclass: person',
+    b'objectclass: organizationalPerson',
+    b'cn: Alison Alison',
+    b'mail: alicealison at example.com',
+    b'modifytimestamp: 4a463e9a',
+    b'',
+    b'dn: mail=foobar at example.org',
+    b'objectclass: top',
+    b'objectclass:  person',
+    b'mail: foobar at example.org',
+    b'modifytimestamp: 4a463e9a',
+]
+
+BLOCKS = [[
+    b'version: 1',
+    b'dn: cn=Alice Alison,mail=alicealison at example.com',
+    b'objectclass: top',
+    b'objectclass: person',
+    b'objectclass: organizationalPerson',
+    b'cn: Alison Alison',
+    b'mail: alicealison at example.com',
+    b'modifytimestamp: 4a463e9a',
+], [
+    b'dn: mail=foobar at example.org',
+    b'objectclass: top',
+    b'objectclass:  person',
+    b'mail: foobar at example.org',
+    b'modifytimestamp: 4a463e9a',
+]]
+
+DNS = [
+    'cn=Alice Alison,mail=alicealison at example.com',
+    'mail=foobar at example.org'
+]
+
+CHANGETYPES = [None, None]
+
+RECORDS = [{
+    'cn': ['Alison Alison'],
+    'mail': ['alicealison at example.com'],
+    'modifytimestamp': ['4a463e9a'],
+    'objectclass': ['top', 'person', 'organizationalPerson'],
+}, {
+    'mail': ['foobar at example.org'],
+    'modifytimestamp': ['4a463e9a'],
+    'objectclass': ['top', 'person'],
+}]
+
+URL = b'https://tools.ietf.org/rfc/rfc2849.txt'
+URL_CONTENT = 'The LDAP Data Interchange Format (LDIF)'
+
+
+class TestUnsafeString(unittest.TestCase):
+    unsafe_chars = ['\0', '\n', '\r']
+    unsafe_chars_init = unsafe_chars + [' ', ':', '<']
+
+    def _test_all(self, unsafes, fn):
+        for i in range(128):  # TODO: test range(255)
+            try:
+                match = ldif3.UNSAFE_STRING_RE.search(fn(i))
+                if i <= 127 and chr(i) not in unsafes:
+                    self.assertIsNone(match)
+                else:
+                    self.assertIsNotNone(match)
+            except AssertionError:
+                print(i)
+                raise
+
+    def test_unsafe_chars(self):
+        self._test_all(self.unsafe_chars, lambda i: 'a%s' % chr(i))
+
+    def test_unsafe_chars_init(self):
+        self._test_all(self.unsafe_chars_init, lambda i: '%s' % chr(i))
+
+    def test_example(self):
+        s = 'cn=Alice, Alison,mail=Alice.Alison at example.com'
+        self.assertIsNone(ldif3.UNSAFE_STRING_RE.search(s))
+
+    def test_trailing_newline(self):
+        self.assertIsNotNone(ldif3.UNSAFE_STRING_RE.search('asd\n'))
+
+
+class TestLower(unittest.TestCase):
+    def test_happy(self):
+        self.assertEqual(ldif3.lower(['ASD', 'HuHu']), ['asd', 'huhu'])
+
+    def test_falsy(self):
+        self.assertEqual(ldif3.lower(None), [])
+
+    def test_dict(self):
+        self.assertEqual(ldif3.lower({'Foo': 'bar'}), ['foo'])
+
+    def test_set(self):
+        self.assertEqual(ldif3.lower(set(['FOo'])), ['foo'])
+
+
+class TestIsDn(unittest.TestCase):
+    def test_happy(self):
+        pass  # TODO
+
+
+class TestLDIFParser(unittest.TestCase):
+    def setUp(self):
+        self.stream = BytesIO(BYTES)
+        self.p = ldif3.LDIFParser(self.stream)
+
+    def test_strip_line_sep(self):
+        self.assertEqual(self.p._strip_line_sep(b'asd \n'), b'asd ')
+        self.assertEqual(self.p._strip_line_sep(b'asd\t\n'), b'asd\t')
+        self.assertEqual(self.p._strip_line_sep(b'asd\r\n'), b'asd')
+        self.assertEqual(self.p._strip_line_sep(b'asd\r\t\n'), b'asd\r\t')
+        self.assertEqual(self.p._strip_line_sep(b'asd\n\r'), b'asd\n\r')
+        self.assertEqual(self.p._strip_line_sep(b'asd'), b'asd')
+        self.assertEqual(self.p._strip_line_sep(b'  asd  '), b'  asd  ')
+
+    def test_iter_unfolded_lines(self):
+        self.assertEqual(list(self.p._iter_unfolded_lines()), LINES)
+
+    def test_iter_blocks(self):
+        self.assertEqual(list(self.p._iter_blocks()), BLOCKS)
+
+    def _test_error(self, fn):
+        self.p._strict = True
+        with self.assertRaises(ValueError):
+            fn()
+
+        with mock.patch('ldif3.log.warning') as warning:
+            self.p._strict = False
+            fn()
+            warning.assert_called()
+
... 159 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/ldif3.git



More information about the Python-modules-commits mailing list