Skip to content

Commit deb411c

Browse files
authored
Merge pull request #171 – Add bytes_strictness
#171
2 parents 671d9f1 + 2920ac2 commit deb411c

File tree

4 files changed

+177
-58
lines changed

4 files changed

+177
-58
lines changed

Doc/bytes_mode.rst

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,37 +43,51 @@ Encoding/decoding to other formats – text, images, etc. – is left to the cal
4343
The bytes mode
4444
--------------
4545

46-
The behavior of python-ldap 3.0 in Python 2 is influenced by a ``bytes_mode``
47-
argument to :func:`ldap.initialize`.
48-
The argument can take these values:
46+
In Python 3, text values are represented as ``str``, the Unicode text type.
4947

50-
``bytes_mode=True``: backwards-compatible
48+
In Python 2, the behavior of python-ldap 3.0 is influenced by a ``bytes_mode``
49+
argument to :func:`ldap.initialize`:
5150

52-
Text values returned from python-ldap are always bytes (``str``).
53-
Text values supplied to python-ldap may be either bytes or Unicode.
54-
The encoding for bytes is always assumed to be UTF-8.
51+
``bytes_mode=True`` (backwards compatible):
52+
Text values are represented as bytes (``str``) encoded using UTF-8.
5553

56-
Not available in Python 3.
54+
``bytes_mode=False`` (future compatible):
55+
Text values are represented as ``unicode``.
5756

58-
``bytes_mode=False``: strictly future-compatible
57+
If not given explicitly, python-ldap will default to ``bytes_mode=True``,
58+
but if an ``unicode`` value supplied to it, if will warn and use that value.
5959

60-
Text values must be represented as ``unicode``.
61-
An error is raised if python-ldap receives a text value as bytes (``str``).
60+
Backwards-compatible behavior is not scheduled for removal until Python 2
61+
itself reaches end of life.
6262

63-
Unspecified: relaxed mode with warnings
6463

65-
Causes a warning on Python 2.
64+
Errors, warnings, and automatic encoding
65+
----------------------------------------
6666

67-
Text values returned from python-ldap are always ``unicode``.
68-
Text values supplied to python-ldap should be ``unicode``;
69-
warnings are emitted when they are not.
67+
While the type of values *returned* from python-ldap is always given by
68+
``bytes_mode``, for Python 2 the behavior for “wrong-type” values *passed in*
69+
can be controlled by the ``bytes_strictness`` argument to
70+
:func:`ldap.initialize`:
7071

71-
The warnings are of type :class:`~ldap.LDAPBytesWarning`, which
72-
is a subclass of :class:`BytesWarning` designed to be easily
73-
:ref:`filtered out <filter-bytes-warning>` if needed.
72+
``bytes_strictness='error'`` (default if ``bytes_mode`` is specified):
73+
A ``TypeError`` is raised.
7474

75-
Backwards-compatible behavior is not scheduled for removal until Python 2
76-
itself reaches end of life.
75+
``bytes_strictness='warn'`` (default when ``bytes_mode`` is not given explicitly):
76+
A warning is raised, and the value is encoded/decoded
77+
using the UTF-8 encoding.
78+
79+
The warnings are of type :class:`~ldap.LDAPBytesWarning`, which
80+
is a subclass of :class:`BytesWarning` designed to be easily
81+
:ref:`filtered out <filter-bytes-warning>` if needed.
82+
83+
``bytes_strictness='silent'``:
84+
The value is automatically encoded/decoded using the UTF-8 encoding.
85+
86+
On Python 3, ``bytes_strictness`` is ignored and a ``TypeError`` is always
87+
raised.
88+
89+
When setting ``bytes_strictness``, an explicit value for ``bytes_mode`` needs
90+
to be given as well.
7791

7892

7993
Porting recommendations

Doc/reference/ldap.rst

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Functions
2929

3030
This module defines the following functions:
3131

32-
.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None]]]]) -> LDAPObject object
32+
.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None]]]]]) -> LDAPObject object
3333
3434
Initializes a new connection object for accessing the given LDAP server,
3535
and return an LDAP object (see :ref:`ldap-objects`) used to perform operations
@@ -53,7 +53,8 @@ This module defines the following functions:
5353
*trace_file* specifies a file-like object as target of the debug log and
5454
*trace_stack_limit* specifies the stack limit of tracebacks in debug log.
5555

56-
The *bytes_mode* argument specifies text/bytes behavior under Python 2.
56+
The *bytes_mode* and *bytes_strictness* arguments specify text/bytes
57+
behavior under Python 2.
5758
See :ref:`text-bytes` for a complete documentation.
5859

5960
Possible values for *trace_level* are
@@ -696,6 +697,9 @@ and wait for and return with the server's result, or with
696697

697698
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
698699

700+
The *dn* argument, and mod_type (second item) of *modlist* are text strings;
701+
see :ref:`bytes_mode`.
702+
699703

700704
.. py:method:: LDAPObject.bind(who, cred, method) -> int
701705
@@ -737,6 +741,8 @@ and wait for and return with the server's result, or with
737741

738742
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
739743

744+
The *dn* and *attr* arguments are text strings; see :ref:`bytes_mode`.
745+
740746
.. note::
741747

742748
A design fault in the LDAP API prevents *value*
@@ -757,6 +763,8 @@ and wait for and return with the server's result, or with
757763

758764
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
759765

766+
The *dn* argument is text string; see :ref:`bytes_mode`.
767+
760768

761769
.. py:method:: LDAPObject.extop(extreq[,serverctrls=None[,clientctrls=None]]]) -> int
762770
@@ -810,6 +818,9 @@ and wait for and return with the server's result, or with
810818
You might want to look into sub-module :py:mod:`ldap.modlist` for
811819
generating *modlist*.
812820

821+
The *dn* argument, and mod_type (second item) of *modlist* are text strings;
822+
see :ref:`bytes_mode`.
823+
813824

814825
.. py:method:: LDAPObject.modrdn(dn, newrdn [, delold=1]) -> int
815826
@@ -826,6 +837,8 @@ and wait for and return with the server's result, or with
826837
This operation is emulated by :py:meth:`rename()` and :py:meth:`rename_s()` methods
827838
since the modrdn2* routines in the C library are deprecated.
828839

840+
The *dn* and *newrdn* arguments are text strings; see :ref:`bytes_mode`.
841+
829842

830843
.. py:method:: LDAPObject.passwd(user, oldpw, newpw [, serverctrls=None [, clientctrls=None]]) -> int
831844
@@ -844,6 +857,8 @@ and wait for and return with the server's result, or with
844857

845858
The asynchronous version returns the initiated message id.
846859

860+
The *user*, *oldpw* and *newpw* arguments are text strings; see :ref:`bytes_mode`.
861+
847862
.. seealso::
848863

849864
:rfc:`3062` - LDAP Password Modify Extended Operation
@@ -865,6 +880,8 @@ and wait for and return with the server's result, or with
865880

866881
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
867882

883+
The *dn* and *newdn* arguments are text strings; see :ref:`bytes_mode`.
884+
868885

869886
.. py:method:: LDAPObject.result([msgid=RES_ANY [, all=1 [, timeout=None]]]) -> 2-tuple
870887
@@ -1015,12 +1032,13 @@ and wait for and return with the server's result, or with
10151032

10161033
*serverctrls* and *clientctrls* like described in section :ref:`ldap-controls`.
10171034

1035+
The *who* and *cred* arguments are text strings; see :ref:`bytes_mode`.
1036+
10181037
.. versionchanged:: 3.0
10191038

10201039
:meth:`~LDAPObject.simple_bind` and :meth:`~LDAPObject.simple_bind_s`
10211040
now accept ``None`` for *who* and *cred*, too.
10221041

1023-
10241042
.. py:method:: LDAPObject.search(base, scope [,filterstr='(objectClass=*)' [, attrlist=None [, attrsonly=0]]]) ->int
10251043
10261044
.. py:method:: LDAPObject.search_s(base, scope [,filterstr='(objectClass=*)' [, attrlist=None [, attrsonly=0]]]) ->list|None
@@ -1073,6 +1091,9 @@ and wait for and return with the server's result, or with
10731091
or :py:meth:`search_ext_s()` (client-side search limit). If non-zero
10741092
not more than *sizelimit* results are returned by the server.
10751093

1094+
The *base* and *filterstr* arguments, and *attrlist* contents,
1095+
are text strings; see :ref:`bytes_mode`.
1096+
10761097
.. versionchanged:: 3.0
10771098

10781099
``filterstr=None`` is equivalent to ``filterstr='(objectClass=*)'``.

Lib/ldap/ldapobject.py

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class SimpleLDAPObject:
9393

9494
def __init__(
9595
self,uri,
96-
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None
96+
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
97+
bytes_strictness=None,
9798
):
9899
self._trace_level = trace_level
99100
self._trace_file = trace_file or sys.stdout
@@ -107,20 +108,26 @@ def __init__(
107108
# Bytes mode
108109
# ----------
109110

110-
# By default, raise a TypeError when receiving invalid args
111-
self.bytes_mode_hardfail = True
112-
if bytes_mode is None and PY2:
113-
_raise_byteswarning(
114-
"Under Python 2, python-ldap uses bytes by default. "
115-
"This will be removed in Python 3 (no bytes for DN/RDN/field names). "
116-
"Please call initialize(..., bytes_mode=False) explicitly.")
117-
bytes_mode = True
118-
# Disable hard failure when running in backwards compatibility mode.
119-
self.bytes_mode_hardfail = False
120-
elif bytes_mode and not PY2:
121-
raise ValueError("bytes_mode is *not* supported under Python 3.")
122-
# On by default on Py2, off on Py3.
111+
if PY2:
112+
if bytes_mode is None:
113+
bytes_mode = True
114+
if bytes_strictness is None:
115+
_raise_byteswarning(
116+
"Under Python 2, python-ldap uses bytes by default. "
117+
"This will be removed in Python 3 (no bytes for "
118+
"DN/RDN/field names). "
119+
"Please call initialize(..., bytes_mode=False) explicitly.")
120+
bytes_strictness = 'warn'
121+
else:
122+
if bytes_strictness is None:
123+
bytes_strictness = 'error'
124+
else:
125+
if bytes_mode:
126+
raise ValueError("bytes_mode is *not* supported under Python 3.")
127+
bytes_mode = False
128+
bytes_strictness = 'error'
123129
self.bytes_mode = bytes_mode
130+
self.bytes_strictness = bytes_strictness
124131

125132
def _bytesify_input(self, arg_name, value):
126133
"""Adapt a value following bytes_mode in Python 2.
@@ -130,38 +137,46 @@ def _bytesify_input(self, arg_name, value):
130137
With bytes_mode ON, takes bytes or None and returns bytes or None.
131138
With bytes_mode OFF, takes unicode or None and returns bytes or None.
132139
133-
This function should be applied on all text inputs (distinguished names
134-
and attribute names in modlists) to convert them to the bytes expected
135-
by the C bindings.
140+
For the wrong argument type (unicode or bytes, respectively),
141+
behavior depends on the bytes_strictness setting.
142+
In all cases, bytes or None are returned (or an exception is raised).
136143
"""
137144
if not PY2:
138145
return value
139-
140146
if value is None:
141147
return value
148+
142149
elif self.bytes_mode:
143150
if isinstance(value, bytes):
144151
return value
152+
elif self.bytes_strictness == 'silent':
153+
pass
154+
elif self.bytes_strictness == 'warn':
155+
_raise_byteswarning(
156+
"Received non-bytes value for '{}' in bytes mode; "
157+
"please choose an explicit "
158+
"option for bytes_mode on your LDAP connection".format(arg_name))
145159
else:
146-
if self.bytes_mode_hardfail:
147160
raise TypeError(
148161
"All provided fields *must* be bytes when bytes mode is on; "
149162
"got type '{}' for '{}'.".format(type(value).__name__, arg_name)
150163
)
151-
else:
152-
_raise_byteswarning(
153-
"Received non-bytes value for '{}' with default (disabled) bytes mode; "
154-
"please choose an explicit "
155-
"option for bytes_mode on your LDAP connection".format(arg_name))
156-
return value.encode('utf-8')
164+
return value.encode('utf-8')
157165
else:
158-
if not isinstance(value, text_type):
166+
if isinstance(value, unicode):
167+
return value.encode('utf-8')
168+
elif self.bytes_strictness == 'silent':
169+
pass
170+
elif self.bytes_strictness == 'warn':
171+
_raise_byteswarning(
172+
"Received non-text value for '{}' with bytes_mode off and "
173+
"bytes_strictness='warn'".format(arg_name))
174+
else:
159175
raise TypeError(
160176
"All provided fields *must* be text when bytes mode is off; "
161177
"got type '{}' for '{}'.".format(type(value).__name__, arg_name)
162178
)
163-
assert not isinstance(value, bytes)
164-
return value.encode('utf-8')
179+
return value
165180

166181
def _bytesify_modlist(self, arg_name, modlist, with_opcode):
167182
"""Adapt a modlist according to bytes_mode.
@@ -1064,7 +1079,7 @@ class ReconnectLDAPObject(SimpleLDAPObject):
10641079
def __init__(
10651080
self,uri,
10661081
trace_level=0,trace_file=None,trace_stack_limit=5,bytes_mode=None,
1067-
retry_max=1,retry_delay=60.0
1082+
bytes_strictness=None, retry_max=1, retry_delay=60.0
10681083
):
10691084
"""
10701085
Parameters like SimpleLDAPObject.__init__() with these
@@ -1078,7 +1093,9 @@ def __init__(
10781093
self._uri = uri
10791094
self._options = []
10801095
self._last_bind = None
1081-
SimpleLDAPObject.__init__(self,uri,trace_level,trace_file,trace_stack_limit,bytes_mode)
1096+
SimpleLDAPObject.__init__(self, uri, trace_level, trace_file,
1097+
trace_stack_limit, bytes_mode,
1098+
bytes_strictness=bytes_strictness)
10821099
self._reconnect_lock = ldap.LDAPLock(desc='reconnect lock within %s' % (repr(self)))
10831100
self._retry_max = retry_max
10841101
self._retry_delay = retry_delay
@@ -1097,6 +1114,11 @@ def __getstate__(self):
10971114

10981115
def __setstate__(self,d):
10991116
"""set up the object from pickled data"""
1117+
hardfail = d.get('bytes_mode_hardfail')
1118+
if hardfail:
1119+
d.setdefault('bytes_strictness', 'error')
1120+
else:
1121+
d.setdefault('bytes_strictness', 'warn')
11001122
self.__dict__.update(d)
11011123
self._last_bind = getattr(SimpleLDAPObject, self._last_bind[0]), self._last_bind[1], self._last_bind[2]
11021124
self._ldap_object_lock = self._ldap_lock()

0 commit comments

Comments
 (0)