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