From 1327439dd28f00cad4e63ea65c93091da4b78267 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 17 Oct 2026 16:56:59 +0200 Subject: [PATCH 1/4] refactor: nit-pick code cleanup * use PEP8 conform names * use enumerate * rename 1 letter variables to something more meaningful * re-indent useless else block Issue: #657 Issue: https://github.com/python-ldap/python-ldap/issues/657 --- Lib/ldap/__init__.py | 5 +--- Lib/ldap/controls/deref.py | 4 +-- Lib/ldap/controls/readentry.py | 4 +-- Lib/ldap/controls/sessiontrack.py | 22 +++++++-------- Lib/ldap/controls/sss.py | 2 +- Lib/ldap/controls/vlv.py | 25 ++++++++--------- Lib/ldap/extop/dds.py | 12 ++++----- Lib/ldap/extop/passwd.py | 4 +-- Lib/ldap/functions.py | 9 ++++--- Lib/ldap/modlist.py | 13 +++++---- Lib/ldap/schema/models.py | 20 ++++++-------- Lib/ldap/schema/subentry.py | 45 ++++++++++--------------------- Lib/ldap/syncrepl.py | 5 +--- Lib/ldapurl.py | 33 +++++++++++------------ 14 files changed, 88 insertions(+), 115 deletions(-) diff --git a/Lib/ldap/__init__.py b/Lib/ldap/__init__.py index 73f40a1c1..011c3ff31 100644 --- a/Lib/ldap/__init__.py +++ b/Lib/ldap/__init__.py @@ -50,10 +50,7 @@ # call into libldap to initialize it right now LIBLDAP_API_INFO = _ldap.get_option(_ldap.OPT_API_INFO) -OPT_NAMES_DICT = {} -for k, v in vars(_ldap).items(): - if k.startswith('OPT_'): - OPT_NAMES_DICT[v] = k +OPT_NAMES_DICT = {val: key for key, val in vars(_ldap).items() if key.startswith('OPT_')} LDAPLockBaseClass = threading.Lock diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index 097126fbe..ffe33420a 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -91,8 +91,8 @@ def _derefSpecs(self) -> DerefSpecs: for i, (deref_attr, deref_attribute_names) in enumerate(self.derefSpecs.items()): deref_spec = DerefSpec() deref_attributes = AttributeList() - for j in range(len(deref_attribute_names)): - deref_attributes.setComponentByPosition(j, deref_attribute_names[j]) + for j, deref_attribute_name in enumerate(deref_attribute_names): + deref_attributes.setComponentByPosition(j, deref_attribute_name) deref_spec.setComponentByName('derefAttr', AttributeDescription(deref_attr)) deref_spec.setComponentByName('attributes', deref_attributes) deref_specs.setComponentByPosition(i, deref_spec) diff --git a/Lib/ldap/controls/readentry.py b/Lib/ldap/controls/readentry.py index fa31282e8..e8c291c7a 100644 --- a/Lib/ldap/controls/readentry.py +++ b/Lib/ldap/controls/readentry.py @@ -37,8 +37,8 @@ def __init__(self, criticality: bool = False, attrList: list[str] | None = None) def encodeControlValue(self) -> bytes: attributeSelection = AttributeDescriptionList() - for i in range(len(self.attrList)): - attributeSelection.setComponentByPosition(i, self.attrList[i]) + for i, attr in enumerate(self.attrList): + attributeSelection.setComponentByPosition(i, attr) return encoder.encode(attributeSelection) # type: ignore def decodeControlValue(self, encodedControlValue: bytes) -> None: diff --git a/Lib/ldap/controls/sessiontrack.py b/Lib/ldap/controls/sessiontrack.py index f43e6796e..39aa44d5f 100644 --- a/Lib/ldap/controls/sessiontrack.py +++ b/Lib/ldap/controls/sessiontrack.py @@ -55,17 +55,15 @@ def __init__( ) -> None: # criticality MUST be false for this control self.criticality = False - self.sessionSourceIp, self.sessionSourceName, self.formatOID, self.sessionTrackingIdentifier = ( - sessionSourceIp, - sessionSourceName, - formatOID, - sessionTrackingIdentifier, - ) + self.sessionSourceIp = sessionSourceIp + self.sessionSourceName = sessionSourceName + self.formatOID = formatOID + self.sessionTrackingIdentifier = sessionTrackingIdentifier def encodeControlValue(self) -> bytes: - s = self.SessionIdentifierControlValue() - s.setComponentByName('sessionSourceIp', LDAPString(self.sessionSourceIp)) - s.setComponentByName('sessionSourceName', LDAPString(self.sessionSourceName)) - s.setComponentByName('formatOID', LDAPOID(self.formatOID)) - s.setComponentByName('sessionTrackingIdentifier', LDAPString(self.sessionTrackingIdentifier)) - return encoder.encode(s) # type: ignore + scv = self.SessionIdentifierControlValue() + scv.setComponentByName('sessionSourceIp', LDAPString(self.sessionSourceIp)) + scv.setComponentByName('sessionSourceName', LDAPString(self.sessionSourceName)) + scv.setComponentByName('formatOID', LDAPOID(self.formatOID)) + scv.setComponentByName('sessionTrackingIdentifier', LDAPString(self.sessionTrackingIdentifier)) + return encoder.encode(scv) # type: ignore diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index d86219595..3cbce4feb 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -79,7 +79,7 @@ def asn1(self) -> SortKeyListType: if reverse_order: rule = rule[1:] if ':' in rule: - attribute_type, ordering_rule = rule.split(':') + attribute_type, ordering_rule = rule.split(':', 1) else: attribute_type, ordering_rule = rule, None q.setComponentByName('attributeType', attribute_type) diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 9a2d993c4..6d1b0e4bd 100644 --- a/Lib/ldap/controls/vlv.py +++ b/Lib/ldap/controls/vlv.py @@ -24,7 +24,8 @@ class ByOffsetType(univ.Sequence): tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0) ) componentType = namedtype.NamedTypes( - namedtype.NamedType('offset', univ.Integer()), namedtype.NamedType('contentCount', univ.Integer()) + namedtype.NamedType('offset', univ.Integer()), + namedtype.NamedType('contentCount', univ.Integer()), ) @@ -74,9 +75,9 @@ def __init__( self.context_id = context_id def encodeControlValue(self) -> bytes: - p = VirtualListViewRequestType() - p.setComponentByName('beforeCount', self.before_count) - p.setComponentByName('afterCount', self.after_count) + vlv = VirtualListViewRequestType() + vlv.setComponentByName('beforeCount', self.before_count) + vlv.setComponentByName('afterCount', self.after_count) if self.offset is not None and self.content_count is not None: by_offset = ByOffsetType() by_offset.setComponentByName('offset', self.offset) @@ -88,10 +89,10 @@ def encodeControlValue(self) -> bytes: target.setComponentByName('greaterThanOrEqual', self.greater_than_or_equal) else: raise NotImplementedError - p.setComponentByName('target', target) + vlv.setComponentByName('target', target) if self.context_id is not None: - p.setComponentByName('contextID', self.context_id) - return encoder.encode(p) # type: ignore + vlv.setComponentByName('contextID', self.context_id) + return encoder.encode(vlv) # type: ignore class VirtualListViewResultType(univ.Enumerated): @@ -125,13 +126,13 @@ def __init__(self, criticality: bool = False) -> None: ResponseControl.__init__(self, self.controlType, criticality) def decodeControlValue(self, encodedControlValue: bytes) -> None: - p, rest = decoder.decode(encodedControlValue, asn1Spec=VirtualListViewResponseType()) + drv, rest = decoder.decode(encodedControlValue, asn1Spec=VirtualListViewResponseType()) assert not rest, 'all data could not be decoded' - self.targetPosition = int(p.getComponentByName('targetPosition')) - self.contentCount = int(p.getComponentByName('contentCount')) - virtual_list_view_result = p.getComponentByName('virtualListViewResult') + self.targetPosition = int(drv.getComponentByName('targetPosition')) + self.contentCount = int(drv.getComponentByName('contentCount')) + virtual_list_view_result = drv.getComponentByName('virtualListViewResult') self.virtualListViewResult = int(virtual_list_view_result) - context_id = p.getComponentByName('contextID') + context_id = drv.getComponentByName('contextID') if context_id.hasValue(): self.contextID: bytes | None = bytes(context_id) else: diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index cf0afb4be..f90cc28bc 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -41,18 +41,18 @@ def __init__( self.requestTtl = requestTtl or self.defaultRequestTtl def encodedRequestValue(self) -> bytes: - p = self.RefreshRequestValue() - p.setComponentByName( + req_value = self.RefreshRequestValue() + req_value.setComponentByName( 'entryName', LDAPDN(self.entryName).subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)), ) - p.setComponentByName( + req_value.setComponentByName( 'requestTtl', univ.Integer(self.requestTtl).subtype( # type: ignore[no-untyped-call] implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1) ), ) - return encoder.encode(p) # type: ignore[no-any-return] + return encoder.encode(req_value) # type: ignore[no-any-return] class RefreshResponse(ExtendedResponse): @@ -69,6 +69,6 @@ class RefreshResponseValue(univ.Sequence): ) def decodeResponseValue(self, value: bytes | None) -> int: - respValue, _ = decoder.decode(value, asn1Spec=self.RefreshResponseValue()) - self.responseTtl = int(respValue.getComponentByName('responseTtl')) + refresh_response, _ = decoder.decode(value, asn1Spec=self.RefreshResponseValue()) + self.responseTtl = int(refresh_response.getComponentByName('responseTtl')) return self.responseTtl diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py index 6ccecbd53..de7f062be 100644 --- a/Lib/ldap/extop/passwd.py +++ b/Lib/ldap/extop/passwd.py @@ -27,6 +27,6 @@ class PasswordModifyResponseValue(univ.Sequence): ) def decodeResponseValue(self, value: bytes | None) -> bytes: - respValue, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue()) - self.genPasswd = bytes(respValue.getComponentByName('genPasswd')) + passmod_resp, _ = decoder.decode(value, asn1Spec=self.PasswordModifyResponseValue()) + self.genPasswd = bytes(passmod_resp.getComponentByName('genPasswd')) return self.genPasswd diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 2d4040b19..9fe11357c 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -36,6 +36,9 @@ # Tracing is only supported in debugging mode import traceback +# format string used for Generalized Time attributes values with UTC +DATETIME_FORMAT_UTC = r'%Y%m%d%H%M%SZ' + def _ldap_function_call( lock: ldap.LDAPLock | None, @@ -140,11 +143,11 @@ def strf_secs(secs: float) -> str: """ Convert seconds since epoch to a string compliant to LDAP syntax GeneralizedTime """ - return time.strftime('%Y%m%d%H%M%SZ', time.gmtime(secs)) + return time.strftime(DATETIME_FORMAT_UTC, time.gmtime(secs)) -def strp_secs(dt_str: str) -> int: +def strp_secs(dt_str: str) -> float: """ Convert LDAP syntax GeneralizedTime to seconds since epoch """ - return timegm(time.strptime(dt_str, '%Y%m%d%H%M%SZ')) + return timegm(time.strptime(dt_str, DATETIME_FORMAT_UTC)) diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index 4c867c932..e8c9d20fc 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -31,7 +31,7 @@ def addModlist( attrvaluelist = [item for item in value if item is not None] if attrvaluelist: modlist.append((attrtype, value)) - return modlist # addModlist() + return modlist def modifyModlist( @@ -64,8 +64,8 @@ def modifyModlist( case_ignore_attr_types_set = {v.lower() for v in case_ignore_attr_types or []} modlist: list[LDAPModListModifyEntry] = [] attrtype_lower_map = {} - for a in old_entry: - attrtype_lower_map[a.lower()] = a + for attr_type in old_entry: + attrtype_lower_map[attr_type.lower()] = attr_type for attrtype, value in new_entry.items(): attrtype_lower = attrtype.lower() if attrtype_lower in ignore_attr_types_set: @@ -101,10 +101,9 @@ def modifyModlist( if not ignore_oldexistent: # Remove all attributes of old_entry which are not present # in new_entry at all - for a, val in attrtype_lower_map.items(): - if a in ignore_attr_types_set: + for atl, attrtype in attrtype_lower_map.items(): + if atl in ignore_attr_types_set: # This attribute type is ignored continue - attrtype = val modlist.append((ldap.MOD_DELETE, attrtype, None)) - return modlist # modifyModlist() + return modlist diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 82e49e2a7..b468f8814 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -98,19 +98,16 @@ def set_id(self, element_id: str) -> None: def get_id(self) -> str: return self.oid - def key_attr(self, key: str, value: str | None, quoted: int = 0) -> str: - if value is None: + @staticmethod + def key_attr(key: str, value: str | None, quoted: int = 0) -> str: + if not value: return '' - elif not isinstance(value, str): - raise TypeError(f'value has to be of str, was {value!r}') - elif value == '': - return '' - elif quoted: + if quoted: return " {} '{}'".format(key, value.replace("'", "\\'")) - else: - return f' {key} {value}' + return f' {key} {value}' - def key_list(self, key: str, values: tuple[str, ...], sep: str = ' ', quoted: int = 0) -> str: + @staticmethod + def key_list(key: str, values: tuple[str, ...], sep: str = ' ', quoted: int = 0) -> str: assert isinstance(values, tuple), TypeError(f'values has to be a tuple, was {values!r}') if not values: return '' @@ -122,8 +119,7 @@ def key_list(self, key: str, values: tuple[str, ...], sep: str = ' ', quoted: in if len(quoted_values) == 1: return f' {key} {quoted_values[0]}' - else: - return f' {key} ( {sep.join(quoted_values)} )' + return f' {key} ( {sep.join(quoted_values)} )' def __str__(self) -> str: result = [str(self.oid)] diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 636083ff6..aee7908c5 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -9,7 +9,7 @@ import copy import warnings from collections.abc import Iterable, MutableMapping -from typing import Any, TypeVar +from typing import Any, TypeVar, cast from urllib.request import urlopen import ldap.schema @@ -98,7 +98,6 @@ def __init__( sub_schema_sub_entry: LDAPEntryDict, check_uniqueness: int = 1, ) -> None: - # SchemaElement class -> Element name -> Element OID self.name2oid: dict[type[SchemaElement], cidict[str]] = {} @@ -151,14 +150,11 @@ def __init__( if check_uniqueness and name in self.name2oid[se_class]: self.non_unique_names[se_class][se_id] = None raise NameNotUnique(attr_value.decode('utf-8', errors='backslashreplace')) - else: - self.name2oid[se_class][name] = se_id + self.name2oid[se_class][name] = se_id # Turn set into list, maybe more handy for applications self.non_unique_oids = list(non_unique_oids) - # return subSchema.__init__() - def ldap_entry(self) -> dict[str, list[str]]: """ Returns a dictionary containing the sub schema sub entry @@ -276,15 +272,12 @@ def getoid( if nameoroid_stripped in self.sed[se_class]: # name_or_oid is already a registered OID return nameoroid_stripped - else: - try: - result_oid = self.name2oid[se_class][nameoroid_stripped] - except KeyError: - if raise_keyerror: - raise KeyError(f'No registered {se_class.__name__}-OID for nameoroid {nameoroid_stripped!r}') - else: - result_oid = nameoroid_stripped - return result_oid + try: + return self.name2oid[se_class][nameoroid_stripped] + except KeyError: + if raise_keyerror: + raise KeyError(f'No registered {se_class.__name__}-OID for nameoroid {nameoroid_stripped!r}') + return nameoroid_stripped def get_inheritedattr( self, @@ -300,10 +293,7 @@ def get_inheritedattr( Raises KeyError if no schema element is found by nameoroid. """ se = self.sed[se_class][self.getoid(se_class, nameoroid)] - try: - result = getattr(se, name) - except AttributeError: - result = None + result = getattr(se, name, None) if result is None and hasattr(se, 'sup') and se.sup: assert isinstance(se.sup, list) # FIXME: sup can be multi-valued @@ -326,19 +316,13 @@ def get_obj( """ se_oid = self.getoid(se_class, nameoroid) try: - se_obj = self.sed[se_class][se_oid] + return cast(SchemaElementSubclass, self.sed[se_class][se_oid]) except KeyError: if raise_keyerror: raise KeyError( f'No ldap.schema.{se_class.__name__} instance with nameoroid {nameoroid!r} and se_oid {se_oid!r}' ) - elif default is None: - return None - else: - se_obj = default - - assert isinstance(se_obj, se_class) - return se_obj + return default def get_inheritedobj( self, @@ -421,9 +405,8 @@ def get_applicable_aux_classes(self, nameoroid: str) -> list[str]: if content_rule: # Return AUXILIARY object classes from DITContentRule instance return list(content_rule.aux) - else: - # list all AUXILIARY object classes - return self.listall(ObjectClass, [('kind', [2])]) + # list all AUXILIARY object classes + return self.listall(ObjectClass, [('kind', [2])]) def attribute_types( # noqa: C901 self, @@ -546,7 +529,7 @@ def attribute_types( # noqa: C901 del l[a] break - return r_must, r_may # attribute_types() + return r_must, r_may def urlfetch( diff --git a/Lib/ldap/syncrepl.py b/Lib/ldap/syncrepl.py index 302a5491c..091645af5 100644 --- a/Lib/ldap/syncrepl.py +++ b/Lib/ldap/syncrepl.py @@ -94,10 +94,7 @@ def __init__( mode: str = 'refreshOnly', reloadHint: bool = False, ) -> None: - if criticality: - self.criticality = True - else: - self.criticality = False + self.criticality = bool(criticality) self.cookie = cookie self.mode = mode self.reloadHint = reloadHint diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index b095466dc..77c6d14b8 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -172,7 +172,7 @@ def __len__(self) -> int: return len(self._data) def __str__(self) -> str: - return ','.join(str(v) for v in self.values()) + return ','.join(str(val) for val in self.values()) def __repr__(self) -> str: return f'<{self.__class__.__module__}.{self.__class__.__name__} instance at {hex(id(self))}: {self._data}>' @@ -185,12 +185,12 @@ def __eq__(self, other: object) -> bool: def parse(self, extListStr: str) -> None: for extension_str in extListStr.strip().split(','): if extension_str: - e = LDAPUrlExtension(extension_str) - if e.extype is not None: - self[e.extype] = e + ext = LDAPUrlExtension(extension_str) + if ext.extype is not None: + self[ext.extype] = ext def unparse(self) -> str: - return ','.join(v.unparse() for v in self.values()) + return ','.join(val.unparse() for val in self.values()) class LDAPUrl: @@ -285,19 +285,18 @@ def _parse(self, ldap_url: str) -> None: self.hostport = unquote(rest) self.dn = '' return + if slash_pos != -1 and (qemark_pos == -1 or (slash_pos < qemark_pos)): + # Slash separates DN from hostport + self.hostport = unquote(rest[:slash_pos]) + # Eat the slash from rest + rest = rest[slash_pos + 1 :] + elif qemark_pos != 1 and (slash_pos == -1 or (slash_pos > qemark_pos)): + # Question mark separates hostport from rest, DN is assumed to be empty + self.hostport = unquote(rest[:qemark_pos]) + # Do not eat question mark + rest = rest[qemark_pos:] else: - if slash_pos != -1 and (qemark_pos == -1 or (slash_pos < qemark_pos)): - # Slash separates DN from hostport - self.hostport = unquote(rest[:slash_pos]) - # Eat the slash from rest - rest = rest[slash_pos + 1 :] - elif qemark_pos != 1 and (slash_pos == -1 or (slash_pos > qemark_pos)): - # Question mark separates hostport from rest, DN is assumed to be empty - self.hostport = unquote(rest[:qemark_pos]) - # Do not eat question mark - rest = rest[qemark_pos:] - else: - raise ValueError('Something completely weird happened!') + raise ValueError('Something completely weird happened!') paramlist = rest.split('?', 4) paramlist_len = len(paramlist) if paramlist_len >= 1: From 01f5313562d2651732a79302f49409ff024b3f7e Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 24 Oct 2026 20:41:20 +0200 Subject: [PATCH 2/4] refactor(ldap.schema): enhance SubSchema.get_syntax() --- Lib/ldap/schema/subentry.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index aee7908c5..302a4c9d1 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -9,7 +9,7 @@ import copy import warnings from collections.abc import Iterable, MutableMapping -from typing import Any, TypeVar, cast +from typing import Any, Optional, TypeVar, cast from urllib.request import urlopen import ldap.schema @@ -351,17 +351,11 @@ def get_syntax(self, nameoroid: str) -> str | None: """ Get the syntax of an attribute type specified by name or OID """ - at_oid = self.getoid(AttributeType, nameoroid) try: - at_obj = self.get_inheritedobj(AttributeType, at_oid) + return cast(Optional[str], self.get_inheritedattr(AttributeType, nameoroid, 'syntax')) except KeyError: return None - if at_obj is None: - return None - else: - return at_obj.syntax - def get_structural_oc(self, oc_list: Iterable[str]) -> str | None: """ Returns OID of structural object class in oc_list From 1c433826d56dc9a97e43857fee06feebe8877cff Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 17 Oct 2026 17:07:12 +0200 Subject: [PATCH 3/4] style: move `__all__` after imports and add some missing declarations of it Issue: #657 Issue: https://github.com/python-ldap/python-ldap/issues/657 --- Lib/ldap/controls/deref.py | 11 +++++------ Lib/ldap/controls/libldap.py | 2 ++ Lib/ldap/controls/pagedresults.py | 6 +++--- Lib/ldap/controls/ppolicy.py | 6 +++--- Lib/ldap/controls/pwdpolicy.py | 5 +++-- Lib/ldap/controls/readentry.py | 3 +++ Lib/ldap/controls/sessiontrack.py | 8 ++++++++ Lib/ldap/controls/simple.py | 13 +++++++++++++ Lib/ldap/controls/sss.py | 11 ++++------- Lib/ldap/controls/vlv.py | 10 +++++----- Lib/ldap/extop/dds.py | 3 +++ Lib/ldap/filter.py | 3 +++ Lib/ldap/functions.py | 22 ++++++++++----------- Lib/ldap/ldapobject.py | 28 ++++++++++++--------------- Lib/ldap/modlist.py | 3 +++ Lib/ldap/resiter.py | 3 +++ Lib/ldap/schema/__init__.py | 1 + Lib/ldap/schema/models.py | 16 ++++++++++++++++ Lib/ldap/schema/subentry.py | 10 ++++++++++ Lib/ldapurl.py | 14 +++++++------- Lib/ldif.py | 32 +++++++++++++++---------------- 21 files changed, 133 insertions(+), 77 deletions(-) diff --git a/Lib/ldap/controls/deref.py b/Lib/ldap/controls/deref.py index ffe33420a..e8d872157 100644 --- a/Lib/ldap/controls/deref.py +++ b/Lib/ldap/controls/deref.py @@ -7,12 +7,6 @@ from __future__ import annotations - -__all__ = [ - 'DEREF_CONTROL_OID', - 'DereferenceControl', -] - from pyasn1.codec.ber import decoder, encoder from pyasn1.type import namedtype, tag, univ from pyasn1_modules.rfc2251 import LDAPDN, AttributeDescription, AttributeDescriptionList, AttributeValue @@ -20,6 +14,11 @@ from ldap.controls import KNOWN_RESPONSE_CONTROLS, LDAPControl +__all__ = [ + 'DEREF_CONTROL_OID', + 'DereferenceControl', +] + DEREF_CONTROL_OID = '1.3.6.1.4.1.4203.666.5.16' diff --git a/Lib/ldap/controls/libldap.py b/Lib/ldap/controls/libldap.py index 42c982ce0..8a260e1f6 100644 --- a/Lib/ldap/controls/libldap.py +++ b/Lib/ldap/controls/libldap.py @@ -13,6 +13,8 @@ from ldap.pkginfo import __version__ +__all__ = ['AssertionControl', 'MatchedValuesControl', 'SimplePagedResultsControl'] + assert _ldap.__version__ == __version__, ImportError( f'ldap {__version__} and _ldap {_ldap.__version__} version mismatch!' ) diff --git a/Lib/ldap/controls/pagedresults.py b/Lib/ldap/controls/pagedresults.py index e692a0c18..fc658b70c 100644 --- a/Lib/ldap/controls/pagedresults.py +++ b/Lib/ldap/controls/pagedresults.py @@ -7,9 +7,6 @@ from __future__ import annotations - -__all__ = ['SimplePagedResultsControl'] - from pyasn1.codec.ber import decoder, encoder from pyasn1.type import namedtype, univ from pyasn1_modules.rfc2251 import LDAPString @@ -17,6 +14,9 @@ from ldap.controls import KNOWN_RESPONSE_CONTROLS, RequestControl, ResponseControl +__all__ = ['SimplePagedResultsControl'] + + class PagedResultsControlValue(univ.Sequence): componentType = namedtype.NamedTypes( namedtype.NamedType('size', univ.Integer()), diff --git a/Lib/ldap/controls/ppolicy.py b/Lib/ldap/controls/ppolicy.py index 296412167..6adc64f86 100644 --- a/Lib/ldap/controls/ppolicy.py +++ b/Lib/ldap/controls/ppolicy.py @@ -7,15 +7,15 @@ from __future__ import annotations - -__all__ = ['PasswordPolicyControl'] - from pyasn1.codec.der import decoder from pyasn1.type import constraint, namedtype, namedval, tag, univ from ldap.controls import KNOWN_RESPONSE_CONTROLS, ResponseControl, ValueLessRequestControl +__all__ = ['PasswordPolicyControl'] + + class PasswordPolicyWarning(univ.Choice): componentType = namedtype.NamedTypes( namedtype.NamedType( diff --git a/Lib/ldap/controls/pwdpolicy.py b/Lib/ldap/controls/pwdpolicy.py index 1af719632..c359cd575 100644 --- a/Lib/ldap/controls/pwdpolicy.py +++ b/Lib/ldap/controls/pwdpolicy.py @@ -5,13 +5,14 @@ See https://www.python-ldap.org/ for project details. """ +from ldap.controls import KNOWN_RESPONSE_CONTROLS, ResponseControl + + __all__ = [ 'PasswordExpiredControl', 'PasswordExpiringControl', ] -from ldap.controls import KNOWN_RESPONSE_CONTROLS, ResponseControl - class PasswordExpiringControl(ResponseControl): """ diff --git a/Lib/ldap/controls/readentry.py b/Lib/ldap/controls/readentry.py index e8c291c7a..c16d8f950 100644 --- a/Lib/ldap/controls/readentry.py +++ b/Lib/ldap/controls/readentry.py @@ -15,6 +15,9 @@ from ldap.controls import KNOWN_RESPONSE_CONTROLS, LDAPControl +__all__ = ['PostReadControl', 'PreReadControl', 'ReadEntryControl'] + + class ReadEntryControl(LDAPControl): """ Base class for read entry control described in RFC 4527 diff --git a/Lib/ldap/controls/sessiontrack.py b/Lib/ldap/controls/sessiontrack.py index 39aa44d5f..28914b9a5 100644 --- a/Lib/ldap/controls/sessiontrack.py +++ b/Lib/ldap/controls/sessiontrack.py @@ -12,6 +12,14 @@ from ldap.controls import RequestControl +__all__ = [ + 'SESSION_TRACKING_CONTROL_OID', + 'SESSION_TRACKING_FORMAT_OID_RADIUS_ACCT_MULTI_SESSION_ID', + 'SESSION_TRACKING_FORMAT_OID_RADIUS_ACCT_SESSION_ID', + 'SESSION_TRACKING_FORMAT_OID_USERNAME', + 'SessionTrackingControl', +] + # OID constants SESSION_TRACKING_CONTROL_OID = '1.3.6.1.4.1.21008.108.63.1' SESSION_TRACKING_FORMAT_OID_RADIUS_ACCT_SESSION_ID = SESSION_TRACKING_CONTROL_OID + '.1' diff --git a/Lib/ldap/controls/simple.py b/Lib/ldap/controls/simple.py index a2afc7f65..c8e79d92c 100644 --- a/Lib/ldap/controls/simple.py +++ b/Lib/ldap/controls/simple.py @@ -15,6 +15,19 @@ from ldap.controls import KNOWN_RESPONSE_CONTROLS, LDAPControl, RequestControl, ResponseControl +__all__ = [ + 'AuthorizationIdentityRequestControl', + 'AuthorizationIdentityResponseControl', + 'BooleanControl', + 'GetEffectiveRightsControl', + 'ManageDSAITControl', + 'OctetStringInteger', + 'ProxyAuthzControl', + 'RelaxRulesControl', + 'ValueLessRequestControl', +] + + class ValueLessRequestControl(RequestControl): """ Base class for controls without a controlValue. diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index 3cbce4feb..f0d29e967 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -7,19 +7,16 @@ from __future__ import annotations - -__all__ = [ - 'SSSRequestControl', - 'SSSResponseControl', -] - - from pyasn1.codec.ber import decoder, encoder from pyasn1.type import constraint, namedtype, namedval, tag, univ from ldap.controls import KNOWN_RESPONSE_CONTROLS, RequestControl, ResponseControl +__all__ = [ + 'SSSRequestControl', + 'SSSResponseControl', +] # SortKeyList ::= SEQUENCE OF SEQUENCE { # attributeType AttributeDescription, # orderingRule [0] MatchingRuleId OPTIONAL, diff --git a/Lib/ldap/controls/vlv.py b/Lib/ldap/controls/vlv.py index 6d1b0e4bd..b08e0fad8 100644 --- a/Lib/ldap/controls/vlv.py +++ b/Lib/ldap/controls/vlv.py @@ -7,17 +7,17 @@ from __future__ import annotations +from pyasn1.codec.ber import decoder, encoder +from pyasn1.type import namedtype, namedval, tag, univ + +from ldap.controls import KNOWN_RESPONSE_CONTROLS, RequestControl, ResponseControl + __all__ = [ 'VLVRequestControl', 'VLVResponseControl', ] -from pyasn1.codec.ber import decoder, encoder -from pyasn1.type import namedtype, namedval, tag, univ - -from ldap.controls import KNOWN_RESPONSE_CONTROLS, RequestControl, ResponseControl - class ByOffsetType(univ.Sequence): tagSet = univ.Sequence.tagSet.tagImplicitly( # type: ignore[no-untyped-call] diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index f90cc28bc..85a1dbe9a 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -14,6 +14,9 @@ from ldap.extop import ExtendedRequest, ExtendedResponse +__all__ = ['RefreshRequest', 'RefreshResponse'] + + class RefreshRequest(ExtendedRequest): requestName = '1.3.6.1.4.1.1466.101.119.1' defaultRequestTtl = 86400 diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 7d5f8c1fa..8e1bb21b9 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -17,6 +17,9 @@ from ldap.pkginfo import __version__ # noqa: F401 +__all__ = ['escape_filter_chars', 'filter_format', 'is_filter', 'time_span_filter'] + + def escape_filter_chars(assertion_value: str, escape_mode: int = 0) -> str: """ Replace all special characters found in assertion_value diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 9fe11357c..dbb946c56 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -6,6 +6,16 @@ from __future__ import annotations +import pprint +import sys +import time +from calendar import timegm +from typing import Any, BinaryIO, Callable, TextIO + +import ldap +from ldap import LDAPError, _ldap +from ldap.dn import explode_dn, explode_rdn +from ldap.ldapobject import LDAPObject from ldap.pkginfo import __version__ # noqa: F401 @@ -20,18 +30,6 @@ 'strp_secs', ] -import pprint -import sys -import time -from calendar import timegm -from typing import Any, BinaryIO, Callable, TextIO - -import ldap -from ldap import LDAPError, _ldap -from ldap.dn import explode_dn, explode_rdn -from ldap.ldapobject import LDAPObject - - if __debug__: # Tracing is only supported in debugging mode import traceback diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index b7b4675ce..cb4807ed6 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -6,35 +6,31 @@ from __future__ import annotations -from os import strerror -from typing import Any, BinaryIO, Callable, TextIO - -from ldap._types import LDAPAddModList, LDAPEntryDict, LDAPModifyModList -from ldap.controls import RequestControl, ResponseControl -from ldap.pkginfo import __author__, __license__, __version__ # noqa: F401 - - -__all__ = ['LDAPBytesWarning', 'LDAPObject', 'ReconnectLDAPObject', 'SimpleLDAPObject'] - - -if __debug__: - # Tracing is only supported in debugging mode - import traceback - import pprint import time import warnings +from os import strerror +from typing import Any, BinaryIO, Callable, TextIO import ldap import ldap.cidict import ldap.functions import ldap.sasl from ldap import LDAPError, _ldap -from ldap.controls import DecodeControlTuples, RequestControlTuples +from ldap._types import LDAPAddModList, LDAPEntryDict, LDAPModifyModList +from ldap.controls import DecodeControlTuples, RequestControl, RequestControlTuples, ResponseControl from ldap.extop import ExtendedRequest, ExtendedResponse, PasswordModifyResponse +from ldap.pkginfo import __author__, __license__, __version__ # noqa: F401 from ldap.schema.subentry import SCHEMA_ATTRS +if __debug__: + # Tracing is only supported in debugging mode + import traceback + +__all__ = ['LDAPBytesWarning', 'LDAPObject', 'ReconnectLDAPObject', 'SimpleLDAPObject'] + + class LDAPBytesWarning(BytesWarning): """Python 2 bytes mode warning""" diff --git a/Lib/ldap/modlist.py b/Lib/ldap/modlist.py index e8c9d20fc..1951c4f99 100644 --- a/Lib/ldap/modlist.py +++ b/Lib/ldap/modlist.py @@ -16,6 +16,9 @@ from ldap.pkginfo import __version__ # noqa: F401 +__all__ = ['addModlist', 'modifyModlist'] + + def addModlist( entry: LDAPEntryDict, ignore_attr_types: list[str] | None = None, diff --git a/Lib/ldap/resiter.py b/Lib/ldap/resiter.py index a3bc0cb9e..264a3b4aa 100644 --- a/Lib/ldap/resiter.py +++ b/Lib/ldap/resiter.py @@ -14,6 +14,9 @@ from ldap.pkginfo import __author__, __license__, __version__ # noqa: F401 +__all__ = ['ResultProcessor'] + + if TYPE_CHECKING: _Base = LDAPObject else: diff --git a/Lib/ldap/schema/__init__.py b/Lib/ldap/schema/__init__.py index 46c9c08ab..7c012b29f 100644 --- a/Lib/ldap/schema/__init__.py +++ b/Lib/ldap/schema/__init__.py @@ -13,3 +13,4 @@ urlfetch, ) from ldap.schema.models import * +from ldap.schema.tokenizer import parse_tokens, split_tokens # noqa: F401 diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index b468f8814..906894464 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -20,6 +20,22 @@ from ldap.schema.tokenizer import LDAPTokenDict, parse_tokens, split_tokens +__all__ = [ + 'NOT_HUMAN_READABLE_LDAP_SYNTAXES', + 'AttributeType', + 'AttributeUsage', + 'DITContentRule', + 'DITStructureRule', + 'Entry', + 'LDAPSyntax', + 'MatchingRule', + 'MatchingRuleUse', + 'NameForm', + 'ObjectClass', + 'SchemaElement', +] + + EntryBase: TypeAlias = MutableMapping[str, list[bytes]] diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 302a4c9d1..db8a9d6bd 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -36,6 +36,16 @@ ) +__all__ = [ + 'SCHEMA_ATTR_MAPPING', + 'SCHEMA_CLASS_MAPPING', + 'NameNotUnique', + 'OIDNotUnique', + 'SubSchema', + 'SubschemaError', +] + + SchemaElementSubclass = TypeVar('SchemaElementSubclass', bound=SchemaElement) SCHEMA_ATTRS = list(SCHEMA_CLASS_MAPPING) diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index 77c6d14b8..dbf1b91af 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -6,8 +6,14 @@ from __future__ import annotations +import html +from collections.abc import Iterator, MutableMapping +from urllib.parse import quote, unquote + +from ldap._types import TypeAlias + -__version__ = '3.4.7' +__version__ = '3.5.0' __all__ = [ 'LDAP_SCOPE_BASE', @@ -24,12 +30,6 @@ 'isLDAPUrl', ] -import html -from collections.abc import Iterator, MutableMapping -from urllib.parse import quote, unquote - -from ldap._types import TypeAlias - LDAP_SCOPE_BASE = 0 LDAP_SCOPE_ONELEVEL = 1 diff --git a/Lib/ldif.py b/Lib/ldif.py index b364f16ad..528c56e57 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -6,22 +6,6 @@ from __future__ import annotations - -__version__ = '3.4.7' - -__all__ = [ - # functions - 'CreateLDIF', - 'LDIFCopy', - 'LDIFParser', - 'LDIFRecordList', - # classes - 'LDIFWriter', - 'ParseLDIF', - # constants - 'ldif_pattern', -] - import re import warnings from base64 import b64decode, b64encode @@ -39,6 +23,22 @@ ) +__version__ = '3.5.0' + +__all__ = [ + # functions + 'CreateLDIF', + 'LDIFCopy', + 'LDIFParser', + 'LDIFRecordList', + # classes + 'LDIFWriter', + 'ParseLDIF', + # constants + 'ldif_pattern', +] + + attrtype_pattern = r'[\w;.-]+(;[\w_-]+)*' attrvalue_pattern = r'(([^,]|\\,)+|".*?")' attrtypeandvalue_pattern = attrtype_pattern + r'[ ]*=[ ]*' + attrvalue_pattern From 2985b30a4bd9110cc0b4257d685b4aa5c69958d6 Mon Sep 17 00:00:00 2001 From: Florian Best Date: Sat, 17 Oct 2026 17:18:39 +0200 Subject: [PATCH 4/4] docs: add more docstrings Issue: #657 Issue: https://github.com/python-ldap/python-ldap/issues/657 --- Lib/ldap/cidict.py | 2 +- Lib/ldap/controls/__init__.py | 2 +- Lib/ldap/controls/sss.py | 18 ++++++++++++++---- Lib/ldap/extop/dds.py | 29 +++++++++++++++++++++++++++++ Lib/ldap/extop/passwd.py | 9 +++++++++ Lib/ldap/filter.py | 3 --- Lib/ldap/schema/tokenizer.py | 5 ++--- Lib/ldapurl.py | 9 +++++++++ Lib/ldif.py | 2 +- 9 files changed, 66 insertions(+), 13 deletions(-) diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index 25126244a..434864a2c 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -93,7 +93,7 @@ def has_key(self, key: str) -> bool: def data(self) -> dict[str, T]: """Compatibility with older IterableUserDict-based implementation""" warnings.warn( - 'ldap.cidict.cidict.data is an internal attribute; it may be ' + 'removed at any time', + 'ldap.cidict.cidict.data is an internal attribute; it may be removed at any time', category=DeprecationWarning, stacklevel=2, ) diff --git a/Lib/ldap/controls/__init__.py b/Lib/ldap/controls/__init__.py index be335307d..e1bfb978b 100644 --- a/Lib/ldap/controls/__init__.py +++ b/Lib/ldap/controls/__init__.py @@ -120,7 +120,7 @@ def RequestControlTuples( ) -> list[tuple[str | None, bool, bytes | None]] | None: """ Return list of readily encoded 3-tuples which can be directly - passed to C module _ldap + passed to C module ldap._ldap ldapControls sequence-type of RequestControl objects diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index f0d29e967..ce9174832 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -17,13 +17,16 @@ 'SSSRequestControl', 'SSSResponseControl', ] -# SortKeyList ::= SEQUENCE OF SEQUENCE { -# attributeType AttributeDescription, -# orderingRule [0] MatchingRuleId OPTIONAL, -# reverseOrder [1] BOOLEAN DEFAULT FALSE } class SortKeyType(univ.Sequence): + """ + SEQUENCE { + attributeType AttributeDescription, + orderingRule [0] MatchingRuleId OPTIONAL, + reverseOrder [1] BOOLEAN DEFAULT FALSE } + """ + componentType = namedtype.NamedTypes( namedtype.NamedType('attributeType', univ.OctetString()), namedtype.OptionalNamedType( @@ -42,6 +45,13 @@ class SortKeyType(univ.Sequence): class SortKeyListType(univ.SequenceOf): + """ + SortKeyList ::= SEQUENCE OF SEQUENCE { + attributeType AttributeDescription, + orderingRule [0] MatchingRuleId OPTIONAL, + reverseOrder [1] BOOLEAN DEFAULT FALSE } + """ + componentType = SortKeyType() # type: ignore[assignment] diff --git a/Lib/ldap/extop/dds.py b/Lib/ldap/extop/dds.py index 85a1dbe9a..c58b3e342 100644 --- a/Lib/ldap/extop/dds.py +++ b/Lib/ldap/extop/dds.py @@ -18,10 +18,26 @@ class RefreshRequest(ExtendedRequest): + """ + Refresh Request + (see https://tools.ietf.org/html/rfc2589#section-4.1) + """ + requestName = '1.3.6.1.4.1.1466.101.119.1' defaultRequestTtl = 86400 class RefreshRequestValue(univ.Sequence): + """ + ASN.1 definition:: + + SEQUENCE { + entryName [0] LDAPDN, + requestTtl [1] INTEGER + } + + (see https://tools.ietf.org/html/rfc2589#section-4.1) + """ + componentType = namedtype.NamedTypes( namedtype.NamedType( 'entryName', LDAPDN().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)) @@ -59,9 +75,22 @@ def encodedRequestValue(self) -> bytes: class RefreshResponse(ExtendedResponse): + """ + Refresh Response + (see https://tools.ietf.org/html/rfc2589#section-4.2) + """ + responseName = '1.3.6.1.4.1.1466.101.119.1' class RefreshResponseValue(univ.Sequence): + """ + ASN.1 definition:: + + SEQUENCE { + responseTtl [1] INTEGER + } + """ + componentType = namedtype.NamedTypes( namedtype.NamedType( 'responseTtl', diff --git a/Lib/ldap/extop/passwd.py b/Lib/ldap/extop/passwd.py index de7f062be..558fbae22 100644 --- a/Lib/ldap/extop/passwd.py +++ b/Lib/ldap/extop/passwd.py @@ -14,9 +14,18 @@ class PasswordModifyResponse(ExtendedResponse): + """ + Password Modify extended response + """ + responseName = None class PasswordModifyResponseValue(univ.Sequence): + """ + PasswdModifyResponseValue ::= SEQUENCE { + genPasswd [0] OCTET STRING OPTIONAL } + """ + componentType = namedtype.NamedTypes( namedtype.OptionalNamedType( 'genPasswd', diff --git a/Lib/ldap/filter.py b/Lib/ldap/filter.py index 8e1bb21b9..04c3fff97 100644 --- a/Lib/ldap/filter.py +++ b/Lib/ldap/filter.py @@ -2,9 +2,6 @@ filters.py - misc stuff for handling LDAP filter strings (see RFC2254) See https://www.python-ldap.org/ for details. - -Compatibility: -- Tested with Python 2.0+ """ from __future__ import annotations diff --git a/Lib/ldap/schema/tokenizer.py b/Lib/ldap/schema/tokenizer.py index 62e6326c6..827362b05 100644 --- a/Lib/ldap/schema/tokenizer.py +++ b/Lib/ldap/schema/tokenizer.py @@ -29,9 +29,8 @@ r"([^'$()\s]+)" # string of length >= 1 without '$() or whitespace r'|' # or r"('(?:[^'\\]|\\.)*'(?!\w))" - # any string or empty string surrounded by unescaped - # single quotes except if right quote is succeeded by - # alphanumeric char + # any string or empty string surrounded by unescaped single quotes + # except if right quote is succeeded by alphanumeric char r'|' # or r'([^\s]+?)', # residue, all non-whitespace strings ).findall diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index dbf1b91af..920ca1231 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -107,6 +107,9 @@ def _parse(self, extension: str) -> None: self.extype = self.extype.strip() def unparse(self) -> str: + """ + generate string representation of single LDAP URL extension + """ if self.exvalue is None: return '{}{}'.format('!' * (self.critical > 0), self.extype) else: @@ -183,6 +186,9 @@ def __eq__(self, other: object) -> bool: return self._data == other._data def parse(self, extListStr: str) -> None: + """ + parse string into list of LDAPURLExtension instances + """ for extension_str in extListStr.strip().split(','): if extension_str: ext = LDAPUrlExtension(extension_str) @@ -190,6 +196,9 @@ def parse(self, extListStr: str) -> None: self[ext.extype] = ext def unparse(self) -> str: + """ + return comma-separated string representation of LDAP URL extensions + """ return ','.join(val.unparse() for val in self.values()) diff --git a/Lib/ldif.py b/Lib/ldif.py index 528c56e57..3f5cbdb30 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -727,7 +727,7 @@ class LDIFRecordList(LDIFParser): Collect all records of a LDIF file. It can be a memory hog! Records are stored in :attr:`.all_records` as a single list - of 2-tuples (dn, entry), after calling :meth:`.parse`. + of 2-tuples (dn, entry), after calling :meth:`LDIFParser.parse`. """ def __init__(