lists.arthurdejong.org
RSS feed

nss-pam-ldapd branch master updated. 0.9.10-6-g0252b05

[Date Prev][Date Next] [Thread Prev][Thread Next]

nss-pam-ldapd branch master updated. 0.9.10-6-g0252b05



This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "nss-pam-ldapd".

The branch, master has been updated
       via  0252b050cc2e8859bf53622561d42108f7e721e8 (commit)
       via  cd887ef577f3913d5919ee32f448b02cd5c614ad (commit)
       via  d717795b9f74e1b9ad384e06e4c09f6608a31912 (commit)
       via  221ce5a2680c1a91b7b87a36d73be5c0ad7e5ddb (commit)
      from  06ee88605e176c77eda2dc0a5499e2baef65e8f1 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
https://arthurdejong.org/git/nss-pam-ldapd/commit/?id=0252b050cc2e8859bf53622561d42108f7e721e8

commit 0252b050cc2e8859bf53622561d42108f7e721e8
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Sep 8 21:51:09 2019 +0200

    Correctly validate shadow requests and responses

diff --git a/pynslcd/shadow.py b/pynslcd/shadow.py
index 0f5441c..59e1af6 100644
--- a/pynslcd/shadow.py
+++ b/pynslcd/shadow.py
@@ -18,6 +18,8 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
+import logging
+
 import cache
 import cfg
 import common
@@ -112,8 +114,11 @@ class ShadowRequest(common.Request):
             flag = 0
         # return results
         for name in names:
-            yield (name, passwd, lastchangedate, mindays, maxdays, warndays,
-                   inactdays, expiredate, flag)
+            if not common.is_valid_name(name):
+                logging.warning('%s: %s: denied by validnames option', dn, 
attmap['uid'])
+            else:
+                yield (name, passwd, lastchangedate, mindays, maxdays, 
warndays,
+                       inactdays, expiredate, flag)
 
 
 class ShadowByNameRequest(ShadowRequest):
@@ -121,7 +126,9 @@ class ShadowByNameRequest(ShadowRequest):
     action = constants.NSLCD_ACTION_SHADOW_BYNAME
 
     def read_parameters(self, fp):
-        return dict(uid=fp.read_string())
+        name = fp.read_string()
+        common.validate_name(name)
+        return dict(uid=name)
 
 
 class ShadowAllRequest(ShadowRequest):

https://arthurdejong.org/git/nss-pam-ldapd/commit/?id=cd887ef577f3913d5919ee32f448b02cd5c614ad

commit cd887ef577f3913d5919ee32f448b02cd5c614ad
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sun Sep 8 11:54:19 2019 +0200

    Update Python interpreter in installed scripts
    
    Ensure that the Python interpreter that is passed to configure ends up
    in the shebang of the Python scripts.
    
    This allows one to pass PYTHON=python3 to configure to install the
    scripts using the Python 3 interpreter.

diff --git a/pynslcd/Makefile.am b/pynslcd/Makefile.am
index cfc264d..383dd3c 100644
--- a/pynslcd/Makefile.am
+++ b/pynslcd/Makefile.am
@@ -37,6 +37,7 @@ constants.py: constants.py.in $(top_srcdir)/nslcd.h
 
 # create a symbolic link for the pynslcd daemon and fix permissions
 install-data-hook:
-       chmod a+rx $(DESTDIR)$(pynslcddir)/pynslcd.py
        $(MKDIR_P) $(DESTDIR)$(sbindir)
        [ -L $(DESTDIR)$(sbindir)/pynslcd ] || $(LN_S) $(pynslcddir)/pynslcd.py 
$(DESTDIR)$(sbindir)/pynslcd
+       chmod a+rx $(DESTDIR)$(pynslcddir)/pynslcd.py
+       sed -i -e '1 s|^#!.*|#! $(PYTHON)|;1 s|^#! \([^/].*\)|#! /usr/bin/env 
\1|' $(DESTDIR)$(pynslcddir)/pynslcd.py
diff --git a/utils/Makefile.am b/utils/Makefile.am
index b8da6f6..a6bccd3 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -33,10 +33,11 @@ clean-local:
 constants.py: ../pynslcd/constants.py
        cp ../pynslcd/constants.py .
 
-# create symbolic links to the commands and fix permissions
+# create symbolic links, fix permissions and set Python interpreter
 install-data-hook:
        $(MKDIR_P) $(DESTDIR)$(bindir)
        set -ex; for cmd in getent chsh ; do \
-         chmod a+rx $(DESTDIR)$(utilsdir)/$$cmd.py ; \
          [ -L $(DESTDIR)$(bindir)/$$cmd.$(MODULE_NAME) ] || $(LN_S) 
$(utilsdir)/$$cmd.py $(DESTDIR)$(bindir)/$$cmd.$(MODULE_NAME) ; \
+         chmod a+rx $(DESTDIR)$(utilsdir)/$$cmd.py ; \
+         sed -i -e '1 s|^#!.*|#! $(PYTHON)|;1 s|^#! \([^/].*\)|#! /usr/bin/env 
\1|' $(DESTDIR)$(utilsdir)/$$cmd.py ; \
        done

https://arthurdejong.org/git/nss-pam-ldapd/commit/?id=d717795b9f74e1b9ad384e06e4c09f6608a31912

commit d717795b9f74e1b9ad384e06e4c09f6608a31912
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Sat Sep 7 17:52:45 2019 +0200

    Improve Python code style
    
    This also adds a flake8 test that checks code style. Note that this test
    is not run by default because it requires network access to create the
    virtualenv with the test software.

diff --git a/.gitignore b/.gitignore
index 8380b28..0ffa803 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,7 @@ stamp-*
 /pynslcd/constants.py
 
 # /tests/
+/tests/flake8-venv
 /tests/lookup_groupbyuser
 /tests/lookup_netgroup
 /tests/lookup_shadow
diff --git a/pynslcd/alias.py b/pynslcd/alias.py
index 371ac2e..8096309 100644
--- a/pynslcd/alias.py
+++ b/pynslcd/alias.py
@@ -1,7 +1,7 @@
 
 # alias.py - lookup functions for email aliases
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,7 +24,9 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn', rfc822MailMember='rfc822MailMember')
+attmap = common.Attributes(
+    cn='cn',
+    rfc822MailMember='rfc822MailMember')
 filter = '(objectClass=nisMailAlias)'
 
 
diff --git a/pynslcd/attmap.py b/pynslcd/attmap.py
index 2af8ec4..61862df 100644
--- a/pynslcd/attmap.py
+++ b/pynslcd/attmap.py
@@ -20,13 +20,14 @@
 
 """Module for handling attribute mappings used for LDAP searches.
 
->>> attrs = Attributes(uid='uid',
-...                    userPassword='userPassword',
-...                    uidNumber='uidNumber',
-...                    gidNumber='gidNumber',
-...                    gecos='"${gecos:-$cn}"',
-...                    homeDirectory='homeDirectory',
-...                    loginShell='loginShell')
+>>> attrs = Attributes(
+...     uid='uid',
+...     userPassword='userPassword',
+...     uidNumber='uidNumber',
+...     gidNumber='gidNumber',
+...     gecos='"${gecos:-$cn}"',
+...     homeDirectory='homeDirectory',
+...     loginShell='loginShell')
 >>> 'cn' in attrs.attributes()
 True
 >>> attrs.translate({'uid': ['UIDVALUE', '2nduidvalue'], 'cn': ['COMMON NAME', 
 >>> ]}) == {
@@ -41,8 +42,8 @@ True
 
 import re
 
-from ldap.filter import escape_filter_chars
 import ldap.dn
+from ldap.filter import escape_filter_chars
 
 from expr import Expression
 
@@ -66,8 +67,7 @@ class SimpleMapping(str):
 
     def mk_filter(self, value):
         return '(%s=%s)' % (
-                self, escape_filter_chars(str(value))
-            )
+            self, escape_filter_chars(str(value)))
 
     def values(self, variables):
         """Expand the expression using the variables specified."""
@@ -123,8 +123,7 @@ class FunctionMapping(object):
 
     def mk_filter(self, value):
         return '(%s=%s)' % (
-                self.attribute, escape_filter_chars(value)
-            )
+            self.attribute, escape_filter_chars(value))
 
     def values(self, variables):
         return [self.function(value)
@@ -156,32 +155,30 @@ class Attributes(dict):
             self[key] = kwargs[key]
 
     def attributes(self):
-        """Return the list of attributes that are referenced in this
-        attribute mapping. These are the attributes that should be
-        requested in the search."""
+        """Return the attributes that are referenced in this attribute mapping.
+
+        These are the attributes that should be requested in the search.
+        """
         attributes = set()
         for mapping in self.values():
             attributes.update(mapping.attributes())
         return list(attributes)
 
     def mk_filter(self, attribute, value):
-        """Construct a search filter for searching for the attribute value
-        combination."""
+        """Construct a search filter for the attribute value combination."""
         mapping = self.get(attribute, SimpleMapping(attribute))
         return mapping.mk_filter(value)
 
     def translate(self, variables):
-        """Return a dictionary with every attribute mapped to their value from
-        the specified variables."""
+        """Return a dictionary with every attribute mapped to their value."""
         results = dict()
         for attribute, mapping in self.items():
             results[attribute] = mapping.values(variables)
         return results
 
     def get_rdn_value(self, dn, attribute):
-        """Extract the attribute value from from DN if possible. Return None
-        otherwise."""
+        """Extract the attribute value from from DN or return None."""
         return self.translate(dict(
-                (x, [y])
-                for x, y, z in ldap.dn.str2dn(dn)[0]
-            ))[attribute][0]
+            (x, [y])
+            for x, y, z in ldap.dn.str2dn(dn)[0]
+        ))[attribute][0]
diff --git a/pynslcd/cache.py b/pynslcd/cache.py
index d077ac7..0be3a71 100644
--- a/pynslcd/cache.py
+++ b/pynslcd/cache.py
@@ -20,19 +20,31 @@
 
 import datetime
 import os
-import sys
-
 import sqlite3
+import sys
 
 
 # TODO: probably create a config table
-# FIXME: have some way to remove stale entries from the cache if all items 
from LDAP are queried (perhas use TTL from all request)
+# FIXME: have some way to remove stale entries from the cache if all items
+#        from LDAP are queried (perhas use TTL from all request)
+
 
+class regroup(object):  # noqa: N801 (this has an iterator name)
+    """Regroup the results in the group column by the key columns.
 
-class regroup(object):
+    Get entries from a queryset that has multiple result rows per wanted
+    entry by combining multiple values. E.g.
+
+      1, 2, 3
+      1, 2, 4
+      1, 2, 5
+
+    into
+
+      1, 2, [3, 4, 5]
+    """
 
     def __init__(self, results, group_by=None, group_column=None):
-        """Regroup the results in the group column by the key columns."""
         self.group_by = tuple(group_by)
         self.group_column = group_column
         self.it = iter(results)
@@ -113,8 +125,10 @@ class Cache(object):
 
     def store(self, *values):
         """Store the values in the cache for the specified table.
+
         The order of the values is the order returned by the Reques.convert()
-        function."""
+        function.
+        """
         # split the values into simple (flat) values and one-to-many values
         simple_values = []
         multi_values = []
@@ -144,8 +158,7 @@ class Cache(object):
                 ''' % (self.tables[n + 1]), ((values[0], x) for x in vlist))
 
     def retrieve(self, parameters):
-        """Retrieve all items from the cache based on the parameters
-        supplied."""
+        """Retrieve all items from the cache based on the parameters 
supplied."""
         query = Query(self.retrieve_sql or '''
             SELECT *
             FROM %s
@@ -165,10 +178,10 @@ class Cache(object):
         return (list(x)[:-1] for x in results)
 
     def __enter__(self):
-        return self.con.__enter__();
+        return self.con.__enter__()
 
     def __exit__(self, *args):
-        return self.con.__exit__(*args);
+        return self.con.__exit__(*args)
 
 
 # the connection to the sqlite database
diff --git a/pynslcd/cfg.py b/pynslcd/cfg.py
index 9a18541..877d442 100644
--- a/pynslcd/cfg.py
+++ b/pynslcd/cfg.py
@@ -133,23 +133,23 @@ _tls_reqcert_options = {'never': ldap.OPT_X_TLS_NEVER,
 
 def _get_maps():
     # separate function as not to pollute the namespace and avoid import loops
-    import alias, ether, group, host, netgroup, network, passwd
-    import protocol, rpc, service, shadow
+    import alias, ether, group, host, netgroup, network, passwd  # noqa: E401
+    import protocol, rpc, service, shadow  # noqa: E401
     import sys
     return dict(
-            alias=alias, aliases=alias,
-            ether=ether, ethers=ether,
-            group=group,
-            host=host, hosts=host,
-            netgroup=netgroup,
-            network=network, networks=network,
-            passwd=passwd,
-            protocol=protocol, protocols=protocol,
-            rpc=rpc,
-            service=service, services=service,
-            shadow=shadow,
-            none=sys.modules[__name__]
-        )
+        alias=alias, aliases=alias,
+        ether=ether, ethers=ether,
+        group=group,
+        host=host, hosts=host,
+        netgroup=netgroup,
+        network=network, networks=network,
+        passwd=passwd,
+        protocol=protocol, protocols=protocol,
+        rpc=rpc,
+        service=service, services=service,
+        shadow=shadow,
+        none=sys.modules[__name__],
+    )
 
 
 class ParseError(Exception):
@@ -163,7 +163,7 @@ class ParseError(Exception):
     __str__ = __repr__
 
 
-def read(filename):
+def read(filename):  # noqa: C901 (many simple branches)
     maps = _get_maps()
     lineno = 0
     for line in open(filename, 'r'):
@@ -173,34 +173,45 @@ def read(filename):
         if re.match(r'(#.*)?$', line, re.IGNORECASE):
             continue
         # parse options with a single integer argument
-        m = 
re.match(r'(?P<keyword>threads|ldap_version|bind_timelimit|timelimit|idle_timelimit|reconnect_sleeptime|reconnect_retrytime|pagesize|nss_min_uid|nss_uid_offset|nss_gid_offset)\s+(?P<value>\d+)',
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'(?P<keyword>threads|ldap_version|bind_timelimit|timelimit|'
+            r'idle_timelimit|reconnect_sleeptime|reconnect_retrytime|pagesize|'
+            r'nss_min_uid|nss_uid_offset|nss_gid_offset)\s+(?P<value>\d+)',
+            line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = int(m.group('value'))
             continue
         # parse options with a single boolean argument
-        m = 
re.match(r'(?P<keyword>referrals|nss_nested_groups|nss_getgrent_skipmembers|nss_disable_enumeration)\s+(?P<value>%s)'
 %
-                         '|'.join(_boolean_options.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            
r'(?P<keyword>referrals|nss_nested_groups|nss_getgrent_skipmembers|'
+            r'nss_disable_enumeration)\s+(?P<value>%s)' % (
+                '|'.join(_boolean_options.keys())),
+            line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = 
_boolean_options[m.group('value').lower()]
             continue
         # parse options with a single no-space value
-        m = 
re.match(r'(?P<keyword>uid|gid|bindpw|rootpwmodpw|sasl_mech)\s+(?P<value>\S+)',
-                     line, re.IGNORECASE)
+        m = re.match(
+            
r'(?P<keyword>uid|gid|bindpw|rootpwmodpw|sasl_mech)\s+(?P<value>\S+)',
+            line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = m.group('value')
             continue
         # parse options with a single value that can contain spaces
-        m = 
re.match(r'(?P<keyword>binddn|rootpwmoddn|sasl_realm|sasl_authcid|sasl_authzid|sasl_secprops|krb5_ccname|tls_cacertdir|tls_cacertfile|tls_randfile|tls_ciphers|tls_cert|tls_key|pam_password_prohibit_message)\s+(?P<value>\S.*)',
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'(?P<keyword>binddn|rootpwmoddn|sasl_realm|sasl_authcid|'
+            r'sasl_authzid|sasl_secprops|krb5_ccname|tls_cacertdir|'
+            r'tls_cacertfile|tls_randfile|tls_ciphers|tls_cert|tls_key|'
+            r'pam_password_prohibit_message)\s+(?P<value>\S.*)',
+            line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = m.group('value')
             continue
         # log <SCHEME> [<LEVEL>]
-        m = re.match(r'log\s+(?P<scheme>syslog|/\S*)(\s+(?P<level>%s))?' %
-                         '|'.join(_log_levels.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'log\s+(?P<scheme>syslog|/\S*)(\s+(?P<level>%s))?' % (
+                '|'.join(_log_levels.keys())),
+            line, re.IGNORECASE)
         if m:
             logs.append((m.group('scheme'), 
_log_levels[str(m.group('level')).lower()]))
             continue
@@ -213,9 +224,10 @@ def read(filename):
             uri = m.group('uri')
             continue
         # base <MAP>? <BASEDN>
-        m = re.match(r'base\s+((?P<map>%s)\s+)?(?P<value>\S.*)' %
-                         '|'.join(maps.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'base\s+((?P<map>%s)\s+)?(?P<value>\S.*)' % (
+                '|'.join(maps.keys())),
+            line, re.IGNORECASE)
         if m:
             mod = maps[str(m.group('map')).lower()]
             if not hasattr(mod, 'bases'):
@@ -223,26 +235,29 @@ def read(filename):
             mod.bases.append(m.group('value'))
             continue
         # filter <MAP> <SEARCHFILTER>
-        m = re.match(r'filter\s+(?P<map>%s)\s+(?P<value>\S.*)' %
-                         '|'.join(maps.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'filter\s+(?P<map>%s)\s+(?P<value>\S.*)' % (
+                '|'.join(maps.keys())),
+            line, re.IGNORECASE)
         if m:
             mod = maps[m.group('map').lower()]
             mod.filter = m.group('value')
             continue
         # scope <MAP>? <SCOPE>
-        m = re.match(r'scope\s+((?P<map>%s)\s+)?(?P<value>%s)' % (
-                         '|'.join(maps.keys()),
-                         '|'.join(_scope_options.keys())),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'scope\s+((?P<map>%s)\s+)?(?P<value>%s)' % (
+                '|'.join(maps.keys()),
+                '|'.join(_scope_options.keys())),
+            line, re.IGNORECASE)
         if m:
             mod = maps[str(m.group('map')).lower()]
             mod.scope = _scope_options[m.group('value').lower()]
             continue
         # map <MAP> <ATTRIBUTE> <ATTMAPPING>
-        m = 
re.match(r'map\s+(?P<map>%s)\s+(?P<attribute>\S+)\s+(?P<value>\S.*)' %
-                         '|'.join(maps.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'map\s+(?P<map>%s)\s+(?P<attribute>\S+)\s+(?P<value>\S.*)' % (
+                '|'.join(maps.keys())),
+            line, re.IGNORECASE)
         if m:
             mod = maps[m.group('map').lower()]
             attribute = m.group('attribute')
@@ -252,15 +267,17 @@ def read(filename):
             # TODO: filter out attributes that cannot be an expression
             continue
         # deref <DEREF>
-        m = re.match(r'deref\s+(?P<value>%s)' % 
'|'.join(_deref_options.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'deref\s+(?P<value>%s)' % '|'.join(_deref_options.keys()),
+            line, re.IGNORECASE)
         if m:
             global deref
             deref = _deref_options[m.group('value').lower()]
             continue
         # nss_initgroups_ignoreusers <USER,USER>|<ALLLOCAL>
-        m = re.match(r'nss_initgroups_ignoreusers\s+(?P<value>\S.*)',
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'nss_initgroups_ignoreusers\s+(?P<value>\S.*)',
+            line, re.IGNORECASE)
         if m:
             users = m.group('value')
             if users.lower() == 'alllocal':
@@ -283,16 +300,18 @@ def read(filename):
             # uid variables
             continue
         # ssl <on|off|start_tls>
-        m = re.match(r'ssl\s+(?P<value>%s)' % '|'.join(_ssl_options.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'ssl\s+(?P<value>%s)' % '|'.join(_ssl_options.keys()),
+            line, re.IGNORECASE)
         if m:
             global ssl
             ssl = _ssl_options[m.group('value').lower()]
             continue
         # sasl_canonicalize yes|no
-        m = 
re.match(r'(ldap_?)?sasl_(?P<no>no)?canon(icali[sz]e)?\s+(?P<value>%s)' %
-                         '|'.join(_boolean_options.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'(ldap_?)?sasl_(?P<no>no)?canon(icali[sz]e)?\s+(?P<value>%s)' % (
+                '|'.join(_boolean_options.keys())),
+            line, re.IGNORECASE)
         if m:
             global sasl_canonicalize
             sasl_canonicalize = _boolean_options[m.group('value').lower()]
@@ -300,24 +319,27 @@ def read(filename):
                 sasl_canonicalize = not sasl_canonicalize
             continue
         # tls_reqcert <demand|hard|yes...>
-        m = re.match(r'tls_reqcert\s+(?P<value>%s)' %
-                         '|'.join(_tls_reqcert_options.keys()),
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'tls_reqcert\s+(?P<value>%s)' % (
+                '|'.join(_tls_reqcert_options.keys())),
+            line, re.IGNORECASE)
         if m:
             global tls_reqcert
             tls_reqcert = _tls_reqcert_options[m.group('value').lower()]
             continue
         # validnames /REGEX/i?
-        m = re.match(r'validnames\s+/(?P<value>.*)/(?P<flags>[i]?)$',
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'validnames\s+/(?P<value>.*)/(?P<flags>[i]?)$',
+            line, re.IGNORECASE)
         if m:
             global validnames
             flags = 0 | re.IGNORECASE if m.group('flags') == 'i' else 0
             validnames = re.compile(m.group('value'), flags=flags)
             continue
         # reconnect_invalidate <MAP>,<MAP>,...
-        m = re.match(r'reconnect_invalidate\s+(?P<value>\S.*)',
-                     line, re.IGNORECASE)
+        m = re.match(
+            r'reconnect_invalidate\s+(?P<value>\S.*)',
+            line, re.IGNORECASE)
         if m:
             dbs = re.split('[ ,]+', m.group('value').lower())
             for db in dbs:
diff --git a/pynslcd/common.py b/pynslcd/common.py
index 2800222..a5b168d 100644
--- a/pynslcd/common.py
+++ b/pynslcd/common.py
@@ -23,45 +23,48 @@ import sys
 
 import ldap
 
-from attmap import Attributes
-#import cache
+from attmap import Attributes  # noqa: F401 (used by other modules)
 import cfg
 import constants
 
 
 def is_valid_name(name):
-    """Checks to see if the specified name seems to be a valid user or group
-    name.
+    """Check if the specified name seems to be a valid user or group name.
 
     This test is based on the definition from POSIX (IEEE Std 1003.1, 2004,
-    3.426 User Name, 3.189 Group Name and 3.276 Portable Filename Character 
Set):
+    3.426 User Name, 3.189 Group Name and 3.276 Portable Filename Character
+    Set):
     
http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_426
     
http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_189
     
http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_276
 
-    The standard defines user names valid if they contain characters from
-    the set [A-Za-z0-9._-] where the hyphen should not be used as first
-    character. As an extension this test allows some more characters."""
+    The standard defines user names valid if they contain characters from the
+    set [A-Za-z0-9._-] where the hyphen should not be used as first
+    character. As an extension this test allows some more characters.
+    """
     return bool(cfg.validnames.search(name))
 
 
 def validate_name(name):
-    """Checks to see if the specified name seems to be a valid user or group
-    name. See is_valid_name()."""
+    """Check if the specified name seems to be a valid user or group name.
+
+    This raises an exception if this is not the case.
+    """
     if not cfg.validnames.search(name):
         raise ValueError('%r: denied by validnames option' % name)
 
 
 class Request(object):
-    """
-    Request handler class. Subclasses are expected to handle actual requests
-    and should implement the following members:
+    """Request handler class.
 
-      action - the NSLCD_ACTION_* action that should trigger this handler
+    Subclasses are expected to handle actual requests and should implement
+    the following members:
 
+      action - the NSLCD_ACTION_* action that should trigger this handler
       read_parameters() - a function that reads the request parameters of the
                           request stream
       write() - function that writes a single LDAP entry to the result stream
+      convert() - function that generates result entries from an LDAP result
 
     """
 
@@ -71,15 +74,10 @@ class Request(object):
         self.calleruid = calleruid
         module = sys.modules[self.__module__]
         self.search = getattr(module, 'Search', None)
-        #if not hasattr(module, 'cache_obj'):
-        #    cache_cls = getattr(module, 'Cache', None)
-        #    module.cache_obj = cache_cls() if cache_cls else None
-        #self.cache = module.cache_obj
         self.cache = None
 
     def read_parameters(self, fp):
-        """This method should read and return the parameters from the
-        stream."""
+        """Read and return the parameters from the stream."""
         pass
 
     def get_results(self, parameters):
@@ -89,16 +87,13 @@ class Request(object):
                 yield values
 
     def handle_request(self, parameters):
-        """This method handles the request based on the parameters read
-        with read_parameters()."""
+        """Handle the request based on the parameters."""
         try:
-            #with cache.con:
-            if True:
-                for values in self.get_results(parameters):
-                    self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
-                    self.write(*values)
-                    if self.cache:
-                        self.cache.store(*values)
+            for values in self.get_results(parameters):
+                self.fp.write_int32(constants.NSLCD_RESULT_BEGIN)
+                self.write(*values)
+                if self.cache:
+                    self.cache.store(*values)
         except ldap.SERVER_DOWN:
             if self.cache:
                 logging.debug('read from cache')
diff --git a/pynslcd/ether.py b/pynslcd/ether.py
index 1be7861..9462ef0 100644
--- a/pynslcd/ether.py
+++ b/pynslcd/ether.py
@@ -1,7 +1,7 @@
 
 # ether.py - lookup functions for ethernet addresses
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -27,18 +27,19 @@ import search
 
 
 def ether_aton(ether):
-    """Converst an ethernet address to binary form in network byte order."""
+    """Convert an ethernet address to binary form in network byte order."""
     return struct.pack('BBBBBB', *(int(x, 16) for x in ether.split(':')))
 
 
 def ether_ntoa(ether, compact=True):
-    """Conversts an ethernet address in network byte order to the string
-    representation."""
+    """Convert an ethernet address in network byte order to a string."""
     fmt = '%x' if compact else '%02x'
     return ':'.join(fmt % x for x in struct.unpack('6B', ether))
 
 
-attmap = common.Attributes(cn='cn', macAddress='macAddress')
+attmap = common.Attributes(
+    cn='cn',
+    macAddress='macAddress')
 filter = '(objectClass=ieee802Device)'
 
 
@@ -53,9 +54,10 @@ class Search(search.LDAPSearch):
         if 'macAddress' in self.parameters:
             ether = self.parameters['macAddress']
             alt_ether = ether_ntoa(ether_aton(ether), compact=False)
-            return '(&%s(|(%s=%s)(%s=%s)))' % (self.filter,
-                      attmap['macAddress'], ether,
-                      attmap['macAddress'], alt_ether)
+            return '(&%s(|(%s=%s)(%s=%s)))' % (
+                self.filter,
+                attmap['macAddress'], ether,
+                attmap['macAddress'], alt_ether)
         return super(Search, self).mk_filter()
 
 
diff --git a/pynslcd/expr.py b/pynslcd/expr.py
index f996d2e..922a612 100644
--- a/pynslcd/expr.py
+++ b/pynslcd/expr.py
@@ -115,8 +115,7 @@ class MyIter(object):
 
 
 class DollarExpression(object):
-    """Class for handling a variable $xxx ${xxx}, ${xxx:-yyy} or ${xxx:+yyy}
-    expression."""
+    """Handle variable $xxx ${xxx}, ${xxx:-yyy} or ${xxx:+yyy} expansion."""
 
     def __init__(self, value):
         """Parse the expression as the start of a $-expression."""
@@ -169,22 +168,29 @@ class DollarExpression(object):
             value = value[0]
         # TODO: try to return multiple values, one for each value of the list
         if self.op == ':-':
+            # ${attr:-expr} use expr as default for variable
             return value if value else self.expr.value(variables)
         elif self.op == ':+':
+            # ${attr:+expr} expand expr if variable is set
             return self.expr.value(variables) if value else ''
         elif self.op == ':':
+            # ${attr:offset:length} provide substring of variable
             offset, length = self.expr.value(variables).split(':')
             offset, length = int(offset), int(length)
             return value[offset:offset + length]
         elif self.op in ('#', '##', '%', '%%'):
             match = fnmatch.translate(self.expr.value(variables))
             if self.op == '#':
+                # ${attr#word} remove shortest match of word
                 match = match.replace('*', '*?').replace(r'\Z', 
r'(?P<replace>.*)\Z')
             elif self.op == '##':
+                # ${attr#word} remove longest match of word
                 match = match.replace(r'\Z', r'(?P<replace>.*?)\Z')
             elif self.op == '%':
+                # ${attr#word} remove shortest match from right
                 match = r'(?P<replace>.*)' + match.replace('*', '*?')
             elif self.op == '%%':
+                # ${attr#word} remove longest match from right
                 match = r'(?P<replace>.*?)' + match
             match = re.match(match, value)
             return match.group('replace') if match else value
diff --git a/pynslcd/group.py b/pynslcd/group.py
index 2280eaf..263e40c 100644
--- a/pynslcd/group.py
+++ b/pynslcd/group.py
@@ -1,7 +1,7 @@
 
 # group.py - group entry lookup routines
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,8 +20,8 @@
 
 import logging
 
-from ldap.filter import escape_filter_chars
 import ldap
+from ldap.filter import escape_filter_chars
 
 import cache
 import cfg
@@ -37,11 +37,12 @@ def clean(lst):
             yield i.replace('\0', '')
 
 
-attmap = common.Attributes(cn='cn',
-                           userPassword='"*"',
-                           gidNumber='gidNumber',
-                           memberUid='memberUid',
-                           member='member')
+attmap = common.Attributes(
+    cn='cn',
+    userPassword='"*"',
+    gidNumber='gidNumber',
+    memberUid='memberUid',
+    member='member')
 filter = '(objectClass=posixGroup)'
 
 
@@ -69,10 +70,10 @@ class Search(search.LDAPSearch):
             entry = passwd.uid2entry(self.conn, memberuid)
             if entry:
                 return '(&%s(|(%s=%s)(%s=%s)))' % (
-                        self.filter,
-                        attmap['memberUid'], escape_filter_chars(memberuid),
-                        attmap['member'], escape_filter_chars(entry[0])
-                    )
+                    self.filter,
+                    attmap['memberUid'], escape_filter_chars(memberuid),
+                    attmap['member'], escape_filter_chars(entry[0]),
+                )
         if 'gidNumber' in self.parameters:
             self.parameters['gidNumber'] -= cfg.nss_gid_offset
         return super(Search, self).mk_filter()
diff --git a/pynslcd/host.py b/pynslcd/host.py
index 04f5337..c6639df 100644
--- a/pynslcd/host.py
+++ b/pynslcd/host.py
@@ -1,7 +1,7 @@
 
 # host.py - lookup functions for host names and addresses
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,7 +24,9 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn', ipHostNumber='ipHostNumber')
+attmap = common.Attributes(
+    cn='cn',
+    ipHostNumber='ipHostNumber')
 filter = '(objectClass=ipHost)'
 
 
diff --git a/pynslcd/invalidator.py b/pynslcd/invalidator.py
index bed03bb..6d2eefe 100644
--- a/pynslcd/invalidator.py
+++ b/pynslcd/invalidator.py
@@ -32,10 +32,10 @@ signalfd = None
 
 # mapping between map name and signal character
 _db_to_char = dict(
-        aliases='A', ethers='E', group='G', hosts='H', netgroup='U',
-        networks='N', passwd='P', protocols='L', rpc='R', services='V',
-        shadow='S', nfsidmap='F',
-    )
+    aliases='A', ethers='E', group='G', hosts='H', netgroup='U',
+    networks='N', passwd='P', protocols='L', rpc='R', services='V',
+    shadow='S', nfsidmap='F',
+)
 _char_to_db = dict((reversed(item) for item in _db_to_char.items()))
 
 
diff --git a/pynslcd/mypidfile.py b/pynslcd/mypidfile.py
index 44570ea..42935e2 100644
--- a/pynslcd/mypidfile.py
+++ b/pynslcd/mypidfile.py
@@ -26,8 +26,10 @@ import cfg
 
 
 class MyPIDLockFile(object):
-    """Implementation of a PIDFile fit for use with the daemon module
-    that locks the PIDFile with fcntl.lockf()."""
+    """A PIDFile for use with the daemon module.
+
+    This class that locks the PIDFile with fcntl.lockf().
+    """
 
     def __init__(self, path):
         self.path = path
diff --git a/pynslcd/netgroup.py b/pynslcd/netgroup.py
index d86e38c..47a4c6e 100644
--- a/pynslcd/netgroup.py
+++ b/pynslcd/netgroup.py
@@ -1,7 +1,7 @@
 
 # netgroup.py - lookup functions for netgroups
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -26,12 +26,14 @@ import constants
 import search
 
 
-_netgroup_triple_re = 
re.compile(r'^\s*\(\s*(?P<host>.*)\s*,\s*(?P<user>.*)\s*,\s*(?P<domain>.*)\s*\)\s*$')
+_netgroup_triple_re = re.compile(
+    r'^\s*\(\s*(?P<host>.*)\s*,\s*(?P<user>.*)\s*,\s*(?P<domain>.*)\s*\)\s*$')
 
 
-attmap = common.Attributes(cn='cn',
-                           nisNetgroupTriple='nisNetgroupTriple',
-                           memberNisNetgroup='memberNisNetgroup')
+attmap = common.Attributes(
+    cn='cn',
+    nisNetgroupTriple='nisNetgroupTriple',
+    memberNisNetgroup='memberNisNetgroup')
 filter = '(objectClass=nisNetgroup)'
 
 
diff --git a/pynslcd/network.py b/pynslcd/network.py
index 01bf6c2..da587b9 100644
--- a/pynslcd/network.py
+++ b/pynslcd/network.py
@@ -1,7 +1,7 @@
 
 # network.py - lookup functions for network names and addresses
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,8 +24,9 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn',
-                           ipNetworkNumber='ipNetworkNumber')
+attmap = common.Attributes(
+    cn='cn',
+    ipNetworkNumber='ipNetworkNumber')
 filter = '(objectClass=ipNetwork)'
 
 
diff --git a/pynslcd/pam.py b/pynslcd/pam.py
index 0fecb67..b372cdd 100644
--- a/pynslcd/pam.py
+++ b/pynslcd/pam.py
@@ -23,9 +23,9 @@ import random
 import socket
 import time
 
+import ldap
 from ldap.controls.ppolicy import PasswordPolicyControl, PasswordPolicyError
 from ldap.filter import escape_filter_chars
-import ldap
 
 import cfg
 import common
@@ -48,24 +48,39 @@ def authenticate(binddn, password):
     for ctrl in ctrls:
         if ctrl.controlType == PasswordPolicyControl.controlType:
             # found a password policy control
-            logging.debug('PasswordPolicyControl found: error=%s (%s), 
timeBeforeExpiration=%s, graceAuthNsRemaining=%s',
+            logging.debug(
+                'PasswordPolicyControl found: error=%s (%s), '
+                'timeBeforeExpiration=%s, graceAuthNsRemaining=%s',
                 'None' if ctrl.error is None else 
PasswordPolicyError(ctrl.error).prettyPrint(),
                 ctrl.error, ctrl.timeBeforeExpiration, 
ctrl.graceAuthNsRemaining)
             if ctrl.error == 0:  # passwordExpired
-                return conn, constants.NSLCD_PAM_AUTHTOK_EXPIRED, 
PasswordPolicyError(ctrl.error).prettyPrint()
+                return (
+                    conn, constants.NSLCD_PAM_AUTHTOK_EXPIRED,
+                    PasswordPolicyError(ctrl.error).prettyPrint())
             elif ctrl.error == 1:  # accountLocked
-                return conn, constants.NSLCD_PAM_ACCT_EXPIRED, 
PasswordPolicyError(ctrl.error).prettyPrint()
+                return (
+                    conn, constants.NSLCD_PAM_ACCT_EXPIRED,
+                    PasswordPolicyError(ctrl.error).prettyPrint())
             elif ctrl.error == 2:  # changeAfterReset
-                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password 
change is needed after reset'
+                return (
+                    conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD,
+                    'Password change is needed after reset')
             elif ctrl.error:
-                return conn, constants.NSLCD_PAM_PERM_DENIED, 
PasswordPolicyError(ctrl.error).prettyPrint()
+                return (
+                    conn, constants.NSLCD_PAM_PERM_DENIED,
+                    PasswordPolicyError(ctrl.error).prettyPrint())
             elif ctrl.timeBeforeExpiration is not None:
-                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password 
will expire in %d seconds' % ctrl.timeBeforeExpiration
+                return (
+                    conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD,
+                    'Password will expire in %d seconds' % 
ctrl.timeBeforeExpiration)
             elif ctrl.graceAuthNsRemaining is not None:
-                return conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD, 'Password 
expired, %d grace logins left' % ctrl.graceAuthNsRemaining
+                return (
+                    conn, constants.NSLCD_PAM_NEW_AUTHTOK_REQD,
+                    'Password expired, %d grace logins left' % 
ctrl.graceAuthNsRemaining)
     # perform search for own object (just to do any kind of search)
-    results = search.LDAPSearch(conn, base=binddn, scope=ldap.SCOPE_BASE,
-                                filter='(objectClass=*)', attributes=['dn', ])
+    results = search.LDAPSearch(
+        conn, base=binddn, scope=ldap.SCOPE_BASE,
+        filter='(objectClass=*)', attributes=['dn'])
     for entry in results:
         if entry[0] == binddn:
             return conn, constants.NSLCD_PAM_SUCCESS, ''
@@ -111,8 +126,7 @@ def update_lastchange(conns, userdn):
 class PAMRequest(common.Request):
 
     def validate(self, parameters):
-        """This method checks the provided username for validity and fills
-        in the DN if needed."""
+        """Check the username for validity and fill in the DN if needed."""
         # check username for validity
         common.validate_name(parameters['username'])
         # look up user DN
@@ -221,11 +235,10 @@ class PAMAuthorisationRequest(PAMRequest):
         # escape all parameters
         variables = dict((k, escape_filter_chars(v)) for k, v in 
parameters.items())
         variables.update(
-                hostname=escape_filter_chars(socket.gethostname()),
-                fqdn=escape_filter_chars(socket.getfqdn()),
-                dn=variables['userdn'],
-                uid=variables['username'],
-            )
+            hostname=escape_filter_chars(socket.gethostname()),
+            fqdn=escape_filter_chars(socket.getfqdn()),
+            dn=variables['userdn'],
+            uid=variables['username'])
         # go over all authz searches
         for x in cfg.pam_authz_searches:
             filter = x.value(variables)
diff --git a/pynslcd/passwd.py b/pynslcd/passwd.py
index 6c3f289..1274f21 100644
--- a/pynslcd/passwd.py
+++ b/pynslcd/passwd.py
@@ -1,7 +1,7 @@
 
 # passwd.py - lookup functions for user account information
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -27,23 +27,25 @@ import constants
 import search
 
 
-attmap = common.Attributes(uid='uid',
-                           userPassword='"*"',
-                           uidNumber='uidNumber',
-                           gidNumber='gidNumber',
-                           gecos='"${gecos:-$cn}"',
-                           homeDirectory='homeDirectory',
-                           loginShell='loginShell',
-                           objectClass='objectClass')
+attmap = common.Attributes(
+    uid='uid',
+    userPassword='"*"',
+    uidNumber='uidNumber',
+    gidNumber='gidNumber',
+    gecos='"${gecos:-$cn}"',
+    homeDirectory='homeDirectory',
+    loginShell='loginShell',
+    objectClass='objectClass')
 filter = '(objectClass=posixAccount)'
 
 
 class Search(search.LDAPSearch):
 
-    case_sensitive = ('uid', 'uidNumber', )
-    limit_attributes = ('uid', 'uidNumber', )
-    required = ('uid', 'uidNumber', 'gidNumber', 'gecos', 'homeDirectory',
-                'loginShell')
+    case_sensitive = ('uid', 'uidNumber')
+    limit_attributes = ('uid', 'uidNumber')
+    required = (
+        'uid', 'uidNumber', 'gidNumber', 'gecos', 'homeDirectory',
+        'loginShell')
 
     def mk_filter(self):
         if 'uidNumber' in self.parameters:
@@ -137,8 +139,7 @@ class PasswdAllRequest(PasswdRequest):
 
 
 def uid2entry(conn, uid):
-    """Look up the user by uid and return the LDAP entry or None if the user
-    was not found."""
+    """Look up the user by uid and return the LDAP entry or None."""
     for dn, attributes in Search(conn, parameters=dict(uid=uid)):
         if any((int(x) + cfg.nss_uid_offset) >= cfg.nss_min_uid for x in 
attributes['uidNumber']):
             return dn, attributes
@@ -148,8 +149,7 @@ def uid2entry(conn, uid):
 
 
 def dn2uid(conn, dn):
-    """Look up the user by dn and return a uid or None if the user was
-    not found."""
+    """Look up the user by dn and return a uid or None."""
     for dn, attributes in Search(conn, base=dn):
         if any((int(x) + cfg.nss_uid_offset) >= cfg.nss_min_uid for x in 
attributes['uidNumber']):
             return attributes['uid'][0]
diff --git a/pynslcd/protocol.py b/pynslcd/protocol.py
index 1472c04..dc41c4b 100644
--- a/pynslcd/protocol.py
+++ b/pynslcd/protocol.py
@@ -1,7 +1,7 @@
 
 # protocol.py - protocol name and number lookup routines
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,7 +24,9 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn', ipProtocolNumber='ipProtocolNumber')
+attmap = common.Attributes(
+    cn='cn',
+    ipProtocolNumber='ipProtocolNumber')
 filter = '(objectClass=ipProtocol)'
 
 
diff --git a/pynslcd/pynslcd.py b/pynslcd/pynslcd.py
index d41e478..0691b61 100755
--- a/pynslcd/pynslcd.py
+++ b/pynslcd/pynslcd.py
@@ -30,13 +30,13 @@ import threading
 import daemon
 import ldap
 
-from tio import TIOStream
 import cfg
 import common
 import constants
 import invalidator
 import mypidfile
 import search
+from tio import TIOStream
 
 
 # the name of the program
@@ -94,7 +94,7 @@ def display_version(fp):
              'Copyright (C) 2010-2019 Arthur de Jong\n'
              'This is free software; see the source for copying conditions.  
There is NO\n'
              'warranty; not even for MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE.\n'
-             % {'PACKAGE_STRING': constants.PACKAGE_STRING, })
+             % {'PACKAGE_STRING': constants.PACKAGE_STRING})
 
 
 def display_usage(fp):
@@ -108,7 +108,7 @@ def display_usage(fp):
              "\n"
              "Report bugs to <%(PACKAGE_BUGREPORT)s>.\n"
              % {'program_name': program_name,
-                'PACKAGE_BUGREPORT': constants.PACKAGE_BUGREPORT, })
+                'PACKAGE_BUGREPORT': constants.PACKAGE_BUGREPORT})
 
 
 def parse_cmdline():
@@ -149,7 +149,7 @@ def parse_cmdline():
 
 
 def create_socket():
-    """Returns a socket ready to answer requests from the client."""
+    """Return a socket ready to answer requests from the client."""
     import socket
     import fcntl
     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
@@ -179,7 +179,7 @@ def getpeercred(fd):
     import struct
     import socket
     try:
-        SO_PEERCRED = getattr(socket, 'SO_PEERCRED', 17)
+        SO_PEERCRED = getattr(socket, 'SO_PEERCRED', 17)  # noqa: N806
         creds = fd.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, 
struct.calcsize('3i'))
         pid, uid, gid = struct.unpack('3i', creds)
         return uid, gid, pid
@@ -204,7 +204,7 @@ handlers.update(common.get_handlers('pam'))
 handlers.update(common.get_handlers('usermod'))
 
 
-def acceptconnection(session):
+def acceptconnection(nslcd_serversocket, session):
     # accept a new connection
     conn, addr = nslcd_serversocket.accept()
     # See: http://docs.python.org/library/socket.html#socket.socket.settimeout
@@ -253,17 +253,17 @@ def disable_nss_ldap():
         logging.warn('probably older NSS module loaded', exc_info=True)
 
 
-def worker():
+def worker(nslcd_serversocket):
     session = search.Connection()
     while True:
         try:
-            acceptconnection(session)
+            acceptconnection(nslcd_serversocket, session)
         except Exception:
             logging.exception('exception in worker')
             # ignore all exceptions, just keep going
 
 
-if __name__ == '__main__':
+def main():  # noqa: C901 (long function)
     # parse options
     parse_cmdline()
     # clean the environment
@@ -277,7 +277,7 @@ if __name__ == '__main__':
     # disable ldap lookups of host names to avoid lookup loop
     disable_nss_ldap()
     # TODO: implement
-    #if myldap_set_debuglevel(cfg.debug) != LDAP_SUCCESS:
+    # if myldap_set_debuglevel(cfg.debug) != LDAP_SUCCESS:
     #    sys.exit(1)
     # read configuration file
     cfg.read(constants.NSLCD_CONF_PATH)
@@ -309,12 +309,12 @@ if __name__ == '__main__':
         ctx = pidfile
     else:
         ctx = daemon.DaemonContext(
-                      pidfile=pidfile,
-                      signal_map={
-                          signal.SIGTERM: u'terminate',
-                          signal.SIGINT: u'terminate',
-                          signal.SIGPIPE: None,
-                      })
+            pidfile=pidfile,
+            signal_map={
+                signal.SIGTERM: u'terminate',
+                signal.SIGINT: u'terminate',
+                signal.SIGPIPE: None,
+            })
     # start daemon
     with ctx:
         try:
@@ -364,7 +364,9 @@ if __name__ == '__main__':
             # start worker threads
             threads = []
             for i in range(cfg.threads):
-                thread = threading.Thread(target=worker, name='thread%d' % i)
+                thread = threading.Thread(
+                    target=worker, args=(nslcd_serversocket, ),
+                    name='thread%d' % i)
                 thread.setDaemon(True)
                 thread.start()
                 logging.debug('started thread %s', thread.getName())
@@ -375,3 +377,7 @@ if __name__ == '__main__':
         except Exception:
             logging.exception('main loop exit')
             # no need to re-raise since we are exiting anyway
+
+
+if __name__ == '__main__':
+    main()
diff --git a/pynslcd/rpc.py b/pynslcd/rpc.py
index 2a241fd..49d9c7c 100644
--- a/pynslcd/rpc.py
+++ b/pynslcd/rpc.py
@@ -1,7 +1,7 @@
 
 # rpc.py - rpc name lookup routines
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -24,7 +24,9 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn', oncRpcNumber='oncRpcNumber')
+attmap = common.Attributes(
+    cn='cn',
+    oncRpcNumber='oncRpcNumber')
 filter = '(objectClass=oncRpc)'
 
 
diff --git a/pynslcd/search.py b/pynslcd/search.py
index b61f638..39850d2 100644
--- a/pynslcd/search.py
+++ b/pynslcd/search.py
@@ -37,8 +37,8 @@ first_search = True
 class Connection(ldap.ldapobject.ReconnectLDAPObject):
 
     def __init__(self):
-        ldap.ldapobject.ReconnectLDAPObject.__init__(self, cfg.uri,
-            retry_max=1, retry_delay=cfg.reconnect_retrytime)
+        ldap.ldapobject.ReconnectLDAPObject.__init__(
+            self, cfg.uri, retry_max=1, retry_delay=cfg.reconnect_retrytime)
         # set connection-specific LDAP options
         if cfg.ldap_version:
             self.set_option(ldap.OPT_PROTOCOL_VERSION, cfg.ldap_version)
@@ -82,9 +82,10 @@ class Connection(ldap.ldapobject.ReconnectLDAPObject):
 
 
 class LDAPSearch(object):
-    """
-    Class that performs an LDAP search. Subclasses are expected to define the
-    actual searches and should implement the following members:
+    """Class that performs an LDAP search.
+
+    Subclasses are expected to define the actual searches and should
+    implement the following members:
 
       case_sensitive - check that these attributes are present in the response
                        if they were in the request
@@ -105,7 +106,6 @@ class LDAPSearch(object):
       scope - search scope, falls back to cfg.scope if absent or empty
       filter - an LDAP search filter
       attmap - an attribute mapping definition (using he Attributes class)
-
     """
 
     canonical_first = []
@@ -157,8 +157,11 @@ class LDAPSearch(object):
         return self.filter
 
     def _transform(self, dn, attributes):
-        """Handle a single search result entry filtering it with the request
-        parameters, search options and attribute mapping."""
+        """Filter and transform search result entry.
+
+        This performs filtering with request parameters, search options and
+        performs attribute mapping.
+        """
         # convert attributes to strings where appropriate
         attributes = dict(
             (attr, [value.decode('utf-8') for value in values] if attr != 
'objectSid' else values)
diff --git a/pynslcd/service.py b/pynslcd/service.py
index c27f485..b0c53e3 100644
--- a/pynslcd/service.py
+++ b/pynslcd/service.py
@@ -1,7 +1,7 @@
 
 # service.py - service entry lookup routines
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -26,9 +26,10 @@ import constants
 import search
 
 
-attmap = common.Attributes(cn='cn',
-                           ipServicePort='ipServicePort',
-                           ipServiceProtocol='ipServiceProtocol')
+attmap = common.Attributes(
+    cn='cn',
+    ipServicePort='ipServicePort',
+    ipServiceProtocol='ipServiceProtocol')
 filter = '(objectClass=ipService)'
 
 
@@ -59,8 +60,10 @@ class Cache(cache.Cache):
             ON DELETE CASCADE ON UPDATE CASCADE,
             FOREIGN KEY(`ipServiceProtocol`) REFERENCES 
`service_cache`(`ipServiceProtocol`)
             ON DELETE CASCADE ON UPDATE CASCADE );
-        CREATE INDEX IF NOT EXISTS `service_alias_idx1` ON 
`service_alias_cache`(`ipServicePort`);
-        CREATE INDEX IF NOT EXISTS `service_alias_idx2` ON 
`service_alias_cache`(`ipServiceProtocol`);
+        CREATE INDEX IF NOT EXISTS `service_alias_idx1`
+            ON `service_alias_cache`(`ipServicePort`);
+        CREATE INDEX IF NOT EXISTS `service_alias_idx2`
+            ON `service_alias_cache`(`ipServiceProtocol`);
     '''
 
     retrieve_sql = '''
diff --git a/pynslcd/shadow.py b/pynslcd/shadow.py
index 7d0ebf9..0f5441c 100644
--- a/pynslcd/shadow.py
+++ b/pynslcd/shadow.py
@@ -1,7 +1,7 @@
 
 # shadow.py - lookup functions for shadow information
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -25,15 +25,16 @@ import constants
 import search
 
 
-attmap = common.Attributes(uid='uid',
-                           userPassword='"*"',
-                           shadowLastChange='"${shadowLastChange:--1}"',
-                           shadowMin='"${shadowMin:--1}"',
-                           shadowMax='"${shadowMax:--1}"',
-                           shadowWarning='"${shadowWarning:--1}"',
-                           shadowInactive='"${shadowInactive:--1}"',
-                           shadowExpire='"${shadowExpire:--1}"',
-                           shadowFlag='"${shadowFlag:-0}"')
+attmap = common.Attributes(
+    uid='uid',
+    userPassword='"*"',
+    shadowLastChange='"${shadowLastChange:--1}"',
+    shadowMin='"${shadowMin:--1}"',
+    shadowMax='"${shadowMax:--1}"',
+    shadowWarning='"${shadowWarning:--1}"',
+    shadowInactive='"${shadowInactive:--1}"',
+    shadowExpire='"${shadowExpire:--1}"',
+    shadowFlag='"${shadowFlag:-0}"')
 filter = '(objectClass=shadowAccount)'
 
 
diff --git a/pynslcd/tio.py b/pynslcd/tio.py
index eb1a695..bec3ace 100644
--- a/pynslcd/tio.py
+++ b/pynslcd/tio.py
@@ -36,8 +36,7 @@ class TIOStreamError(Exception):
 
 
 class TIOStream(object):
-    """File-like object that allows reading and writing nslcd-protocol
-    entities."""
+    """File-like object for reading and writing nslcd-protocol entities."""
 
     def __init__(self, conn):
         conn.setblocking(1)
@@ -65,8 +64,10 @@ class TIOStream(object):
         return value
 
     def read_address(self):
-        """Read an address (usually IPv4 or IPv6) from the stream and return
-        the address as a string representation."""
+        """Read an address (usually IPv4 or IPv6) from the stream.
+
+        This returns the address as a string representation.
+        """
         af = self.read_int32()
         return socket.inet_ntop(af, self.read_bytes(maxsize=64))
 
@@ -103,8 +104,7 @@ class TIOStream(object):
         return socket.AF_INET6, socket.inet_pton(socket.AF_INET6, value)
 
     def write_address(self, value):
-        """Write an address (usually IPv4 or IPv6) in a string representation
-        to the stream."""
+        """Write an address (usually IPv4 or IPv6) to the stream."""
         # first try to make it into an IPv6 address
         af, address = TIOStream._to_address(value)
         self.write_int32(af)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index a92b7d0..1bbbcc2 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -47,13 +47,15 @@ check_PROGRAMS = test_dict test_set test_tio test_expr 
test_getpeercred \
 EXTRA_DIST = README nslcd-test.conf usernames.txt testenv.sh test_myldap.sh \
              test_nsscmds.sh test_ldapcmds.sh test_pamcmds.sh \
              test_pamcmds.expect test_manpages.sh \
-             test_pycompile.sh test_pylint.sh pylint.rc test_doctest.sh \
+             test_pycompile.sh test_doctest.sh \
+             test_pylint.sh pylint.rc \
+             test_flake8.sh flake8.ini \
              test_pynslcd_cache.py \
              setup_slapd.sh config.ldif test.ldif
 
 CLEANFILES = $(EXTRA_PROGRAMS) test_pamcmds.log
 clean-local:
-       -rm -rf *.pyc *.pyo __pycache__
+       -rm -rf *.pyc *.pyo __pycache__ flake8-venv
 
 AM_CPPFLAGS = -I$(top_srcdir)
 AM_CFLAGS = $(PTHREAD_CFLAGS) -g
diff --git a/tests/flake8.ini b/tests/flake8.ini
new file mode 100644
index 0000000..b45c50e
--- /dev/null
+++ b/tests/flake8.ini
@@ -0,0 +1,14 @@
+[flake8]
+ignore =
+  D100,D101,D102,D103  # FIXME: we miss quite a few docstrings
+  D105  # Missing docstring in magic method
+  D107  # Missing docstring in __init__
+  E306  # we don't want blank lines around line functions
+  Q000  # FIXME: find better solution
+  T001  # we use print statements in utils
+  W504  # we put the binary operator on the preceding line
+max-complexity = 16
+max-line-length = 100
+multiline-quotes = '''
+avoid-escape = false
+filename = *.py,*.py.in
diff --git a/tests/test_flake8.sh b/tests/test_flake8.sh
new file mode 100755
index 0000000..461ec36
--- /dev/null
+++ b/tests/test_flake8.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+# test_flake8.sh - run Python flake8 tests
+#
+# Copyright (C) 2019 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+set -e
+
+# find source directory
+srcdir="${srcdir-`dirname "$0"`}"
+builddir="${builddir-`dirname "$0"`}"
+top_srcdir="${top_srcdir-${srcdir}/..}"
+top_builddir="${top_builddir-${builddir}/..}"
+python="${PYTHON-python}"
+
+# if Python is missing, ignore
+if ! ${python} --version > /dev/null 2> /dev/null
+then
+  echo "Python (${python}) not found"
+  exit 77
+fi
+
+# find virtualenv command
+if ! virtualenv --version > /dev/null 2>&1
+then
+  echo "virtualenv: command not found"
+  exit 77
+fi
+
+# create virtualenv
+venv="${builddir}/flake8-venv"
+[ -x "$venv"/bin/pip ] || virtualenv "$venv" --python="$python"
+"$venv"/bin/pip install \
+  flake8 \
+  flake8-author \
+  flake8-blind-except \
+  flake8-class-newline \
+  flake8-commas \
+  flake8-deprecated \
+  flake8-docstrings \
+  flake8-exact-pin \
+  flake8-print \
+  flake8-quotes \
+  flake8-tidy-imports \
+  flake8-tuple \
+  pep8-naming
+
+# run flake8 over pynslcd
+"$venv"/bin/flake8 \
+  --config="${srcdir}/flake8.ini" \
+  "${top_srcdir}/pynslcd"
+
+# run flake8 over utils
+"$venv"/bin/flake8 \
+  --config="${srcdir}/flake8.ini" \
+  "${top_srcdir}/utils"
+
+# run flake8 over tests
+"$venv"/bin/flake8 \
+  --config="${srcdir}/flake8.ini" \
+  "${top_srcdir}/tests"/*.py
diff --git a/tests/test_pynslcd_cache.py b/tests/test_pynslcd_cache.py
index a57cedb..dcabb04 100755
--- a/tests/test_pynslcd_cache.py
+++ b/tests/test_pynslcd_cache.py
@@ -45,22 +45,28 @@ class TestAlias(unittest.TestCase):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
-            ['alias1', ['member1', 'member2']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias1')),
+            [
+                ['alias1', ['member1', 'member2']],
+            ])
 
     def test_by_member(self):
-        
self.assertItemsEqual(self.cache.retrieve(dict(rfc822MailMember='member1')), [
-            ['alias1', ['member1', 'member2']],
-            ['alias2', ['member1', 'member3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(rfc822MailMember='member1')),
+            [
+                ['alias1', ['member1', 'member2']],
+                ['alias2', ['member1', 'member3']],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            ['alias1', ['member1', 'member2']],
-            ['alias2', ['member1', 'member3']],
-            ['alias3', []],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                ['alias1', ['member1', 'member2']],
+                ['alias2', ['member1', 'member3']],
+                ['alias3', []],
+            ])
 
 
 class TestEther(unittest.TestCase):
@@ -75,24 +81,32 @@ class TestEther(unittest.TestCase):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='name1')), [
-            ['name1', '0:18:8a:54:1a:11'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='name2')), [
-            ['name2', '0:18:8a:54:1a:22'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='name1')),
+            [
+                ['name1', '0:18:8a:54:1a:11'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='name2')),
+            [
+                ['name2', '0:18:8a:54:1a:22'],
+            ])
 
     def test_by_ether(self):
         # ideally we should also support alternate representations
-        
self.assertItemsEqual(self.cache.retrieve(dict(macAddress='0:18:8a:54:1a:22')), 
[
-            ['name2', '0:18:8a:54:1a:22'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(macAddress='0:18:8a:54:1a:22')),
+            [
+                ['name2', '0:18:8a:54:1a:22'],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            ['name1', '0:18:8a:54:1a:11'],
-            ['name2', '0:18:8a:54:1a:22'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                ['name1', '0:18:8a:54:1a:11'],
+                ['name2', '0:18:8a:54:1a:22'],
+            ])
 
 
 class TestGroup(unittest.TestCase):
@@ -103,48 +117,64 @@ class TestGroup(unittest.TestCase):
         cache.store('group1', 'pass1', 10, ['user1', 'user2'])
         cache.store('group2', 'pass2', 20, ['user1', 'user2', 'user3'])
         cache.store('group3', 'pass3', 30, [])
-        cache.store('group4', 'pass4', 40, ['user2', ])
+        cache.store('group4', 'pass4', 40, ['user2'])
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='group1')), [
-            ['group1', 'pass1', 10, ['user1', 'user2']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='group3')), [
-            ['group3', 'pass3', 30, []],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='group1')),
+            [
+                ['group1', 'pass1', 10, ['user1', 'user2']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='group3')),
+            [
+                ['group3', 'pass3', 30, []],
+            ])
 
     def test_by_gid(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(gidNumber=10)), [
-            ['group1', 'pass1', 10, ['user1', 'user2']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(gidNumber=40)), [
-            ['group4', 'pass4', 40, ['user2']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(gidNumber=10)),
+            [
+                ['group1', 'pass1', 10, ['user1', 'user2']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(gidNumber=40)),
+            [
+                ['group4', 'pass4', 40, ['user2']],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            ['group1', 'pass1', 10, ['user1', 'user2']],
-            ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
-            ['group3', 'pass3', 30, []],
-            ['group4', 'pass4', 40, ['user2']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                ['group1', 'pass1', 10, ['user1', 'user2']],
+                ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
+                ['group3', 'pass3', 30, []],
+                ['group4', 'pass4', 40, ['user2']],
+            ])
 
     def test_bymember(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(memberUid='user1')), [
-            ['group1', 'pass1', 10, ['user1', 'user2']],
-            ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(memberUid='user2')), [
-            ['group1', 'pass1', 10, ['user1', 'user2']],
-            ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
-            ['group4', 'pass4', 40, ['user2']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(memberUid='user3')), [
-            ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(memberUid='user1')),
+            [
+                ['group1', 'pass1', 10, ['user1', 'user2']],
+                ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(memberUid='user2')),
+            [
+                ['group1', 'pass1', 10, ['user1', 'user2']],
+                ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
+                ['group4', 'pass4', 40, ['user2']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(memberUid='user3')),
+            [
+                ['group2', 'pass2', 20, ['user1', 'user2', 'user3']],
+            ])
 
 
 class TestHost(unittest.TestCase):
@@ -152,32 +182,42 @@ class TestHost(unittest.TestCase):
     def setUp(self):
         import host
         cache = host.Cache()
-        cache.store('hostname1', [], ['127.0.0.1', ])
+        cache.store('hostname1', [], ['127.0.0.1'])
         cache.store('hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3'])
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='hostname1')), [
-            ['hostname1', [], ['127.0.0.1']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='hostname2')), [
-            ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='hostname1')),
+            [
+                ['hostname1', [], ['127.0.0.1']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='hostname2')),
+            [
+                ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
     def test_by_alias(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
-            ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias2')), [
-            ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias1')),
+            [
+                ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias2')),
+            [
+                ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
     def test_by_address(self):
-        
self.assertItemsEqual(self.cache.retrieve(dict(ipHostNumber='127.0.0.3')), [
-            ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipHostNumber='127.0.0.3')),
+            [
+                ['hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
 
 class TestNetgroup(unittest.TestCase):
@@ -185,19 +225,31 @@ class TestNetgroup(unittest.TestCase):
     def setUp(self):
         import netgroup
         cache = netgroup.Cache()
-        cache.store('netgroup1', ['(host1, user1,)', '(host1, user2,)', 
'(host2, user1,)'], ['netgroup2', ])
-        cache.store('netgroup2', ['(host3, user1,)', '(host3, user3,)'], [])
+        cache.store(
+            'netgroup1',
+            ['(host1, user1,)', '(host1, user2,)', '(host2, user1,)'],
+            ['netgroup2'])
+        cache.store(
+            'netgroup2', ['(host3, user1,)', '(host3, user3,)'], [])
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='netgroup1')), [
-            ['netgroup1', ['(host1, user1,)', '(host1, user2,)', '(host2, 
user1,)'], ['netgroup2', ]],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='netgroup2')), [
-            ['netgroup2', ['(host3, user1,)', '(host3, user3,)'], []],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='netgroup1')),
+            [
+                [
+                    'netgroup1',
+                    ['(host1, user1,)', '(host1, user2,)', '(host2, user1,)'],
+                    ['netgroup2'],
+                ],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='netgroup2')),
+            [
+                ['netgroup2', ['(host3, user1,)', '(host3, user3,)'], []],
+            ])
 
 
 class TestNetwork(unittest.TestCase):
@@ -205,32 +257,42 @@ class TestNetwork(unittest.TestCase):
     def setUp(self):
         import network
         cache = network.Cache()
-        cache.store('networkname1', [], ['127.0.0.1', ])
+        cache.store('networkname1', [], ['127.0.0.1'])
         cache.store('networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3'])
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='networkname1')), [
-            ['networkname1', [], ['127.0.0.1']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='networkname2')), [
-            ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='networkname1')),
+            [
+                ['networkname1', [], ['127.0.0.1']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='networkname2')),
+            [
+                ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
     def test_by_alias(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
-            ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias2')), [
-            ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias1')),
+            [
+                ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias2')),
+            [
+                ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
     def test_by_address(self):
-        
self.assertItemsEqual(self.cache.retrieve(dict(ipNetworkNumber='127.0.0.3')), [
-            ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', '127.0.0.3']],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipNetworkNumber='127.0.0.3')),
+            [
+                ['networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3']],
+            ])
 
 
 class TestPasswd(unittest.TestCase):
@@ -245,26 +307,36 @@ class TestPasswd(unittest.TestCase):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(uid='name')), [
-            [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uid='name')),
+            [
+                [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
+            ])
 
     def test_by_unknown_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(uid='notfound')), [])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uid='notfound')),
+            [])
 
     def test_by_number(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(uidNumber=100)), [
-            [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(uidNumber=101)), [
-            ['name2', 'passwd2', 101, 202, 'gecos2', '/home/user2', 
'/bin/bash'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uidNumber=100)),
+            [
+                [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uidNumber=101)),
+            [
+                ['name2', 'passwd2', 101, 202, 'gecos2', '/home/user2', 
'/bin/bash'],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
-            [u'name2', u'passwd2', 101, 202, u'gecos2', u'/home/user2', 
u'/bin/bash'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                [u'name', u'passwd', 100, 200, u'gecos', u'/home/user', 
u'/bin/bash'],
+                [u'name2', u'passwd2', 101, 202, u'gecos2', u'/home/user2', 
u'/bin/bash'],
+            ])
 
 
 class TestProtocol(unittest.TestCase):
@@ -273,48 +345,66 @@ class TestProtocol(unittest.TestCase):
         import protocol
         cache = protocol.Cache()
         cache.store('protocol1', ['alias1', 'alias2'], 100)
-        cache.store('protocol2', ['alias3', ], 200)
+        cache.store('protocol2', ['alias3'], 200)
         cache.store('protocol3', [], 300)
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='protocol1')), [
-            ['protocol1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='protocol2')), [
-            ['protocol2', ['alias3', ], 200],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='protocol3')), [
-            ['protocol3', [], 300],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='protocol1')),
+            [
+                ['protocol1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='protocol2')),
+            [
+                ['protocol2', ['alias3'], 200],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='protocol3')),
+            [
+                ['protocol3', [], 300],
+            ])
 
     def test_by_unknown_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='notfound')), [])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='notfound')),
+            [])
 
     def test_by_number(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(ipProtocolNumber=100)), 
[
-            ['protocol1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(ipProtocolNumber=200)), 
[
-            ['protocol2', ['alias3', ], 200],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipProtocolNumber=100)),
+            [
+                ['protocol1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipProtocolNumber=200)),
+            [
+                ['protocol2', ['alias3'], 200],
+            ])
 
     def test_by_alias(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
-            ['protocol1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias3')), [
-            ['protocol2', ['alias3', ], 200],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias1')),
+            [
+                ['protocol1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias3')),
+            [
+                ['protocol2', ['alias3'], 200],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            ['protocol1', ['alias1', 'alias2'], 100],
-            ['protocol2', ['alias3'], 200],
-            ['protocol3', [], 300],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                ['protocol1', ['alias1', 'alias2'], 100],
+                ['protocol2', ['alias3'], 200],
+                ['protocol3', [], 300],
+            ])
 
 
 class TestRpc(unittest.TestCase):
@@ -323,48 +413,66 @@ class TestRpc(unittest.TestCase):
         import rpc
         cache = rpc.Cache()
         cache.store('rpc1', ['alias1', 'alias2'], 100)
-        cache.store('rpc2', ['alias3', ], 200)
+        cache.store('rpc2', ['alias3'], 200)
         cache.store('rpc3', [], 300)
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='rpc1')), [
-            ['rpc1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='rpc2')), [
-            ['rpc2', ['alias3', ], 200],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='rpc3')), [
-            ['rpc3', [], 300],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='rpc1')),
+            [
+                ['rpc1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='rpc2')),
+            [
+                ['rpc2', ['alias3'], 200],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='rpc3')),
+            [
+                ['rpc3', [], 300],
+            ])
 
     def test_by_unknown_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='notfound')), [])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='notfound')),
+            [])
 
     def test_by_number(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(oncRpcNumber=100)), [
-            ['rpc1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(oncRpcNumber=200)), [
-            ['rpc2', ['alias3', ], 200],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(oncRpcNumber=100)),
+            [
+                ['rpc1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(oncRpcNumber=200)),
+            [
+                ['rpc2', ['alias3'], 200],
+            ])
 
     def test_by_alias(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
-            ['rpc1', ['alias1', 'alias2'], 100],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='alias3')), [
-            ['rpc2', ['alias3', ], 200],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias1')),
+            [
+                ['rpc1', ['alias1', 'alias2'], 100],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='alias3')),
+            [
+                ['rpc2', ['alias3'], 200],
+            ])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            ['rpc1', ['alias1', 'alias2'], 100],
-            ['rpc2', ['alias3'], 200],
-            ['rpc3', [], 300],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}),
+            [
+                ['rpc1', ['alias1', 'alias2'], 100],
+                ['rpc2', ['alias3'], 200],
+                ['rpc3', [], 300],
+            ])
 
 
 class TestService(unittest.TestCase):
@@ -374,61 +482,85 @@ class TestService(unittest.TestCase):
         cache = service.Cache()
         cache.store('service1', ['alias1', 'alias2'], 100, 'tcp')
         cache.store('service1', ['alias1', 'alias2'], 100, 'udp')
-        cache.store('service2', ['alias3', ], 200, 'udp')
+        cache.store('service2', ['alias3'], 200, 'udp')
         cache.store('service3', [], 300, 'udp')
         self.cache = cache
         if not hasattr(self, 'assertItemsEqual'):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service1')), [
-            ['service1', ['alias1', 'alias2'], 100, 'tcp'],
-            ['service1', ['alias1', 'alias2'], 100, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service2')), [
-            ['service2', ['alias3', ], 200, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service3')), [
-            ['service3', [], 300, 'udp'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service1')),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'tcp'],
+                ['service1', ['alias1', 'alias2'], 100, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service2')),
+            [
+                ['service2', ['alias3'], 200, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service3')),
+            [
+                ['service3', [], 300, 'udp'],
+            ])
 
     def test_by_name_and_protocol(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service1', 
ipServiceProtocol='udp')), [
-            ['service1', ['alias1', 'alias2'], 100, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service1', 
ipServiceProtocol='tcp')), [
-            ['service1', ['alias1', 'alias2'], 100, 'tcp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service2', 
ipServiceProtocol='udp')), [
-            ['service2', ['alias3', ], 200, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(cn='service2', 
ipServiceProtocol='tcp')), [
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service1', ipServiceProtocol='udp')),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service1', ipServiceProtocol='tcp')),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'tcp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service2', ipServiceProtocol='udp')),
+            [
+                ['service2', ['alias3'], 200, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(cn='service2', ipServiceProtocol='tcp')),
+            [])
 
     def test_by_unknown_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='notfound')), [])
 
     def test_by_number(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=100)), [
-            ['service1', ['alias1', 'alias2'], 100, 'tcp'],
-            ['service1', ['alias1', 'alias2'], 100, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=200)), [
-            ['service2', ['alias3', ], 200, 'udp'],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=100)),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'tcp'],
+                ['service1', ['alias1', 'alias2'], 100, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=200)),
+            [
+                ['service2', ['alias3'], 200, 'udp'],
+            ])
 
     def test_by_number_and_protocol(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=100, 
ipServiceProtocol='udp')), [
-            ['service1', ['alias1', 'alias2'], 100, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=100, 
ipServiceProtocol='tcp')), [
-            ['service1', ['alias1', 'alias2'], 100, 'tcp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=200, 
ipServiceProtocol='udp')), [
-            ['service2', ['alias3', ], 200, 'udp'],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(ipServicePort=200, 
ipServiceProtocol='tcp')), [
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=100, 
ipServiceProtocol='udp')),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=100, 
ipServiceProtocol='tcp')),
+            [
+                ['service1', ['alias1', 'alias2'], 100, 'tcp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=200, 
ipServiceProtocol='udp')),
+            [
+                ['service2', ['alias3'], 200, 'udp'],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(ipServicePort=200, 
ipServiceProtocol='tcp')),
+            [])
 
     def test_by_alias(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
@@ -436,14 +568,14 @@ class TestService(unittest.TestCase):
             ['service1', ['alias1', 'alias2'], 100, 'tcp'],
         ])
         self.assertItemsEqual(self.cache.retrieve(dict(cn='alias3')), [
-            ['service2', ['alias3', ], 200, 'udp'],
+            ['service2', ['alias3'], 200, 'udp'],
         ])
 
     def test_all(self):
         self.assertItemsEqual(self.cache.retrieve({}), [
             ['service1', ['alias1', 'alias2'], 100, 'tcp'],
             ['service1', ['alias1', 'alias2'], 100, 'udp'],
-            ['service2', ['alias3', ], 200, 'udp'],
+            ['service2', ['alias3'], 200, 'udp'],
             ['service3', [], 300, 'udp'],
         ])
 
@@ -460,21 +592,28 @@ class Testshadow(unittest.TestCase):
             self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(uid='name')), [
-            [u'name', u'passwd', 15639, 0, 7, -1, -1, -1, 0],
-        ])
-        self.assertItemsEqual(self.cache.retrieve(dict(uid='name2')), [
-            [u'name2', u'passwd2', 15639, 0, 7, -1, -1, -1, 0],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uid='name')),
+            [
+                [u'name', u'passwd', 15639, 0, 7, -1, -1, -1, 0],
+            ])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uid='name2')),
+            [
+                [u'name2', u'passwd2', 15639, 0, 7, -1, -1, -1, 0],
+            ])
 
     def test_by_unknown_name(self):
-        self.assertItemsEqual(self.cache.retrieve(dict(uid='notfound')), [])
+        self.assertItemsEqual(
+            self.cache.retrieve(dict(uid='notfound')),
+            [])
 
     def test_all(self):
-        self.assertItemsEqual(self.cache.retrieve({}), [
-            [u'name', u'passwd', 15639, 0, 7, -1, -1, -1, 0],
-            [u'name2', u'passwd2', 15639, 0, 7, -1, -1, -1, 0],
-        ])
+        self.assertItemsEqual(
+            self.cache.retrieve({}), [
+                [u'name', u'passwd', 15639, 0, 7, -1, -1, -1, 0],
+                [u'name2', u'passwd2', 15639, 0, 7, -1, -1, -1, 0],
+            ])
 
 
 if __name__ == '__main__':
diff --git a/utils/chsh.py b/utils/chsh.py
index c48459d..e7537e7 100755
--- a/utils/chsh.py
+++ b/utils/chsh.py
@@ -3,7 +3,7 @@
 
 # chsh.py - program for changing the login shell using nslcd
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -22,11 +22,11 @@
 
 import argparse
 
-from cmdline import VersionAction, ListShellsAction
 import constants
 import nslcd
 import shells
 import users
+from cmdline import ListShellsAction, VersionAction
 
 
 # set up command line parser
@@ -42,11 +42,16 @@ parser.add_argument('username', metavar='USER', nargs='?',
 
 def ask_shell(oldshell):
     """Ask the user to provide a shell."""
-    shell = raw_input('  Login Shell [%s]: ' % oldshell)
+    # Provide Python 2 compatibility
+    try:
+        input = raw_input
+    except NameError:
+        pass
+    shell = input('  Login Shell [%s]: ' % oldshell)
     return shell or oldshell
 
 
-if __name__ == '__main__':
+def main():
     # parse arguments
     args = parser.parse_args()
     # check username part
@@ -64,8 +69,12 @@ if __name__ == '__main__':
         shell = ask_shell(user.shell)
         shells.check(shell, user.asroot)
     # perform the modification
-    result = nslcd.usermod(
+    nslcd.usermod(
         user.username, user.asroot, password, {
             constants.NSLCD_USERMOD_SHELL: shell,
         })
     # TODO: print proper response
+
+
+if __name__ == '__main__':
+    main()
diff --git a/utils/getent.py b/utils/getent.py
index c14e5e3..1de53c9 100755
--- a/utils/getent.py
+++ b/utils/getent.py
@@ -26,9 +26,9 @@ import socket
 import struct
 import sys
 
+import constants
 from cmdline import VersionAction
 from nslcd import NslcdClient
-import constants
 
 
 epilog = '''
@@ -56,9 +56,8 @@ parser.add_argument('keys', metavar='KEY', nargs='*',
 def write_aliases(con):
     while con.get_response() == constants.NSLCD_RESULT_BEGIN:
         print('%-16s%s' % (
-                con.read_string() + ': ',
-                ', '.join(con.read_stringlist()),
-            ))
+            con.read_string() + ': ',
+            ', '.join(con.read_stringlist())))
 
 
 def getent_aliases(database, keys=None):
@@ -95,11 +94,10 @@ def getent_ethers(database, keys=None):
 def write_group(con):
     while con.get_response() == constants.NSLCD_RESULT_BEGIN:
         print('%s:%s:%d:%s' % (
-                con.read_string(),
-                con.read_string(),
-                con.read_int32(),
-                ','.join(con.read_stringlist()),
-            ))
+            con.read_string(),
+            con.read_string(),
+            con.read_int32(),
+            ','.join(con.read_stringlist())))
 
 
 def getent_group(database, keys=None):
@@ -181,9 +179,7 @@ def _read_netgroup(con):
             members.append(con.read_string())
         elif member_type == constants.NSLCD_NETGROUP_TYPE_TRIPLE:
             tripples.append((
-                    con.read_string(), con.read_string(),
-                    con.read_string()
-                ))
+                con.read_string(), con.read_string(), con.read_string()))
         else:
             break
     return name, members, tripples
@@ -212,10 +208,9 @@ def _get_getgroups(con, recurse, netgroups=None):
 def write_netgroup(con, recurse):
     for name, members, tripples in _get_getgroups(con, recurse):
         print('%-15s %s' % (name, ' '.join(
-                members +
-                ['(%s, %s, %s)' % (host, user, domain)
-                 for host, user, domain in tripples]
-            )))
+            members +
+            ['(%s, %s, %s)' % (host, user, domain)
+             for host, user, domain in tripples])))
 
 
 def getent_netgroup(database, keys=None):
@@ -261,14 +256,13 @@ def getent_networks(database, keys=None):
 def write_passwd(con):
     while con.get_response() == constants.NSLCD_RESULT_BEGIN:
         print('%s:%s:%d:%d:%s:%s:%s' % (
-                con.read_string(),
-                con.read_string(),
-                con.read_int32(),
-                con.read_int32(),
-                con.read_string(),
-                con.read_string(),
-                con.read_string(),
-            ))
+            con.read_string(),
+            con.read_string(),
+            con.read_int32(),
+            con.read_int32(),
+            con.read_string(),
+            con.read_string(),
+            con.read_string()))
 
 
 def getent_passwd(database, keys=None):
@@ -365,16 +359,15 @@ def _shadow_value2str(number):
 def write_shadow(con):
     while con.get_response() == constants.NSLCD_RESULT_BEGIN:
         print('%s:%s:%s:%s:%s:%s:%s:%s:%s' % (
-                con.read_string(),
-                con.read_string(),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-                _shadow_value2str(con.read_int32()),
-            ))
+            con.read_string(),
+            con.read_string(),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32()),
+            _shadow_value2str(con.read_int32())))
 
 
 def getent_shadow(database, keys=None):
@@ -387,7 +380,7 @@ def getent_shadow(database, keys=None):
         write_shadow(con)
 
 
-if __name__ == '__main__':
+def main():  # noqa: C901
     args = parser.parse_args()
     try:
         if args.database == 'aliases':
@@ -417,3 +410,7 @@ if __name__ == '__main__':
     except struct.error:
         print('Problem communicating with nslcd')
         sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/utils/nslcd.py b/utils/nslcd.py
index 82e5bbb..8132a9a 100644
--- a/utils/nslcd.py
+++ b/utils/nslcd.py
@@ -40,7 +40,7 @@ class NslcdClient(object):
         fcntl.fcntl(self.sock, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
         # connect to nslcd
         self.sock.connect(constants.NSLCD_SOCKET)
-        #self.sock.setblocking(1)
+        # self.sock.setblocking(1)
         self.fp = os.fdopen(self.sock.fileno(), 'r+b', 0)
         # write a request header with a request code
         self.action = action
diff --git a/utils/users.py b/utils/users.py
index 25fb9da..b5932d0 100644
--- a/utils/users.py
+++ b/utils/users.py
@@ -2,7 +2,7 @@
 
 # users.py - functions for validating the user to change information for
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -40,8 +40,11 @@ class User(object):
         self.asroot = self.myuid != self.uid
 
     def check(self):
-        """Check if the user we want to modify is an LDAP user and whether
-        we may modify the user information."""
+        """Check whether we can modify the user.
+
+        Check if the user is an LDAP user and whether we may modify the user
+        information.
+        """
         if self.asroot and self.myuid != 0:
             print("%s: you may not modify user '%s'.\n" %
                   (sys.argv[0], self.username))
@@ -53,8 +56,7 @@ class User(object):
         # FIXME: only ask the password if we require it
         # (e.g. when root and nslcd has userpwmoddn we don't need to)
         return getpass.getpass(
-                'LDAP administrator password: '
-                if self.asroot else
-                'LDAP password for %s: ' % self.username
-            )
+            'LDAP administrator password: '
+            if self.asroot else
+            'LDAP password for %s: ' % self.username)
         # FIXME: check if the provided password is valid

https://arthurdejong.org/git/nss-pam-ldapd/commit/?id=221ce5a2680c1a91b7b87a36d73be5c0ad7e5ddb

commit 221ce5a2680c1a91b7b87a36d73be5c0ad7e5ddb
Author: Arthur de Jong <arthur@arthurdejong.org>
Date:   Mon Sep 2 23:32:30 2019 +0200

    Add Python 3 support
    
    This ensures that both pynslcd and the command-line utilities work with
    Python3 as interpreter and runs some tests with all installed Python
    interpreters.
    
    This drops support for Python 2.6 and extends 5a84be2 to perform more
    testing with Python 3.

diff --git a/configure.ac b/configure.ac
index e2c34b4..97b35cc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2006 Luke Howard
 # Copyright (C) 2006 West Consulting
-# Copyright (C) 2006-2018 Arthur de Jong
+# Copyright (C) 2006-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -23,7 +23,7 @@ AC_PREREQ(2.61)
 AC_COPYRIGHT(
 [Copyright (C) 2006 Luke Howard
 Copyright (C) 2006 West Consulting
-Copyright (C) 2006-2018 Arthur de Jong
+Copyright (C) 2006-2019 Arthur de Jong
 
 This configure script is derived from configure.ac which is free software;
 you can redistribute it and/or modify it under the terms of the GNU Lesser
@@ -93,7 +93,7 @@ then
 fi
 
 # check for Python and modules
-AM_PATH_PYTHON(2.5,, [:])
+AM_PATH_PYTHON(2.7,, [:])
 AM_CONDITIONAL([HAVE_PYTHON], [test "$PYTHON" != ":"])
 if test "x$PYTHON" != "x:"
 then
@@ -681,7 +681,7 @@ fi
 if test "x$enable_utils" = "xyes"
 then
   # check Python interpreter
-  AM_PATH_PYTHON(2.5,, AC_MSG_ERROR([Python is required]))
+  AM_PATH_PYTHON(2.7,, AC_MSG_ERROR([Python is required]))
   AX_PYTHON_MODULE(argparse)
   if test "x$HAVE_PYMOD_ARGPARSE" != "xyes"
   then
@@ -1006,7 +1006,7 @@ fi
 if test "x$enable_pynslcd" = "xyes"
 then
   # check Python interpreter
-  AM_PATH_PYTHON(2.5,, AC_MSG_ERROR([Python is required]))
+  AM_PATH_PYTHON(2.7,, AC_MSG_ERROR([Python is required]))
   AX_PYTHON_MODULE(daemon)
   AX_PYTHON_MODULE(fcntl)
   AX_PYTHON_MODULE(fnmatch)
diff --git a/pynslcd/Makefile.am b/pynslcd/Makefile.am
index a61ff65..cfc264d 100644
--- a/pynslcd/Makefile.am
+++ b/pynslcd/Makefile.am
@@ -1,6 +1,6 @@
 # Makefile.am - use automake to generate Makefile.in
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -31,7 +31,7 @@ all-local: $(nodist_pynslcd_PYTHON)
 
 # clean up locally created compiled Python files
 clean-local:
-       rm -f *.pyc *.pyo
+       -rm -rf *.pyc *.pyo __pycache__
 
 constants.py: constants.py.in $(top_srcdir)/nslcd.h
 
diff --git a/pynslcd/attmap.py b/pynslcd/attmap.py
index 6012d39..2af8ec4 100644
--- a/pynslcd/attmap.py
+++ b/pynslcd/attmap.py
@@ -1,7 +1,7 @@
 
 # attmap.py - attribute mapping class
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -29,8 +29,10 @@
 ...                    loginShell='loginShell')
 >>> 'cn' in attrs.attributes()
 True
->>> attrs.translate({'uid': ['UIDVALUE', '2nduidvalue'], 'cn': ['COMMON NAME', 
]})
-{'uid': ['UIDVALUE', '2nduidvalue'], 'loginShell': [], 'userPassword': [], 
'uidNumber': [], 'gidNumber': [], 'gecos': ['COMMON NAME'], 'homeDirectory': []}
+>>> attrs.translate({'uid': ['UIDVALUE', '2nduidvalue'], 'cn': ['COMMON NAME', 
]}) == {
+...     'uid': ['UIDVALUE', '2nduidvalue'], 'loginShell': [], 'userPassword': 
[],
+...     'uidNumber': [], 'gidNumber': [], 'gecos': ['COMMON NAME'], 
'homeDirectory': []}
+True
 >>> attrs['uidNumber']  # a representation fit for logging and filters
 'uidNumber'
 >>> attrs['gecos']
@@ -53,7 +55,7 @@ __all__ = ('Attributes', )
 
 
 # regular expression to match function attributes
-attribute_func_re = re.compile('^(?P<function>[a-z]+)\((?P<attribute>.*)\)$')
+attribute_func_re = re.compile(r'^(?P<function>[a-z]+)\((?P<attribute>.*)\)$')
 
 
 class SimpleMapping(str):
@@ -72,13 +74,19 @@ class SimpleMapping(str):
         return variables.get(self, [])
 
 
-class ExpressionMapping(str):
+class ExpressionMapping(object):
     """Class for parsing and expanding an expression."""
 
     def __init__(self, value):
         """Parse the expression as a string."""
+        self.value = value
         self.expression = Expression(value[1:-1])
-        super(ExpressionMapping, self).__init__(value)
+
+    def __str__(self):
+        return self.value
+
+    def __repr__(self):
+        return repr(str(self))
 
     def values(self, variables):
         """Expand the expression using the variables specified."""
@@ -89,7 +97,7 @@ class ExpressionMapping(str):
         return self.expression.variables()
 
 
-class FunctionMapping(str):
+class FunctionMapping(object):
     """Mapping to a function to another attribute."""
 
     def __init__(self, mapping):
@@ -97,7 +105,12 @@ class FunctionMapping(str):
         m = attribute_func_re.match(mapping)
         self.attribute = m.group('attribute')
         self.function = getattr(self, m.group('function'))
-        super(FunctionMapping, self).__init__(mapping)
+
+    def __str__(self):
+        return self.mapping
+
+    def __repr__(self):
+        return repr(str(self))
 
     def upper(self, value):
         return value.upper()
@@ -147,7 +160,7 @@ class Attributes(dict):
         attribute mapping. These are the attributes that should be
         requested in the search."""
         attributes = set()
-        for mapping in self.itervalues():
+        for mapping in self.values():
             attributes.update(mapping.attributes())
         return list(attributes)
 
@@ -161,7 +174,7 @@ class Attributes(dict):
         """Return a dictionary with every attribute mapped to their value from
         the specified variables."""
         results = dict()
-        for attribute, mapping in self.iteritems():
+        for attribute, mapping in self.items():
             results[attribute] = mapping.values(variables)
         return results
 
diff --git a/pynslcd/cache.py b/pynslcd/cache.py
index 99b520d..d077ac7 100644
--- a/pynslcd/cache.py
+++ b/pynslcd/cache.py
@@ -1,7 +1,7 @@
 
 # cache.py - caching layer for pynslcd
 #
-# Copyright (C) 2012, 2013 Arthur de Jong
+# Copyright (C) 2012-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -56,14 +56,20 @@ class regroup(object):
         row[self.group_column] = list(self._grouper(self.tgtkey))
         return row
 
+    def __next__(self):
+        return self.next()
+
     def _grouper(self, tgtkey):
         """Generate the group columns."""
-        while self.currkey == tgtkey:
-            value = self.currvalue[self.group_column]
-            if value is not None:
-                yield value
-            self.currvalue = next(self.it)    # Exit on StopIteration
-            self.currkey = self.keyfunc(self.currvalue)
+        try:
+            while self.currkey == tgtkey:
+                value = self.currvalue[self.group_column]
+                if value is not None:
+                    yield value
+                self.currvalue = next(self.it)
+                self.currkey = self.keyfunc(self.currvalue)
+        except StopIteration:
+            pass
 
 
 class Query(object):
diff --git a/pynslcd/cfg.py b/pynslcd/cfg.py
index dcbc8f7..9a18541 100644
--- a/pynslcd/cfg.py
+++ b/pynslcd/cfg.py
@@ -1,7 +1,7 @@
 
 # cfg.py - module for accessing configuration information
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -170,42 +170,42 @@ def read(filename):
         lineno += 1
         line = line.strip()
         # skip comments and blank lines
-        if re.match('(#.*)?$', line, re.IGNORECASE):
+        if re.match(r'(#.*)?$', line, re.IGNORECASE):
             continue
         # parse options with a single integer argument
-        m = 
re.match('(?P<keyword>threads|ldap_version|bind_timelimit|timelimit|idle_timelimit|reconnect_sleeptime|reconnect_retrytime|pagesize|nss_min_uid|nss_uid_offset|nss_gid_offset)\s+(?P<value>\d+)',
+        m = 
re.match(r'(?P<keyword>threads|ldap_version|bind_timelimit|timelimit|idle_timelimit|reconnect_sleeptime|reconnect_retrytime|pagesize|nss_min_uid|nss_uid_offset|nss_gid_offset)\s+(?P<value>\d+)',
                      line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = int(m.group('value'))
             continue
         # parse options with a single boolean argument
-        m = 
re.match('(?P<keyword>referrals|nss_nested_groups|nss_getgrent_skipmembers|nss_disable_enumeration)\s+(?P<value>%s)'
 %
+        m = 
re.match(r'(?P<keyword>referrals|nss_nested_groups|nss_getgrent_skipmembers|nss_disable_enumeration)\s+(?P<value>%s)'
 %
                          '|'.join(_boolean_options.keys()),
                      line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = 
_boolean_options[m.group('value').lower()]
             continue
         # parse options with a single no-space value
-        m = 
re.match('(?P<keyword>uid|gid|bindpw|rootpwmodpw|sasl_mech)\s+(?P<value>\S+)',
+        m = 
re.match(r'(?P<keyword>uid|gid|bindpw|rootpwmodpw|sasl_mech)\s+(?P<value>\S+)',
                      line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = m.group('value')
             continue
         # parse options with a single value that can contain spaces
-        m = 
re.match('(?P<keyword>binddn|rootpwmoddn|sasl_realm|sasl_authcid|sasl_authzid|sasl_secprops|krb5_ccname|tls_cacertdir|tls_cacertfile|tls_randfile|tls_ciphers|tls_cert|tls_key|pam_password_prohibit_message)\s+(?P<value>\S.*)',
+        m = 
re.match(r'(?P<keyword>binddn|rootpwmoddn|sasl_realm|sasl_authcid|sasl_authzid|sasl_secprops|krb5_ccname|tls_cacertdir|tls_cacertfile|tls_randfile|tls_ciphers|tls_cert|tls_key|pam_password_prohibit_message)\s+(?P<value>\S.*)',
                      line, re.IGNORECASE)
         if m:
             globals()[m.group('keyword').lower()] = m.group('value')
             continue
         # log <SCHEME> [<LEVEL>]
-        m = re.match('log\s+(?P<scheme>syslog|/\S*)(\s+(?P<level>%s))?' %
+        m = re.match(r'log\s+(?P<scheme>syslog|/\S*)(\s+(?P<level>%s))?' %
                          '|'.join(_log_levels.keys()),
                      line, re.IGNORECASE)
         if m:
             logs.append((m.group('scheme'), 
_log_levels[str(m.group('level')).lower()]))
             continue
         # uri <URI>
-        m = re.match('uri\s+(?P<uri>\S+)', line, re.IGNORECASE)
+        m = re.match(r'uri\s+(?P<uri>\S+)', line, re.IGNORECASE)
         if m:
             # FIXME: support multiple URI values
             # FIXME: support special DNS and DNS:domain values
@@ -213,7 +213,7 @@ def read(filename):
             uri = m.group('uri')
             continue
         # base <MAP>? <BASEDN>
-        m = re.match('base\s+((?P<map>%s)\s+)?(?P<value>\S.*)' %
+        m = re.match(r'base\s+((?P<map>%s)\s+)?(?P<value>\S.*)' %
                          '|'.join(maps.keys()),
                      line, re.IGNORECASE)
         if m:
@@ -223,7 +223,7 @@ def read(filename):
             mod.bases.append(m.group('value'))
             continue
         # filter <MAP> <SEARCHFILTER>
-        m = re.match('filter\s+(?P<map>%s)\s+(?P<value>\S.*)' %
+        m = re.match(r'filter\s+(?P<map>%s)\s+(?P<value>\S.*)' %
                          '|'.join(maps.keys()),
                      line, re.IGNORECASE)
         if m:
@@ -231,7 +231,7 @@ def read(filename):
             mod.filter = m.group('value')
             continue
         # scope <MAP>? <SCOPE>
-        m = re.match('scope\s+((?P<map>%s)\s+)?(?P<value>%s)' % (
+        m = re.match(r'scope\s+((?P<map>%s)\s+)?(?P<value>%s)' % (
                          '|'.join(maps.keys()),
                          '|'.join(_scope_options.keys())),
                      line, re.IGNORECASE)
@@ -240,7 +240,7 @@ def read(filename):
             mod.scope = _scope_options[m.group('value').lower()]
             continue
         # map <MAP> <ATTRIBUTE> <ATTMAPPING>
-        m = 
re.match('map\s+(?P<map>%s)\s+(?P<attribute>\S+)\s+(?P<value>\S.*)' %
+        m = 
re.match(r'map\s+(?P<map>%s)\s+(?P<attribute>\S+)\s+(?P<value>\S.*)' %
                          '|'.join(maps.keys()),
                      line, re.IGNORECASE)
         if m:
@@ -252,14 +252,14 @@ def read(filename):
             # TODO: filter out attributes that cannot be an expression
             continue
         # deref <DEREF>
-        m = re.match('deref\s+(?P<value>%s)' % '|'.join(_deref_options.keys()),
+        m = re.match(r'deref\s+(?P<value>%s)' % 
'|'.join(_deref_options.keys()),
                      line, re.IGNORECASE)
         if m:
             global deref
             deref = _deref_options[m.group('value').lower()]
             continue
         # nss_initgroups_ignoreusers <USER,USER>|<ALLLOCAL>
-        m = re.match('nss_initgroups_ignoreusers\s+(?P<value>\S.*)',
+        m = re.match(r'nss_initgroups_ignoreusers\s+(?P<value>\S.*)',
                      line, re.IGNORECASE)
         if m:
             users = m.group('value')
@@ -274,7 +274,7 @@ def read(filename):
             nss_initgroups_ignoreusers.update(users)
             continue
         # pam_authz_search <FILTER>
-        m = re.match('pam_authz_search\s+(?P<value>\S.*)', line, re.IGNORECASE)
+        m = re.match(r'pam_authz_search\s+(?P<value>\S.*)', line, 
re.IGNORECASE)
         if m:
             from expr import Expression
             pam_authz_searches.append(Expression(m.group('value')))
@@ -283,14 +283,14 @@ def read(filename):
             # uid variables
             continue
         # ssl <on|off|start_tls>
-        m = re.match('ssl\s+(?P<value>%s)' % '|'.join(_ssl_options.keys()),
+        m = re.match(r'ssl\s+(?P<value>%s)' % '|'.join(_ssl_options.keys()),
                      line, re.IGNORECASE)
         if m:
             global ssl
             ssl = _ssl_options[m.group('value').lower()]
             continue
         # sasl_canonicalize yes|no
-        m = 
re.match('(ldap_?)?sasl_(?P<no>no)?canon(icali[sz]e)?\s+(?P<value>%s)' %
+        m = 
re.match(r'(ldap_?)?sasl_(?P<no>no)?canon(icali[sz]e)?\s+(?P<value>%s)' %
                          '|'.join(_boolean_options.keys()),
                      line, re.IGNORECASE)
         if m:
@@ -300,7 +300,7 @@ def read(filename):
                 sasl_canonicalize = not sasl_canonicalize
             continue
         # tls_reqcert <demand|hard|yes...>
-        m = re.match('tls_reqcert\s+(?P<value>%s)' %
+        m = re.match(r'tls_reqcert\s+(?P<value>%s)' %
                          '|'.join(_tls_reqcert_options.keys()),
                      line, re.IGNORECASE)
         if m:
@@ -308,7 +308,7 @@ def read(filename):
             tls_reqcert = _tls_reqcert_options[m.group('value').lower()]
             continue
         # validnames /REGEX/i?
-        m = re.match('validnames\s+/(?P<value>.*)/(?P<flags>[i]?)$',
+        m = re.match(r'validnames\s+/(?P<value>.*)/(?P<flags>[i]?)$',
                      line, re.IGNORECASE)
         if m:
             global validnames
@@ -316,12 +316,12 @@ def read(filename):
             validnames = re.compile(m.group('value'), flags=flags)
             continue
         # reconnect_invalidate <MAP>,<MAP>,...
-        m = re.match('reconnect_invalidate\s+(?P<value>\S.*)',
+        m = re.match(r'reconnect_invalidate\s+(?P<value>\S.*)',
                      line, re.IGNORECASE)
         if m:
             dbs = re.split('[ ,]+', m.group('value').lower())
             for db in dbs:
-                if db not in maps.keys() + ['nfsidmap']:
+                if db not in list(maps.keys()) + ['nfsidmap']:
                     raise ParseError(filename, lineno, 'map %s unknown' % db)
             reconnect_invalidate.update(dbs)
             continue
diff --git a/pynslcd/common.py b/pynslcd/common.py
index 97899ad..2800222 100644
--- a/pynslcd/common.py
+++ b/pynslcd/common.py
@@ -1,7 +1,7 @@
 
 # common.py - functions that are used by different modules
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -130,7 +130,7 @@ def get_handlers(module):
     """Return a dictionary mapping actions to Request classes."""
     import inspect
     res = {}
-    if isinstance(module, basestring):
+    if isinstance(module, (type(''), type(u''))):
         module = __import__(module, globals())
     for name, cls in inspect.getmembers(module, inspect.isclass):
         if issubclass(cls, Request) and hasattr(cls, 'action'):
diff --git a/pynslcd/expr.py b/pynslcd/expr.py
index eec505b..f996d2e 100644
--- a/pynslcd/expr.py
+++ b/pynslcd/expr.py
@@ -1,7 +1,7 @@
 
 # expr.py - expression handling functions
 #
-# Copyright (C) 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2011-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -18,7 +18,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 # 02110-1301 USA
 
-"""Module for handling expressions used for LDAP searches.
+r"""Module for handling expressions used for LDAP searches.
 
 >>> expr = Expression('foo=$foo')
 >>> expr.value(dict(foo='XX'))
@@ -91,6 +91,9 @@ class MyIter(object):
         except IndexError:
             return None
 
+    def __next__(self):
+        return self.next()
+
     def startswith(self, value):
         return self.value[self.pos].startswith(value)
 
diff --git a/pynslcd/invalidator.py b/pynslcd/invalidator.py
index 4f260c3..bed03bb 100644
--- a/pynslcd/invalidator.py
+++ b/pynslcd/invalidator.py
@@ -1,7 +1,7 @@
 
 # invalidator.py - functions for invalidating external caches
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -57,7 +57,7 @@ def exec_invalidate(*args):
         else:  # p.returncode < 0
             logging.error('invalidator: %s (pid %d) killed by signal %d%s',
                           cmd, p.pid, -p.returncode, output)
-    except:
+    except Exception:
         logging.warn('invalidator: %s failed', cmd, exc_info=True)
 
 
@@ -72,7 +72,7 @@ def loop(fd):
     os.chdir('/')
     os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin'
     while True:
-        db = os.read(fd, 1)
+        db = os.read(fd, 1).decode('ascii')
         if db == '':
             break  # close process down
         db = _char_to_db.get(db, None)
@@ -107,6 +107,6 @@ def invalidate(db=None):
     else:
         db = ''.join(_db_to_char[x] for x in cfg.reconnect_invalidate)
     try:
-        os.write(signalfd, db)
-    except:
+        os.write(signalfd, db.encode('ascii'))
+    except Exception:
         logging.warn('requesting invalidation (%s) failed', db, exc_info=True)
diff --git a/pynslcd/mypidfile.py b/pynslcd/mypidfile.py
index 2bf158f..44570ea 100644
--- a/pynslcd/mypidfile.py
+++ b/pynslcd/mypidfile.py
@@ -1,7 +1,7 @@
 
 # mypidfile.py - functions for properly locking a PIDFile
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -40,11 +40,11 @@ class MyPIDLockFile(object):
             os.mkdir(piddir)
             u, gid = cfg.get_usergid()
             os.chown(piddir, u.u.pw_uid, gid)
-        fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0644)
+        fd = os.open(self.path, os.O_RDWR | os.O_CREAT, 0o644)
         try:
             fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             pidfile = os.fdopen(fd, 'w')
-        except:
+        except Exception:
             os.close(fd)
             raise
         pidfile.write('%d\n' % os.getpid())
@@ -62,13 +62,13 @@ class MyPIDLockFile(object):
     def is_locked(self):
         """Check whether the file is already present and locked."""
         try:
-            fd = os.open(self.path, os.O_RDWR, 0644)
+            fd = os.open(self.path, os.O_RDWR, 0o644)
             # Python doesn't seem to have F_TEST so we'll just try to lock
             fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
             # if we're here we must have aquired the lock
             fcntl.lockf(fd, fcntl.LOCK_UN)
             return False
-        except (IOError, OSError), e:
+        except (IOError, OSError) as e:
             if e.errno == errno.ENOENT:
                 return False
             if e.errno in (errno.EACCES, errno.EAGAIN):
diff --git a/pynslcd/pam.py b/pynslcd/pam.py
index 76e90a4..0fecb67 100644
--- a/pynslcd/pam.py
+++ b/pynslcd/pam.py
@@ -1,7 +1,7 @@
 
 # pam.py - functions authentication, authorisation and session handling
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -88,21 +88,21 @@ def pwmod(conn, userdn, oldpassword, newpassword):
 def update_lastchange(conns, userdn):
     """Try to update the shadowLastChange attribute of the entry."""
     attribute = shadow.attmap['shadowLastChange']
-    if attribute == '${shadowLastChange:--1}':
+    if str(attribute) == '"${shadowLastChange:--1}"':
         attribute = 'shadowLastChange'
-    if not attribute or '$' in attribute:
+    if not attribute or '$' in str(attribute):
         raise ValueError('shadowLastChange has unsupported mapping')
     # build the value for the new attribute
     if attribute.lower() == 'pwdlastset':
         # for AD we use another timestamp */
-        value = '%d000000000' % (time.time() / 100L + (134774L * 864L))
+        value = '%d000000000' % (int(time.time()) // 100 + (134774 * 864))
     else:
         # time in days since Jan 1, 1970
-        value = '%d' % (time.time() / (60 * 60 * 24))
+        value = '%d' % (int(time.time()) // (60 * 60 * 24))
     # perform the modification, return at first success
     for conn in conns:
         try:
-            conn.modify_s(userdn, [(ldap.MOD_REPLACE, attribute, [value])])
+            conn.modify_s(userdn, [(ldap.MOD_REPLACE, attribute, 
[value.encode('utf-8')])])
             return
         except ldap.LDAPError:
             pass  # ignore error and try next connection
@@ -181,10 +181,10 @@ class PAMAuthenticationRequest(PAMRequest):
         # try authentication
         try:
             conn, authz, msg = authenticate(binddn, password)
-        except ldap.INVALID_CREDENTIALS, e:
+        except ldap.INVALID_CREDENTIALS as e:
             try:
                 msg = e[0]['desc']
-            except:
+            except Exception:
                 msg = str(e)
             logging.debug('bind failed: %s', msg)
             self.write(parameters['username'], 
authc=constants.NSLCD_PAM_AUTH_ERR, msg=msg)
@@ -301,10 +301,10 @@ class PAMPasswordModificationRequest(PAMRequest):
             pwmod(conn, parameters['userdn'], parameters['oldpassword'], 
parameters['newpassword'])
             # try to update lastchange with normal or user connection
             update_lastchange((self.conn, conn), parameters['userdn'])
-        except ldap.INVALID_CREDENTIALS, e:
+        except ldap.INVALID_CREDENTIALS as e:
             try:
                 msg = e[0]['desc']
-            except:
+            except Exception:
                 msg = str(e)
             logging.debug('pwmod failed: %s', msg)
             self.write(constants.NSLCD_PAM_PERM_DENIED, msg)
diff --git a/pynslcd/pynslcd.py b/pynslcd/pynslcd.py
index d367a8c..d41e478 100755
--- a/pynslcd/pynslcd.py
+++ b/pynslcd/pynslcd.py
@@ -2,7 +2,7 @@
 
 # pynslcd.py - main daemon module
 #
-# Copyright (C) 2010-2017 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -91,7 +91,7 @@ def display_version(fp):
     fp.write('%(PACKAGE_STRING)s\n'
              'Written by Arthur de Jong.\n'
              '\n'
-             'Copyright (C) 2010-2017 Arthur de Jong\n'
+             'Copyright (C) 2010-2019 Arthur de Jong\n'
              'This is free software; see the source for copying conditions.  
There is NO\n'
              'warranty; not even for MERCHANTABILITY or FITNESS FOR A 
PARTICULAR PURPOSE.\n'
              % {'PACKAGE_STRING': constants.PACKAGE_STRING, })
@@ -138,7 +138,7 @@ def parse_cmdline():
                 sys.exit(0)
         if len(args):
             raise getopt.GetoptError('unrecognized option \'%s\'' % args[0], 
args[0])
-    except getopt.GetoptError, reason:
+    except getopt.GetoptError as reason:
         sys.stderr.write(
             "%(program_name)s: %(reason)s\n"
             "Try '%(program_name)s --help' for more information.\n" % {
@@ -163,7 +163,7 @@ def create_socket():
     # close the file descriptor on exit
     fcntl.fcntl(sock, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
     # set permissions of socket so anybody can do requests
-    os.chmod(constants.NSLCD_SOCKET, 0666)
+    os.chmod(constants.NSLCD_SOCKET, 0o666)
     # start listening for connections
     sock.listen(socket.SOMAXCONN)
     return sock
@@ -258,7 +258,7 @@ def worker():
     while True:
         try:
             acceptconnection(session)
-        except:
+        except Exception:
             logging.exception('exception in worker')
             # ignore all exceptions, just keep going
 
@@ -288,7 +288,7 @@ if __name__ == '__main__':
     except ImportError:
         pass
     # set a default umask for the pidfile and socket
-    os.umask(0022)
+    os.umask(0o022)
     # see if someone already locked the pidfile
     pidfile = mypidfile.MyPIDLockFile(constants.NSLCD_PIDFILE)
     # see if --check option was given
@@ -372,6 +372,6 @@ if __name__ == '__main__':
             # wait for all threads to die
             for thread in threads:
                 thread.join(10000)
-        except:
+        except Exception:
             logging.exception('main loop exit')
             # no need to re-raise since we are exiting anyway
diff --git a/pynslcd/search.py b/pynslcd/search.py
index 4a57ab3..b61f638 100644
--- a/pynslcd/search.py
+++ b/pynslcd/search.py
@@ -1,7 +1,7 @@
 
 # search.py - functions for searching the LDAP database
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -159,6 +159,10 @@ class LDAPSearch(object):
     def _transform(self, dn, attributes):
         """Handle a single search result entry filtering it with the request
         parameters, search options and attribute mapping."""
+        # convert attributes to strings where appropriate
+        attributes = dict(
+            (attr, [value.decode('utf-8') for value in values] if attr != 
'objectSid' else values)
+            for attr, values in attributes.items())
         # translate the attributes using the attribute mapping
         if self.attmap:
             attributes = self.attmap.translate(attributes)
diff --git a/pynslcd/tio.py b/pynslcd/tio.py
index ef0fda7..eb1a695 100644
--- a/pynslcd/tio.py
+++ b/pynslcd/tio.py
@@ -1,7 +1,7 @@
 
 # tio.py - I/O functions
 #
-# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
+# Copyright (C) 2010-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -21,6 +21,7 @@
 import os
 import socket
 import struct
+import sys
 
 
 # definition for reading and writing INT32 values
@@ -42,35 +43,48 @@ class TIOStream(object):
         conn.setblocking(1)
         conn.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, 
_struct_timeval.pack(0, 500000))
         conn.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, 
_struct_timeval.pack(60, 0))
-        self.fp = os.fdopen(conn.fileno(), 'w+b', 1024 * 1024)
+        self.readfd = os.fdopen(conn.fileno(), 'rb', 1024)
+        self.writefd = os.fdopen(conn.fileno(), 'wb', 1024 * 1024)
 
     def read(self, size):
-        return self.fp.read(size)
+        return self.readfd.read(size)
 
     def read_int32(self):
         return _int32.unpack(self.read(_int32.size))[0]
 
-    def read_string(self, maxsize=None):
+    def read_bytes(self, maxsize=None):
         num = self.read_int32()
         if maxsize and num >= maxsize:
             raise TIOStreamError()
         return self.read(num)
 
+    def read_string(self):
+        value = self.read_bytes()
+        if sys.version_info[0] >= 3:
+            value = value.decode('utf-8')
+        return value
+
     def read_address(self):
         """Read an address (usually IPv4 or IPv6) from the stream and return
         the address as a string representation."""
         af = self.read_int32()
-        return socket.inet_ntop(af, self.read_string(maxsize=64))
+        return socket.inet_ntop(af, self.read_bytes(maxsize=64))
 
     def write(self, value):
-        self.fp.write(value)
+        self.writefd.write(value)
 
     def write_int32(self, value):
         self.write(_int32.pack(value))
 
-    def write_string(self, value):
+    def write_bytes(self, value):
         self.write_int32(len(value))
-        self.write(value)
+        if value:
+            self.write(value)
+
+    def write_string(self, value):
+        if sys.version_info[0] >= 3:
+            value = value.encode('utf-8')
+        self.write_bytes(value)
 
     def write_stringlist(self, value):
         lst = tuple(value)
@@ -94,11 +108,15 @@ class TIOStream(object):
         # first try to make it into an IPv6 address
         af, address = TIOStream._to_address(value)
         self.write_int32(af)
-        self.write_string(address)
+        self.write_bytes(address)
 
     def close(self):
         try:
-            self.fp.close()
+            self.writefd.close()
+        except IOError:
+            pass
+        try:
+            self.readfd.close()
         except IOError:
             pass
 
diff --git a/pynslcd/usermod.py b/pynslcd/usermod.py
index 9622fb2..4e37ded 100644
--- a/pynslcd/usermod.py
+++ b/pynslcd/usermod.py
@@ -1,7 +1,7 @@
 
 # usermod.py - functions for modifying user information
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -117,10 +117,10 @@ class UserModRequest(pam.PAMRequest):
                 conn, authz, msg = pam.authenticate(binddn, password)
                 conn.modify_s(parameters['userdn'], mods)
                 logging.info('changed information for %s', 
parameters['userdn'])
-            except (ldap.INVALID_CREDENTIALS, ldap.INSUFFICIENT_ACCESS), e:
+            except (ldap.INVALID_CREDENTIALS, ldap.INSUFFICIENT_ACCESS) as e:
                 try:
                     msg = e[0]['desc']
-                except:
+                except Exception:
                     msg = str(e)
                 logging.debug('modification failed: %s', msg)
                 self.write_result(constants.NSLCD_USERMOD_RESULT, msg)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 0a7854e..a92b7d0 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,7 +1,7 @@
 # Makefile.am - use automake to generate Makefile.in
 #
 # Copyright (C) 2006 West Consulting
-# Copyright (C) 2006-2017 Arthur de Jong
+# Copyright (C) 2006-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -32,6 +32,10 @@ if ENABLE_UTILS
   TESTS += test_ldapcmds.sh
 endif
 
+TEST_EXTENSIONS = .sh .py
+SH_LOG_COMPILER = $(SHELL)
+PY_LOG_COMPILER = $(PYTHON)
+
 AM_TESTS_ENVIRONMENT = PYTHON='@PYTHON@'; export PYTHON; \
                        builddir=$(builddir); export builddir;
 
@@ -48,6 +52,8 @@ EXTRA_DIST = README nslcd-test.conf usernames.txt testenv.sh 
test_myldap.sh \
              setup_slapd.sh config.ldif test.ldif
 
 CLEANFILES = $(EXTRA_PROGRAMS) test_pamcmds.log
+clean-local:
+       -rm -rf *.pyc *.pyo __pycache__
 
 AM_CPPFLAGS = -I$(top_srcdir)
 AM_CFLAGS = $(PTHREAD_CFLAGS) -g
diff --git a/tests/test_doctest.sh b/tests/test_doctest.sh
index ed51141..b799652 100755
--- a/tests/test_doctest.sh
+++ b/tests/test_doctest.sh
@@ -2,7 +2,7 @@
 
 # test_doctest.sh - run Python doctests
 #
-# Copyright (C) 2016 Arthur de Jong
+# Copyright (C) 2016-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -28,16 +28,33 @@ top_srcdir="${top_srcdir-${srcdir}/..}"
 top_builddir="${top_builddir-${builddir}/..}"
 python="${PYTHON-python}"
 
+# Find Python interpreters
+find_python() {
+  for p in ${python} python python2 python2.7 python3 python3.5 python3.6 
python3.7 python3.8
+  do
+    if [ -x "$(which $p)" ]
+    then
+      readlink -f `which $p` 2> /dev/null || true
+    fi
+  done
+}
+interpreters=`find_python | sort -u`
+
 # if Python is missing, ignore
-if ! ${python} --version > /dev/null 2> /dev/null
+if [ -z "$interpreters" ]
 then
   echo "Python (${python}) not found"
   exit 77
 fi
 
 # run doctests
-for dir in pynslcd utils
+for python in $interpreters
 do
-  echo "Running doctests in $dir..."
-  PYTHONPATH="${top_builddir}/${dir}" ${python} -m doctest -v 
"${top_srcdir}/${dir}"/*.py
+  if ${python} -c 'import ldap'
+  then
+    echo "Running pynslcd doctests with $python..."
+    PYTHONPATH="${top_builddir}/pynslcd" ${python} -m doctest -v 
"${top_srcdir}/pynslcd"/*.py
+  fi
+  echo "Running pynslcd doctests with $python..."
+  PYTHONPATH="${top_builddir}/utils" ${python} -m doctest -v 
"${top_srcdir}/utils"/*.py
 done
diff --git a/tests/test_ldapcmds.sh b/tests/test_ldapcmds.sh
index a9c2efb..f82e82b 100755
--- a/tests/test_ldapcmds.sh
+++ b/tests/test_ldapcmds.sh
@@ -2,7 +2,7 @@
 
 # test_ldapcmds.sh - simple test script to test lookups
 #
-# Copyright (C) 2017 Arthur de Jong
+# Copyright (C) 2017-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -33,6 +33,10 @@ python="${PYTHON-python}"
 PYTHONPATH="${top_srcdir}/utils:${top_builddir}/utils"
 export PYTHONPATH
 
+# Force UTF-8 encoding for repeatable tests
+PYTHONIOENCODING='utf-8'
+export PYTHONIOENCODING
+
 # ensure that we are running in the test environment
 "$srcdir/testenv.sh" check_nslcd || exit 77
 
@@ -142,7 +146,7 @@ check "getent.ldap group testgroup | sortgroup" << EOM
 testgroup:*:6100:arthur,test,testuser4
 EOM
 
-check "getent.ldap group users" << EOM
+check "getent.ldap group users | sortgroup" << EOM
 users:*:100:arthur,test
 EOM
 
diff --git a/tests/test_pycompile.sh b/tests/test_pycompile.sh
index c429b1f..42af34f 100755
--- a/tests/test_pycompile.sh
+++ b/tests/test_pycompile.sh
@@ -2,7 +2,7 @@
 
 # test_pycompile.sh - see if all Python files compile
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -50,14 +50,14 @@ for root, dirs, files in os.walk(top_srcdir):
             filename = os.path.join(root, f)
             try:
                 py_compile.compile(filename, tmpfile, doraise=True)
-            except py_compile.PyCompileError, e:
-                print 'Compiling %s ...' % os.path.abspath(filename)
-                print e
+            except py_compile.PyCompileError as e:
+                print('Compiling %s ...' % os.path.abspath(filename))
+                print(e)
                 errors_found += 1
 
 os.unlink(tmpfile)
 
 if errors_found:
-    print '%d errors found' % errors_found
+    print('%d errors found' % errors_found)
     sys.exit(1)
 "
diff --git a/tests/test_pylint.sh b/tests/test_pylint.sh
index b97600a..388d233 100755
--- a/tests/test_pylint.sh
+++ b/tests/test_pylint.sh
@@ -2,7 +2,7 @@
 
 # test_pylint.sh - run pylint on the source to find errors
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -26,9 +26,19 @@ srcdir="${srcdir-`dirname "$0"`}"
 top_srcdir="${top_srcdir-${srcdir}/..}"
 builddir="${builddir-`dirname "$0"`}"
 top_builddir="${top_builddir-${builddir}/..}"
+PYLINT="${PYLINT-pylint}"
+
+# Find Pylint
+for p in ${PYLINT} pylint pylint3
+do
+  if "$p" --version > /dev/null 2> /dev/null
+  then
+    pylint="$p"
+  fi
+done
 
 # if Pylint is missing, ignore
-if ! pylint --version > /dev/null 2> /dev/null
+if [ -z "$pylint" ]
 then
   echo "Pylint not found"
   exit 77
@@ -48,7 +58,7 @@ do
   echo "Running pylint in $dir..."
   dir_builddir="$(cd "${top_builddir}/${dir}" && pwd)"
   ( cd "${top_srcdir}/${dir}" ;
-    PYTHONPATH="${dir_builddir}" pylint --errors-only --rcfile "$rcfile" 
--disable "$disable" *.py)
+    PYTHONPATH="${dir_builddir}" "$pylint" --errors-only --rcfile "$rcfile" 
--disable "$disable" *.py)
 done
 
 # Pylint has the following exit codes:
diff --git a/tests/test_pynslcd_cache.py b/tests/test_pynslcd_cache.py
index 5c15b01..a57cedb 100755
--- a/tests/test_pynslcd_cache.py
+++ b/tests/test_pynslcd_cache.py
@@ -2,7 +2,7 @@
 
 # test_pynslcd_cache.py - tests for the pynslcd caching functionality
 #
-# Copyright (C) 2013 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -41,6 +41,8 @@ class TestAlias(unittest.TestCase):
         cache.store('alias2', ['member1', 'member3'])
         cache.store('alias3', [])
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='alias1')), [
@@ -69,6 +71,8 @@ class TestEther(unittest.TestCase):
         cache.store('name1', '0:18:8a:54:1a:11')
         cache.store('name2', '0:18:8a:54:1a:22')
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='name1')), [
@@ -101,6 +105,8 @@ class TestGroup(unittest.TestCase):
         cache.store('group3', 'pass3', 30, [])
         cache.store('group4', 'pass4', 40, ['user2', ])
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='group1')), [
@@ -149,6 +155,8 @@ class TestHost(unittest.TestCase):
         cache.store('hostname1', [], ['127.0.0.1', ])
         cache.store('hostname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3'])
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='hostname1')), [
@@ -180,6 +188,8 @@ class TestNetgroup(unittest.TestCase):
         cache.store('netgroup1', ['(host1, user1,)', '(host1, user2,)', 
'(host2, user1,)'], ['netgroup2', ])
         cache.store('netgroup2', ['(host3, user1,)', '(host3, user3,)'], [])
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='netgroup1')), [
@@ -198,6 +208,8 @@ class TestNetwork(unittest.TestCase):
         cache.store('networkname1', [], ['127.0.0.1', ])
         cache.store('networkname2', ['alias1', 'alias2'], ['127.0.0.2', 
'127.0.0.3'])
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='networkname1')), [
@@ -229,6 +241,8 @@ class TestPasswd(unittest.TestCase):
         cache.store('name', 'passwd', 100, 200, 'gecos', '/home/user', 
'/bin/bash')
         cache.store('name2', 'passwd2', 101, 202, 'gecos2', '/home/user2', 
'/bin/bash')
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(uid='name')), [
@@ -262,6 +276,8 @@ class TestProtocol(unittest.TestCase):
         cache.store('protocol2', ['alias3', ], 200)
         cache.store('protocol3', [], 300)
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='protocol1')), [
@@ -310,6 +326,8 @@ class TestRpc(unittest.TestCase):
         cache.store('rpc2', ['alias3', ], 200)
         cache.store('rpc3', [], 300)
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='rpc1')), [
@@ -359,6 +377,8 @@ class TestService(unittest.TestCase):
         cache.store('service2', ['alias3', ], 200, 'udp')
         cache.store('service3', [], 300, 'udp')
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(cn='service1')), [
@@ -436,6 +456,8 @@ class Testshadow(unittest.TestCase):
         cache.store('name', 'passwd', 15639, 0, 7, -1, -1, -1, 0)
         cache.store('name2', 'passwd2', 15639, 0, 7, -1, -1, -1, 0)
         self.cache = cache
+        if not hasattr(self, 'assertItemsEqual'):
+            self.assertItemsEqual = self.assertCountEqual
 
     def test_by_name(self):
         self.assertItemsEqual(self.cache.retrieve(dict(uid='name')), [
diff --git a/utils/Makefile.am b/utils/Makefile.am
index 7b1f9df..b8da6f6 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -1,6 +1,6 @@
 # Makefile.am - use automake to generate Makefile.in
 #
-# Copyright (C) 2013-2016 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -27,7 +27,7 @@ all-local: $(nodist_utils_PYTHON)
 
 # clean up locally created compiled Python files
 clean-local:
-       rm -f *.pyc *.pyo
+       -rm -rf *.pyc *.pyo __pycache__
 
 # copy constants module
 constants.py: ../pynslcd/constants.py
diff --git a/utils/getent.py b/utils/getent.py
index ae00c5e..c14e5e3 100755
--- a/utils/getent.py
+++ b/utils/getent.py
@@ -3,7 +3,7 @@
 
 # getent.py - program for querying nslcd
 #
-# Copyright (C) 2013-2017 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -110,7 +110,7 @@ def getent_group(database, keys=None):
         if database == 'group.bymember':
             con = NslcdClient(constants.NSLCD_ACTION_GROUP_BYMEMBER)
             con.write_string(key)
-        elif re.match('^\d+$', key):
+        elif re.match(r'^\d+$', key):
             con = NslcdClient(constants.NSLCD_ACTION_GROUP_BYGID)
             con.write_int32(int(key))
         else:
@@ -276,7 +276,7 @@ def getent_passwd(database, keys=None):
         write_passwd(NslcdClient(constants.NSLCD_ACTION_PASSWD_ALL))
         return
     for key in keys:
-        if re.match('^\d+$', key):
+        if re.match(r'^\d+$', key):
             con = NslcdClient(constants.NSLCD_ACTION_PASSWD_BYUID)
             con.write_int32(int(key))
         else:
@@ -298,7 +298,7 @@ def getent_protocols(database, keys=None):
         write_protocols(NslcdClient(constants.NSLCD_ACTION_PROTOCOL_ALL))
         return
     for key in keys:
-        if re.match('^\d+$', key):
+        if re.match(r'^\d+$', key):
             con = NslcdClient(constants.NSLCD_ACTION_PROTOCOL_BYNUMBER)
             con.write_int32(int(key))
         else:
@@ -320,7 +320,7 @@ def getent_rpc(database, keys=None):
         write_rpc(NslcdClient(constants.NSLCD_ACTION_RPC_ALL))
         return
     for key in keys:
-        if re.match('^\d+$', key):
+        if re.match(r'^\d+$', key):
             con = NslcdClient(constants.NSLCD_ACTION_RPC_BYNUMBER)
             con.write_int32(int(key))
         else:
@@ -347,7 +347,7 @@ def getent_services(database, keys=None):
         protocol = ''
         if '/' in value:
             value, protocol = value.split('/', 1)
-        if re.match('^\d+$', value):
+        if re.match(r'^\d+$', value):
             con = NslcdClient(constants.NSLCD_ACTION_SERVICE_BYNUMBER)
             con.write_int32(int(value))
             con.write_string(protocol)
diff --git a/utils/nslcd.py b/utils/nslcd.py
index bb720a3..82e5bbb 100644
--- a/utils/nslcd.py
+++ b/utils/nslcd.py
@@ -2,7 +2,7 @@
 
 # nslcd.py - functions for doing nslcd requests
 #
-# Copyright (C) 2013-2017 Arthur de Jong
+# Copyright (C) 2013-2019 Arthur de Jong
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -55,12 +55,13 @@ class NslcdClient(object):
 
     def write_bytes(self, value):
         self.write_int32(len(value))
-        self.write(value)
+        if value:
+            self.write(value)
 
     def write_string(self, value):
         if sys.version_info[0] >= 3:
             value = value.encode('utf-8')
-        self.write_bytes(value.encode('utf-8'))
+        self.write_bytes(value)
 
     def write_ether(self, value):
         value = struct.pack('BBBBBB', *(int(x, 16) for x in value.split(':')))

-----------------------------------------------------------------------

Summary of changes:
 .gitignore                                |   1 +
 configure.ac                              |  10 +-
 pynslcd/Makefile.am                       |   7 +-
 pynslcd/alias.py                          |   6 +-
 pynslcd/attmap.py                         |  78 ++--
 pynslcd/cache.py                          |  53 ++-
 pynslcd/cfg.py                            | 146 +++++---
 pynslcd/common.py                         |  57 ++-
 pynslcd/ether.py                          |  18 +-
 pynslcd/expr.py                           |  17 +-
 pynslcd/group.py                          |  23 +-
 pynslcd/host.py                           |   6 +-
 pynslcd/invalidator.py                    |  18 +-
 pynslcd/mypidfile.py                      |  16 +-
 pynslcd/netgroup.py                       |  12 +-
 pynslcd/network.py                        |   7 +-
 pynslcd/pam.py                            |  67 ++--
 pynslcd/passwd.py                         |  34 +-
 pynslcd/protocol.py                       |   6 +-
 pynslcd/pynslcd.py                        |  54 +--
 pynslcd/rpc.py                            |   6 +-
 pynslcd/search.py                         |  25 +-
 pynslcd/service.py                        |  15 +-
 pynslcd/shadow.py                         |  34 +-
 pynslcd/tio.py                            |  50 ++-
 pynslcd/usermod.py                        |   6 +-
 tests/Makefile.am                         |  12 +-
 tests/flake8.ini                          |  14 +
 tests/test_doctest.sh                     |  27 +-
 tests/{test_doctest.sh => test_flake8.sh} |  49 ++-
 tests/test_ldapcmds.sh                    |   8 +-
 tests/test_pycompile.sh                   |  10 +-
 tests/test_pylint.sh                      |  16 +-
 tests/test_pynslcd_cache.py               | 601 +++++++++++++++++++-----------
 utils/Makefile.am                         |   9 +-
 utils/chsh.py                             |  19 +-
 utils/getent.py                           |  79 ++--
 utils/nslcd.py                            |   9 +-
 utils/users.py                            |  16 +-
 39 files changed, 1013 insertions(+), 628 deletions(-)
 create mode 100644 tests/flake8.ini
 copy tests/{test_doctest.sh => test_flake8.sh} (54%)


hooks/post-receive
-- 
nss-pam-ldapd