Skip to content

Commit 4bc1f58

Browse files
authored
Merge pull request python-ldap#128 – When raising LDAPBytesWarning, walk the stack to determine stacklevel
python-ldap#128
2 parents c1007fa + db98910 commit 4bc1f58

File tree

4 files changed

+108
-12
lines changed

4 files changed

+108
-12
lines changed

Lib/ldap/functions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
# Tracing is only supported in debugging mode
2828
import traceback
2929

30+
# See _raise_byteswarning in ldapobject.py
31+
_LDAP_WARN_SKIP_FRAME = True
32+
3033

3134
def _ldap_function_call(lock,func,*args,**kwargs):
3235
"""

Lib/ldap/ldapobject.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,33 @@
3939
text_type = str
4040

4141

42+
# See SimpleLDAPObject._bytesify_input
43+
_LDAP_WARN_SKIP_FRAME = True
44+
4245
class LDAPBytesWarning(BytesWarning):
4346
"""python-ldap bytes mode warning
4447
"""
4548

49+
def _raise_byteswarning(message):
50+
"""Raise LDAPBytesWarning
51+
"""
52+
53+
# Call stacks that raise the warning tend to be complicated, so
54+
# getting a useful stacklevel is tricky.
55+
# We walk stack frames, ignoring functions in uninteresting files,
56+
# based on the _LDAP_WARN_SKIP_FRAME marker in globals().
57+
stacklevel = 2
58+
try:
59+
getframe = sys._getframe
60+
except AttributeError:
61+
pass
62+
else:
63+
frame = sys._getframe(stacklevel)
64+
while frame and frame.f_globals.get('_LDAP_WARN_SKIP_FRAME'):
65+
stacklevel += 1
66+
frame = frame.f_back
67+
warnings.warn(message, LDAPBytesWarning, stacklevel=stacklevel+1)
68+
4669

4770
class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT):
4871
"""
@@ -87,13 +110,10 @@ def __init__(
87110
# By default, raise a TypeError when receiving invalid args
88111
self.bytes_mode_hardfail = True
89112
if bytes_mode is None and PY2:
90-
warnings.warn(
113+
_raise_byteswarning(
91114
"Under Python 2, python-ldap uses bytes by default. "
92115
"This will be removed in Python 3 (no bytes for DN/RDN/field names). "
93-
"Please call initialize(..., bytes_mode=False) explicitly.",
94-
LDAPBytesWarning,
95-
stacklevel=2,
96-
)
116+
"Please call initialize(..., bytes_mode=False) explicitly.")
97117
bytes_mode = True
98118
# Disable hard failure when running in backwards compatibility mode.
99119
self.bytes_mode_hardfail = False
@@ -126,12 +146,10 @@ def _bytesify_input(self, value):
126146
if self.bytes_mode_hardfail:
127147
raise TypeError("All provided fields *must* be bytes when bytes mode is on; got %r" % (value,))
128148
else:
129-
warnings.warn(
130-
"Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit "
131-
"option for bytes_mode on your LDAP connection" % (value,),
132-
LDAPBytesWarning,
133-
stacklevel=6,
134-
)
149+
_raise_byteswarning(
150+
"Received non-bytes value %r with default (disabled) bytes mode; "
151+
"please choose an explicit "
152+
"option for bytes_mode on your LDAP connection" % (value,))
135153
return value.encode('utf-8')
136154
else:
137155
if not isinstance(value, text_type):

Modules/ldapmodule.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ PyObject* init_ldap_module(void)
7272
LDAPinit_functions(d);
7373
LDAPinit_control(d);
7474

75+
/* Marker for LDAPBytesWarning stack walking
76+
* See _raise_byteswarning in ldapobject.py
77+
*/
78+
if (PyModule_AddIntConstant(m, "_LDAP_WARN_SKIP_FRAME", 1) != 0) {
79+
return NULL;
80+
}
81+
7582
/* Check for errors */
7683
if (PyErr_Occurred())
7784
Py_FatalError("can't initialize module _ldap");

Tests/t_ldapobject.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
PY2 = False
1717
text_type = str
1818

19+
import contextlib
20+
import linecache
1921
import os
2022
import unittest
23+
import warnings
2124
import pickle
2225
import warnings
2326
from slapdtest import SlapdTestCase, requires_sasl
@@ -329,7 +332,7 @@ def test_ldapbyteswarning(self):
329332
self.assertIsInstance(self.server.suffix, text_type)
330333
with warnings.catch_warnings(record=True) as w:
331334
warnings.resetwarnings()
332-
warnings.simplefilter('default')
335+
warnings.simplefilter('always', ldap.LDAPBytesWarning)
333336
conn = self._get_bytes_ldapobject(explicit=False)
334337
result = conn.search_s(
335338
self.server.suffix,
@@ -350,6 +353,71 @@ def test_ldapbyteswarning(self):
350353
"LDAP connection" % self.server.suffix
351354
)
352355

356+
@contextlib.contextmanager
357+
def catch_byteswarnings(self, *args, **kwargs):
358+
with warnings.catch_warnings(record=True) as w:
359+
conn = self._get_bytes_ldapobject(*args, **kwargs)
360+
warnings.resetwarnings()
361+
warnings.simplefilter('always', ldap.LDAPBytesWarning)
362+
yield conn, w
363+
364+
def _check_byteswarning(self, warning, expected_message):
365+
self.assertIs(warning.category, ldap.LDAPBytesWarning)
366+
self.assertIn(expected_message, text_type(warning.message))
367+
368+
def _normalize(filename):
369+
# Python 2 likes to report the ".pyc" file in warnings,
370+
# tracebacks or __file__.
371+
# Use the corresponding ".py" in that case.
372+
if filename.endswith('.pyc'):
373+
return filename[:-1]
374+
return filename
375+
376+
# Assert warning points to a line marked CORRECT LINE in this file
377+
self.assertEquals(_normalize(warning.filename), _normalize(__file__))
378+
self.assertIn(
379+
'CORRECT LINE',
380+
linecache.getline(warning.filename, warning.lineno)
381+
)
382+
383+
def _test_byteswarning_level_search(self, methodname):
384+
with self.catch_byteswarnings(explicit=False) as (conn, w):
385+
method = getattr(conn, methodname)
386+
result = method(
387+
self.server.suffix.encode('utf-8'),
388+
ldap.SCOPE_SUBTREE,
389+
'(cn=Foo*)',
390+
attrlist=['*'], # CORRECT LINE
391+
)
392+
self.assertEqual(len(result), 4)
393+
394+
self.assertEqual(len(w), 2, w)
395+
396+
self._check_byteswarning(
397+
w[0], u"Received non-bytes value u'(cn=Foo*)'")
398+
399+
self._check_byteswarning(
400+
w[1], u"Received non-bytes value u'*'")
401+
402+
@unittest.skipUnless(PY2, "no bytes_mode under Py3")
403+
def test_byteswarning_level_search(self):
404+
self._test_byteswarning_level_search('search_s')
405+
self._test_byteswarning_level_search('search_st')
406+
self._test_byteswarning_level_search('search_ext_s')
407+
408+
@unittest.skipUnless(PY2, "no bytes_mode under Py3")
409+
def test_byteswarning_initialize(self):
410+
with warnings.catch_warnings(record=True) as w:
411+
warnings.resetwarnings()
412+
warnings.simplefilter('always', ldap.LDAPBytesWarning)
413+
bytes_uri = self.server.ldap_uri.decode('utf-8')
414+
self.ldap_object_class(bytes_uri) # CORRECT LINE
415+
416+
self.assertEqual(len(w), 1, w)
417+
418+
self._check_byteswarning(
419+
w[0], u"Under Python 2, python-ldap uses bytes by default.")
420+
353421

354422
class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
355423
"""

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy