Skip to content

Commit a3723bc

Browse files
tiranencukou
authored andcommitted
When raising LDAPBytesWarning, walk the stack to determine stacklevel
Closes: #108 Signed-off-by: Christian Heimes <cheimes@redhat.com> Some simplification by: Petr Viktorin
1 parent cf24a54 commit a3723bc

File tree

3 files changed

+89
-2
lines changed

3 files changed

+89
-2
lines changed

Lib/ldap/ldapobject.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
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
"""
@@ -126,11 +129,27 @@ def _bytesify_input(self, value):
126129
if self.bytes_mode_hardfail:
127130
raise TypeError("All provided fields *must* be bytes when bytes mode is on; got %r" % (value,))
128131
else:
132+
# Raise LDAPBytesWarning.
133+
# Call stacks with _bytesify_input tend to be complicated, so
134+
# getting a useful stacklevel is tricky.
135+
# We walk stack frames, ignoring all functions in this file
136+
# and in the _ldap extension, based on a marker in globals().
137+
stacklevel = 0
138+
try:
139+
getframe = sys._getframe
140+
except AttributeError:
141+
pass
142+
else:
143+
frame = sys._getframe(stacklevel)
144+
# walk up the stacks until we leave the file
145+
while frame and frame.f_globals.get('_LDAP_WARN_SKIP_FRAME'):
146+
stacklevel += 1
147+
frame = frame.f_back
129148
warnings.warn(
130149
"Received non-bytes value %r with default (disabled) bytes mode; please choose an explicit "
131150
"option for bytes_mode on your LDAP connection" % (value,),
132151
LDAPBytesWarning,
133-
stacklevel=6,
152+
stacklevel=stacklevel+1,
134153
)
135154
return value.encode('utf-8')
136155
else:

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 SimpleLDAPObject._bytesify_input 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: 62 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,64 @@ 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 _test_byteswarning_level_search(self, methodname):
365+
with self.catch_byteswarnings(explicit=False) as (conn, w):
366+
method = getattr(conn, methodname)
367+
result = method(
368+
self.server.suffix.encode('utf-8'),
369+
ldap.SCOPE_SUBTREE,
370+
'(cn=Foo*)',
371+
attrlist=['*'], # CORRECT LINE
372+
)
373+
self.assertEqual(len(result), 4)
374+
375+
self.assertEqual(len(w), 2, w)
376+
377+
def _normalize(filename):
378+
# Python 2 likes to report the ".pyc" file in warnings,
379+
# tracebacks or __file__.
380+
# Use the corresponding ".py" in that case.
381+
if filename.endswith('.pyc'):
382+
return filename[:-1]
383+
return filename
384+
385+
self.assertIs(w[0].category, ldap.LDAPBytesWarning)
386+
self.assertIn(
387+
u"Received non-bytes value u'(cn=Foo*)'",
388+
text_type(w[0].message)
389+
)
390+
self.assertEqual(_normalize(w[1].filename), _normalize(__file__))
391+
self.assertEqual(_normalize(w[0].filename), _normalize(__file__))
392+
self.assertIn(
393+
'CORRECT LINE',
394+
linecache.getline(w[0].filename, w[0].lineno)
395+
)
396+
397+
self.assertIs(w[1].category, ldap.LDAPBytesWarning)
398+
self.assertIn(
399+
u"Received non-bytes value u'*'",
400+
text_type(w[1].message)
401+
)
402+
self.assertIn(_normalize(w[1].filename), _normalize(__file__))
403+
self.assertIn(
404+
'CORRECT LINE',
405+
linecache.getline(w[1].filename, w[1].lineno)
406+
)
407+
408+
@unittest.skipUnless(PY2, "no bytes_mode under Py3")
409+
def test_byteswarning_level_search(self):
410+
self._test_byteswarning_level_search('search_s')
411+
self._test_byteswarning_level_search('search_st')
412+
self._test_byteswarning_level_search('search_ext_s')
413+
353414

354415
class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject):
355416
"""

0 commit comments

Comments
 (0)