[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