diff --git a/.travis.yml b/.travis.yml index 22224cab..021fa401 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,25 +14,14 @@ addons: # Note: when updating Python versions, also change setup.py and tox.ini matrix: include: - - python: 2.7 - env: - - TOXENV=py27 - - WITH_GCOV=1 - - python: 3.4 - env: - - TOXENV=py34 - - WITH_GCOV=1 - - python: 3.5 - env: - - TOXENV=py35 - - WITH_GCOV=1 - python: 3.6 env: - TOXENV=py36 - WITH_GCOV=1 - - python: pypy + - python: pypy3 env: - - TOXENV=pypy + - TOXENV=pypy3 + - CFLAGS_std="-std=c99" - python: 3.7 env: - TOXENV=py37 @@ -53,10 +42,6 @@ matrix: - WITH_GCOV=1 dist: xenial sudo: true - - python: 2.7 - env: - - TOXENV=py2-nosasltls - - WITH_GCOV=1 - python: 3.6 env: - TOXENV=py3-nosasltls @@ -68,7 +53,7 @@ matrix: env: TOXENV=doc allow_failures: - env: - - TOXENV=pypy + - TOXENV=pypy3 env: global: @@ -87,4 +72,3 @@ install: - pip install tox-travis tox codecov script: CFLAGS="$CFLAGS_warnings $CFLAGS_std" tox - diff --git a/Doc/bytes_mode.rst b/Doc/bytes_mode.rst index 0d207457..3a984bf0 100644 --- a/Doc/bytes_mode.rst +++ b/Doc/bytes_mode.rst @@ -1,34 +1,12 @@ .. _text-bytes: +.. _bytes_mode: Bytes/text management ===================== -Python 3 introduces a hard distinction between *text* (``str``) – sequences of -characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of -8-bit values used to encode *any* kind of data for storage or transmission. - -Python 2 has the same distinction between ``str`` (bytes) and -``unicode`` (text). -However, values can be implicitly converted between these types as needed, -e.g. when comparing or writing to disk or the network. -The implicit encoding and decoding can be a source of subtle bugs when not -designed and tested adequately. - -In python-ldap 2.x (for Python 2), bytes were used for all fields, -including those guaranteed to be text. - -From version 3.0, python-ldap uses text where appropriate. -On Python 2, the :ref:`bytes mode ` setting influences how text is -handled. - - -What's text, and what's bytes ------------------------------ - The LDAP protocol states that some fields (distinguished names, relative distinguished names, attribute names, queries) be encoded in UTF-8. -In python-ldap, these are represented as text (``str`` on Python 3, -``unicode`` on Python 2). +In python-ldap, these are represented as text (``str`` on Python 3). Attribute *values*, on the other hand, **MAY** contain any type of data, including text. @@ -38,102 +16,26 @@ Thus, attribute values are *always* treated as ``bytes``. Encoding/decoding to other formats – text, images, etc. – is left to the caller. -.. _bytes_mode: - -The bytes mode --------------- - -In Python 3, text values are represented as ``str``, the Unicode text type. - -In Python 2, the behavior of python-ldap 3.0 is influenced by a ``bytes_mode`` -argument to :func:`ldap.initialize`: - -``bytes_mode=True`` (backwards compatible): - Text values are represented as bytes (``str``) encoded using UTF-8. - -``bytes_mode=False`` (future compatible): - Text values are represented as ``unicode``. - -If not given explicitly, python-ldap will default to ``bytes_mode=True``, -but if a ``unicode`` value is supplied to it, it will warn and use that value. - -Backwards-compatible behavior is not scheduled for removal until Python 2 -itself reaches end of life. - - -Errors, warnings, and automatic encoding ----------------------------------------- - -While the type of values *returned* from python-ldap is always given by -``bytes_mode``, for Python 2 the behavior for “wrong-type” values *passed in* -can be controlled by the ``bytes_strictness`` argument to -:func:`ldap.initialize`: +Historical note +--------------- -``bytes_strictness='error'`` (default if ``bytes_mode`` is specified): - A ``TypeError`` is raised. - -``bytes_strictness='warn'`` (default when ``bytes_mode`` is not given explicitly): - A warning is raised, and the value is encoded/decoded - using the UTF-8 encoding. - - The warnings are of type :class:`~ldap.LDAPBytesWarning`, which - is a subclass of :class:`BytesWarning` designed to be easily - :ref:`filtered out ` if needed. - -``bytes_strictness='silent'``: - The value is automatically encoded/decoded using the UTF-8 encoding. - -On Python 3, ``bytes_strictness`` is ignored and a ``TypeError`` is always -raised. - -When setting ``bytes_strictness``, an explicit value for ``bytes_mode`` needs -to be given as well. - - -Porting recommendations ------------------------ - -Since end of life of Python 2 is coming in a few years, projects are strongly -urged to make their code compatible with Python 3. General instructions for -this are provided :ref:`in Python documentation ` and in the -`Conservative porting guide`_. - -.. _Conservative porting guide: https://portingguide.readthedocs.io/en/latest/ - - -When porting from python-ldap 2.x, users are advised to update their code -to set ``bytes_mode=False``, and fix any resulting failures. - -The typical usage is as follows. -Note that only the result's *values* are of the ``bytes`` type: - -.. code-block:: pycon - - >>> import ldap - >>> con = ldap.initialize('ldap://localhost:389', bytes_mode=False) - >>> con.simple_bind_s(u'login', u'secret_password') - >>> results = con.search_s(u'ou=people,dc=example,dc=org', ldap.SCOPE_SUBTREE, u"(cn=Raphaël)") - >>> results - [ - ("cn=Raphaël,ou=people,dc=example,dc=org", { - 'cn': [b'Rapha\xc3\xabl'], - 'sn': [b'Barrois'], - }), - ] - - -.. _filter-bytes-warning: - -Filtering warnings ------------------- +Python 3 introduced a hard distinction between *text* (``str``) – sequences of +characters (formally, *Unicode codepoints*) – and ``bytes`` – sequences of +8-bit values used to encode *any* kind of data for storage or transmission. -The bytes mode warnings can be filtered out and ignored with a -simple filter. +Python 2 had the same distinction between ``str`` (bytes) and +``unicode`` (text). +However, values could be implicitly converted between these types as needed, +e.g. when comparing or writing to disk or the network. +The implicit encoding and decoding can be a source of subtle bugs when not +designed and tested adequately. -.. code-block:: python +In python-ldap 2.x (for Python 2), bytes were used for all fields, +including those guaranteed to be text. - import warnings - import ldap +From version 3.0 to 3.3, python-ldap uses text where appropriate. +On Python 2, special ``bytes_mode`` and ``bytes_strictness`` settings +influenced how text was handled. - if hasattr(ldap, 'LDAPBytesWarning'): - warnings.simplefilter('ignore', ldap.LDAPBytesWarning) +From version 3.3 on, only Python 3 is supported. The “bytes mode” settings +are deprecated and do nothing. diff --git a/Doc/faq.rst b/Doc/faq.rst index 38a645fe..2152873b 100644 --- a/Doc/faq.rst +++ b/Doc/faq.rst @@ -29,7 +29,7 @@ Usage .. _pyldap: https://pypi.org/project/pyldap/ -**Q**: Does it work with Python 2.6? (1.5|2.0|2.1|2.2|2.3|2.4|2.5)? +**Q**: Does it work with Python 2.7? (1.5|2.0|2.1|2.2|2.3|2.4|2.5|2.6|2.7)? **A**: No. Old versions of python-ldap are still available from PyPI, though. diff --git a/Doc/reference/ldap.rst b/Doc/reference/ldap.rst index a6d69287..89c98064 100644 --- a/Doc/reference/ldap.rst +++ b/Doc/reference/ldap.rst @@ -29,7 +29,7 @@ Functions This module defines the following functions: -.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [bytes_mode=None, [bytes_strictness=None, [fileno=None]]]]]]) -> LDAPObject object +.. py:function:: initialize(uri [, trace_level=0 [, trace_file=sys.stdout [, trace_stack_limit=None, [fileno=None]]]]) -> LDAPObject object Initializes a new connection object for accessing the given LDAP server, and return an :class:`~ldap.ldapobject.LDAPObject` used to perform operations @@ -63,10 +63,6 @@ This module defines the following functions: *trace_file* specifies a file-like object as target of the debug log and *trace_stack_limit* specifies the stack limit of tracebacks in debug log. - The *bytes_mode* and *bytes_strictness* arguments specify text/bytes - behavior under Python 2. - See :ref:`text-bytes` for a complete documentation. - Possible values for *trace_level* are :py:const:`0` for no logging, :py:const:`1` for only logging the method calls with arguments, @@ -78,6 +74,10 @@ This module defines the following functions: Any additional keyword arguments are passed to ``LDAPObject``. It is also fine to instantiate a ``LDAPObject`` (or a subclass) directly. + The function additionally takes *bytes_mode* and *bytes_strictness* keyword + arguments, which are deprecated and ignored. See :ref:`bytes_mode` for + details. + .. seealso:: :rfc:`4516` - Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator @@ -86,6 +86,10 @@ This module defines the following functions: The *fileno* argument was added. + .. deprecated:: 3.4 + + *bytes_mode* and *bytes_strictness* arguments are deprecated. + .. py:function:: get_option(option) -> int|string @@ -730,12 +734,16 @@ Warnings .. py:class:: LDAPBytesWarning - Raised when bytes/text mismatch in non-strict bytes mode. + This warning is deprecated. python-ldap no longer raises it. - See :ref:`bytes_mode` for details. + It used to be raised under Python 2 when bytes/text mismatch in non-strict + bytes mode. See :ref:`bytes_mode` for details. .. versionadded:: 3.0.0 + .. versionchanged:: 3.4.0 + + Deprecated. .. _ldap-objects: diff --git a/Doc/reference/ldapurl.rst b/Doc/reference/ldapurl.rst index 96b5ed24..de86de7e 100644 --- a/Doc/reference/ldapurl.rst +++ b/Doc/reference/ldapurl.rst @@ -9,8 +9,7 @@ This module parses and generates LDAP URLs. It is implemented in pure Python and does not rely on any non-standard modules. Therefore it can be used stand- -alone without the rest of the python-ldap package. Compatibility note: This -module has been solely tested on Python 2.x and above. +alone without the rest of the python-ldap package. .. seealso:: diff --git a/Doc/sample_workflow.rst b/Doc/sample_workflow.rst index 8a43553d..60d60cac 100644 --- a/Doc/sample_workflow.rst +++ b/Doc/sample_workflow.rst @@ -31,8 +31,6 @@ python-ldap won't affect the rest of your system:: $ python3 -m venv __venv__ -(For Python 2, install `virtualenv`_ and use it instead of ``python3 -m venv``.) - .. _git: https://git-scm.com/ .. _virtualenv: https://virtualenv.pypa.io/en/stable/ diff --git a/Lib/ldap/cidict.py b/Lib/ldap/cidict.py index 48aeacb4..a5ac29e0 100644 --- a/Lib/ldap/cidict.py +++ b/Lib/ldap/cidict.py @@ -7,7 +7,7 @@ """ import warnings -from ldap.compat import MutableMapping +from collections.abc import MutableMapping from ldap import __version__ diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py index 901457b2..a287ce4e 100644 --- a/Lib/ldap/compat.py +++ b/Lib/ldap/compat.py @@ -1,115 +1,23 @@ """Compatibility wrappers for Py2/Py3.""" - -import sys -import os - -if sys.version_info[0] < 3: - from UserDict import UserDict, IterableUserDict - from urllib import quote - from urllib import quote_plus - from urllib import unquote as urllib_unquote - from urllib import urlopen - from urlparse import urlparse - from collections import MutableMapping - - def unquote(uri): - """Specialized unquote that uses UTF-8 for parsing.""" - uri = uri.encode('ascii') - unquoted = urllib_unquote(uri) - return unquoted.decode('utf-8') - - # Old-style of re-raising an exception is SyntaxError in Python 3, - # so hide behind exec() so the Python 3 parser doesn't see it - exec('''def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - raise exc_type, exc_value, exc_traceback - ''') - -else: - from collections import UserDict - IterableUserDict = UserDict - from urllib.parse import quote, quote_plus, unquote, urlparse - from urllib.request import urlopen - from collections.abc import MutableMapping - - def reraise(exc_type, exc_value, exc_traceback): - """Re-raise an exception given information from sys.exc_info() - - Note that unlike six.reraise, this does not support replacing the - traceback. All arguments must come from a single sys.exc_info() call. - """ - # In Python 3, all exception info is contained in one object. - raise exc_value - -try: - from shutil import which -except ImportError: - # shutil.which() from Python 3.6 - # "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, - # 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; - # All Rights Reserved" - def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. - - """ - # Check that a given file can be accessed with the correct mode. - # Additionally check that `file` is not a directory, as on Windows - # directories pass the os.access check. - def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) - and not os.path.isdir(fn)) - - # If we're given a path with a directory part, look it up directly rather - # than referring to PATH directories. This includes checking relative to the - # current directory, e.g. ./script - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - return None - - if path is None: - path = os.environ.get("PATH", os.defpath) - if not path: - return None - path = path.split(os.pathsep) - - if sys.platform == "win32": - # The current directory takes precedence on Windows. - if not os.curdir in path: - path.insert(0, os.curdir) - - # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) - # See if the given file matches any of the expected path extensions. - # This will allow us to short circuit when given "python.exe". - # If it does match, only test that one, otherwise we have to try - # others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] - else: - # On other platforms you don't have things like PATHEXT to tell you - # what file suffixes are executable, so just pass on cmd as-is. - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if not normdir in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name - return None +import warnings + +warnings.warn( + "The ldap.compat module is deprecated and will be removed in the future", + DeprecationWarning, +) + +from collections import UserDict +IterableUserDict = UserDict +from urllib.parse import quote, quote_plus, unquote, urlparse +from urllib.request import urlopen +from collections.abc import MutableMapping +from shutil import which + +def reraise(exc_type, exc_value, exc_traceback): + """Re-raise an exception given information from sys.exc_info() + + Note that unlike six.reraise, this does not support replacing the + traceback. All arguments must come from a single sys.exc_info() call. + """ + # In Python 3, all exception info is contained in one object. + raise exc_value diff --git a/Lib/ldap/controls/sss.py b/Lib/ldap/controls/sss.py index 5cdfbdac..5001987b 100644 --- a/Lib/ldap/controls/sss.py +++ b/Lib/ldap/controls/sss.py @@ -22,10 +22,6 @@ from pyasn1.type import univ, namedtype, tag, namedval, constraint from pyasn1.codec.ber import encoder, decoder -PY2 = sys.version_info[0] <= 2 -if not PY2: - basestring = str - # SortKeyList ::= SEQUENCE OF SEQUENCE { # attributeType AttributeDescription, @@ -63,7 +59,7 @@ def __init__( ): RequestControl.__init__(self,self.controlType,criticality) self.ordering_rules = ordering_rules - if isinstance(ordering_rules, basestring): + if isinstance(ordering_rules, str): ordering_rules = [ordering_rules] for rule in ordering_rules: rule = rule.split(':') diff --git a/Lib/ldap/dn.py b/Lib/ldap/dn.py index a066ac19..886ff877 100644 --- a/Lib/ldap/dn.py +++ b/Lib/ldap/dn.py @@ -3,8 +3,6 @@ See https://www.python-ldap.org/ for details. """ - -import sys from ldap.pkginfo import __version__ import _ldap @@ -47,8 +45,6 @@ def str2dn(dn,flags=0): """ if not dn: return [] - if sys.version_info[0] < 3 and isinstance(dn, unicode): - dn = dn.encode('utf-8') return ldap.functions._ldap_function_call(None,_ldap.str2dn,dn,flags) diff --git a/Lib/ldap/functions.py b/Lib/ldap/functions.py index 31ab00f7..8c9dc626 100644 --- a/Lib/ldap/functions.py +++ b/Lib/ldap/functions.py @@ -27,9 +27,6 @@ # Tracing is only supported in debugging mode import traceback -# See _raise_byteswarning in ldapobject.py -_LDAP_WARN_SKIP_FRAME = True - def _ldap_function_call(lock,func,*args,**kwargs): """ diff --git a/Lib/ldap/ldapobject.py b/Lib/ldap/ldapobject.py index b44d90cd..e29ee9d7 100644 --- a/Lib/ldap/ldapobject.py +++ b/Lib/ldap/ldapobject.py @@ -3,9 +3,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - from os import strerror from ldap.pkginfo import __version__, __author__, __license__ @@ -28,43 +25,19 @@ from ldap.schema import SCHEMA_ATTRS from ldap.controls import LDAPControl,DecodeControlTuples,RequestControlTuples from ldap.extop import ExtendedRequest,ExtendedResponse,PasswordModifyResponse -from ldap.compat import reraise from ldap import LDAPError -PY2 = sys.version_info[0] <= 2 -if PY2: - text_type = unicode -else: - text_type = str - - -# See SimpleLDAPObject._bytesify_input -_LDAP_WARN_SKIP_FRAME = True class LDAPBytesWarning(BytesWarning): - """python-ldap bytes mode warning - """ - -def _raise_byteswarning(message): - """Raise LDAPBytesWarning - """ + """Python 2 bytes mode warning""" - # Call stacks that raise the warning tend to be complicated, so - # getting a useful stacklevel is tricky. - # We walk stack frames, ignoring functions in uninteresting files, - # based on the _LDAP_WARN_SKIP_FRAME marker in globals(). - stacklevel = 2 - try: - getframe = sys._getframe - except AttributeError: - pass - else: - frame = sys._getframe(stacklevel) - while frame and frame.f_globals.get('_LDAP_WARN_SKIP_FRAME'): - stacklevel += 1 - frame = frame.f_back - warnings.warn(message, LDAPBytesWarning, stacklevel=stacklevel+1) + def __init__(self, *args, **kwargs): + warnings.warn( + "LDAPBytesWarning is deprecated and will be removed in the future", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) class NO_UNIQUE_ENTRY(ldap.NO_SUCH_OBJECT): @@ -114,185 +87,16 @@ def __init__( self.timeout = -1 self.protocol_version = ldap.VERSION3 - # Bytes mode - # ---------- - - if PY2: - if bytes_mode is None: - bytes_mode = True - if bytes_strictness is None: - _raise_byteswarning( - "Under Python 2, python-ldap uses bytes by default. " - "This will be removed in Python 3 (no bytes for " - "DN/RDN/field names). " - "Please call initialize(..., bytes_mode=False) explicitly.") - bytes_strictness = 'warn' - else: - if bytes_strictness is None: - bytes_strictness = 'error' - else: - if bytes_mode: - raise ValueError("bytes_mode is *not* supported under Python 3.") - bytes_mode = False - bytes_strictness = 'error' - self.bytes_mode = bytes_mode - self.bytes_strictness = bytes_strictness - - def _bytesify_input(self, arg_name, value): - """Adapt a value following bytes_mode in Python 2. - - In Python 3, returns the original value unmodified. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes unicode or None and returns bytes or None. - - For the wrong argument type (unicode or bytes, respectively), - behavior depends on the bytes_strictness setting. - In all cases, bytes or None are returned (or an exception is raised). - """ - if not PY2: - return value - if value is None: - return value - - elif self.bytes_mode: - if isinstance(value, bytes): - return value - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-bytes value for '{}' in bytes mode; " - "please choose an explicit " - "option for bytes_mode on your LDAP connection".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be bytes when bytes mode is on; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value.encode('utf-8') - else: - if isinstance(value, unicode): - return value.encode('utf-8') - elif self.bytes_strictness == 'silent': - pass - elif self.bytes_strictness == 'warn': - _raise_byteswarning( - "Received non-text value for '{}' with bytes_mode off and " - "bytes_strictness='warn'".format(arg_name)) - else: - raise TypeError( - "All provided fields *must* be text when bytes mode is off; " - "got type '{}' for '{}'.".format(type(value).__name__, arg_name) - ) - return value - - def _bytesify_modlist(self, arg_name, modlist, with_opcode): - """Adapt a modlist according to bytes_mode. - - A modlist is a tuple of (op, attr, value), where: - - With bytes_mode ON, attr is checked to be bytes - - With bytes_mode OFF, attr is converted from unicode to bytes - - value is *always* bytes - """ - if not PY2: - return modlist - if with_opcode: - return tuple( - (op, self._bytesify_input(arg_name, attr), val) - for op, attr, val in modlist - ) - else: - return tuple( - (self._bytesify_input(arg_name, attr), val) - for attr, val in modlist - ) - - def _unbytesify_text_value(self, value): - """Adapt a 'known text, UTF-8 encoded' returned value following bytes_mode. - - With bytes_mode ON, takes bytes or None and returns bytes or None. - With bytes_mode OFF, takes bytes or None and returns unicode or None. - - This function should only be applied on field *values*; distinguished names - or field *names* are already natively handled in result4. - """ - if value is None: - return value - - # Preserve logic of assertions only under Python 2 - if PY2: - assert isinstance(value, bytes), "Expected bytes value, got text instead (%r)" % (value,) - - if self.bytes_mode: - return value - else: - return value.decode('utf-8') - - def _maybe_rebytesify_text(self, value): - """Re-encodes text to bytes if needed by bytes_mode. + if bytes_mode: + raise ValueError("bytes_mode is *not* supported under Python 3.") - Takes unicode (and checks for it), and returns: - - bytes under bytes_mode - - unicode otherwise. - """ - if not PY2: - return value + @property + def bytes_mode(self): + return False - if value is None: - return value - - assert isinstance(value, text_type), "Should return text, got bytes instead (%r)" % (value,) - if not self.bytes_mode: - return value - else: - return value.encode('utf-8') - - def _bytesify_result_value(self, result_value): - """Applies bytes_mode to a result value. - - Such a value can either be: - - a dict mapping an attribute name to its list of values - (where attribute names are unicode and values bytes) - - a list of referals (which are unicode) - """ - if not PY2: - return result_value - if hasattr(result_value, 'items'): - # It's a attribute_name: [values] dict - return { - self._maybe_rebytesify_text(key): value - for (key, value) in result_value.items() - } - elif isinstance(result_value, bytes): - return result_value - else: - # It's a list of referals - # Example value: - # [u'ldap://DomainDnsZones.xxxx.root.local/DC=DomainDnsZones,DC=xxxx,DC=root,DC=local'] - return [self._maybe_rebytesify_text(referal) for referal in result_value] - - def _bytesify_results(self, results, with_ctrls=False): - """Converts a "results" object according to bytes_mode. - - Takes: - - a list of (dn, {field: [values]}) if with_ctrls is False - - a list of (dn, {field: [values]}, ctrls) if with_ctrls is True - - And, if bytes_mode is on, converts dn and fields to bytes. - """ - if not PY2: - return results - if with_ctrls: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields), ctrls) - for (dn, fields, ctrls) in results - ] - else: - return [ - (self._maybe_rebytesify_text(dn), self._bytesify_result_value(fields)) - for (dn, fields) in results - ] + @property + def bytes_strictness(self): + return 'error' def _ldap_lock(self,desc=''): if ldap.LIBLDAP_R: @@ -326,7 +130,6 @@ def _ldap_call(self,func,*args,**kwargs): finally: self._ldap_object_lock.release() except LDAPError as e: - exc_type,exc_value,exc_traceback = sys.exc_info() try: if 'info' not in e.args[0] and 'errno' in e.args[0]: e.args[0]['info'] = strerror(e.args[0]['errno']) @@ -334,10 +137,7 @@ def _ldap_call(self,func,*args,**kwargs): pass if __debug__ and self._trace_level>=2: self._trace_file.write('=> LDAPError - %s: %s\n' % (e.__class__.__name__,str(e))) - try: - reraise(exc_type, exc_value, exc_traceback) - finally: - exc_type = exc_value = exc_traceback = None + raise else: if __debug__ and self._trace_level>=2: if not diagnostic_message_success is None: @@ -413,9 +213,6 @@ def add_ext(self,dn,modlist,serverctrls=None,clientctrls=None): The parameter modlist is similar to the one passed to modify(), except that no operation integer need be included in the tuples. """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=False) return self._ldap_call(self._l.add_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def add_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -440,9 +237,6 @@ def simple_bind(self,who=None,cred=None,serverctrls=None,clientctrls=None): """ simple_bind([who='' [,cred='']]) -> int """ - if PY2: - who = self._bytesify_input('who', who) - cred = self._bytesify_input('cred', cred) return self._ldap_call(self._l.simple_bind,who,cred,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def simple_bind_s(self,who=None,cred=None,serverctrls=None,clientctrls=None): @@ -519,9 +313,6 @@ def compare_ext(self,dn,attr,value,serverctrls=None,clientctrls=None): A design bug in the library prevents value from containing nul characters. """ - if PY2: - dn = self._bytesify_input('dn', dn) - attr = self._bytesify_input('attr', attr) return self._ldap_call(self._l.compare_ext,dn,attr,value,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def compare_ext_s(self,dn,attr,value,serverctrls=None,clientctrls=None): @@ -552,7 +343,6 @@ def delete_ext(self,dn,serverctrls=None,clientctrls=None): form returns the message id of the initiated request, and the result can be obtained from a subsequent call to result(). """ - dn = self._bytesify_input('dn', dn) return self._ldap_call(self._l.delete_ext,dn,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def delete_ext_s(self,dn,serverctrls=None,clientctrls=None): @@ -601,9 +391,6 @@ def modify_ext(self,dn,modlist,serverctrls=None,clientctrls=None): """ modify_ext(dn, modlist[,serverctrls=None[,clientctrls=None]]) -> int """ - if PY2: - dn = self._bytesify_input('dn', dn) - modlist = self._bytesify_modlist('modlist', modlist, with_opcode=True) return self._ldap_call(self._l.modify_ext,dn,modlist,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def modify_ext_s(self,dn,modlist,serverctrls=None,clientctrls=None): @@ -657,10 +444,6 @@ def modrdn_s(self,dn,newrdn,delold=1): return self.rename_s(dn,newrdn,None,delold) def passwd(self,user,oldpw,newpw,serverctrls=None,clientctrls=None): - if PY2: - user = self._bytesify_input('user', user) - oldpw = self._bytesify_input('oldpw', oldpw) - newpw = self._bytesify_input('newpw', newpw) return self._ldap_call(self._l.passwd,user,oldpw,newpw,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def passwd_s(self, user, oldpw, newpw, serverctrls=None, clientctrls=None, extract_newpw=False): @@ -689,10 +472,6 @@ def rename(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls This actually corresponds to the rename* routines in the LDAP-EXT C API library. """ - if PY2: - dn = self._bytesify_input('dn', dn) - newrdn = self._bytesify_input('newrdn', newrdn) - newsuperior = self._bytesify_input('newsuperior', newsuperior) return self._ldap_call(self._l.rename,dn,newrdn,newsuperior,delold,RequestControlTuples(serverctrls),RequestControlTuples(clientctrls)) def rename_s(self,dn,newrdn,newsuperior=None,delold=1,serverctrls=None,clientctrls=None): @@ -781,8 +560,6 @@ def result4(self,msgid=ldap.RES_ANY,all=1,timeout=None,add_ctrls=0,add_intermedi if add_ctrls: resp_data = [ (t,r,DecodeControlTuples(c,resp_ctrl_classes)) for t,r,c in resp_data ] decoded_resp_ctrls = DecodeControlTuples(resp_ctrls,resp_ctrl_classes) - if resp_data is not None: - resp_data = self._bytesify_results(resp_data, with_ctrls=add_ctrls) return resp_type, resp_data, resp_msgid, decoded_resp_ctrls, resp_name, resp_value def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverctrls=None,clientctrls=None,timeout=-1,sizelimit=0): @@ -830,24 +607,8 @@ def search_ext(self,base,scope,filterstr=None,attrlist=None,attrsonly=0,serverct The amount of search results retrieved can be limited with the sizelimit parameter if non-zero. """ - - if PY2: - base = self._bytesify_input('base', base) - if filterstr is None: - # workaround for default argument, - # see https://github.com/python-ldap/python-ldap/issues/147 - if self.bytes_mode: - filterstr = b'(objectClass=*)' - else: - filterstr = u'(objectClass=*)' - else: - filterstr = self._bytesify_input('filterstr', filterstr) - if attrlist is not None: - attrlist = tuple(self._bytesify_input('attrlist', a) - for a in attrlist) - else: - if filterstr is None: - filterstr = '(objectClass=*)' + if filterstr is None: + filterstr = '(objectClass=*)' return self._ldap_call( self._l.search_ext, base,scope,filterstr, @@ -944,12 +705,8 @@ def search_subschemasubentry_s(self,dn=None): Returns: None or text/bytes depending on bytes_mode. """ - if self.bytes_mode: - empty_dn = b'' - attrname = b'subschemaSubentry' - else: - empty_dn = u'' - attrname = u'subschemaSubentry' + empty_dn = u'' + attrname = u'subschemaSubentry' if dn is None: dn = empty_dn try: @@ -972,9 +729,8 @@ def search_subschemasubentry_s(self,dn=None): # If dn was already root DSE we can return here return None else: - # With legacy bytes mode, return bytes; otherwise, since this is a DN, - # RFCs impose that the field value *can* be decoded to UTF-8. - return self._unbytesify_text_value(search_subschemasubentry_dn) + if search_subschemasubentry_dn is not None: + return search_subschemasubentry_dn.decode('utf-8') except IndexError: return None @@ -1002,14 +758,9 @@ def read_subschemasubentry_s(self,subschemasubentry_dn,attrs=None): """ Returns the sub schema sub entry's data """ - if self.bytes_mode: - filterstr = b'(objectClass=subschema)' - if attrs is None: - attrs = [attr.encode('utf-8') for attr in SCHEMA_ATTRS] - else: - filterstr = u'(objectClass=subschema)' - if attrs is None: - attrs = SCHEMA_ATTRS + filterstr = u'(objectClass=subschema)' + if attrs is None: + attrs = SCHEMA_ATTRS try: subschemasubentry = self.read_s( subschemasubentry_dn, @@ -1044,12 +795,8 @@ def read_rootdse_s(self, filterstr=None, attrlist=None): """ convenience wrapper around read_s() for reading rootDSE """ - if self.bytes_mode: - base = b'' - attrlist = attrlist or [b'*', b'+'] - else: - base = u'' - attrlist = attrlist or [u'*', u'+'] + base = u'' + attrlist = attrlist or [u'*', u'+'] ldap_rootdse = self.read_s( base, filterstr=filterstr, @@ -1062,10 +809,7 @@ def get_naming_contexts(self): returns all attribute values of namingContexts in rootDSE if namingContexts is not present (not readable) then empty list is returned """ - if self.bytes_mode: - name = b'namingContexts' - else: - name = u'namingContexts' + name = u'namingContexts' return self.read_rootdse_s( attrlist=[name] ).get(name, []) diff --git a/Lib/ldap/schema/models.py b/Lib/ldap/schema/models.py index 7520b2b1..0ebd61e7 100644 --- a/Lib/ldap/schema/models.py +++ b/Lib/ldap/schema/models.py @@ -7,7 +7,7 @@ import sys import ldap.cidict -from ldap.compat import IterableUserDict +from collections import UserDict as IterableUserDict from ldap.schema.tokenizer import split_tokens,extract_tokens @@ -47,7 +47,7 @@ class SchemaElement: } def __init__(self,schema_element_str=None): - if sys.version_info >= (3, 0) and isinstance(schema_element_str, bytes): + if isinstance(schema_element_str, bytes): schema_element_str = schema_element_str.decode('utf-8') if schema_element_str: l = split_tokens(schema_element_str) diff --git a/Lib/ldap/schema/subentry.py b/Lib/ldap/schema/subentry.py index 215f148e..86e996f0 100644 --- a/Lib/ldap/schema/subentry.py +++ b/Lib/ldap/schema/subentry.py @@ -5,10 +5,9 @@ """ import copy +from urllib.request import urlopen import ldap.cidict,ldap.schema - -from ldap.compat import urlopen from ldap.schema.models import * import ldapurl diff --git a/Lib/ldapurl.py b/Lib/ldapurl.py index 7a0017c6..0e03fcc2 100644 --- a/Lib/ldapurl.py +++ b/Lib/ldapurl.py @@ -16,7 +16,8 @@ 'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl' ] -from ldap.compat import quote, unquote, MutableMapping +from collections.abc import MutableMapping +from urllib.parse import quote, unquote LDAP_SCOPE_BASE = 0 LDAP_SCOPE_ONELEVEL = 1 diff --git a/Lib/ldif.py b/Lib/ldif.py index f07f42dd..0afebd84 100644 --- a/Lib/ldif.py +++ b/Lib/ldif.py @@ -3,9 +3,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - __version__ = '3.3.0' __all__ = [ @@ -25,7 +22,8 @@ from io import StringIO import warnings -from ldap.compat import urlparse, urlopen +from urllib.parse import urlparse +from urllib.request import urlopen attrtype_pattern = r'[\w;.-]+(;[\w_-]+)*' attrvalue_pattern = r'(([^,]|\\,)+|".*?")' diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index de4c3e53..cebd0df1 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -4,9 +4,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import socket import sys @@ -16,12 +13,13 @@ import atexit from logging.handlers import SysLogHandler import unittest +from shutil import which +from urllib.parse import quote_plus # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' import ldap -from ldap.compat import quote_plus, which HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/Modules/LDAPObject.h b/Modules/LDAPObject.h index 1b6066db..4af0b382 100644 --- a/Modules/LDAPObject.h +++ b/Modules/LDAPObject.h @@ -5,15 +5,9 @@ #include "common.h" -#if PYTHON_API_VERSION < 1007 -typedef PyObject *_threadstate; -#else -typedef PyThreadState *_threadstate; -#endif - typedef struct { PyObject_HEAD LDAP *ldap; - _threadstate _save; /* for thread saving on referrals */ + PyThreadState *_save; /* for thread saving on referrals */ int valid; } LDAPObject; @@ -36,7 +30,7 @@ extern LDAPObject *newLDAPObject(LDAP *); #define LDAP_END_ALLOW_THREADS( l ) \ { \ LDAPObject *lo = (l); \ - _threadstate _save = lo->_save; \ + PyThreadState *_save = lo->_save; \ lo->_save = NULL; \ PyEval_RestoreThread( _save ); \ } diff --git a/Modules/ldapmodule.c b/Modules/ldapmodule.c index 8bd55ab4..34d5a24c 100644 --- a/Modules/ldapmodule.c +++ b/Modules/ldapmodule.c @@ -72,13 +72,6 @@ init_ldap_module(void) LDAPinit_functions(d); LDAPinit_control(d); - /* Marker for LDAPBytesWarning stack walking - * See _raise_byteswarning in ldapobject.py - */ - if (PyModule_AddIntConstant(m, "_LDAP_WARN_SKIP_FRAME", 1) != 0) { - return NULL; - } - /* Check for errors */ if (PyErr_Occurred()) Py_FatalError("can't initialize module _ldap"); diff --git a/Tests/t_bind.py b/Tests/t_bind.py index 3e2b67f8..ba90c4cd 100644 --- a/Tests/t_bind.py +++ b/Tests/t_bind.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest @@ -44,19 +33,6 @@ def test_unicode_bind(self): l = self._get_ldapobject(False) l.simple_bind("CN=user", self.unicode_val) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unicode_bind_bytesmode(self): - l = self._get_ldapobject(True) - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_unicode, self.unicode_val_bytes) - - with self.assertRaises(TypeError): - l.simple_bind_s(self.dn_bytes, self.unicode_val) - - # Works when encoded to UTF-8 - with self.assertRaises(ldap.INVALID_CREDENTIALS): - l.simple_bind_s(self.dn_bytes, self.unicode_val_bytes) - def test_unicode_bind_no_bytesmode(self): l = self._get_ldapobject(False) with self.assertRaises(TypeError): diff --git a/Tests/t_cext.py b/Tests/t_cext.py index a19d3c33..2fa4f56c 100644 --- a/Tests/t_cext.py +++ b/Tests/t_cext.py @@ -4,9 +4,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import contextlib import errno import os diff --git a/Tests/t_edit.py b/Tests/t_edit.py index a5b3f657..f79ff18f 100644 --- a/Tests/t_edit.py +++ b/Tests/t_edit.py @@ -1,14 +1,3 @@ -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import os import unittest diff --git a/Tests/t_ldap_dn.py b/Tests/t_ldap_dn.py index fd36f866..d62ec719 100644 --- a/Tests/t_ldap_dn.py +++ b/Tests/t_ldap_dn.py @@ -4,9 +4,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - # from Python's standard lib import os import unittest diff --git a/Tests/t_ldap_syncrepl.py b/Tests/t_ldap_syncrepl.py index b8a6ab63..7ec97075 100644 --- a/Tests/t_ldap_syncrepl.py +++ b/Tests/t_ldap_syncrepl.py @@ -4,19 +4,11 @@ See https://www.python-ldap.org/ for details. """ - - import os import shelve -import sys import unittest import binascii -if sys.version_info[0] <= 2: - PY2 = True -else: - PY2 = False - # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' @@ -434,18 +426,6 @@ def setUp(self): self.suffix = self.server.suffix -@unittest.skipUnless(PY2, "no bytes_mode under Py3") -class TestSyncreplBytesMode(BaseSyncreplTests, SlapdTestCase): - def setUp(self): - super(TestSyncreplBytesMode, self).setUp() - self.tester = SyncreplClient( - self.server.ldap_uri, - self.server.root_dn.encode('utf-8'), - self.server.root_pw.encode('utf-8'), - bytes_mode=True - ) - self.suffix = self.server.suffix.encode('utf-8') - class DecodeSyncreplProtoTests(unittest.TestCase): """ Tests of the ASN.1 decoder for tricky cases or past issues to ensure that diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 459ba768..75da0f43 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -4,25 +4,11 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - -import sys - -if sys.version_info[0] <= 2: - PY2 = True - text_type = unicode -else: - PY2 = False - text_type = str - import errno -import contextlib import linecache import os import socket import unittest -import warnings import pickle # Switch off processing .ldaprc or ldap.conf before importing _ldap @@ -115,44 +101,29 @@ def test_reject_bytes_base(self): l.search_s( base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', ['*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'base'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - # Python 3.4.x does not include 'search_ext()' in message - self.assertEqual( - "search_ext() argument 1 must be str, not bytes", - text_type(e.exception) - ) + # Python 3.4.x does not include 'search_ext()' in message + self.assertEqual( + "search_ext() argument 1 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'filterstr'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - "search_ext() argument 3 must be str, not bytes", - text_type(e.exception) - ) + self.assertEqual( + "search_ext() argument 3 must be str, not bytes", + str(e.exception) + ) with self.assertRaises(TypeError) as e: l.search_s( base, ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*'] ) - if PY2: - self.assertIn( - u"got type 'str' for 'attrlist'", text_type(e.exception) - ) - elif sys.version_info >= (3, 5, 0): - self.assertEqual( - ('attrs_from_List(): expected string in list', b'*'), - e.exception.args - ) + self.assertEqual( + ('attrs_from_List(): expected string in list', b'*'), + e.exception.args + ) def test_search_keys_are_text(self): base = self.server.suffix @@ -161,143 +132,12 @@ def test_search_keys_are_text(self): result.sort() dn, fields = result[0] self.assertEqual(dn, 'cn=Foo1,%s' % base) - self.assertEqual(type(dn), text_type) + self.assertEqual(type(dn), str) for key, values in fields.items(): - self.assertEqual(type(key), text_type) + self.assertEqual(type(key), str) for value in values: self.assertEqual(type(value), bytes) - def _get_bytes_ldapobject(self, explicit=True, **kwargs): - if explicit: - kwargs.setdefault('bytes_mode', True) - else: - kwargs = {} - return self._open_ldap_conn( - who=self.server.root_dn.encode('utf-8'), - cred=self.server.root_pw.encode('utf-8'), - **kwargs - ) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_requires_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - with self.assertRaises(TypeError): - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - with self.assertRaises(TypeError): - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_results_have_bytes(self): - l = self._get_bytes_ldapobject() - base = self.server.suffix - result = l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - result.sort() - dn, fields = result[0] - self.assertEqual(dn, b'cn=Foo1,%s' % base) - self.assertEqual(type(dn), bytes) - for key, values in fields.items(): - self.assertEqual(type(key), bytes) - for value in values: - self.assertEqual(type(value), bytes) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_search_defaults(self): - l = self._get_bytes_ldapobject() - base = 'cn=Foo1,' + self.server.suffix - kwargs = dict( - base=base.encode('utf-8'), - scope=ldap.SCOPE_SUBTREE, - # filterstr=b'(objectClass=*)' - ) - expected = [ - ( - base, - {'cn': [b'Foo1'], 'objectClass': [b'organizationalRole']} - ), - ] - - result = l.search_s(**kwargs) - self.assertEqual(result, expected) - result = l.search_st(**kwargs) - self.assertEqual(result, expected) - result = l.search_ext_s(**kwargs) - self.assertEqual(result, expected) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_unset_bytesmode_search_warns_bytes(self): - l = self._get_bytes_ldapobject(explicit=False) - base = self.server.suffix - - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, '(cn=Foo*)', [b'*']) - l.search_s(base.encode('utf-8'), ldap.SCOPE_SUBTREE, b'(cn=Foo*)', ['*']) - l.search_s(base, ldap.SCOPE_SUBTREE, b'(cn=Foo*)', [b'*']) - - def _search_wrong_type(self, bytes_mode, strictness): - if bytes_mode: - l = self._get_bytes_ldapobject(bytes_strictness=strictness) - else: - l = self._open_ldap_conn(bytes_mode=False, - bytes_strictness=strictness) - base = 'cn=Foo1,' + self.server.suffix - if not bytes_mode: - base = base.encode('utf-8') - result = l.search_s(base, scope=ldap.SCOPE_SUBTREE) - return result[0][-1]['cn'] - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_bytesmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_silent(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='silent') - self.assertEqual(w, []) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_warn(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - self._search_wrong_type(bytes_mode=True, strictness='warn') - self.assertEqual(len(w), 1) - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_textmode_error(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - with self.assertRaises(TypeError): - self._search_wrong_type(bytes_mode=True, strictness='error') - self.assertEqual(w, []) - def test_search_accepts_unicode_dn(self): base = self.server.suffix l = self._ldap_conn @@ -319,7 +159,7 @@ def test_attrlist_accepts_unicode(self): result.sort() for dn, attrs in result: - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(attrs, {}) def test001_search_subtree(self): @@ -422,7 +262,7 @@ def test_find_unique_entry(self): def test_search_subschema(self): l = self._ldap_conn dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, text_type) + self.assertIsInstance(dn, str) self.assertEqual(dn, "cn=Subschema") subschema = l.read_subschemasubentry_s(dn) self.assertIsInstance(subschema, dict) @@ -437,25 +277,6 @@ def test_search_subschema(self): ] ) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_search_subschema_have_bytes(self): - l = self._get_bytes_ldapobject() - dn = l.search_subschemasubentry_s() - self.assertIsInstance(dn, bytes) - self.assertEqual(dn, b"cn=Subschema") - subschema = l.read_subschemasubentry_s(dn) - self.assertIsInstance(subschema, dict) - self.assertEqual( - sorted(subschema), - [ - b'attributeTypes', - b'ldapSyntaxes', - b'matchingRuleUse', - b'matchingRules', - b'objectClasses' - ] - ) - def test004_enotconn(self): l = self.ldap_object_class('ldap://127.0.0.1:42') try: @@ -516,45 +337,9 @@ def test_simple_bind_noarg(self): l.simple_bind_s(None, None) self.assertEqual(l.whoami_s(), u'') - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_ldapbyteswarning(self): - self.assertIsSubclass(ldap.LDAPBytesWarning, BytesWarning) - self.assertIsSubclass(ldap.LDAPBytesWarning, Warning) - self.assertIsInstance(self.server.suffix, text_type) - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - conn = self._get_bytes_ldapobject(explicit=False) - result = conn.search_s( - self.server.suffix, - ldap.SCOPE_SUBTREE, - b'(cn=Foo*)', - attrlist=[b'*'], - ) - self.assertEqual(len(result), 4) - - # ReconnectLDAP only emits one warning - self.assertGreaterEqual(len(w), 1, w) - msg = w[-1] - self.assertIs(msg.category, ldap.LDAPBytesWarning) - self.assertEqual( - text_type(msg.message), - "Received non-bytes value for 'base' in bytes " - "mode; please choose an explicit option for bytes_mode on your " - "LDAP connection" - ) - - @contextlib.contextmanager - def catch_byteswarnings(self, *args, **kwargs): - with warnings.catch_warnings(record=True) as w: - conn = self._get_bytes_ldapobject(*args, **kwargs) - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - yield conn, w - def _check_byteswarning(self, warning, expected_message): self.assertIs(warning.category, ldap.LDAPBytesWarning) - self.assertIn(expected_message, text_type(warning.message)) + self.assertIn(expected_message, str(warning.message)) def _normalize(filename): # Python 2 likes to report the ".pyc" file in warnings, @@ -571,44 +356,6 @@ def _normalize(filename): linecache.getline(warning.filename, warning.lineno) ) - def _test_byteswarning_level_search(self, methodname): - with self.catch_byteswarnings(explicit=False) as (conn, w): - method = getattr(conn, methodname) - result = method( - self.server.suffix.encode('utf-8'), - ldap.SCOPE_SUBTREE, - '(cn=Foo*)', - attrlist=['*'], # CORRECT LINE - ) - self.assertEqual(len(result), 4) - - self.assertEqual(len(w), 2, w) - - self._check_byteswarning( - w[0], u"Received non-bytes value for 'filterstr'") - - self._check_byteswarning( - w[1], u"Received non-bytes value for 'attrlist'") - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_level_search(self): - self._test_byteswarning_level_search('search_s') - self._test_byteswarning_level_search('search_st') - self._test_byteswarning_level_search('search_ext_s') - - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_byteswarning_initialize(self): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - warnings.simplefilter('always', ldap.LDAPBytesWarning) - bytes_uri = self.server.ldap_uri.decode('utf-8') - self.ldap_object_class(bytes_uri) # CORRECT LINE - - self.assertEqual(len(w), 1, w) - - self._check_byteswarning( - w[0], u"Under Python 2, python-ldap uses bytes by default.") - @requires_tls() def test_multiple_starttls(self): # Test for openldap does not re-register nss shutdown callbacks @@ -642,17 +389,6 @@ def test_dse(self): [self.server.suffix.encode('utf-8')] ) - @unittest.skipUnless(PY2, "no bytes_mode under Py3") - def test_dse_bytes(self): - l = self._get_bytes_ldapobject() - dse = l.read_rootdse_s() - self.assertIsInstance(dse, dict) - self.assertEqual(dse[u'supportedLDAPVersion'], [b'3']) - self.assertEqual( - l.get_naming_contexts(), - [self.server.suffix.encode('utf-8')] - ) - def test_compare_s_true(self): base = self.server.suffix l = self._ldap_conn @@ -720,8 +456,6 @@ def test_passwd_s(self): password = respvalue.genPasswd self.assertIsInstance(password, bytes) - if PY2: - password = password.decode('utf-8') # try changing password back respoid, respvalue = l.passwd_s(dn, password, "initial") @@ -777,8 +511,6 @@ def test103_reconnect_get_state(self): str('_trace_level'): ldap._trace_level, str('_trace_stack_limit'): 5, str('_uri'): self.server.ldap_uri, - str('bytes_mode'): l1.bytes_mode, - str('bytes_strictness'): l1.bytes_strictness, str('timeout'): -1, }, ) @@ -812,12 +544,6 @@ def test105_reconnect_restore(self): class Test03_SimpleLDAPObjectWithFileno(Test00_SimpleLDAPObject): - def _get_bytes_ldapobject(self, explicit=True, **kwargs): - raise unittest.SkipTest("Test opens two sockets") - - def _search_wrong_type(self, bytes_mode, strictness): - raise unittest.SkipTest("Test opens two sockets") - def _open_ldap_conn(self, who=None, cred=None, **kwargs): if hasattr(self, '_sock'): raise RuntimeError("socket already connected") diff --git a/Tests/t_ldapurl.py b/Tests/t_ldapurl.py index 1408efcf..398dc892 100644 --- a/Tests/t_ldapurl.py +++ b/Tests/t_ldapurl.py @@ -4,17 +4,13 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import unittest +from urllib.parse import quote # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' -from ldap.compat import quote - import ldapurl from ldapurl import LDAPUrl diff --git a/Tests/t_ldif.py b/Tests/t_ldif.py index 048b3f40..254e68d6 100644 --- a/Tests/t_ldif.py +++ b/Tests/t_ldif.py @@ -4,9 +4,6 @@ See https://www.python-ldap.org/ for details. """ - -from __future__ import unicode_literals - import os import textwrap import unittest diff --git a/setup.py b/setup.py index 69747853..22c7c741 100644 --- a/setup.py +++ b/setup.py @@ -7,15 +7,10 @@ import sys,os from setuptools import setup, Extension -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - raise RuntimeError('This software requires Python 2.7 or 3.x.') -if sys.version_info[0] >= 3 and sys.version_info < (3, 4): - raise RuntimeError('The C API from Python 3.4+ is required.') +if sys.version_info < (3, 6): + raise RuntimeError('The C API from Python 3.6+ is required.') -if sys.version_info[0] >= 3: - from configparser import ConfigParser -else: - from ConfigParser import ConfigParser +from configparser import ConfigParser sys.path.insert(0, os.path.join(os.getcwd(), 'Lib/ldap')) import pkginfo @@ -88,14 +83,11 @@ class OpenLDAP2: 'Programming Language :: C', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', # Note: when updating Python versions, also change .travis.yml and tox.ini 'Topic :: Database', @@ -169,6 +161,6 @@ class OpenLDAP2: 'pyasn1_modules >= 0.1.5', ], zip_safe=False, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.6', test_suite = 'Tests', ) diff --git a/tox.ini b/tox.ini index 81a38bf5..13a0e9bd 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ [tox] # Note: when updating Python versions, also change setup.py and .travis.yml -envlist = py27,py34,py35,py36,py37,py38,py39,{py2,py3}-nosasltls,doc,py3-trace +envlist = py36,py37,py38,py39,py3-nosasltls,doc,py3-trace minver = 1.8 [testenv] @@ -16,19 +16,8 @@ passenv = WITH_GCOV commands = {envpython} -bb -Werror \ -m unittest discover -v -s Tests -p 't_*' -[testenv:py27] -# No warnings with Python 2.7 -passenv = {[testenv]passenv} -commands = - {envpython} -m unittest discover -v -s Tests -p 't_*' - -[testenv:py34] -# No warnings with Python 3.4 -passenv = {[testenv]passenv} -commands = {[testenv:py27]commands} - -[testenv:py2-nosasltls] -basepython = python2 +[testenv:py3-nosasltls] +basepython = python3 # don't install, install dependencies manually skip_install = true deps = @@ -43,15 +32,7 @@ commands = {envpython} setup.py clean --all {envpython} setup.py build_ext -UHAVE_SASL,HAVE_TLS {envpython} setup.py install --single-version-externally-managed --root=/ - {[testenv:py27]commands} - -[testenv:py3-nosasltls] -basepython = python3 -skip_install = {[testenv:py2-nosasltls]skip_install} -deps = {[testenv:py2-nosasltls]deps} -passenv = {[testenv:py2-nosasltls]passenv} -setenv = {[testenv:py2-nosasltls]setenv} -commands = {[testenv:py2-nosasltls]commands} + {[testenv]commands} [testenv:py3-trace] basepython = python3 @@ -62,17 +43,11 @@ setenv = PYTHON_LDAP_TRACE_FILE={envtmpdir}/trace.log commands = {[testenv]commands} -[testenv:pypy] -# PyPy doesn't have working setup.py test +[testenv:pypy3] +basepython = pypy3 deps = pytest commands = {envpython} -m pytest -[testenv:pypy35] -# PyPy-5.9.0 -basepython = pypy3.5 -deps = {[testenv:pypy]deps} -commands = {[testenv:pypy]commands} - [testenv:doc] basepython = python3 deps =