diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 738d86fa..00000000
--- a/.coveragerc
+++ /dev/null
@@ -1,27 +0,0 @@
-[run]
-branch = True
-source =
- ldap
- ldif
- ldapurl
- slapdtest
-
-[paths]
-source =
- Lib/
- .tox/*/lib/python*/site-packages/
-
-[report]
-ignore_errors = False
-precision = 1
-exclude_lines =
- pragma: no cover
- raise NotImplementedError
- if 0:
- if __name__ == .__main__.:
- if PY2
- if not PY2
-
-[html]
-directory = build/htmlcov
-title = python-ldap coverage report
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 37843f31..06d0b2ed 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,7 +1,7 @@
---
name: CI
-on:
+on:
push:
pull_request:
schedule:
@@ -20,18 +20,15 @@ jobs:
fail-fast: false
matrix:
python-version:
- - "3.7"
- - "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
+ - "3.13"
- "pypy3.9"
- image:
+ - "pypy3.10"
+ image:
- "ubuntu-22.04"
- include:
- - python-version: "3.6"
- image: "ubuntu-20.04"
steps:
- name: Checkout
uses: "actions/checkout@v4"
@@ -43,7 +40,7 @@ jobs:
- name: Disable AppArmor
run: sudo aa-disable /usr/sbin/slapd
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
diff --git a/.github/workflows/tox-fedora.yml b/.github/workflows/tox-fedora.yml
index b86303fe..bc6f45c5 100644
--- a/.github/workflows/tox-fedora.yml
+++ b/.github/workflows/tox-fedora.yml
@@ -9,7 +9,7 @@ jobs:
tox_test:
name: Tox env "${{matrix.tox_env}}" on Fedora
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Run Tox tests
uses: fedora-python/tox-github-action@main
with:
@@ -17,23 +17,19 @@ jobs:
dnf_install: >
@c-development openldap-devel python3-devel
openldap-servers openldap-clients lcov clang-analyzer valgrind
- enchant
+ enchant python3-setuptools
strategy:
matrix:
tox_env:
- - py36
- - py37
- - py38
- py39
- py310
- py311
- py312
- - c90-py36
- - c90-py37
+ - py313
- py3-nosasltls
- py3-trace
- pypy3
- doc
# Use GitHub's Linux Docker host
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
diff --git a/.gitignore b/.gitignore
index bab21878..75a13538 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,6 @@
*.pyc
__pycache__/
.tox
-.coverage*
-!.coveragerc
/.cache
/.pytest_cache
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..91fb6028
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,22 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: Doc/conf.py
+
+# We recommend specifying your dependencies to enable reproducible builds:
+# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+python:
+ install:
+ - requirements: Doc/requirements.txt
diff --git a/CHANGES b/CHANGES
index 500fa1e7..0491b6ef 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,20 @@
+Released 3.4.4 2022-11-17
+
+Fixes:
+* Reconnect race condition in ReconnectLDAPObject is now fixed
+* Socket ownership is now claimed once we've passed it to libldap
+* LDAP_set_option string formats are now compatible with Python 3.12
+
+Doc/
+* Security Policy was created
+* Broken article links are fixed now
+* Bring Conscious Language improvements
+
+Infrastructure:
+* Add testing and document support for Python 3.10, 3.11, and 3.12
+
+
+----------------------------------------------------------------
Released 3.4.3 2022-09-15
This is a minor release to bring back the removed OPT_X_TLS option.
diff --git a/Demo/pyasn1/syncrepl.py b/Demo/pyasn1/syncrepl.py
index f1f24e19..754b237a 100644
--- a/Demo/pyasn1/syncrepl.py
+++ b/Demo/pyasn1/syncrepl.py
@@ -76,7 +76,7 @@ def syncrepl_entry(self, dn, attributes, uuid):
logger.debug('Detected %s of entry %r', change_type, dn)
# If we have a cookie then this is not our first time being run,
# so it must be a change
- if 'ldap_cookie' in self.__data:
+ if 'cookie' in self.__data:
self.perform_application_sync(dn, attributes, previous_attributes)
def syncrepl_delete(self,uuids):
@@ -98,7 +98,7 @@ def syncrepl_present(self,uuids,refreshDeletes=False):
deletedEntries = [
uuid
for uuid in self.__data.keys()
- if uuid not in self.__presentUUIDs and uuid != 'ldap_cookie'
+ if uuid not in self.__presentUUIDs and uuid != 'cookie'
]
self.syncrepl_delete( deletedEntries )
# Phase is now completed, reset the list
diff --git a/Doc/contributing.rst b/Doc/contributing.rst
index bbaab491..de63a2e3 100644
--- a/Doc/contributing.rst
+++ b/Doc/contributing.rst
@@ -19,7 +19,7 @@ Communication
Always keep in mind that python-ldap is developed and maintained by volunteers.
We're happy to share our work, and to work with you to make the library better,
-but (until you pay someone), there's obligation to provide assistance.
+but (until you pay someone), there's no obligation to provide assistance.
So, keep it friendly, respectful, and supportive!
@@ -72,9 +72,6 @@ If you're used to open-source Python development with Git, here's the gist:
.. _the bug tracker: https://github.com/python-ldap/python-ldap/issues
.. _tox: https://tox.readthedocs.io/en/latest/
-Or, if you prefer to avoid closed-source services:
-
-* ``git clone https://pagure.io/python-ldap``
* Send bug reports and patches to the mailing list.
* Run tests with `tox`_; ignore Python interpreters you don't have locally.
* Read the documentation directly at `Read the Docs`_.
@@ -203,8 +200,6 @@ remember:
* Consider making the summary line suitable for the CHANGES document,
and starting it with a prefix like ``Lib:`` or ``Tests:``.
-* Push to Pagure as well.
-
If you have good reason to break the “rules”, go ahead and break them,
but mention why.
@@ -224,7 +219,7 @@ If you are tasked with releasing python-ldap, remember to:
* Run ``python setup.py sdist``, and smoke-test the resulting package
(install in a clean virtual environment, import ``ldap``).
* Create GPG-signed Git tag: ``git tag -s python-ldap-{version}``.
- Push it to GitHub and Pagure.
+ Push it to GitHub.
* Release the ``sdist`` on PyPI.
* Announce the release on the mailing list.
Mention the Git hash.
diff --git a/Doc/installing.rst b/Doc/installing.rst
index e4518c11..1c7ec8c3 100644
--- a/Doc/installing.rst
+++ b/Doc/installing.rst
@@ -63,8 +63,8 @@ to get up to date information which versions are available.
Windows
-------
-Unofficial packages for Windows are available on
-`Christoph Gohlke's page `_.
+Unofficial binary builds for Windows are provided by Christoph Gohlke, available at
+`python-ldap-build `_.
`FreeBSD `_
@@ -76,12 +76,23 @@ The CVS repository of FreeBSD contains the package
macOS
-----
-You can install directly with pip::
+You can install directly with pip. First install Xcode command line tools::
$ xcode-select --install
- $ pip install python-ldap \
- --global-option=build_ext \
- --global-option="-I$(xcrun --show-sdk-path)/usr/include/sasl"
+
+Then install python-ldap::
+
+ $ pip install python-ldap
+
+For custom installations, you may need to set environment variables::
+
+ $ export CPPFLAGS="-I$(xcrun --show-sdk-path)/usr/include/sasl"
+ $ pip install python-ldap
+
+If using Homebrew::
+
+ $ brew install openldap
+ $ pip install python-ldap
.. _install-source:
@@ -90,11 +101,14 @@ Installing from Source
======================
-python-ldap is built and installed using the Python setuptools.
-From a source repository::
+python-ldap is built and installed using modern Python packaging standards
+with pyproject.toml configuration. From a source repository::
- $ python -m pip install setuptools
- $ python setup.py install
+ $ pip install .
+
+For development installation with editable mode::
+
+ $ pip install -e .
If you have more than one Python interpreter installed locally, you should
use the same one you plan to use python-ldap with.
@@ -143,10 +157,15 @@ Packages for building::
Debian
------
+Packages for building::
+
+ # apt-get install build-essential ldap-utils \
+ libldap2-dev libsasl2-dev
+
Packages for building and testing::
- # apt-get install build-essential python3-dev \
- libldap2-dev libsasl2-dev slapd ldap-utils tox \
+ # apt-get install build-essential ldap-utils \
+ libldap2-dev libsasl2-dev slapd python3-dev tox \
lcov valgrind
.. note::
diff --git a/Doc/reference/ldap-syncrepl.rst b/Doc/reference/ldap-syncrepl.rst
index b3b2cf9a..046b15a9 100644
--- a/Doc/reference/ldap-syncrepl.rst
+++ b/Doc/reference/ldap-syncrepl.rst
@@ -20,3 +20,6 @@ This module defines the following classes:
.. autoclass:: ldap.syncrepl.SyncreplConsumer
:members:
+
+.. autoclass:: ldap.syncrepl.OpenLDAPSyncreplCookie
+ :members:
diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst
index d059dfa4..1d095adb 100644
--- a/Doc/reference/ldap.rst
+++ b/Doc/reference/ldap.rst
@@ -38,7 +38,8 @@ This module defines the following functions:
The *uri* parameter may be a comma- or whitespace-separated list of URIs
containing only the schema, the host, and the port fields. Note that
when using multiple URIs you cannot determine to which URI your client
- gets connected.
+ gets connected. If *uri* is :py:const:`None`, the default URIs from
+ ``ldap.conf`` or :py:const:`OPT_URI` global option will be used.
If *fileno* parameter is given then the file descriptor will be used to
connect to an LDAP server. The *fileno* must either be a socket file
@@ -604,13 +605,13 @@ The module defines the following exceptions:
.. py:exception:: COMPARE_FALSE
A compare operation returned false.
- (This exception should only be seen asynchronous operations, because
+ (This exception should only be seen in asynchronous operations, because
:py:meth:`~LDAPObject.compare_s()` returns a boolean result.)
.. py:exception:: COMPARE_TRUE
A compare operation returned true.
- (This exception should only be seen asynchronous operations, because
+ (This exception should only be seen in asynchronous operations, because
:py:meth:`~LDAPObject.compare_s()` returns a boolean result.)
.. py:exception:: CONFIDENTIALITY_REQUIRED
@@ -1364,7 +1365,7 @@ and wait for and return with the server's result, or with
This synchronous method implements the LDAP "Who Am I?"
extended operation.
- It is useful for finding out to find out which identity
+ It is useful for finding out which identity
is assumed by the LDAP server after a SASL bind.
.. seealso::
diff --git a/Doc/spelling_wordlist.txt b/Doc/spelling_wordlist.txt
index e6c2aedd..4381ebee 100644
--- a/Doc/spelling_wordlist.txt
+++ b/Doc/spelling_wordlist.txt
@@ -25,6 +25,7 @@ changeNumber
changesOnly
changeType
changeTypes
+Christoph
cidict
clientctrls
conf
@@ -56,8 +57,10 @@ filterstr
filterStr
formatOID
func
+Gohlke
GPG
Heimdal
+Homebrew
hostport
hrefTarget
hrefText
@@ -98,13 +101,13 @@ oc
oid
oids
OpenLDAP
-Pagure
postalAddress
pre
previousDN
processResultsCount
Proxied
py
+pyproject
pytest
rdn
readthedocs
@@ -145,6 +148,7 @@ syncrepl
syntaxes
timelimit
TLS
+toml
tracebacks
tuple
tuples
@@ -161,5 +165,6 @@ userPassword
usr
uuids
Valgrind
+Xcode
whitespace
workflow
diff --git a/INSTALL b/INSTALL
index b9b13d2d..224df4a4 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,8 +1,7 @@
Quick build instructions:
edit setup.cfg (see Build/ for platform-specific examples)
- python setup.py build
- python setup.py install
+ pip install .
Detailed instructions are in Doc/installing.rst, or online at:
diff --git a/Lib/ldap/controls/openldap.py b/Lib/ldap/controls/openldap.py
index 24040ed7..26c76868 100644
--- a/Lib/ldap/controls/openldap.py
+++ b/Lib/ldap/controls/openldap.py
@@ -51,6 +51,7 @@ class SearchNoOpMixIn:
"""
def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)',timeout=-1):
+ msg_id = None
try:
msg_id = self.search_ext(
base,
@@ -66,9 +67,10 @@ def noop_search_st(self,base,scope=ldap.SCOPE_SUBTREE,filterstr='(objectClass=*)
ldap.TIMELIMIT_EXCEEDED,
ldap.SIZELIMIT_EXCEEDED,
ldap.ADMINLIMIT_EXCEEDED
- ) as e:
- self.abandon(msg_id)
- raise e
+ ):
+ if msg_id is not None:
+ self.abandon(msg_id)
+ raise
else:
noop_srch_ctrl = [
c
diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py
index a9d96846..64d7d0e9 100644
--- a/Lib/ldap/dn.py
+++ b/Lib/ldap/dn.py
@@ -26,7 +26,8 @@ def escape_dn_chars(s):
s = s.replace('>' ,'\\>')
s = s.replace(';' ,'\\;')
s = s.replace('=' ,'\\=')
- s = s.replace('\000' ,'\\\000')
+ # RFC 4514 requires NULL (U+0000) to be escaped as hex pair "\00"
+ s = s.replace('\x00' ,'\\00')
if s[-1]==' ':
s = ''.join((s[:-1],'\\ '))
if s[0]=='#' or s[0]==' ':
@@ -48,12 +49,17 @@ def str2dn(dn,flags=0):
return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags)
-def dn2str(dn):
+def dn2str(dn, flags=0):
"""
This function takes a decomposed DN as parameter and returns
- a single string. It's the inverse to str2dn() but will always
- return a DN in LDAPv3 format compliant to RFC 4514.
+ a single string. It's the inverse to str2dn() but will by default always
+ return a DN in LDAPv3 format compliant to RFC 4514 if not otherwise specified
+ via flags.
+
+ See also the OpenLDAP man-page ldap_dn2str(3)
"""
+ if flags:
+ return ldap.functions._ldap_function_call(None, _ldap.dn2str, dn, flags)
return ','.join([
'+'.join([
'='.join((atype,escape_dn_chars(avalue or '')))
@@ -61,6 +67,7 @@ def dn2str(dn):
for rdn in dn
])
+
def explode_dn(dn, notypes=False, flags=0):
"""
explode_dn(dn [, notypes=False [, flags=0]]) -> list
@@ -116,3 +123,8 @@ def is_dn(s,flags=0):
return False
else:
return True
+
+
+def normalize(s, flags=0):
+ """Returns a normalized distinguished name (DN)"""
+ return dn2str(str2dn(s, flags), flags)
diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py
index 7fab0813..a970d71d 100644
--- a/Lib/ldap/extop/dds.py
+++ b/Lib/ldap/extop/dds.py
@@ -35,6 +35,7 @@ class RefreshRequestValue(univ.Sequence):
)
def __init__(self,requestName=None,entryName=None,requestTtl=None):
+ super().__init__(requestName or self.requestName, b'')
self.entryName = entryName
self.requestTtl = requestTtl or self.defaultRequestTtl
diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py
index 782737aa..5bd41b21 100644
--- a/Lib/ldap/filter.py
+++ b/Lib/ldap/filter.py
@@ -24,6 +24,8 @@ def escape_filter_chars(assertion_value,escape_mode=0):
If 1 all NON-ASCII chars are escaped.
If 2 all chars are escaped.
"""
+ if not isinstance(assertion_value, str):
+ raise TypeError("assertion_value must be of type str.")
if escape_mode:
r = []
if escape_mode==1:
diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py
index 7a9c17f6..7e7b8158 100644
--- a/Lib/ldap/ldapobject.py
+++ b/Lib/ldap/ldapobject.py
@@ -521,7 +521,7 @@ def result(self,msgid=ldap.RES_ANY,all=1,timeout=None):
The method returns a tuple of the form (result_type,
result_data). The result_type is one of the constants RES_*.
- See search() for a description of the search result's
+ See search_ext() for a description of the search result's
result_data, otherwise the result_data is normally meaningless.
The result() method will block for timeout seconds, or
@@ -588,7 +588,7 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct
values are stored in a list as dictionary value.
The DN in dn is extracted using the underlying ldap_get_dn(),
- which may raise an exception of the DN is malformed.
+ which may raise an exception if the DN is malformed.
If attrsonly is non-zero, the values of attrs will be
meaningless (they are not transmitted in the result).
@@ -820,8 +820,7 @@ def get_naming_contexts(self):
class ReconnectLDAPObject(SimpleLDAPObject):
"""
:py:class:`SimpleLDAPObject` subclass whose synchronous request methods
- automatically reconnect and re-try in case of server failure
- (:exc:`ldap.SERVER_DOWN`).
+ automatically reconnect and re-try in case of server failure.
The first arguments are same as for the :py:func:`~ldap.initialize()`
function.
@@ -833,6 +832,10 @@ class ReconnectLDAPObject(SimpleLDAPObject):
* retry_delay: specifies the time in seconds between reconnect attempts.
This class also implements the pickle protocol.
+
+ .. versionadded:: 3.5
+ The exceptions :py:exc:`ldap.SERVER_DOWN`, :py:exc:`ldap.UNAVAILABLE`, :py:exc:`ldap.CONNECT_ERROR` and
+ :py:exc:`ldap.TIMEOUT` (configurable via :py:attr:`_reconnect_exceptions`) now trigger a reconnect.
"""
__transient_attrs__ = {
@@ -842,6 +845,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
'_reconnect_lock',
'_last_bind',
}
+ _reconnect_exceptions = (ldap.SERVER_DOWN, ldap.UNAVAILABLE, ldap.CONNECT_ERROR, ldap.TIMEOUT)
def __init__(
self,uri,
@@ -877,7 +881,10 @@ def __getstate__(self):
for k,v in self.__dict__.items()
if k not in self.__transient_attrs__
}
- state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2]
+ if self._last_bind is None:
+ state['_last_bind'] = None
+ else:
+ state['_last_bind'] = self._last_bind[0].__name__, self._last_bind[1], self._last_bind[2]
return state
def __setstate__(self,d):
@@ -888,7 +895,8 @@ def __setstate__(self,d):
else:
d.setdefault('bytes_strictness', 'warn')
self.__dict__.update(d)
- self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2]
+ if self._last_bind is not None:
+ self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2]
self._ldap_object_lock = self._ldap_lock()
self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
# XXX cannot pickle file, use default trace file
@@ -970,7 +978,7 @@ def _apply_method_s(self,func,*args,**kwargs):
self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=False)
try:
return func(self,*args,**kwargs)
- except ldap.SERVER_DOWN:
+ except self._reconnect_exceptions:
# Try to reconnect
self.reconnect(self._uri,retry_max=self._retry_max,retry_delay=self._retry_delay,force=True)
# Re-try last operation
diff --git a/Lib/ldap/pkginfo.py b/Lib/ldap/pkginfo.py
index 026e9101..18ead66c 100644
--- a/Lib/ldap/pkginfo.py
+++ b/Lib/ldap/pkginfo.py
@@ -1,6 +1,6 @@
"""
meta attributes for packaging which does not import any dependencies
"""
-__version__ = '3.4.3'
+__version__ = '3.4.4'
__author__ = 'python-ldap project'
__license__ = 'Python style'
diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py
index 1708b468..0e1b6a3f 100644
--- a/Lib/ldap/syncrepl.py
+++ b/Lib/ldap/syncrepl.py
@@ -4,6 +4,7 @@
See https://www.python-ldap.org/ for project details.
"""
+from typing import AnyStr, Dict, List, Tuple, Union
from uuid import UUID
# Imports from pyasn1
@@ -12,8 +13,10 @@
from ldap.pkginfo import __version__, __author__, __license__
from ldap.controls import RequestControl, ResponseControl, KNOWN_RESPONSE_CONTROLS
+from ldap import RES_SEARCH_RESULT, RES_SEARCH_ENTRY, RES_INTERMEDIATE
__all__ = [
+ 'OpenLDAPSyncreplCookie',
'SyncreplConsumer',
]
@@ -407,7 +410,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0):
all=0,
)
- if type == 101:
+ if type == RES_SEARCH_RESULT:
# search result. This marks the end of a refreshOnly session.
# look for a SyncDone control, save the cookie, and if necessary
# delete non-present entries.
@@ -420,7 +423,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0):
return False
- elif type == 100:
+ elif type == RES_SEARCH_ENTRY:
# search entry with associated SyncState control
for m in msg:
dn, attrs, ctrls = m
@@ -439,7 +442,7 @@ def syncrepl_poll(self, msgid=-1, timeout=None, all=0):
self.syncrepl_set_cookie(c.cookie)
break
- elif type == 121:
+ elif type == RES_INTERMEDIATE:
# Intermediate message. If it is a SyncInfoMessage, parse it
for m in msg:
rname, resp, ctrls = m
@@ -534,3 +537,71 @@ def syncrepl_refreshdone(self):
follows.
"""
pass
+
+
+class OpenLDAPSyncreplCookie:
+ """
+ OpenLDAPSyncreplCookie - allows a consumer to track a cookie across a
+ refreshAndPersist syncrepl session against a multi-provider OpenLDAP cluster
+ """
+
+ rid: int = 0
+ sid: int = 0
+ _csnset: Dict[int, str]
+
+ def __init__(self, cookie: AnyStr = "") -> None:
+ self._csnset = {}
+
+ if cookie:
+ self.update(cookie)
+
+ def _parse_csn(self, csn: str) -> Tuple[str, str, str, str]:
+ time, order, sid, other = csn.split('#', 3)
+ return (time, order, sid, other)
+
+ def _parse_cookie(self, cookie: AnyStr) -> Dict[str, Union[str, List[str]]]:
+ if isinstance(cookie, bytes):
+ cookie = cookie.decode()
+
+ result = {}
+ parts = cookie.split(',')
+ for part in parts:
+ if part.startswith('rid='):
+ result['rid'] = part[4:]
+ elif part.startswith('sid='):
+ result['sid'] = part[4:]
+ elif part.startswith('csn='):
+ result['csn'] = part[4:].split(';')
+ elif part.startswith('delcsn='):
+ result['delcsn'] = part[7:]
+ else:
+ # Did not recognize this cookie part
+ pass
+ return result
+
+ def update(self, cookie: AnyStr):
+ """
+ Update the CSN set based on a cookie we just received, use in
+ syncrepl_set_cookie() to track the session state.
+ """
+ components = self._parse_cookie(cookie)
+ for csn in components.get('csn', []):
+ _, _, sid, _ = self._parse_csn(csn)
+ if sid not in self._csnset or self._csnset[sid] < csn:
+ self._csnset[sid] = csn
+
+ return self
+
+ def unparse(self) -> str:
+ """
+ Return the cookie as a string, use in syncrepl_get_cookie() or when
+ storing the state for later use.
+ """
+ cookie = 'rid={:03},sid={:03x}'.format(self.rid or 0, self.sid or 0)
+ if self._csnset:
+ cookie += ',csn='
+ cookie += ';'.join(csn for sid, csn in sorted(self._csnset.items()))
+ return cookie
+
+ def __str__(self):
+ return self.unparse()
diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py
index 964076d3..b4dfd890 100644
--- a/Lib/ldapurl.py
+++ b/Lib/ldapurl.py
@@ -4,7 +4,7 @@
See https://www.python-ldap.org/ for details.
"""
-__version__ = '3.4.3'
+__version__ = '3.4.4'
__all__ = [
# constants
diff --git a/Lib/ldif.py b/Lib/ldif.py
index ae1d643d..fa41321c 100644
--- a/Lib/ldif.py
+++ b/Lib/ldif.py
@@ -3,7 +3,7 @@
See https://www.python-ldap.org/ for details.
"""
-__version__ = '3.4.3'
+__version__ = '3.4.4'
__all__ = [
# constants
diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py
index 7ab7d2bd..7c410180 100644
--- a/Lib/slapdtest/__init__.py
+++ b/Lib/slapdtest/__init__.py
@@ -4,7 +4,7 @@
See https://www.python-ldap.org/ for details.
"""
-__version__ = '3.4.3'
+__version__ = '3.4.4'
from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler
from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls
diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py
index 36841110..00764ac3 100644
--- a/Lib/slapdtest/_slapdtest.py
+++ b/Lib/slapdtest/_slapdtest.py
@@ -41,6 +41,11 @@
cn: module
olcModuleLoad: back_%(database)s
+dn: olcDatabase=config,cn=config
+objectClass: olcDatabaseConfig
+olcDatabase: config
+olcRootDN: %(rootdn)s
+
dn: olcDatabase=%(database)s,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
@@ -467,8 +472,16 @@ def restart(self):
"""
Restarts the slapd server with same data
"""
- self._proc.terminate()
+ self.terminate()
self.wait()
+ self.resume()
+
+ def terminate(self):
+ """Terminate slapd server"""
+ self._proc.terminate()
+
+ def resume(self):
+ """Start slapd server"""
self._start_slapd()
def wait(self):
diff --git a/MANIFEST.in b/MANIFEST.in
index 687d2b0c..bedea8d6 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
include MANIFEST.in Makefile CHANGES INSTALL LICENCE README TODO
-include tox.ini .coveragerc
+include tox.ini
include Modules/*.c Modules/*.h
recursive-include Build *.cfg*
recursive-include Lib *.py
diff --git a/Makefile b/Makefile
index 577ba883..da23b374 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,6 @@ Modules/constants_generated.h: Lib/ldap/constants.py
.PHONY: clean
clean:
rm -rf build dist *.egg-info .tox MANIFEST
- rm -f .coverage .coverage.*
find . \( -name '*.py[co]' -or -name '*.so*' -or -name '*.dylib' \) \
-delete
find . -depth -name __pycache__ -exec rm -rf {} \;
@@ -89,7 +88,8 @@ valgrind: build $(PYTHON_SUPP)
autoformat: indent black
indent:
- indent Modules/*.c Modules/*.h
+ indent Modules/*.c
+ indent -npsl Modules/pythonldap.h
rm -f Modules/*.c~ Modules/*.h~
black:
diff --git a/Modules/LDAPObject.c b/Modules/LDAPObject.c
index da18d575..71fac73e 100644
--- a/Modules/LDAPObject.c
+++ b/Modules/LDAPObject.c
@@ -1,16 +1,10 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
+#include "pythonldap.h"
#include "patchlevel.h"
#include
#include
-#include "constants.h"
-#include "LDAPObject.h"
-#include "ldapcontrol.h"
-#include "message.h"
-#include "berval.h"
-#include "options.h"
#ifdef HAVE_SASL
#include
@@ -276,13 +270,8 @@ attrs_from_List(PyObject *attrlist, char ***attrsp)
if (attrlist == Py_None) {
/* None means a NULL attrlist */
-#if PY_MAJOR_VERSION == 2
- }
- else if (PyBytes_Check(attrlist)) {
-#else
}
else if (PyUnicode_Check(attrlist)) {
-#endif
/* caught by John Benninghoff */
LDAPerror_TypeError
("attrs_from_List(): expected *list* of strings, not a string",
@@ -293,11 +282,7 @@ attrs_from_List(PyObject *attrlist, char ***attrsp)
PyObject *item = NULL;
Py_ssize_t i, len, strlen;
-#if PY_MAJOR_VERSION >= 3
const char *str;
-#else
- char *str;
-#endif
seq = PySequence_Fast(attrlist, "expected list of strings or None");
if (seq == NULL)
@@ -315,24 +300,12 @@ attrs_from_List(PyObject *attrlist, char ***attrsp)
item = PySequence_Fast_GET_ITEM(seq, i);
if (item == NULL)
goto error;
-#if PY_MAJOR_VERSION == 2
- /* Encoded in Python to UTF-8 */
- if (!PyBytes_Check(item)) {
- LDAPerror_TypeError
- ("attrs_from_List(): expected bytes in list", item);
- goto error;
- }
- if (PyBytes_AsStringAndSize(item, &str, &strlen) == -1) {
- goto error;
- }
-#else
if (!PyUnicode_Check(item)) {
LDAPerror_TypeError
("attrs_from_List(): expected string in list", item);
goto error;
}
str = PyUnicode_AsUTF8AndSize(item, &strlen);
-#endif
/* Make a copy. PyBytes_AsString* / PyUnicode_AsUTF8* return
* internal values that must be treated like const char. Python
* 3.7 actually returns a const char.
@@ -521,7 +494,7 @@ l_ldap_add_ext(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_simple_bind */
@@ -572,7 +545,7 @@ l_ldap_simple_bind(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
#ifdef HAVE_SASL
@@ -730,7 +703,7 @@ l_ldap_sasl_bind_s(LDAPObject *self, PyObject *args)
}
else if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(ldaperror);
+ return PyLong_FromLong(ldaperror);
}
static PyObject *
@@ -757,15 +730,9 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args)
* unsigned int, we need to use the "I" flag if we're running Python 2.3+ and a
* "i" otherwise.
*/
-#if (PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION < 3)
- if (!PyArg_ParseTuple
- (args, "sOOOi:sasl_interactive_bind_s", &who, &SASLObject,
- &serverctrls, &clientctrls, &sasl_flags))
-#else
if (!PyArg_ParseTuple
(args, "sOOOI:sasl_interactive_bind_s", &who, &SASLObject,
&serverctrls, &clientctrls, &sasl_flags))
-#endif
return NULL;
if (not_valid(self))
@@ -809,7 +776,7 @@ l_ldap_sasl_interactive_bind_s(LDAPObject *self, PyObject *args)
if (msgid != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
#endif
@@ -858,7 +825,7 @@ l_ldap_cancel(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
#endif
@@ -912,7 +879,7 @@ l_ldap_compare_ext(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_delete_ext */
@@ -958,7 +925,7 @@ l_ldap_delete_ext(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_modify_ext */
@@ -1015,7 +982,7 @@ l_ldap_modify_ext(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_rename */
@@ -1065,7 +1032,7 @@ l_ldap_rename(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_result4 */
@@ -1281,7 +1248,7 @@ l_ldap_search_ext(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_whoami_s (available since OpenLDAP 2.1.13) */
@@ -1451,7 +1418,7 @@ l_ldap_passwd(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* ldap_extended_operation */
@@ -1502,7 +1469,7 @@ l_ldap_extended_operation(LDAPObject *self, PyObject *args)
if (ldaperror != LDAP_SUCCESS)
return LDAPerror(self->ldap);
- return PyInt_FromLong(msgid);
+ return PyLong_FromLong(msgid);
}
/* methods */
diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h
deleted file mode 100644
index 4af0b382..00000000
--- a/Modules/LDAPObject.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_LDAPObject
-#define __h_LDAPObject
-
-#include "common.h"
-
-typedef struct {
- PyObject_HEAD LDAP *ldap;
- PyThreadState *_save; /* for thread saving on referrals */
- int valid;
-} LDAPObject;
-
-extern PyTypeObject LDAP_Type;
-
-#define LDAPObject_Check(v) (Py_TYPE(v) == &LDAP_Type)
-
-extern LDAPObject *newLDAPObject(LDAP *);
-
-/* macros to allow thread saving in the context of an LDAP connection */
-
-#define LDAP_BEGIN_ALLOW_THREADS( l ) \
- { \
- LDAPObject *lo = (l); \
- if (lo->_save != NULL) \
- Py_FatalError( "saving thread twice?" ); \
- lo->_save = PyEval_SaveThread(); \
- }
-
-#define LDAP_END_ALLOW_THREADS( l ) \
- { \
- LDAPObject *lo = (l); \
- PyThreadState *_save = lo->_save; \
- lo->_save = NULL; \
- PyEval_RestoreThread( _save ); \
- }
-
-#endif /* __h_LDAPObject */
diff --git a/Modules/berval.c b/Modules/berval.c
index 6917baef..39cc98a8 100644
--- a/Modules/berval.c
+++ b/Modules/berval.c
@@ -1,7 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "berval.h"
+#include "pythonldap.h"
/*
* Copies out the data from a berval, and returns it as a new Python object,
diff --git a/Modules/berval.h b/Modules/berval.h
deleted file mode 100644
index 9c427240..00000000
--- a/Modules/berval.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_berval
-#define __h_berval
-
-#include "common.h"
-
-PyObject *LDAPberval_to_object(const struct berval *bv);
-PyObject *LDAPberval_to_unicode_object(const struct berval *bv);
-
-#endif /* __h_berval_ */
diff --git a/Modules/common.c b/Modules/common.c
index 9d7001c0..4cfee744 100644
--- a/Modules/common.c
+++ b/Modules/common.c
@@ -1,7 +1,7 @@
/* Miscellaneous common routines
* See https://www.python-ldap.org/ for details. */
-#include "common.h"
+#include "pythonldap.h"
/* dynamically add the methods into the module dictionary d */
diff --git a/Modules/common.h b/Modules/common.h
deleted file mode 100644
index bc554c85..00000000
--- a/Modules/common.h
+++ /dev/null
@@ -1,68 +0,0 @@
-/* common utility macros
- * See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_common
-#define __h_common
-
-#define PY_SSIZE_T_CLEAN
-
-#include "Python.h"
-
-#if defined(HAVE_CONFIG_H)
-#include "config.h"
-#endif
-
-#include
-#include
-#include
-
-#if LDAP_VENDOR_VERSION < 20400
-#error Current python-ldap requires OpenLDAP 2.4.x
-#endif
-
-#if LDAP_VENDOR_VERSION >= 20448
- /* openldap.h with ldap_init_fd() was introduced in 2.4.48
- * see https://bugs.openldap.org/show_bug.cgi?id=8671
- */
-#define HAVE_LDAP_INIT_FD 1
-#include
-#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428))
-/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */
-#undef HAVE_LDAP_INIT_FD
-#else
- /* ldap_init_fd() has been around for a very long time
- * SSSD has been defining the function for a while, so it's probably OK.
- */
-#define HAVE_LDAP_INIT_FD 1
-#define LDAP_PROTO_TCP 1
-#define LDAP_PROTO_UDP 2
-#define LDAP_PROTO_IPC 3
-extern int ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url,
- LDAP **ldp);
-#endif
-
-#if defined(MS_WINDOWS)
-#include
-#else /* unix */
-#include
-#include
-#include
-#endif
-
-#include
-#define streq( a, b ) \
- ( (*(a)==*(b)) && 0==strcmp(a,b) )
-
-extern PyObject *LDAPerror_TypeError(const char *, PyObject *);
-
-void LDAPadd_methods(PyObject *d, PyMethodDef *methods);
-
-#define PyNone_Check(o) ((o) == Py_None)
-
-/* Py2/3 compatibility */
-#if PY_VERSION_HEX >= 0x03000000
-/* In Python 3, alias PyInt to PyLong */
-#define PyInt_FromLong PyLong_FromLong
-#endif
-
-#endif /* __h_common_ */
diff --git a/Modules/constants.c b/Modules/constants.c
index 8d6f63b0..f0a0da94 100644
--- a/Modules/constants.c
+++ b/Modules/constants.c
@@ -1,9 +1,7 @@
/* constants defined for LDAP
* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "constants.h"
-#include "ldapcontrol.h"
+#include "pythonldap.h"
/* the base exception class */
@@ -107,20 +105,20 @@ LDAPraise_for_message(LDAP *l, LDAPMessage *m)
}
if (msgtype > 0) {
- pyresult = PyInt_FromLong(msgtype);
+ pyresult = PyLong_FromLong(msgtype);
if (pyresult)
PyDict_SetItemString(info, "msgtype", pyresult);
Py_XDECREF(pyresult);
}
if (msgid >= 0) {
- pyresult = PyInt_FromLong(msgid);
+ pyresult = PyLong_FromLong(msgid);
if (pyresult)
PyDict_SetItemString(info, "msgid", pyresult);
Py_XDECREF(pyresult);
}
- pyresult = PyInt_FromLong(errnum);
+ pyresult = PyLong_FromLong(errnum);
if (pyresult)
PyDict_SetItemString(info, "result", pyresult);
Py_XDECREF(pyresult);
@@ -131,7 +129,7 @@ LDAPraise_for_message(LDAP *l, LDAPMessage *m)
Py_XDECREF(str);
if (myerrno != 0) {
- pyerrno = PyInt_FromLong(myerrno);
+ pyerrno = PyLong_FromLong(myerrno);
if (pyerrno)
PyDict_SetItemString(info, "errno", pyerrno);
Py_XDECREF(pyerrno);
diff --git a/Modules/constants.h b/Modules/constants.h
deleted file mode 100644
index 7b9ce53e..00000000
--- a/Modules/constants.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_constants_
-#define __h_constants_
-
-#include "common.h"
-
-extern int LDAPinit_constants(PyObject *m);
-extern PyObject *LDAPconstant(int);
-
-extern PyObject *LDAPexception_class;
-extern PyObject *LDAPerror(LDAP *);
-extern PyObject *LDAPraise_for_message(LDAP *, LDAPMessage *m);
-PyObject *LDAPerr(int errnum);
-
-#ifndef LDAP_CONTROL_PAGE_OID
-#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319"
-#endif /* !LDAP_CONTROL_PAGE_OID */
-
-#ifndef LDAP_CONTROL_VALUESRETURNFILTER
-#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */
-#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */
-
-#endif /* __h_constants_ */
diff --git a/Modules/functions.c b/Modules/functions.c
index b811708f..3f7f7eca 100644
--- a/Modules/functions.c
+++ b/Modules/functions.c
@@ -1,11 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "functions.h"
-#include "LDAPObject.h"
-#include "berval.h"
-#include "constants.h"
-#include "options.h"
+#include "pythonldap.h"
/* ldap_initialize */
@@ -17,7 +12,7 @@ l_ldap_initialize(PyObject *unused, PyObject *args)
int ret;
PyThreadState *save;
- if (!PyArg_ParseTuple(args, "s:initialize", &uri))
+ if (!PyArg_ParseTuple(args, "z:initialize", &uri))
return NULL;
save = PyEval_SaveThread();
@@ -160,6 +155,222 @@ l_ldap_str2dn(PyObject *unused, PyObject *args)
return result;
}
+/* ldap_dn2str */
+
+static void
+_free_dn_structure(LDAPDN dn)
+{
+ if (dn == NULL)
+ return;
+
+ for (LDAPRDN *rdn = dn; *rdn != NULL; rdn++) {
+ for (LDAPAVA **avap = *rdn; *avap != NULL; avap++) {
+ LDAPAVA *ava = *avap;
+
+ if (ava->la_attr.bv_val) {
+ free(ava->la_attr.bv_val);
+ }
+ if (ava->la_value.bv_val) {
+ free(ava->la_value.bv_val);
+ }
+ free(ava);
+ }
+ free(*rdn);
+ }
+ free(dn);
+}
+
+/*
+ * Convert a Python list-of-list-of-(str, str, int) into an LDAPDN and
+ * call ldap_dn2bv to build a DN string.
+ *
+ * Python signature: dn2str(dn: list[list[tuple[str, str, int]]], flags: int) -> str
+ * Returns the DN string on success, or raises TypeError or RuntimeError on error.
+ */
+static PyObject *
+l_ldap_dn2str(PyObject *self, PyObject *args)
+{
+ PyObject *dn_list = NULL;
+ int flags = 0;
+ LDAPDN dn = NULL;
+ LDAPAVA *ava;
+ LDAPAVA **rdn;
+ BerValue str = { 0, NULL };
+ PyObject *py_rdn_seq = NULL, *py_ava_item = NULL;
+ PyObject *py_name = NULL, *py_value = NULL, *py_encoding = NULL;
+ PyObject *result = NULL;
+ Py_ssize_t nrdns = 0, navas = 0, name_len = 0, value_len = 0;
+ int i = 0, j = 0;
+ int ldap_err;
+ const char *name_utf8, *value_utf8;
+
+ const char *type_error_message = "expected list[list[tuple[str, str, int]]]";
+
+ if (!PyArg_ParseTuple(args, "Oi:dn2str", &dn_list, &flags)) {
+ return NULL;
+ }
+
+ if (!PySequence_Check(dn_list)) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ return NULL;
+ }
+
+ nrdns = PySequence_Size(dn_list);
+ if (nrdns < 0) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ return NULL;
+ }
+
+ /* Allocate array of LDAPRDN pointers (+1 for NULL terminator) */
+ dn = (LDAPRDN *) calloc((size_t)nrdns + 1, sizeof(LDAPRDN));
+ if (dn == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ for (i = 0; i < nrdns; i++) {
+ py_rdn_seq = PySequence_GetItem(dn_list, i); /* New reference */
+ if (py_rdn_seq == NULL) {
+ goto error_cleanup;
+ }
+ if (!PySequence_Check(py_rdn_seq)) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ goto error_cleanup;
+ }
+
+ navas = PySequence_Size(py_rdn_seq);
+ if (navas < 0) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ goto error_cleanup;
+ }
+
+ /* Allocate array of LDAPAVA* pointers (+1 for NULL terminator) */
+ rdn = (LDAPAVA **)calloc((size_t)navas + 1, sizeof(LDAPAVA *));
+ if (rdn == NULL) {
+ PyErr_NoMemory();
+ goto error_cleanup;
+ }
+
+ for (j = 0; j < navas; j++) {
+ py_ava_item = PySequence_GetItem(py_rdn_seq, j); /* New reference */
+ if (py_ava_item == NULL) {
+ goto error_cleanup;
+ }
+ /* Expect a 3‐tuple: (name: str, value: str, encoding: int) */
+ if (!PyTuple_Check(py_ava_item) || PyTuple_Size(py_ava_item) != 3) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ goto error_cleanup;
+ }
+
+ py_name = PyTuple_GetItem(py_ava_item, 0); /* Borrowed reference */
+ py_value = PyTuple_GetItem(py_ava_item, 1); /* Borrowed reference */
+ py_encoding = PyTuple_GetItem(py_ava_item, 2); /* Borrowed reference */
+
+ if (!PyUnicode_Check(py_name) || !PyUnicode_Check(py_value) || !PyLong_Check(py_encoding)) {
+ PyErr_SetString(PyExc_TypeError, type_error_message);
+ goto error_cleanup;
+ }
+
+ name_len = 0;
+ value_len = 0;
+ name_utf8 = PyUnicode_AsUTF8AndSize(py_name, &name_len);
+ value_utf8 = PyUnicode_AsUTF8AndSize(py_value, &value_len);
+ if (name_utf8 == NULL || value_utf8 == NULL) {
+ goto error_cleanup;
+ }
+
+ ava = (LDAPAVA *) calloc(1, sizeof(LDAPAVA));
+
+ if (ava == NULL) {
+ PyErr_NoMemory();
+ goto error_cleanup;
+ }
+
+ ava->la_attr.bv_val = (char *)malloc((size_t)name_len + 1);
+ if (ava->la_attr.bv_val == NULL) {
+ free(ava);
+ PyErr_NoMemory();
+ goto error_cleanup;
+ }
+ memcpy(ava->la_attr.bv_val, name_utf8, (size_t)name_len);
+ ava->la_attr.bv_val[name_len] = '\0';
+ ava->la_attr.bv_len = (ber_len_t) name_len;
+
+ ava->la_value.bv_val = (char *)malloc((size_t)value_len + 1);
+ if (ava->la_value.bv_val == NULL) {
+ free(ava->la_attr.bv_val);
+ free(ava);
+ PyErr_NoMemory();
+ goto error_cleanup;
+ }
+ memcpy(ava->la_value.bv_val, value_utf8, (size_t)value_len);
+ ava->la_value.bv_val[value_len] = '\0';
+ ava->la_value.bv_len = (ber_len_t) value_len;
+
+ ava->la_flags = (int)PyLong_AsLong(py_encoding);
+ if (PyErr_Occurred()) {
+ /* Encoding conversion failed */
+ free(ava->la_attr.bv_val);
+ free(ava->la_value.bv_val);
+ free(ava);
+ goto error_cleanup;
+ }
+
+ rdn[j] = ava;
+ Py_DECREF(py_ava_item);
+ py_ava_item = NULL;
+ }
+
+ /* Null‐terminate the RDN */
+ rdn[navas] = NULL;
+
+ dn[i] = rdn;
+ Py_DECREF(py_rdn_seq);
+ py_rdn_seq = NULL;
+ }
+
+ /* Null‐terminate the DN */
+ dn[nrdns] = NULL;
+
+ /* Call ldap_dn2bv to build a DN string */
+ ldap_err = ldap_dn2bv(dn, &str, flags);
+ if (ldap_err != LDAP_SUCCESS) {
+ PyErr_SetString(PyExc_RuntimeError, ldap_err2string(ldap_err));
+ goto error_cleanup;
+ }
+
+ result = PyUnicode_FromString(str.bv_val);
+ if (result == NULL) {
+ goto error_cleanup;
+ }
+
+ /* Free the memory allocated by ldap_dn2bv */
+ ldap_memfree(str.bv_val);
+ str.bv_val = NULL;
+
+ /* Free our local DN structure */
+ _free_dn_structure(dn);
+ dn = NULL;
+
+ return result;
+
+ error_cleanup:
+ /* Free any partially built DN structure */
+ _free_dn_structure(dn);
+ dn = NULL;
+
+ /* If ldap_dn2bv allocated something, free it */
+ if (str.bv_val) {
+ ldap_memfree(str.bv_val);
+ str.bv_val = NULL;
+ }
+
+ /* Cleanup Python temporaries */
+ Py_XDECREF(py_ava_item);
+ Py_XDECREF(py_rdn_seq);
+ return NULL;
+}
+
/* ldap_set_option (global options) */
static PyObject *
@@ -196,6 +407,7 @@ static PyMethodDef methods[] = {
{"initialize_fd", (PyCFunction)l_ldap_initialize_fd, METH_VARARGS},
#endif
{"str2dn", (PyCFunction)l_ldap_str2dn, METH_VARARGS},
+ {"dn2str", (PyCFunction)l_ldap_dn2str, METH_VARARGS},
{"set_option", (PyCFunction)l_ldap_set_option, METH_VARARGS},
{"get_option", (PyCFunction)l_ldap_get_option, METH_VARARGS},
{NULL, NULL}
diff --git a/Modules/functions.h b/Modules/functions.h
deleted file mode 100644
index 2aef9740..00000000
--- a/Modules/functions.h
+++ /dev/null
@@ -1,9 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_functions_
-#define __h_functions_
-
-#include "common.h"
-extern void LDAPinit_functions(PyObject *);
-
-#endif /* __h_functions_ */
diff --git a/Modules/ldapcontrol.c b/Modules/ldapcontrol.c
index e287e9a3..4a37b614 100644
--- a/Modules/ldapcontrol.c
+++ b/Modules/ldapcontrol.c
@@ -1,10 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "LDAPObject.h"
-#include "ldapcontrol.h"
-#include "berval.h"
-#include "constants.h"
+#include "pythonldap.h"
/* Prints to stdout the contents of an array of LDAPControl objects */
diff --git a/Modules/ldapcontrol.h b/Modules/ldapcontrol.h
deleted file mode 100644
index 74cae423..00000000
--- a/Modules/ldapcontrol.h
+++ /dev/null
@@ -1,13 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_ldapcontrol
-#define __h_ldapcontrol
-
-#include "common.h"
-
-void LDAPinit_control(PyObject *d);
-void LDAPControl_List_DEL(LDAPControl **);
-int LDAPControls_from_object(PyObject *, LDAPControl ***);
-PyObject *LDAPControls_to_List(LDAPControl **ldcs);
-
-#endif /* __h_ldapcontrol */
diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c
index 34d5a24c..cb3f58fb 100644
--- a/Modules/ldapmodule.c
+++ b/Modules/ldapmodule.c
@@ -1,17 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "constants.h"
-#include "functions.h"
-#include "ldapcontrol.h"
-
-#include "LDAPObject.h"
-
-#if PY_MAJOR_VERSION >= 3
-PyMODINIT_FUNC PyInit__ldap(void);
-#else
-PyMODINIT_FUNC init_ldap(void);
-#endif
+#include "pythonldap.h"
#define _STR(x) #x
#define STR(x) _STR(x)
@@ -33,27 +22,24 @@ static PyMethodDef methods[] = {
{NULL, NULL}
};
+static struct PyModuleDef ldap_moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "_ldap", /* m_name */
+ "", /* m_doc */
+ -1, /* m_size */
+ methods, /* m_methods */
+};
+
/* module initialisation */
-/* Common initialization code */
-PyObject *
-init_ldap_module(void)
+PyMODINIT_FUNC
+PyInit__ldap()
{
PyObject *m, *d;
/* Create the module and add the functions */
-#if PY_MAJOR_VERSION >= 3
- static struct PyModuleDef ldap_moduledef = {
- PyModuleDef_HEAD_INIT,
- "_ldap", /* m_name */
- "", /* m_doc */
- -1, /* m_size */
- methods, /* m_methods */
- };
m = PyModule_Create(&ldap_moduledef);
-#else
- m = Py_InitModule("_ldap", methods);
-#endif
+
/* Initialize LDAP class */
if (PyType_Ready(&LDAP_Type) < 0) {
Py_DECREF(m);
@@ -78,17 +64,3 @@ init_ldap_module(void)
return m;
}
-
-#if PY_MAJOR_VERSION < 3
-PyMODINIT_FUNC
-init_ldap()
-{
- init_ldap_module();
-}
-#else
-PyMODINIT_FUNC
-PyInit__ldap()
-{
- return init_ldap_module();
-}
-#endif
diff --git a/Modules/message.c b/Modules/message.c
index 22aa313c..f1403237 100644
--- a/Modules/message.c
+++ b/Modules/message.c
@@ -1,10 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "message.h"
-#include "berval.h"
-#include "ldapcontrol.h"
-#include "constants.h"
+#include "pythonldap.h"
/*
* Converts an LDAP message into a Python structure.
diff --git a/Modules/message.h b/Modules/message.h
deleted file mode 100644
index ed73f32c..00000000
--- a/Modules/message.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-#ifndef __h_message
-#define __h_message
-
-#include "common.h"
-
-extern PyObject *LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
- int add_intermediates);
-
-#endif /* __h_message_ */
diff --git a/Modules/options.c b/Modules/options.c
index a621f81a..4577b075 100644
--- a/Modules/options.c
+++ b/Modules/options.c
@@ -1,11 +1,6 @@
/* See https://www.python-ldap.org/ for details. */
-#include "common.h"
-#include "constants.h"
-#include "LDAPObject.h"
-#include "ldapcontrol.h"
-#include "options.h"
-#include "berval.h"
+#include "pythonldap.h"
void
set_timeval_from_double(struct timeval *tv, double d)
@@ -373,7 +368,7 @@ LDAP_get_option(LDAPObject *self, int option)
res = LDAP_int_get_option(self, option, &intval);
if (res != LDAP_OPT_SUCCESS)
return option_error(res, "ldap_get_option");
- return PyInt_FromLong(intval);
+ return PyLong_FromLong(intval);
#ifdef LDAP_OPT_TCP_USER_TIMEOUT
case LDAP_OPT_TCP_USER_TIMEOUT:
diff --git a/Modules/options.h b/Modules/options.h
deleted file mode 100644
index fd6a5ce2..00000000
--- a/Modules/options.h
+++ /dev/null
@@ -1,7 +0,0 @@
-/* See https://www.python-ldap.org/ for details. */
-
-int LDAP_optionval_by_name(const char *name);
-int LDAP_set_option(LDAPObject *self, int option, PyObject *value);
-PyObject *LDAP_get_option(LDAPObject *self, int option);
-
-void set_timeval_from_double(struct timeval *tv, double d);
diff --git a/Modules/pythonldap.h b/Modules/pythonldap.h
new file mode 100644
index 00000000..7703af5e
--- /dev/null
+++ b/Modules/pythonldap.h
@@ -0,0 +1,131 @@
+/* common utility macros
+ * See https://www.python-ldap.org/ for details. */
+
+#ifndef pythonldap_h
+#define pythonldap_h
+
+/* *** common *** */
+#define PY_SSIZE_T_CLEAN
+
+#include "Python.h"
+
+#if defined(HAVE_CONFIG_H)
+#include "config.h"
+#endif
+
+#include
+#include
+#include
+
+#if LDAP_VENDOR_VERSION < 20400
+#error Current python-ldap requires OpenLDAP 2.4.x
+#endif
+
+#if LDAP_VENDOR_VERSION >= 20448
+ /* openldap.h with ldap_init_fd() was introduced in 2.4.48
+ * see https://bugs.openldap.org/show_bug.cgi?id=8671
+ */
+#define HAVE_LDAP_INIT_FD 1
+#include
+#elif (defined(__APPLE__) && (LDAP_VENDOR_VERSION == 20428))
+/* macOS system libldap 2.4.28 does not have ldap_init_fd symbol */
+#undef HAVE_LDAP_INIT_FD
+#else
+ /* ldap_init_fd() has been around for a very long time
+ * SSSD has been defining the function for a while, so it's probably OK.
+ */
+#define HAVE_LDAP_INIT_FD 1
+#define LDAP_PROTO_TCP 1
+#define LDAP_PROTO_UDP 2
+#define LDAP_PROTO_IPC 3
+LDAP_F(int) ldap_init_fd(ber_socket_t fd, int proto, LDAP_CONST char *url,
+ LDAP **ldp);
+#endif
+
+#if defined(MS_WINDOWS)
+#include
+#else /* unix */
+#include
+#include
+#include
+#endif
+
+#define PYLDAP_FUNC(rtype) rtype
+#define PYLDAP_DATA(rtype) extern rtype
+
+PYLDAP_FUNC(PyObject *) LDAPerror_TypeError(const char *, PyObject *);
+
+PYLDAP_FUNC(void) LDAPadd_methods(PyObject *d, PyMethodDef *methods);
+
+#define PyNone_Check(o) ((o) == Py_None)
+
+/* *** berval *** */
+PYLDAP_FUNC(PyObject *) LDAPberval_to_object(const struct berval *bv);
+PYLDAP_FUNC(PyObject *) LDAPberval_to_unicode_object(const struct berval *bv);
+
+/* *** constants *** */
+PYLDAP_FUNC(int) LDAPinit_constants(PyObject *m);
+
+PYLDAP_DATA(PyObject *) LDAPexception_class;
+PYLDAP_FUNC(PyObject *) LDAPerror(LDAP *);
+PYLDAP_FUNC(PyObject *) LDAPraise_for_message(LDAP *, LDAPMessage *m);
+PYLDAP_FUNC(PyObject *) LDAPerr(int errnum);
+
+#ifndef LDAP_CONTROL_PAGE_OID
+#define LDAP_CONTROL_PAGE_OID "1.2.840.113556.1.4.319"
+#endif /* !LDAP_CONTROL_PAGE_OID */
+
+#ifndef LDAP_CONTROL_VALUESRETURNFILTER
+#define LDAP_CONTROL_VALUESRETURNFILTER "1.2.826.0.1.3344810.2.3" /* RFC 3876 */
+#endif /* !LDAP_CONTROL_VALUESRETURNFILTER */
+
+/* *** functions *** */
+PYLDAP_FUNC(void) LDAPinit_functions(PyObject *);
+
+/* *** ldapcontrol *** */
+PYLDAP_FUNC(void) LDAPinit_control(PyObject *d);
+PYLDAP_FUNC(void) LDAPControl_List_DEL(LDAPControl **);
+PYLDAP_FUNC(int) LDAPControls_from_object(PyObject *, LDAPControl ***);
+PYLDAP_FUNC(PyObject *) LDAPControls_to_List(LDAPControl **ldcs);
+
+/* *** ldapobject *** */
+typedef struct {
+ PyObject_HEAD LDAP *ldap;
+ PyThreadState *_save; /* for thread saving on referrals */
+ int valid;
+} LDAPObject;
+
+PYLDAP_DATA(PyTypeObject) LDAP_Type;
+PYLDAP_FUNC(LDAPObject *) newLDAPObject(LDAP *);
+
+/* macros to allow thread saving in the context of an LDAP connection */
+
+#define LDAP_BEGIN_ALLOW_THREADS( l ) \
+ { \
+ LDAPObject *lo = (l); \
+ if (lo->_save != NULL) \
+ Py_FatalError( "saving thread twice?" ); \
+ lo->_save = PyEval_SaveThread(); \
+ }
+
+#define LDAP_END_ALLOW_THREADS( l ) \
+ { \
+ LDAPObject *lo = (l); \
+ PyThreadState *_save = lo->_save; \
+ lo->_save = NULL; \
+ PyEval_RestoreThread( _save ); \
+ }
+
+/* *** messages *** */
+PYLDAP_FUNC(PyObject *)
+LDAPmessage_to_python(LDAP *ld, LDAPMessage *m, int add_ctrls,
+ int add_intermediates);
+
+/* *** options *** */
+PYLDAP_FUNC(int) LDAP_optionval_by_name(const char *name);
+PYLDAP_FUNC(int) LDAP_set_option(LDAPObject *self, int option,
+ PyObject *value);
+PYLDAP_FUNC(PyObject *) LDAP_get_option(LDAPObject *self, int option);
+PYLDAP_FUNC(void) set_timeval_from_double(struct timeval *tv, double d);
+
+#endif /* pythonldap_h */
diff --git a/Tests/__init__.py b/Tests/__init__.py
index ea28d0ce..1a6a8836 100644
--- a/Tests/__init__.py
+++ b/Tests/__init__.py
@@ -21,3 +21,4 @@
from . import t_untested_mods
from . import t_ldap_controls_libldap
from . import t_ldap_options
+from . import t_ldap_syncrepl
diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py
index 86d36403..c4d9cb6c 100644
--- a/Tests/t_ldap_dn.py
+++ b/Tests/t_ldap_dn.py
@@ -40,23 +40,24 @@ def test_escape_dn_chars(self):
test function escape_dn_chars()
"""
self.assertEqual(ldap.dn.escape_dn_chars('foobar'), 'foobar')
- self.assertEqual(ldap.dn.escape_dn_chars('foo,bar'), 'foo\\,bar')
- self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), 'foo\\=bar')
+ self.assertEqual(ldap.dn.escape_dn_chars('foo,bar'), r'foo\,bar')
+ self.assertEqual(ldap.dn.escape_dn_chars('foo=bar'), r'foo\=bar')
self.assertEqual(ldap.dn.escape_dn_chars('foo#bar'), 'foo#bar')
- self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), '\\#foobar')
+ self.assertEqual(ldap.dn.escape_dn_chars('#foobar'), r'\#foobar')
self.assertEqual(ldap.dn.escape_dn_chars('foo bar'), 'foo bar')
- self.assertEqual(ldap.dn.escape_dn_chars(' foobar'), '\\ foobar')
- self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ ')
- self.assertEqual(ldap.dn.escape_dn_chars(' '), '\\ \\ ')
- self.assertEqual(ldap.dn.escape_dn_chars('foobar '), 'foobar\\ ')
- self.assertEqual(ldap.dn.escape_dn_chars('f+o>o,bo\\,b\\o,bo\,b\=3.6"
+keywords = ["ldap", "directory", "authentication"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: System Administrators",
+ "Operating System :: OS Independent",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX",
+ "Programming Language :: C",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Database",
+ "Topic :: Internet",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP",
+ "License :: OSI Approved :: Python Software Foundation License",
+]
+dependencies = [
+ "pyasn1 >= 0.3.7",
+ "pyasn1_modules >= 0.1.5",
+]
+
+[project.urls]
+Homepage = "https://www.python-ldap.org/"
+Documentation = "https://python-ldap.readthedocs.io/"
+Repository = "https://github.com/python-ldap/python-ldap"
+Download = "https://pypi.org/project/python-ldap/"
+Changelog = "https://github.com/python-ldap/python-ldap/blob/main/CHANGES"
+
+
+
+[tool.setuptools]
+zip-safe = false
+include-package-data = true
+license-files = ["LICENCE", "LICENCE.MIT"]
+# Explicitly list all Python modules
+py-modules = ["ldapurl", "ldif"]
+
+[tool.setuptools.dynamic]
+version = {attr = "ldap.pkginfo.__version__"}
+
+[tool.setuptools.packages.find]
+where = ["Lib"]
+
+[tool.setuptools.package-dir]
+"" = "Lib"
[tool.isort]
-line_length=88
-known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest']
-sections=['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER']
+line_length = 88
+known_first_party = ["ldap", "_ldap", "ldapurl", "ldif", "slapdtest"]
+sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
+
+[tool.coverage.run]
+branch = true
+source = [
+ "ldap",
+ "ldif",
+ "ldapurl",
+ "slapdtest",
+]
+
+[tool.coverage.paths]
+source = [
+ "Lib/",
+ ".tox/*/lib/python*/site-packages/",
+]
+
+[tool.coverage.report]
+ignore_errors = false
+precision = 1
+exclude_lines = [
+ "pragma: no cover",
+ "raise NotImplementedError",
+ "if 0:",
+ "if __name__ == .__main__.:",
+ "if PY2",
+ "if not PY2",
+]
+
+[tool.coverage.html]
+directory = "build/htmlcov"
+title = "python-ldap coverage report"
diff --git a/setup.py b/setup.py
index 6da3f491..f2e816be 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,9 @@
"""
-setup.py - Setup package with the help Python's DistUtils
+setup.py - C extension module configuration for python-ldap
See https://www.python-ldap.org/ for details.
+This file handles only the C extension modules (_ldap) configuration,
+while pyproject.toml handles all project metadata, dependencies, and other settings.
"""
import sys,os
@@ -54,54 +56,8 @@ class OpenLDAP2:
LDAP_CLASS.extra_link_args.append('-pg')
LDAP_CLASS.libs.append('gcov')
-#-- Let distutils/setuptools do the rest
-name = 'python-ldap'
-
+#-- C extension modules configuration only
setup(
- #-- Package description
- name = name,
- license=pkginfo.__license__,
- version=pkginfo.__version__,
- description = 'Python modules for implementing LDAP clients',
- long_description = """python-ldap:
- python-ldap provides an object-oriented API to access LDAP directory servers
- from Python programs. Mainly it wraps the OpenLDAP 2.x libs for that purpose.
- Additionally the package contains modules for other LDAP-related stuff
- (e.g. processing LDIF, LDAPURLs, LDAPv3 schema, LDAPv3 extended operations
- and controls, etc.).
- """,
- author = 'python-ldap project',
- author_email = 'python-ldap@python.org',
- url = 'https://www.python-ldap.org/',
- download_url = 'https://pypi.org/project/python-ldap/',
- classifiers = [
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'Intended Audience :: System Administrators',
- 'Operating System :: OS Independent',
- 'Operating System :: MacOS :: MacOS X',
- 'Operating System :: Microsoft :: Windows',
- 'Operating System :: POSIX',
- 'Programming Language :: C',
-
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8',
- 'Programming Language :: Python :: 3.9',
- 'Programming Language :: Python :: 3.10',
- 'Programming Language :: Python :: 3.11',
- 'Programming Language :: Python :: 3.12',
- # Note: when updating Python versions, also change tox.ini and .github/workflows/*
-
- 'Topic :: Database',
- 'Topic :: Internet',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- 'Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP',
- 'License :: OSI Approved :: Python Software Foundation License',
- ],
- #-- C extension modules
ext_modules = [
Extension(
'_ldap',
@@ -117,15 +73,8 @@ class OpenLDAP2:
'Modules/berval.c',
],
depends = [
- 'Modules/LDAPObject.h',
- 'Modules/berval.h',
- 'Modules/common.h',
+ 'Modules/pythonldap.h',
'Modules/constants_generated.h',
- 'Modules/constants.h',
- 'Modules/functions.h',
- 'Modules/ldapcontrol.h',
- 'Modules/message.h',
- 'Modules/options.h',
],
libraries = LDAP_CLASS.libs,
include_dirs = ['Modules'] + LDAP_CLASS.include_dirs,
@@ -144,28 +93,4 @@ class OpenLDAP2:
]
),
],
- #-- Python "stand alone" modules
- py_modules = [
- 'ldapurl',
- 'ldif',
-
- ],
- packages = [
- 'ldap',
- 'ldap.controls',
- 'ldap.extop',
- 'ldap.schema',
- 'slapdtest',
- 'slapdtest.certs',
- ],
- package_dir = {'': 'Lib',},
- data_files = LDAP_CLASS.extra_files,
- include_package_data=True,
- install_requires=[
- 'pyasn1 >= 0.3.7',
- 'pyasn1_modules >= 0.1.5',
- ],
- zip_safe=False,
- python_requires='>=3.6',
- test_suite = 'Tests',
)
diff --git a/tox.ini b/tox.ini
index beade024..0741ef29 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,22 +5,21 @@
[tox]
# Note: when updating Python versions, also change setup.py and .github/worlflows/*
-envlist = py{36,37,38,39,310,311,312},c90-py{36,37},py3-nosasltls,doc,py3-trace,pypy3.9
+envlist = py{39,310,311,312},py3-nosasltls,doc,py3-trace,pypy3.9
minver = 1.8
[gh-actions]
python =
- 3.6: py36
- 3.7: py37
- 3.8: py38, doc, py3-nosasltls
- 3.9: py39, py3-trace
+ 3.9: py39, py3-trace, doc, py3-nosasltls
3.10: py310
3.11: py311
3.12: py312
+ 3.13: py313
pypy3.9: pypy3.9
+ pypy3.10: pypy3.10
[testenv]
-deps =
+deps = setuptools
passenv = WITH_GCOV
# - Enable BytesWarning
# - Turn all warnings into exceptions.
@@ -34,6 +33,7 @@ commands = {envpython} -bb -Werror \
setenv =
CFLAGS=-Wno-int-in-bool-context -Werror -std=c99
+
[testenv:py3-nosasltls]
basepython = python3
# don't install, install dependencies manually
@@ -98,6 +98,7 @@ deps =
markdown
sphinx
sphinxcontrib-spelling
+ setuptools
commands =
{envpython} setup.py check --restructuredtext --metadata --strict
{envpython} -m markdown README -f {envtmpdir}/README.html