New Upstream Release - python-bonsai

Ready changes

Summary

Merged new upstream version: 1.5.1+ds (was: 1.5.0+ds).

Resulting package

Built on 2022-12-31T02:10 (took 2m52s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases python3-bonsai-dbgsymapt install -t fresh-releases python3-bonsai-docapt install -t fresh-releases python3-bonsai

Lintian Result

Diff

diff --git a/.appveyor.yml b/.appveyor.yml
index 4ce66b6..0e5884a 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -35,6 +35,14 @@ environment:
       PYTHON_VERSION: "3.10.x"
       PYTHON_ARCH: "64"
 
+    - PYTHON: "C:\\Python311"
+      PYTHON_VERSION: "3.11.x"
+      PYTHON_ARCH: "32"
+
+    - PYTHON: "C:\\Python311-x64"
+      PYTHON_VERSION: "3.11.x"
+      PYTHON_ARCH: "64"
+
 install:
   # If there is a newer build queued for the same PR, cancel this one.
   # The AppVeyor 'rollout builds' option is supposed to serve the same
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index f9827f9..a1211a5 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -8,15 +8,15 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
     env:
       PYTHON: ${{ matrix.python-version }}
       OS: ubuntu
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install OS dependencies
@@ -47,15 +47,17 @@ jobs:
     - name: Check container and LDAP tools
       run: |
         docker exec server ps aux
-        ldapwhoami -Y DIGEST-MD5 -h bonsai.test -U admin -w p@ssword
-        ldapsearch -h bonsai.test -b "" -s base 'objectclass=*' -x -LLL +
+        ldapwhoami -Y DIGEST-MD5 -H ldap://bonsai.test -U admin -w p@ssword
+        ldapsearch -H ldap://bonsai.test -b "" -s base 'objectclass=*' -x -LLL +
         ldapsearch -VV
         saslpluginviewer
+    - name: Check Python sysconfig
+      run: python -m sysconfig
     - name: Install package
       run: |
         printf "\n\n[options]\nzip_safe = False" >> setup.cfg
         export CFLAGS="-coverage"
-        python setup.py install
+        python -m pip install -v .
     - name: Run tests
       run: |
         export BONSAI_DOCKER_IP=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' server`
@@ -65,7 +67,7 @@ jobs:
         echo $BONSAI_INSTALL_PATH
         python -m pytest -v --cov-config .coveragerc --cov-report= --cov=$BONSAI_INSTALL_PATH
     - name: Upload coverage
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         directory: "."
         env_vars: OS,PYTHON
@@ -76,15 +78,15 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
     env:
       PYTHON: ${{ matrix.python-version }}
       OS: macos
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install Python dependencies
@@ -117,15 +119,17 @@ jobs:
     - name: Check container and LDAP tools
       run: |
         docker exec server ps aux
-        ldapwhoami -Y DIGEST-MD5 -h bonsai.test -U admin -w p@ssword
-        ldapsearch -x -h bonsai.test -b "" -s base 'objectclass=*' -LLL +
-        ldapsearch -VV
+        /usr/local/opt/openldap/bin/ldapwhoami -Y DIGEST-MD5 -H ldap://bonsai.test -U admin -w p@ssword
+        /usr/local/opt/openldap/bin/ldapsearch -x -H ldap://bonsai.test -b "" -s base 'objectclass=*' -LLL +
+        /usr/local/opt/openldap/bin/ldapsearch -VV
+    - name: Check Python sysconfig
+      run: python -m sysconfig
     - name: Install package
       run: |
         printf "[build_ext]\ninclude_dirs=/usr/local/opt/openldap/include\nlibrary_dirs=/usr/local/opt/openldap/lib" > ./setup.cfg
         printf "\n\n[options]\nzip_safe = False" >> setup.cfg
         export CFLAGS="-coverage"
-        python setup.py install
+        python -m pip install -v .
     - name: Check linking
       run: |
         otool -L ./build/*/bonsai/*.so
@@ -136,7 +140,7 @@ jobs:
         echo $BONSAI_INSTALL_PATH
         py.test -v --cov-config .coveragerc --cov-report= --cov=$BONSAI_INSTALL_PATH
     - name: Upload coverage
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         directory: "."
         env_vars: OS,PYTHON
@@ -148,7 +152,7 @@ jobs:
         python setup.py bdist_wheel
         delocate-wheel -v ./dist/bonsai-*.whl
     - name: Upload wheel
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: wheel
         path: ./dist/bonsai-*.whl
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c78ff73..0060268 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,33 @@
 Changelog
 ==========
-[1.5.0 - 2022-08-24]
+
+[1.5.1] - 2022-12-03
+--------------------
+
+Changed
+~~~~~~~
+
+-  Improved type annotations, set __all__ attributes for the modules.
+
+Added
+~~~~~
+
+-  py.typed file for type checking. (Issue #74)
+-  Python 3.11 to the CI pipeline.
+
+Fixed
+~~~~~
+
+-  UnboundLocalError in AIOConnectionPool.spawn (PR #77, thanks to @rra)
+   and in ConnectionPool.spawn.
+-  Parsing LDIF values containing colon as a non-first character
+   in LDIFReader. (Issue #72)
+-  Passing keyword arguments to the connect method while opening
+   connections in ConnectionPool.
+
+
+[1.5.0] - 2022-08-24
+--------------------
 
 Changed
 ~~~~~~~
@@ -26,7 +53,7 @@ Fixed
 -  SetUp method of the Tornado tests for Tornado 6.2.
 
 
-[1.4.0 - 2022-03-12]
+[1.4.0] - 2022-03-12
 --------------------
 
 Changed
@@ -56,7 +83,7 @@ Fixed
    is provided. (Issue #59, thanks to @morian)
 
 
-[1.3.0 - 2021-08-24]
+[1.3.0] - 2021-08-24
 --------------------
 
 Changed
@@ -81,7 +108,7 @@ Fixed
 -  Deadlock when waiting for finishing the init thread on macOS.
 
 
-[1.2.1 - 2020-12-31]
+[1.2.1] - 2020-12-31
 --------------------
 
 Changed
diff --git a/README.rst b/README.rst
index 6a82bc7..58dab3d 100644
--- a/README.rst
+++ b/README.rst
@@ -91,8 +91,7 @@ Using with asyncio:
                 who = await conn.whoami()
                 print(who)
 
-        loop = asyncio.get_event_loop()
-        loop.run_until_complete(do())
+        asyncio.run(do())
 
 Changelog
 ---------
@@ -107,4 +106,4 @@ the `GitHub page`_.
 
 .. _online: http://bonsai.readthedocs.org/en/latest/
 .. _here: https://github.com/noirello/bonsai/blob/master/CHANGELOG.rst
-.. _GitHub page: https://github.com/Noirello/bonsai/issues
+.. _GitHub page: https://github.com/noirello/bonsai/issues
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index fa0d778..e3f0f5f 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -19,6 +19,8 @@ stages:
           python.version: '3.9'
         Python310:
           python.version: '3.10'
+        Python311:
+          python.version: '3.11'
     steps:
     - task: UsePythonVersion@0
       inputs:
@@ -55,16 +57,18 @@ stages:
       displayName: Configure Docker container
     - script: |
         docker exec server ps aux
-        ldapwhoami -Y DIGEST-MD5 -h bonsai.test -U admin -w p@ssword
-        ldapsearch -h bonsai.test -b "" -s base 'objectclass=*' -x -LLL +
+        ldapwhoami -Y DIGEST-MD5 -H ldap://bonsai.test -U admin -w p@ssword
+        ldapsearch -H ldap://bonsai.test -b "" -s base 'objectclass=*' -x -LLL +
         ldapsearch -VV
         saslpluginviewer
       displayName: Check container and LDAP tools
+    - script: python -m sysconfig
+      displayName: Check Python sysconfig
     - script: |
         set -e
         printf "\n\n[options]\nzip_safe = False" >> setup.cfg
         export CFLAGS="-coverage"
-        python setup.py install
+        python -m pip install -v .
       displayName: Install package
     - script: |
         export BONSAI_DOCKER_IP=`docker inspect --format '{{ .NetworkSettings.IPAddress }}' server`
@@ -100,6 +104,8 @@ stages:
           python.version: '3.9'
         Python310:
           python.version: '3.10'
+        Python311:
+          python.version: '3.11'
     steps:
     - task: UsePythonVersion@0
       inputs:
@@ -136,16 +142,18 @@ stages:
       displayName: Configure Docker container
     - script: |
         docker exec server ps aux
-        ldapwhoami -Y DIGEST-MD5 -h bonsai.test -U admin -w p@ssword
-        ldapsearch -x -h bonsai.test -b "" -s base 'objectclass=*' -LLL +
-        ldapsearch -VV
+        /usr/local/opt/openldap/bin/ldapwhoami -Y DIGEST-MD5 -H ldap://bonsai.test -U admin -w p@ssword
+        /usr/local/opt/openldap/bin/ldapsearch -x -H ldap://bonsai.test -b "" -s base 'objectclass=*' -LLL +
+        /usr/local/opt/openldap/bin/ldapsearch -VV
       displayName: Check container and LDAP tools
+    - script: python -m sysconfig
+      displayName: Check Python sysconfig
     - script: |
         set -e
         printf "[build_ext]\ninclude_dirs=/usr/local/opt/openldap/include\nlibrary_dirs=/usr/local/opt/openldap/lib" > ./setup.cfg
         printf "\n\n[options]\nzip_safe = False" >> setup.cfg
         export CFLAGS="-coverage"
-        python setup.py install
+        python -m pip install -v .
       displayName: Install package
     - script: otool -L ./build/*/bonsai/*.so
       displayName: Check linking
@@ -194,7 +202,7 @@ stages:
           python.version: '3.8-slim-bullseye'
         nightlyPython:
           openldap.version: '2.4.57'
-          python.version: '3.11.0rc1-slim-bullseye'
+          python.version: '3.12.0a1-slim-bullseye'
     steps:
     - script: docker build -t bonsai -f ./.ci/docker/Dockerfile .
       displayName: Build Docker image (server)
@@ -236,7 +244,7 @@ stages:
         docker exec client python3 -m pip list
       displayName: Install Python dependencies
     - script: |
-        docker exec client python3 setup.py install
+        docker exec client python3 -m pip install -v .
       displayName: Install package
     - script: |
         docker exec client bash -c 'sed -i.bak "s/127.0.0.1/$BONSAI_DOCKER_IP/g" ./tests/test.ini'
diff --git a/debian/changelog b/debian/changelog
index b55f3b3..7a29b71 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-bonsai (1.5.1+ds-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 31 Dec 2022 02:08:07 -0000
+
 python-bonsai (1.5.0+ds-3) unstable; urgency=medium
 
   [ Debian Janitor ]
diff --git a/debian/patches/docs-disable-furo-theme.patch b/debian/patches/docs-disable-furo-theme.patch
index d89beea..1720acb 100644
--- a/debian/patches/docs-disable-furo-theme.patch
+++ b/debian/patches/docs-disable-furo-theme.patch
@@ -10,10 +10,10 @@ Signed-off-by: Robin Jarry <robin@jarry.cc>
  docs/conf.py | 1 -
  1 file changed, 1 deletion(-)
 
-diff --git a/docs/conf.py b/docs/conf.py
-index 6e869cefc982..31ad7cfc4e59 100644
---- a/docs/conf.py
-+++ b/docs/conf.py
+Index: python-bonsai.git/docs/conf.py
+===================================================================
+--- python-bonsai.git.orig/docs/conf.py
++++ python-bonsai.git/docs/conf.py
 @@ -135,7 +135,6 @@ pygments_style = 'sphinx'
  
  # The theme to use for HTML and HTML Help pages.  See the documentation for
@@ -22,6 +22,3 @@ index 6e869cefc982..31ad7cfc4e59 100644
  #html_theme = 'alabaster'
  
  # Theme options are theme-specific and customize the look and feel of a theme
--- 
-2.37.2
-
diff --git a/debian/patches/setup-do-not-use-distutils.patch b/debian/patches/setup-do-not-use-distutils.patch
index f77ea65..a055d4a 100644
--- a/debian/patches/setup-do-not-use-distutils.patch
+++ b/debian/patches/setup-do-not-use-distutils.patch
@@ -8,10 +8,10 @@ Signed-off-by: Robin Jarry <robin@jarry.cc>
  setup.py | 2 +-
  1 file changed, 1 insertion(+), 1 deletion(-)
 
-diff --git a/setup.py b/setup.py
-index 3205f55e259d..04555ad13bcb 100644
---- a/setup.py
-+++ b/setup.py
+Index: python-bonsai.git/setup.py
+===================================================================
+--- python-bonsai.git.orig/setup.py
++++ python-bonsai.git/setup.py
 @@ -4,7 +4,7 @@ import tempfile
  
  from contextlib import contextmanager
@@ -21,6 +21,3 @@ index 3205f55e259d..04555ad13bcb 100644
  
  from setuptools.command.build_ext import build_ext
  from setuptools import setup, Extension
--- 
-2.37.2
-
diff --git a/docs/advanced.rst b/docs/advanced.rst
index 48b284b..fcd1eee 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -568,8 +568,7 @@ An example for asynchronous search and modify with `asyncio`:
             entry['mail'] = "chuck@nerdherd.com"
             await entry.modify()
 
-    loop = asyncio.get_event_loop()
-    loop.run_until_complete(do())
+    asyncio.run(do())
 
 To work with other non-blocking I/O modules the default asynchronous class has to be set to a
 different one with :meth:`LDAPClient.set_async_connection_class`.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index b129eb6..3e5f9fc 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-sphinx==5.1.1
+sphinx==5.3.0
 sphinx_rtd_theme==1.0.0
-furo==2022.6.21
+furo==2022.9.29
 readthedocs-sphinx-search==0.1.1
diff --git a/pyproject.toml b/pyproject.toml
index cb5ef1e..62b2421 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,16 @@
 [tool.poetry]
 name = "bonsai"
-version = "1.5.0"
+version = "1.5.1"
 description = "Python 3 module for accessing LDAP directory servers."
 authors = ["noirello <noirello@gmail.com>"]
 license = "MIT"
 
 [tool.poetry.dependencies]
 python = "^3.7.2"
-gevent = { version = "^21.1.2", optional = true }
-tornado = { version = "^6.1", optional = true }
+gevent = { version = "^22.10.2", optional = true }
+tornado = { version = "^6.2", optional = true }
 trio = { version = "<1.0", optional = true }
+typing-extensions = { version = ">=4.0.0", python = "<3.8" }
 
 [tool.poetry.extras]
 gevent = ["gevent"]
@@ -17,15 +18,15 @@ tornado = ["tornado"]
 trio = ["trio"]
 
 [tool.poetry.dev-dependencies]
-pytest = "^7.0.1"
-pytest-cov = "^3.0.0"
+pytest = "^7.2.0"
+pytest-cov = "^4.0.0"
 pytest-timeout = "^2.1.0"
 codecov = "^2.1.11"
-sphinx = "^5.1.0"
-furo = "2022.6.21"
+sphinx = "^5.3.0"
+furo = "2022.9.29"
 pylint = "^2.14.5"
-mypy = "^0.971"
-black = "^22.6.0"
+mypy = "^0.991"
+black = "22.10.0"
 pydivert = { version = "^2.1", markers = "sys_platform == 'win32'" }
 # The following two are delocate dependencies,
 # that need to be specified to make poetry happy.
diff --git a/setup.py b/setup.py
index 3205f55..22fccb0 100644
--- a/setup.py
+++ b/setup.py
@@ -142,10 +142,12 @@ setup(
     author_email="noirello@gmail.com",
     url="https://github.com/noirello/bonsai",
     long_description=LONG_DESC,
+    long_description_content_type="text/x-rst",
     license="MIT",
     ext_modules=[BONSAI_MODULE],
     cmdclass={"build_ext": BuildExt},
     package_dir={"bonsai": "src/bonsai"},
+    package_data={"bonsai": ["py.typed"]},
     packages=[
         "bonsai",
         "bonsai.active_directory",
@@ -155,7 +157,9 @@ setup(
         "bonsai.trio",
     ],
     include_package_data=True,
-    install_requires=[],
+    install_requires=[
+        'typing-extensions >= 4.0.0 ; python_version < "3.8"',
+    ],
     extras_require={
         "gevent": ["gevent>=1.4.0"],
         "tornado": ["tornado>=5.1.1"],
diff --git a/src/bonsai/__init__.py b/src/bonsai/__init__.py
index 60eda06..bd629b7 100644
--- a/src/bonsai/__init__.py
+++ b/src/bonsai/__init__.py
@@ -11,4 +11,57 @@ from .ldif import LDIFError, LDIFReader, LDIFWriter
 from .errors import *
 from .utils import *
 
-__version__ = "1.5.0"
+__version__ = "1.5.1"
+
+__all__ = [
+    "LDAPClient",
+    "LDAPConnection",
+    "LDAPDN",
+    "LDAPEntry",
+    "LDAPModOp",
+    "LDAPReference",
+    "LDAPSearchScope",
+    "LDAPURL",
+    "LDAPValueList",
+    "LDIFError",
+    "LDIFReader",
+    "LDIFWriter",
+    # Errors
+    "LDAPError",
+    "InvalidDN",
+    "ConnectionError",
+    "AuthenticationError",
+    "AuthMethodNotSupported",
+    "ObjectClassViolation",
+    "AlreadyExists",
+    "InvalidMessageID",
+    "ClosedConnection",
+    "InsufficientAccess",
+    "TimeoutError",
+    "ProtocolError",
+    "UnwillingToPerform",
+    "NoSuchObjectError",
+    "AffectsMultipleDSA",
+    "SizeLimitError",
+    "NotAllowedOnNonleaf",
+    "NoSuchAttribute",
+    "TypeOrValueExists",
+    "PasswordPolicyError",
+    "PasswordExpired",
+    "AccountLocked",
+    "ChangeAfterReset",
+    "PasswordModNotAllowed",
+    "MustSupplyOldPassword",
+    "InsufficientPasswordQuality",
+    "PasswordTooShort",
+    "PasswordTooYoung",
+    "PasswordInHistory",
+    # Util functions
+    "escape_attribute_value",
+    "escape_filter_exp",
+    "get_tls_impl_name",
+    "get_vendor_info",
+    "has_krb5_support",
+    "set_connect_async",
+    "set_debug",
+]
diff --git a/src/bonsai/active_directory/__init__.py b/src/bonsai/active_directory/__init__.py
index f91d5bf..2a6a736 100644
--- a/src/bonsai/active_directory/__init__.py
+++ b/src/bonsai/active_directory/__init__.py
@@ -348,3 +348,16 @@ class UserAccountControl:
         return sum(
             self.__flag_values[key] for key, val in self.properties.items() if val
         )
+
+
+__all__ = [
+    "ACE",
+    "ACEFlag",
+    "ACERight",
+    "ACEType",
+    "ACL",
+    "ACLRevision",
+    "SecurityDescriptor",
+    "SID",
+    "UserAccountControl",
+]
diff --git a/src/bonsai/active_directory/sid.py b/src/bonsai/active_directory/sid.py
index 98b1df9..ddc2de7 100644
--- a/src/bonsai/active_directory/sid.py
+++ b/src/bonsai/active_directory/sid.py
@@ -58,7 +58,7 @@ class SID:
         """Return the string format of the SID."""
         ident_auth = (
             hex(self.__identifier_authority)
-            if self.__identifier_authority > 2 ** 32
+            if self.__identifier_authority > 2**32
             else self.__identifier_authority
         )
         subauths = (
@@ -72,7 +72,7 @@ class SID:
         """The representation of SID class."""
         return f"<{self.__class__.__name__}: {str(self)}>"
 
-    def __eq__(self, other: Any) -> bool:
+    def __eq__(self, other: object) -> bool:
         """
         Check equality of two SIDs by their identifier_authority and list
         of subauthorities, or if the other object is a string than by their
@@ -205,6 +205,6 @@ class SID:
         return alias
 
     @property
-    def size(self):
+    def size(self) -> int:
         """The binary size of the SID in bytes."""
         return 8 + len(self.subauthorities) * 4
diff --git a/src/bonsai/asyncio/__init__.py b/src/bonsai/asyncio/__init__.py
index 5e816b8..c4a9720 100644
--- a/src/bonsai/asyncio/__init__.py
+++ b/src/bonsai/asyncio/__init__.py
@@ -1,2 +1,5 @@
 from .aioconnection import AIOLDAPConnection
 from .aiopool import AIOConnectionPool
+
+
+__all__ = ["AIOLDAPConnection", "AIOConnectionPool"]
diff --git a/src/bonsai/asyncio/aiopool.py b/src/bonsai/asyncio/aiopool.py
index 2902585..95dacfc 100644
--- a/src/bonsai/asyncio/aiopool.py
+++ b/src/bonsai/asyncio/aiopool.py
@@ -1,5 +1,6 @@
 import asyncio
 from contextlib import asynccontextmanager
+from typing import Any, AsyncGenerator, Optional
 
 from ..pool import ConnectionPool, ClosedPool, EmptyPool
 
@@ -10,7 +11,8 @@ MYPY = False
 if MYPY:
     from ..ldapclient import LDAPClient
 
-class AIOConnectionPool(ConnectionPool):
+
+class AIOConnectionPool(ConnectionPool[AIOLDAPConnection]):
     """
     A connection pool that can be used with asnycio tasks. It's inherited from
     :class:`bonsai.pool.ConnectionPool`.
@@ -31,8 +33,8 @@ class AIOConnectionPool(ConnectionPool):
         client: "LDAPClient",
         minconn: int = 1,
         maxconn: int = 10,
-        loop=None,
-        **kwargs
+        loop: Optional[asyncio.AbstractEventLoop] = None,
+        **kwargs: Any
     ):
         super().__init__(client, minconn, maxconn, **kwargs)
         self._loop = loop
@@ -83,11 +85,15 @@ class AIOConnectionPool(ConnectionPool):
             self._lock.notify_all()
 
     @asynccontextmanager
-    async def spawn(self, *args, **kwargs):
+    async def spawn(
+        self, *args: Any, **kwargs: Any
+    ) -> AsyncGenerator[AIOLDAPConnection, None]:
+        conn = None
         try:
             if self._closed:
                 await self.open()
             conn = await self.get(*args, **kwargs)
             yield conn
         finally:
-            await self.put(conn)
+            if conn:
+                await self.put(conn)
diff --git a/src/bonsai/gevent/__init__.py b/src/bonsai/gevent/__init__.py
index 55539b2..0433086 100644
--- a/src/bonsai/gevent/__init__.py
+++ b/src/bonsai/gevent/__init__.py
@@ -1 +1,3 @@
 from .geventconnection import GeventLDAPConnection
+
+__all__ = ["GeventLDAPConnection"]
diff --git a/src/bonsai/ldapclient.py b/src/bonsai/ldapclient.py
index f0c8f25..4887be5 100644
--- a/src/bonsai/ldapclient.py
+++ b/src/bonsai/ldapclient.py
@@ -4,7 +4,7 @@
    :synopsis: For managing LDAP connections.
 
 """
-from typing import Any, Union, List, Optional, Dict, TypeVar
+from typing import Any, Union, List, Optional, Dict, Type
 
 from .ldapurl import LDAPURL
 from .ldapconnection import BaseLDAPConnection, LDAPConnection
@@ -12,8 +12,6 @@ from .ldapconnection import LDAPSearchScope
 from .ldapentry import LDAPEntry
 from .asyncio import AIOLDAPConnection
 
-CT = TypeVar("CT", bound=BaseLDAPConnection)
-
 
 class LDAPClient:
     """
@@ -29,23 +27,23 @@ class LDAPClient:
         """Init method."""
         self.__tls = tls
         self.set_url(url)
-        self.__credentials = None  # type: Optional[Dict[str, Optional[str]]]
-        self.__raw_list = []  # type: List[str]
+        self.__credentials: Optional[Dict[str, Optional[str]]] = None
+        self.__raw_list: List[str] = []
         self.__mechanism = "SIMPLE"
         self.__cert_policy = -1
-        self.__ca_cert = ""  # type: Optional[str]
-        self.__ca_cert_dir = ""  # type: Optional[str]
-        self.__client_cert = ""  # type: Optional[str]
-        self.__client_key = ""  # type: Optional[str]
-        self.__async_conn = AIOLDAPConnection
+        self.__ca_cert: Optional[str] = ""
+        self.__ca_cert_dir: Optional[str] = ""
+        self.__client_cert: Optional[str] = ""
+        self.__client_key: Optional[str] = ""
+        self.__async_conn: Type[BaseLDAPConnection] = AIOLDAPConnection
         self.__ppolicy_ctrl = False
-        self.__ext_dn = None  # type: Optional[int]
-        self.__sd_flags = None  # type: Optional[int]
+        self.__ext_dn: Optional[int] = None
+        self.__sd_flags: Optional[int] = None
         self.__auto_acquire = True
         self.__chase_referrals = False
         self.__ignore_referrals = True
         self.__managedsait_ctrl = False
-        self.__sasl_sec_props = None
+        self.__sasl_sec_props: Optional[str] = None
 
     def set_raw_attributes(self, raw_list: List[str]) -> None:
         """
@@ -653,11 +651,8 @@ class LDAPClient:
                 return None
 
     def connect(
-        self,
-        is_async: bool = False,
-        timeout: Optional[float] = None,
-        **kwargs: Dict[str, Any]
-    ) -> CT:
+        self, is_async: bool = False, timeout: Optional[float] = None, **kwargs: Any
+    ) -> BaseLDAPConnection:
         """
         Open a connection to the LDAP server.
 
diff --git a/src/bonsai/ldapconnection.py b/src/bonsai/ldapconnection.py
index 47a4848..a2ca1b1 100644
--- a/src/bonsai/ldapconnection.py
+++ b/src/bonsai/ldapconnection.py
@@ -49,7 +49,7 @@ class BaseLDAPConnection(ldapconnection, metaclass=ABCMeta):
             dname = str(dname)
         return self._evaluate(super().delete(dname, recursive), timeout)
 
-    def open(self, timeout: Optional[float] = None) -> Any:
+    def open(self, timeout: Optional[float] = None) -> "BaseLDAPConnection":
         return self._evaluate(super().open(), timeout)
 
     def modify_password(
diff --git a/src/bonsai/ldapdn.py b/src/bonsai/ldapdn.py
index 3db57e4..4e40533 100644
--- a/src/bonsai/ldapdn.py
+++ b/src/bonsai/ldapdn.py
@@ -14,7 +14,10 @@ class LDAPDN:
     __slots__ = ("__strdn",)
 
     _attrtype = r"[A-Za-z ][\w-]*|\d+(?:\.\d+)*"
-    _attrvalue = r'#(?:[\dA-Fa-f]{2})+|(?:[^,=\+<>#;\\"]|\\[,=\+<>#;\\" ]' r'|\\[\dA-Fa-f]{2})*|"(?:[^\\"]|\\[,=\+<>#;\\"]|\\[\dA-Fa-f]{2})*"'
+    _attrvalue = (
+        r'#(?:[\dA-Fa-f]{2})+|(?:[^,=\+<>#;\\"]|\\[,=\+<>#;\\" ]'
+        r'|\\[\dA-Fa-f]{2})*|"(?:[^\\"]|\\[,=\+<>#;\\"]|\\[\dA-Fa-f]{2})*"'
+    )
     _namecomp = r"({typ})=({val})(?:\+({typ})=({val}))*".format(
         typ=_attrtype, val=_attrvalue
     )
@@ -43,7 +46,7 @@ class LDAPDN:
 
     @staticmethod
     def __sanitize(strdn: str, reverse: bool = False) -> str:
-        """ Sanitizing special characters."""
+        """Sanitizing special characters."""
         char_list = [
             (r"\\", "\\5C"),
             (r"\,", "\\2C"),
@@ -102,7 +105,7 @@ class LDAPDN:
         rdns[idx] = re.split(r"(?<!\\),", value)
         self.__strdn = ",".join(rdns)
 
-    def __eq__(self, other: Any) -> bool:
+    def __eq__(self, other: object) -> bool:
         """
         Check equality of two LDAPDNs by their string formats or
         their sanitized string formats.
@@ -113,20 +116,20 @@ class LDAPDN:
         )
 
     def __str__(self) -> str:
-        """ Return the full string format of the distinguished name. """
+        """Return the full string format of the distinguished name."""
         return self.__strdn
 
     def __len__(self) -> int:
-        """ Return the number of RDNs of the distinguished name. """
+        """Return the number of RDNs of the distinguished name."""
         return len(re.split(r"(?<!\\),", self.__strdn))
 
     def __repr__(self) -> str:
-        """ The representation of LDAPDN class. """
+        """The representation of LDAPDN class."""
         return "<LDAPDN %s>" % str(self)
 
     @property
     def rdns(self) -> Tuple[Tuple[Tuple[str, str], ...], ...]:
-        """ The tuple of relative distinguished name."""
+        """The tuple of relative distinguished name."""
         return tuple(
             self.__str_rdn_to_tuple(rdn)
             for rdn in re.split(r"(?<!\\),", self.__sanitize(self.__strdn))
@@ -134,5 +137,5 @@ class LDAPDN:
 
     @rdns.setter
     def rdns(self, value: Any = None) -> None:
-        """ The tuple of relative distinguished names."""
+        """The tuple of relative distinguished names."""
         raise ValueError("RDNs attribute cannot be set.")
diff --git a/src/bonsai/ldapentry.py b/src/bonsai/ldapentry.py
index 35b653f..9a1d0bd 100644
--- a/src/bonsai/ldapentry.py
+++ b/src/bonsai/ldapentry.py
@@ -26,7 +26,7 @@ CT = TypeVar("CT", bound="BaseLDAPConnection")
 
 
 class LDAPModOp(IntEnum):
-    """ Enumeration for LDAP modification operations. """
+    """Enumeration for LDAP modification operations."""
 
     ADD = 0  #: For adding new values to the attribute.
     DELETE = 1  #: For deleting existing values from the attribute list.
@@ -116,7 +116,7 @@ class LDAPEntry(ldapentry):
                 self.__setitem__(key, value)
 
     def clear(self) -> None:
-        """ Remove all items from the dictionary. """
+        """Remove all items from the dictionary."""
         keys = list(self.keys())
         for key in keys:
             try:
@@ -175,7 +175,7 @@ class LDAPEntry(ldapentry):
         except IndexError:
             raise KeyError("popitem(): LDAPEntry is empty")
 
-    def __eq__(self, other: Any) -> bool:
+    def __eq__(self, other: object) -> bool:
         """
         Two LDAPEntry objects are considered equals, if their DN is the same.
 
@@ -297,4 +297,3 @@ class LDAPEntry(ldapentry):
             return (item for item in super().values() if item is not self.dn)
         else:
             return super().values()
-
diff --git a/src/bonsai/ldapurl.py b/src/bonsai/ldapurl.py
index 15ea246..47a3920 100644
--- a/src/bonsai/ldapurl.py
+++ b/src/bonsai/ldapurl.py
@@ -22,7 +22,7 @@ class LDAPURL:
     __slots__ = ("__hostinfo", "__searchinfo", "__extensions", "__ipv6")
 
     def __init__(self, strurl: Optional[str] = None) -> None:
-        """ Init method. """
+        """Init method."""
         self.__hostinfo = ("ldap", "localhost", 389)  # type: Tuple[str, str, int]
         # Default values to the search parameters.
         self.__searchinfo = (
@@ -37,11 +37,11 @@ class LDAPURL:
             self.__str2url(strurl)
 
     def __delattr__(self, attr: str) -> None:
-        """ None of the attributes can be deleted. """
+        """None of the attributes can be deleted."""
         raise AttributeError("%s cannot be deleted." % attr)
 
     def __str2url(self, strurl: str) -> None:
-        """ Parsing string url to LDAPURL."""
+        """Parsing string url to LDAPURL."""
         # Form: [scheme]://[host]:[port]/[basedn]?[attrs]?[scope]?[filter]?[exts]
         scheme, host, port = self.__hostinfo
         binddn, attrlist, scope, filter_exp = self.__searchinfo
@@ -83,7 +83,7 @@ class LDAPURL:
 
     @staticmethod
     def is_valid_hostname(hostname: str) -> Tuple[bool, bool]:
-        """ Validate a hostname. """
+        """Validate a hostname."""
         hostname_regex = re.compile(
             r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]"
             r"*[a-zA-Z0-9])\.)*([A-Za-z0-9]|"
@@ -101,12 +101,12 @@ class LDAPURL:
 
     @property
     def host(self) -> str:
-        """ The hostname. """
+        """The hostname."""
         return self.__hostinfo[1]
 
     @host.setter
     def host(self, value: str) -> None:
-        """ Setter for hostname. """
+        """Setter for hostname."""
         # RegExp for valid hostname.
         valid, ipv6 = self.is_valid_hostname(value)
         if not valid:
@@ -117,12 +117,12 @@ class LDAPURL:
 
     @property
     def port(self) -> int:
-        """ The portnumber. """
+        """The portnumber."""
         return self.__hostinfo[2]
 
     @port.setter
     def port(self, value: int) -> None:
-        """ Setter for portnumber. """
+        """Setter for portnumber."""
         if isinstance(value, int) and (value > 0 and value < 65535):
             self.__hostinfo = (self.__hostinfo[0], self.__hostinfo[1], value)
         else:
@@ -130,12 +130,12 @@ class LDAPURL:
 
     @property
     def scheme(self) -> str:
-        """ The URL scheme."""
+        """The URL scheme."""
         return self.__hostinfo[0]
 
     @scheme.setter
     def scheme(self, value: str) -> None:
-        """ Setter for URL scheme."""
+        """Setter for URL scheme."""
         # It must be ldap, ldaps or ldapi
         if isinstance(value, str) and value.lower() in ("ldap", "ldaps", "ldapi"):
             self.__hostinfo = (value.lower(), self.__hostinfo[1], self.__hostinfo[2])
@@ -144,12 +144,12 @@ class LDAPURL:
 
     @property
     def basedn(self) -> LDAPDN:
-        """ The LDAP distinguished name for binding. """
+        """The LDAP distinguished name for binding."""
         return self.__searchinfo[0]
 
     @basedn.setter
     def basedn(self, value: Union[LDAPDN, str]) -> None:
-        """ Setter for LDAP distinguished name for binding. """
+        """Setter for LDAP distinguished name for binding."""
         self.__searchinfo = (
             LDAPDN(str(value)),
             self.__searchinfo[1],
@@ -159,17 +159,17 @@ class LDAPURL:
 
     @property
     def attributes(self) -> List[str]:
-        """ The searching attributes. """
+        """The searching attributes."""
         return self.__searchinfo[1]
 
     @property
     def scope(self) -> str:
-        """ The searching scope. """
+        """The searching scope."""
         return self.__searchinfo[2]
 
     @scope.setter
     def scope(self, value: str) -> None:
-        """ Setter for searching scope. """
+        """Setter for searching scope."""
         if isinstance(value, str):
             if value.lower() in ("base", "one", "sub"):
                 self.__searchinfo = (
@@ -188,7 +188,7 @@ class LDAPURL:
 
     @property
     def scope_num(self) -> int:
-        """ Return the searching scope number. """
+        """Return the searching scope number."""
         if self.scope == "base":
             return 0
         if self.scope == "one":
@@ -200,7 +200,7 @@ class LDAPURL:
 
     @property
     def filter_exp(self) -> str:
-        """ The searching filter expression. """
+        """The searching filter expression."""
         return self.__searchinfo[3]
 
     def get_address(self) -> str:
@@ -216,7 +216,7 @@ class LDAPURL:
         else:
             return f"{self.__hostinfo[0]}://{self.__hostinfo[1]}:{self.__hostinfo[2]:d}"
 
-    def __eq__(self, other: Any) -> bool:
+    def __eq__(self, other: object) -> bool:
         """
         Check equality of two LDAPURL or an LDAPURL and a string.
         """
@@ -237,10 +237,10 @@ class LDAPURL:
                 return False
             return self == other
         else:
-            return False
+            return NotImplemented
 
     def __str__(self) -> str:
-        """ Returns the full format of LDAP URL. """
+        """Returns the full format of LDAP URL."""
         strurl = self.get_address()
         strattrs = ""
         strexts = ""
@@ -267,5 +267,5 @@ class LDAPURL:
         return strurl
 
     def __repr__(self) -> str:
-        """ The LDAPURL representation. """
+        """The LDAPURL representation."""
         return "<LDAPURL %s>" % str(self)
diff --git a/src/bonsai/ldapvaluelist.py b/src/bonsai/ldapvaluelist.py
index 89cc863..7a9b38d 100644
--- a/src/bonsai/ldapvaluelist.py
+++ b/src/bonsai/ldapvaluelist.py
@@ -1,5 +1,10 @@
 from typing import Any, List, Union, Iterable, Optional
 
+try:
+    from typing import SupportsIndex
+except ImportError:
+    from typing_extensions import SupportsIndex
+
 import bonsai
 
 
@@ -58,7 +63,7 @@ class LDAPValueList(list):
     def __contains__(self, item: Any) -> bool:
         return bonsai.utils._unique_contains(self, item)[0]
 
-    def __delitem__(self, idx: Union[int, slice]) -> None:
+    def __delitem__(self, idx: Union[SupportsIndex, slice]) -> None:
         old_value = super().__getitem__(idx)
         if isinstance(idx, slice):
             for item in old_value:
@@ -83,7 +88,7 @@ class LDAPValueList(list):
         self.extend(other)
         return self
 
-    def __setitem__(self, idx: Union[int, slice], value: Any) -> None:
+    def __setitem__(self, idx: Union[SupportsIndex, slice], value: Any) -> None:
         old_value = self[idx]
         if isinstance(idx, slice):
             for item in value:
@@ -131,7 +136,7 @@ class LDAPValueList(list):
         self.__status = 1
         super().extend(items)
 
-    def insert(self, idx: int, value: Any) -> None:
+    def insert(self, idx: SupportsIndex, value: Any) -> None:
         """
         Insert a unique item at a given position.
 
@@ -159,7 +164,7 @@ class LDAPValueList(list):
         self.__status = 1
         self.__balance(self.__added, self.__deleted, obj)
 
-    def pop(self, idx: int = -1) -> Any:
+    def pop(self, idx: SupportsIndex = -1) -> Any:
         """
         Remove the item at the given position in the LDAPValueList, and
         return it. If no index is specified, pop() removes and returns the
@@ -193,12 +198,12 @@ class LDAPValueList(list):
         return new_list
 
     @property
-    def added(self) -> List:
+    def added(self) -> List[str]:
         """List of the added values."""
         return self.__added
 
     @property
-    def deleted(self) -> List:
+    def deleted(self) -> List[str]:
         """List of the deleted values."""
         return self.__deleted
 
diff --git a/src/bonsai/ldif.py b/src/bonsai/ldif.py
index b1085b3..6b79dd4 100644
--- a/src/bonsai/ldif.py
+++ b/src/bonsai/ldif.py
@@ -3,7 +3,19 @@ import io
 import os
 from collections import defaultdict
 from itertools import groupby
-from typing import TextIO, Iterable, Iterator, Any, Optional, List, Union
+from typing import (
+    Dict,
+    TextIO,
+    Iterable,
+    Iterator,
+    Any,
+    Optional,
+    List,
+    Union,
+    Mapping,
+    Callable,
+    KeysView
+)
 
 from .ldapentry import LDAPEntry, LDAPModOp
 from .ldapvaluelist import LDAPValueList
@@ -12,7 +24,7 @@ from .errors import LDAPError
 
 
 class LDIFError(LDAPError):
-    """ General exception that is raised during reading or writing an LDIF file. """
+    """General exception that is raised during reading or writing an LDIF file."""
 
     code = -300
 
@@ -33,19 +45,20 @@ class LDIFReader:
     def __init__(
         self, input_file: TextIO, autoload: bool = True, max_length: int = 76
     ) -> None:
-        """ Init method. """
+        """Init method."""
         if not isinstance(max_length, int):
             raise TypeError("The max_length must be int.")
+        self.__file: TextIO
         self.input_file = input_file
         self.autoload = autoload
         self.max_length = max_length
-        self.version = None  # type: Optional[int]
+        self.version: Optional[int] = None
         self.__entries = self.__read_attributes()
         self.__num_of_entries = 0
         self.__resource_handlers = {"file": self.__load_file}
 
     def __read_attributes(self) -> Iterator[List[str]]:
-        buffer = []  # type: List[str]
+        buffer: List[str] = []
         comment = False
         for num, line in enumerate(self.__file):
             try:
@@ -77,16 +90,16 @@ class LDIFReader:
     @staticmethod
     def __convert(val: Union[str, bytes]) -> Union[str, bytes, int]:
         try:
-            val = int(val)
+            return int(val)
         except ValueError:
             try:
-                val = val.decode("UTF-8")
+                return val.decode("UTF-8")
             except (ValueError, AttributeError):
                 pass
         return val
 
     @staticmethod
-    def __find_key(searched_key: str, keylist: List[str]) -> Optional[str]:
+    def __find_key(searched_key: str, keylist: KeysView[str]) -> Optional[str]:
         for key in keylist:
             if key.lower() == searched_key.lower():
                 return key
@@ -101,7 +114,7 @@ class LDIFReader:
         with open(abs_filepath, "rb") as resource:
             return resource.read()
 
-    def load_resource(self, url: str) -> Union[str, bytes]:
+    def load_resource(self, url: str) -> bytes:
         try:
             scheme, _ = url.split(":", maxsplit=1)
             return self.__resource_handlers[scheme](url)
@@ -121,24 +134,34 @@ class LDIFReader:
         ]
         self.__num_of_entries += 1
         for block in attr_blocks:
-            attr_dict = defaultdict(LDAPValueList)
+            attr_dict: Dict[str, LDAPValueList] = defaultdict(LDAPValueList)
             for attrval in block:
                 try:
                     if ":: " in attrval:
                         attr, val = attrval.split(":: ")
                         val = base64.b64decode(val)
                     elif ": " in attrval:
-                        attr, val = attrval.split(": ")
+                        attr, val = attrval.split(": ", maxsplit=1)
+                        if ord(val[0]) > 127 or val[0] in (
+                            "\0",
+                            "\n",
+                            "\r",
+                            " ",
+                            ":",
+                            "<",
+                        ):
+                            raise ValueError("Not a safe first character in value.")
                     elif ":< " in attrval:
                         attr, val = attrval.split(":< ")
                         if self.__autoload:
                             val = self.load_resource(val)
                     else:
-                        raise ValueError()
-                except ValueError:
+                        raise ValueError("Missing valid attribute value separator.")
+                except ValueError as err:
                     raise LDIFError(
-                        f"Invalid attribute value pair: '{attrval}' for entry #{self.__num_of_entries}."
-                    ) from None
+                        f"Invalid attribute value pair: '{attrval}'"
+                        f" for entry #{self.__num_of_entries}."
+                    ) from err
                 if attr.lower() == "changetype":
                     change_type = val.lower()
                 elif attr.lower() == "dn":
@@ -175,29 +198,29 @@ class LDIFReader:
         return entry
 
     @property
-    def input_file(self):
-        """ The file-like object of an LDIF file. """
+    def input_file(self) -> TextIO:
+        """The file-like object of an LDIF file."""
         return self.__file
 
     @input_file.setter
-    def input_file(self, value: io.TextIOBase):
+    def input_file(self, value: TextIO) -> None:
         if not isinstance(value, io.TextIOBase):
             raise TypeError("The input_file must be file-like object in text mode.")
         self.__file = value
 
     @property
-    def autoload(self):
-        """ Enable/disable autoloading resources in LDIF files. """
+    def autoload(self) -> bool:
+        """Enable/disable autoloading resources in LDIF files."""
         return self.__autoload
 
     @autoload.setter
-    def autoload(self, value: bool):
+    def autoload(self, value: bool) -> None:
         if not isinstance(value, bool):
             raise TypeError("The autoload property must be bool.")
         self.__autoload = value
 
     @property
-    def resource_handlers(self):
+    def resource_handlers(self) -> Mapping[str, Callable[[str], bytes]]:
         """
         A dictionary of supported resource types. The keys are the schemes,
         while the values are functions that expect the full URL parameters
@@ -218,9 +241,10 @@ class LDIFWriter:
     """
 
     def __init__(self, output_file: TextIO, max_length: int = 76) -> None:
-        """ Init method. """
+        """Init method."""
         if not isinstance(max_length, int):
             raise TypeError("The max_length must be int.")
+        self.__file: TextIO
         self.output_file = output_file
         self.max_length = max_length
 
@@ -313,12 +337,12 @@ class LDIFWriter:
         self.__file.write("\n")
 
     @property
-    def output_file(self):
-        """ The file-like object for an LDIF file. """
+    def output_file(self) -> TextIO:
+        """The file-like object for an LDIF file."""
         return self.__file
 
     @output_file.setter
-    def output_file(self, value: io.TextIOBase):
+    def output_file(self, value: TextIO) -> None:
         if not isinstance(value, io.TextIOBase):
             raise TypeError("The output_file must be file-like object in text mode.")
         self.__file = value
diff --git a/src/bonsai/pool.py b/src/bonsai/pool.py
index 90b9fbf..80afce7 100644
--- a/src/bonsai/pool.py
+++ b/src/bonsai/pool.py
@@ -1,6 +1,8 @@
 import threading
 from contextlib import contextmanager
-from typing import Optional, Dict, Any
+from typing import Optional, Any, Set, Generic, TypeVar, Generator
+
+from .ldapconnection import BaseLDAPConnection, LDAPConnection
 
 MYPY = False
 
@@ -9,24 +11,27 @@ if MYPY:
 
 
 class PoolError(Exception):
-    """ Connection pool related errors. """
+    """Connection pool related errors."""
 
     pass
 
 
 class ClosedPool(PoolError):
-    """ Raised, when the connection pool is closed. """
+    """Raised, when the connection pool is closed."""
 
     pass
 
 
 class EmptyPool(PoolError):
-    """ Raised, when the connection pool is empty. """
+    """Raised, when the connection pool is empty."""
 
     pass
 
 
-class ConnectionPool:
+T = TypeVar("T", bound=BaseLDAPConnection)
+
+
+class ConnectionPool(Generic[T]):
     """
     A connection pool object for managing multiple open connections.
 
@@ -42,13 +47,9 @@ class ConnectionPool:
     """
 
     def __init__(
-        self,
-        client: "LDAPClient",
-        minconn: int = 1,
-        maxconn: int = 10,
-        **kwargs: Dict[str, Any]
+        self, client: "LDAPClient", minconn: int = 1, maxconn: int = 10, **kwargs: Any
     ) -> None:
-        """ Init method. """
+        """Init method."""
         if minconn < 0:
             raise ValueError("The minconn must be positive.")
         if minconn > maxconn:
@@ -58,8 +59,8 @@ class ConnectionPool:
         self._client = client
         self._kwargs = kwargs
         self._closed = True
-        self._idles = set()
-        self._used = set()
+        self._idles: Set[T] = set()
+        self._used: Set[T] = set()
 
     def open(self) -> None:
         """
@@ -67,10 +68,10 @@ class ConnectionPool:
         connections.
         """
         for _ in range(self._minconn - self.idle_connection - self.shared_connection):
-            self._idles.add(self._client.connect(self._kwargs))
+            self._idles.add(self._client.connect(**self._kwargs))
         self._closed = False
 
-    def get(self):
+    def get(self) -> T:
         """
         Get a connection from the connection pool.
 
@@ -84,13 +85,13 @@ class ConnectionPool:
             conn = self._idles.pop()
         except KeyError:
             if len(self._used) < self._maxconn:
-                conn = self._client.connect(self._kwargs)
+                conn = self._client.connect(**self._kwargs)
             else:
                 raise EmptyPool("Pool is empty.") from None
         self._used.add(conn)
         return conn
 
-    def put(self, conn) -> None:
+    def put(self, conn: T) -> None:
         """
         Put back a connection to the connection pool. The caller is allowed to
         close the connection (if, for instance, it is in an error state), in
@@ -112,7 +113,7 @@ class ConnectionPool:
             raise PoolError("The %r is not managed by this pool." % conn) from None
 
     def close(self) -> None:
-        """ Close the pool and all of its managed connections. """
+        """Close the pool and all of its managed connections."""
         for conn in self._idles:
             conn.close()
         for conn in self._used:
@@ -122,7 +123,7 @@ class ConnectionPool:
         self._used = set()
 
     @contextmanager
-    def spawn(self, *args, **kwargs):
+    def spawn(self, *args: Any, **kwargs: Any) -> Generator[T, None, None]:
         """
         Context manager method that acquires a connection from the pool
         and returns it on exit. It also opens the pool if it hasn't been
@@ -133,13 +134,15 @@ class ConnectionPool:
         :params \\*\\*kwargs: the keyword arguments passed to
                 :meth:`bonsai.pool.ConnectionPool.get`.
         """
+        conn = None
         try:
             if self._closed:
                 self.open()
             conn = self.get(*args, **kwargs)
             yield conn
         finally:
-            self.put(conn)
+            if conn:
+                self.put(conn)
 
     @property
     def empty(self) -> bool:
@@ -159,28 +162,28 @@ class ConnectionPool:
 
     @property
     def shared_connection(self) -> int:
-        """ The number of shared connections. """
+        """The number of shared connections."""
         return len(self._used)
 
     @property
     def idle_connection(self) -> int:
-        """ the number of idle connection. """
+        """the number of idle connection."""
         return len(self._idles)
 
     @property
     def max_connection(self) -> int:
-        """ The maximal number of connections that the pool can have. """
+        """The maximal number of connections that the pool can have."""
         return self._maxconn
 
     @max_connection.setter
-    def max_connection(self, val) -> None:
-        """ The maximal number of connections that the pool can have. """
+    def max_connection(self, val: int) -> None:
+        """The maximal number of connections that the pool can have."""
         if val < self._minconn:
             raise ValueError("The maxconn must be greater than minconn.")
         self._maxconn = val
 
 
-class ThreadedConnectionPool(ConnectionPool):
+class ThreadedConnectionPool(ConnectionPool[LDAPConnection]):
     """
     A connection pool that can be shared between threads. It's inherited from
     :class:`bonsai.pool.ConnectionPool`.
@@ -204,14 +207,14 @@ class ThreadedConnectionPool(ConnectionPool):
         minconn: int = 1,
         maxconn: int = 10,
         block: bool = True,
-        **kwargs: Dict[str, Any]
+        **kwargs: Any
     ) -> None:
-        """ Init method. """
+        """Init method."""
         super().__init__(client, minconn, maxconn, **kwargs)
         self._block = block
         self._lock = threading.Condition()
 
-    def get(self, timeout: Optional[float] = None):
+    def get(self, timeout: Optional[float] = None) -> LDAPConnection:
         """
         Get a connection from the connection pool.
 
@@ -227,7 +230,7 @@ class ThreadedConnectionPool(ConnectionPool):
             self._lock.notify()
             return conn
 
-    def put(self, conn) -> None:
+    def put(self, conn: LDAPConnection) -> None:
         with self._lock:
             super().put(conn)
             self._lock.notify()
diff --git a/src/bonsai/py.typed b/src/bonsai/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/src/bonsai/tornado/__init__.py b/src/bonsai/tornado/__init__.py
index f993305..83018fa 100644
--- a/src/bonsai/tornado/__init__.py
+++ b/src/bonsai/tornado/__init__.py
@@ -1 +1,3 @@
 from .tornadoconnection import TornadoLDAPConnection
+
+__all__ = ["TornadoLDAPConnection"]
diff --git a/src/bonsai/trio/__init__.py b/src/bonsai/trio/__init__.py
index 5eedd06..c67428d 100644
--- a/src/bonsai/trio/__init__.py
+++ b/src/bonsai/trio/__init__.py
@@ -1 +1,3 @@
 from .trioconnection import TrioLDAPConnection
+
+__all__ = ["TrioLDAPConnection"]
diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py
index 039fae4..e396b33 100644
--- a/tests/test_asyncio.py
+++ b/tests/test_asyncio.py
@@ -20,8 +20,7 @@ def asyncio_test(func):
     @wraps(func)
     def wrapper(*args, **kwargs):
         future = func(*args, **kwargs)
-        loop = asyncio.get_event_loop()
-        loop.run_until_complete(future)
+        asyncio.run(future)
 
     return wrapper
 
diff --git a/tests/test_ldifreader.py b/tests/test_ldifreader.py
index 9ea477e..e59d818 100644
--- a/tests/test_ldifreader.py
+++ b/tests/test_ldifreader.py
@@ -7,7 +7,7 @@ from bonsai import LDIFReader, LDIFError
 
 
 def test_init_params():
-    """ Test constructor parameters for LDIFReader. """
+    """Test constructor parameters for LDIFReader."""
     with pytest.raises(TypeError):
         _ = LDIFReader("wrong")
     with pytest.raises(TypeError):
@@ -21,7 +21,7 @@ def test_init_params():
 
 
 def test_version():
-    """ Test setting version attribute from LDIF. """
+    """Test setting version attribute from LDIF."""
     text = "version: 1\ndn: cn=test\ncn: test\n"
     with StringIO(text) as ldif:
         reader = LDIFReader(ldif)
@@ -31,7 +31,7 @@ def test_version():
 
 
 def test_missing_dn():
-    """ Test missing distinguished name in LDIF entry. """
+    """Test missing distinguished name in LDIF entry."""
     text = "changetype: add\nsn: test\ncn: test\n"
     with StringIO(text) as ldif:
         reader = LDIFReader(ldif)
@@ -42,13 +42,14 @@ def test_missing_dn():
 
 
 def test_invalid_file():
-    """ Test invalid and too long line. """
+    """Test invalid lines."""
     text = " invalid\n"
     with StringIO(text) as ldif:
         reader = LDIFReader(ldif)
         with pytest.raises(LDIFError) as excinfo:
             _ = next(reader)
         assert "Parser error" in str(excinfo.value)
+        assert isinstance(excinfo.value.__context__, IndexError)
     text = "dn: cn=test\nnotvalid attribute\n"
     with StringIO(text) as ldif:
         reader = LDIFReader(ldif)
@@ -56,24 +57,50 @@ def test_invalid_file():
             _ = next(reader)
         assert "Invalid attribute value pair:" in str(excinfo.value)
         assert "entry #1" in str(excinfo.value)
-    text = "dn: cn=toolong\n"
+        assert "value separator" in str(excinfo.value.__context__)
+    text = "dn: :cn=test notvalid attribute\n"
     with StringIO(text) as ldif:
-        reader = LDIFReader(ldif, max_length=12)
+        reader = LDIFReader(ldif)
         with pytest.raises(LDIFError) as excinfo:
             _ = next(reader)
-        assert "too long" in str(excinfo.value)
-        assert "Line 1" in str(excinfo.value)
-    text = "dn: cn=test notvalid: attribute\n"
+        assert "Invalid attribute value pair:" in str(excinfo.value)
+        assert "entry #1" in str(excinfo.value)
+        assert "safe first character" in str(excinfo.value.__context__)
+
+
+def test_invalid_base64():
+    """Test LDIF line with invalid base64 data."""
+    text = "dn:: cn=test notvalid: attribute\n"
+    with StringIO(text) as ldif:
+        reader = LDIFReader(ldif)
+        with pytest.raises(LDIFError) as excinfo:
+            _ = next(reader)
+        assert "Invalid attribute value pair:" in str(excinfo.value)
+        assert "entry #1" in str(excinfo.value)
+        assert "Incorrect padding" in str(excinfo.value.__context__)
+    text = "cn:: dGV4dCx2YWx1ZSxkYXRhNNO"
     with StringIO(text) as ldif:
         reader = LDIFReader(ldif)
         with pytest.raises(LDIFError) as excinfo:
             _ = next(reader)
         assert "Invalid attribute value pair:" in str(excinfo.value)
         assert "entry #1" in str(excinfo.value)
+        assert "Incorrect padding" in str(excinfo.value.__context__)
+
+
+def test_too_long_line():
+    """Test LDIF input with too long line."""
+    text = "dn: cn=toolong\n"
+    with StringIO(text) as ldif:
+        reader = LDIFReader(ldif, max_length=12)
+        with pytest.raises(LDIFError) as excinfo:
+            _ = next(reader)
+        assert "too long" in str(excinfo.value)
+        assert "Line 1" in str(excinfo.value)
 
 
 def test_comment():
-    """ Test parsing comment lines in LDIF files. """
+    """Test parsing comment lines in LDIF files."""
     ldif = "# DN: cn=test\ndn: cn=test\n#Other comment line.\ncn: test\n"
     with StringIO(ldif) as test:
         reader = LDIFReader(test)
@@ -88,7 +115,7 @@ def test_comment():
 
 
 def test_input_file():
-    """ Test input_file property. """
+    """Test input_file property."""
     inp = StringIO()
     ldif = LDIFReader(inp)
     assert ldif.input_file == inp
@@ -100,7 +127,7 @@ def test_input_file():
 
 
 def test_autoload():
-    """ Test autoload property. """
+    """Test autoload property."""
     inp = StringIO()
     ldif = LDIFReader(inp)
     assert ldif.autoload == True
@@ -111,7 +138,7 @@ def test_autoload():
 
 
 def test_resource_handlers():
-    """ Test resource_handlers property. """
+    """Test resource_handlers property."""
     inp = StringIO()
     ldif = LDIFReader(inp)
     assert isinstance(ldif.resource_handlers, dict)
@@ -123,7 +150,7 @@ def test_resource_handlers():
 
 
 def test_multiline_attribute():
-    """ Test parsing multiline attributes in LDIF. """
+    """Test parsing multiline attributes in LDIF."""
     text = "dn: cn=unimaginably+sn=very,ou=very,dc=very,dc=long,\n dc=line\ncn: unimaginably\nsn: very\nsn: long\n"
     with StringIO(text) as test:
         reader = LDIFReader(test)
@@ -135,7 +162,7 @@ def test_multiline_attribute():
 
 
 def test_multiple_entries():
-    """ Test parsing multiple entries in one LDIF. """
+    """Test parsing multiple entries in one LDIF."""
     text = "dn: cn=test1\ncn: test1\n\ndn: cn=test2\ncn: test2\n"
     with StringIO(text) as test:
         reader = LDIFReader(test)
@@ -146,7 +173,7 @@ def test_multiple_entries():
 
 
 def test_encoded_attributes():
-    """ Test parsing base64 encoded attributes. """
+    """Test parsing base64 encoded attributes."""
     attr = "test"
     text = f"version: 1\ndn: cn=test\ncn:: {base64.b64encode(attr.encode('UTF-8')).decode('UTF-8')}\n"
     with StringIO(text) as test:
@@ -157,7 +184,7 @@ def test_encoded_attributes():
 
 
 def test_load_resource():
-    """ Test load_resource method. """
+    """Test load_resource method."""
     curdir = os.path.abspath(os.path.dirname(__file__))
     with StringIO() as test:
         test.name = "dummy"
@@ -175,7 +202,7 @@ def test_load_resource():
 
 
 def test_url_attribute():
-    """ Test URL attribute in LDIF file. """
+    """Test URL attribute in LDIF file."""
     text = "dn: cn=test\ncn: test1\njpegPhoto:< file://./testenv/test.jpeg\n"
     with StringIO(text) as test:
         test.name = __file__
@@ -187,7 +214,7 @@ def test_url_attribute():
 
 
 def test_changetype():
-    """ Test changetype attribute in LDIF file. """
+    """Test changetype attribute in LDIF file."""
     text = "dn: cn=test\nchangetype: add\ncn: test\n"
     with StringIO(text) as test:
         reader = LDIFReader(test)
@@ -198,7 +225,7 @@ def test_changetype():
 
 
 def test_missing_attribute():
-    """ Test missing attribute in LDIF-CHANGE. """
+    """Test missing attribute in LDIF-CHANGE."""
     text = "dn: cn=test\nchangetype: modify\nadd: sn\ncn: test\n"
     with StringIO(text) as test:
         reader = LDIFReader(test)
@@ -206,8 +233,20 @@ def test_missing_attribute():
             _ = next(reader)
 
 
+def test_value_with_colon():
+    """Test attribute value with containing colon."""
+    text = "dn: cn=test\npostaladdress: p.o. box: 1234\ncn: test\n"
+    with StringIO(text) as test:
+        test.name = __file__
+        reader = LDIFReader(test)
+        ent = next(reader)
+    assert ent.dn == "cn=test"
+    assert ent["postaladdress"][0] == "p.o. box: 1234"
+    assert ent["cn"][0] == "test"
+
+
 def test_modify_change():
-    """ Test loading modified attributes from LDIF-CHANGE. """
+    """Test loading modified attributes from LDIF-CHANGE."""
     text = """dn: cn=test
 changetype: modify
 add: sn

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.1.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.1.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.1.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.1.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai/py.typed

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/lib/debug/.build-id/d6/3d73b2bcb89dc2778e1bbe909cebab1bc1ed3e.debug
-rw-r--r--  root/root   /usr/lib/debug/.dwz/x86_64-linux-gnu/python3-bonsai.debug
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.0.egg-info/PKG-INFO
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.0.egg-info/dependency_links.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.0.egg-info/requires.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai-1.5.0.egg-info/top_level.txt
-rw-r--r--  root/root   /usr/lib/python3/dist-packages/bonsai/_bonsai.cpython-311-x86_64-linux-gnu.so

Control files of package python3-bonsai: lines which differ (wdiff format)

  • Depends: libc6 (>= 2.34), libgssapi-krb5-2 (>= 1.17), libkrb5-3 (>= 1.10+dfsg~alpha1), libldap-2.5-0 (>= 2.5.4), python3 (<< 3.12), 3.11), python3 (>= 3.10~), python3:any

Control files of package python3-bonsai-dbgsym: lines which differ (wdiff format)

  • Build-Ids: 9c2f9a58b009264fc9b28163c823edfa4c9b1d6e d63d73b2bcb89dc2778e1bbe909cebab1bc1ed3e

No differences were encountered between the control files of package python3-bonsai-doc

More details

Full run details