Repository: randombit/botan
Pull request #3046 (2022-09-14 15:54:32)

Sunset Python 2 support
I guess its time to say goodbye to Python 2. 

This updates the documentation accordingly, uses `/usr/bin/env python3` in all python scripts I found in the repo and removes some python2/3 specific switches in the CI.

Note that on the current master a simple `./configure.py` doesn't work anymore for modern distributions (and macOS for that matter). They don't ship with a `python` executable in the $PATH anymore. Hence, one needs to work-around with some `ln -s /usr/bin/python3 /usr/local/bin/python` hackery.

Also, some minor script adjustments were necessary. I smoke-tested all scripts that I updated, though cannot rule out that things in some old dev-scripts fall apart.

This is very much work in progress to see CI results.


reneme: AppVeyor doesn't seem to find a `python3` binary. I guess we can ignore that and later see how [GitHub Actions](https://github.com/randombit/botan/pull/3007) fares with that.

Also the emscripten target fails, [that's handled independently](https://github.com/randombit/botan/pull/3045).
randombit: At least `configure.py`, `test_cli.py, and `botan2.py` have a number of conditional jumps based on `sys.version_info` to handle Python2 vs Python3 differences, which should be removed.
reneme: > At least configure.py, test_cli.py, and botan2.py have a number of conditional jumps based on sys.version_info to handle Python2 vs Python3 differences, which should be removed.

Deleting code! ❤️
reneme: This works, now. Though it breaks the build on AppVeyor. `master` won't build once this is merged. ..

Instead, lets hold on until [the switch to GitHub Actions (Windows)](https://github.com/randombit/botan/pull/3007#discussion_r970662405) is done and merged, then rebase and (probably) make it green on Windows.
randombit: > Note that on the current master a simple ./configure.py doesn't work anymore for modern distributions (and macOS for that matter). They don't ship with a python executable in the $PATH anymore.

Good to know. I use Arch which still ships with a symlink by default (pointing to `python3`). I guess in CI we didn't notice this because of explicit `python2` / `python3` invocations.

Patch:
diff --git a/configure.py b/configure.py
index 2f3743405..b45f5087b 100755
--- a/configure.py
+++ b/configure.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 # pylint: disable=too-many-lines
 
@@ -7,16 +7,12 @@ Configuration program for botan
 
 (C) 2009-2020 Jack Lloyd
 (C) 2015,2016,2017 Simon Warta (Kullo GmbH)
+(C) 2019-2022 René Meusel (neXenio GmbH, Rohde & Schwarz Cybersecurity GmbH)
 
 Botan is released under the Simplified BSD License (see license.txt)
 
-This script is regularly tested with CPython 2.7 and 3.5, and
-occasionally tested with CPython 2.6 and PyPy 4.
-
-Support for CPython 2.6 will be dropped eventually, but is kept up for as
-long as reasonably convenient.
-
-CPython 2.5 and earlier are not supported.
+This script is regularly tested with CPython 3.x, and
+occasionally tested PyPy 4.
 
 On Jython target detection does not work (use --os and --cpu).
 """
@@ -81,7 +77,7 @@ def parse_version_file(version_path):
             results[key] = val
     return results
 
-class Version(object):
+class Version:
     """
     Version information are all static members
     """
@@ -173,7 +169,7 @@ class Version(object):
 
 
 
-class SourcePaths(object):
+class SourcePaths:
     """
     A collection of paths defined by the project structure and
     independent of user configurations.
@@ -197,7 +193,7 @@ class SourcePaths(object):
         self.sphinx_config_dir = os.path.join(self.configs_dir, 'sphinx')
 
 
-class BuildPaths(object): # pylint: disable=too-many-instance-attributes
+class BuildPaths: # pylint: disable=too-many-instance-attributes
     """
     Constructor
     """
@@ -703,7 +699,7 @@ def take_options_from_env(options):
     options.cxxflags = update_from_env(options.cxxflags, 'CXXFLAGS', 'cxxflags')
     options.ldflags = update_from_env(options.ldflags, 'LDFLAGS', 'ldflags')
 
-class LexResult(object):
+class LexResult:
     pass
 
 
@@ -792,7 +788,7 @@ def lex_me_harder(infofile, allowed_groups, allowed_maps, name_val_pairs):
 
     return out
 
-class InfoObject(object):
+class InfoObject:
     def __init__(self, infofile):
         """
         Constructor sets members `infofile`, `lives_in`, `parent_module` and `basename`
@@ -1709,7 +1705,7 @@ def process_template_string(template_text, variables, template_source):
 
     The template language supports (un-nested) conditionals.
     """
-    class SimpleTemplate(object):
+    class SimpleTemplate:
 
         def __init__(self, vals):
             self.vals = vals
@@ -2304,7 +2300,7 @@ def create_template_vars(source_paths, build_paths, options, modules, cc, arch,
 
     return variables
 
-class ModulesChooser(object):
+class ModulesChooser:
     """
     Determine which modules to load based on options, target, etc
     """
@@ -2613,7 +2609,7 @@ def portable_symlink(file_path, target_dir, method):
         raise UserError('Unknown link method %s' % (method))
 
 
-class AmalgamationHelper(object):
+class AmalgamationHelper:
     # All include types may have trailing comment like e.g. '#include <vector> // IWYU pragma: export'
     _any_include = re.compile(r'#include <(.*)>')
     _botan_include = re.compile(r'#include <botan/(.*)>')
@@ -2667,7 +2663,7 @@ class AmalgamationHelper(object):
 """ % (Version.as_string()))
 
 
-class AmalgamationHeader(object):
+class AmalgamationHeader:
     def __init__(self, input_filepaths):
 
         self.included_already = set()
@@ -2728,16 +2724,13 @@ class AmalgamationHeader(object):
             f.write("\n#endif // %s\n" % (include_guard))
 
 
-class AmalgamationGenerator(object):
+class AmalgamationGenerator:
     _header_guard_pattern = re.compile(r'^#define BOTAN_.*_H_\s*$')
     _header_endif_pattern = re.compile(r'^#endif.*$')
 
     @staticmethod
     def read_header(filepath):
-        encoding_kwords = {}
-        if sys.version_info[0] == 3:
-            encoding_kwords['encoding'] = 'utf8'
-        with open(filepath, **encoding_kwords) as f:
+        with open(filepath, encoding='utf8') as f:
             raw_content = f.readlines()
             return AmalgamationGenerator.strip_header_goop(filepath, raw_content)
 
@@ -2777,10 +2770,6 @@ class AmalgamationGenerator(object):
         self._options = options
 
     def generate(self):
-        encoding_kwords = {}
-        if sys.version_info[0] == 3:
-            encoding_kwords['encoding'] = 'utf8'
-
         pub_header_amalag = AmalgamationHeader(self._build_paths.public_headers)
         amalgamation_header_fsname = '%s.h' % (self._filename_prefix)
         logging.info('Writing amalgamation header to %s', amalgamation_header_fsname)
@@ -2795,7 +2784,7 @@ class AmalgamationGenerator(object):
         amalgamation_fsname = '%s.cpp' % (self._filename_prefix)
         logging.info('Writing amalgamation source to %s', amalgamation_fsname)
 
-        amalgamation_file = open(amalgamation_fsname, 'w', **encoding_kwords)
+        amalgamation_file = open(amalgamation_fsname, 'w', encoding='utf8')
 
         AmalgamationHelper.write_banner(amalgamation_file)
         amalgamation_file.write('\n#include "%s"\n\n' % (amalgamation_header_fsname))
@@ -2808,7 +2797,7 @@ class AmalgamationGenerator(object):
 
         for mod in sorted(self._modules, key=lambda module: module.basename):
             for src in sorted(mod.source):
-                with open(src, 'r', **encoding_kwords) as f:
+                with open(src, 'r', encoding='utf8') as f:
                     for line in f:
                         if AmalgamationHelper.is_botan_include(line):
                             # Botan headers are inlined in amalgamation headers
@@ -2853,7 +2842,7 @@ def have_program(program):
     return False
 
 
-class BotanConfigureLogHandler(logging.StreamHandler, object):
+class BotanConfigureLogHandler(logging.StreamHandler):
     def emit(self, record):
         # Do the default stuff first
         super(BotanConfigureLogHandler, self).emit(record)
diff --git a/doc/building.rst b/doc/building.rst
index 6240d449c..f963cc4a8 100644
--- a/doc/building.rst
+++ b/doc/building.rst
@@ -18,7 +18,7 @@ system, primarily due to lack of access and interest.  Please contact the
 maintainer if you would like to build Botan on such a system.
 
 Botan's build is controlled by configure.py, which is a `Python
-<https://www.python.org>`_ script. Python 2.7 or later is required.
+<https://www.python.org>`_ script. Python 3.x or later is required.
 
 .. highlight:: none
 
@@ -31,10 +31,10 @@ For the impatient, this works for most systems::
 Or using ``nmake``, if you're compiling on Windows with Visual C++. On
 platforms that do not understand the '#!' convention for beginning
 script files, or that have Python installed in an unusual spot, you
-might need to prefix the ``configure.py`` command with ``python`` or
-``/path/to/python``::
+might need to prefix the ``configure.py`` command with ``python3`` or
+``/path/to/python3``::
 
-  $ python ./configure.py [arguments]
+  $ python3 ./configure.py [arguments]
 
 Configuring the Build
 ---------------------------------
@@ -42,7 +42,7 @@ Configuring the Build
 The first step is to run ``configure.py``, which is a Python script
 that creates various directories, config files, and a Makefile for
 building everything. This script should run under a vanilla install of
-Python 2.7 or Python 3.x.
+Python 3.x.
 
 The script will attempt to guess what kind of system you are trying to
 compile for (and will print messages telling you what it guessed).
@@ -204,7 +204,7 @@ You need to have a copy of Python installed, and have both Python and
 your chosen compiler in your path. Open a command shell (or the SDK
 shell), and run::
 
-   $ python configure.py --cc=msvc --os=windows
+   $ python3 configure.py --cc=msvc --os=windows
    $ nmake
    $ nmake check
    $ nmake install
@@ -214,7 +214,7 @@ which enables you to run multiple build jobs in parallel.
 
 For MinGW, use::
 
-   $ python configure.py --cc=gcc --os=mingw
+   $ python3 configure.py --cc=gcc --os=mingw
    $ make
 
 By default the install target will be ``C:\botan``; you can modify
diff --git a/doc/deprecated.rst b/doc/deprecated.rst
index bfe89a539..d1a807cdd 100644
--- a/doc/deprecated.rst
+++ b/doc/deprecated.rst
@@ -108,9 +108,3 @@ Other API deprecations
   directly on ``MessageAuthenticationCode`` and ``HashFunction``. So
   this only affects you if you are directly referencing
   ``Botan::Buffered_Computation`` in some way.
-
-Deprecated Build Targets
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-- Configuring a build (with ``configure.py``) using Python2. In a future
-  major release, Python3 will be required.
diff --git a/doc/dev_ref/configure.rst b/doc/dev_ref/configure.rst
index 50a639772..3e3d669d6 100644
--- a/doc/dev_ref/configure.rst
+++ b/doc/dev_ref/configure.rst
@@ -60,11 +60,6 @@ Makefile, ``build.h`` and other artifacts.
 When Modifying ``configure.py``
 --------------------------------
 
-For now, any changes to ``configure.py`` must work under both CPython 2.7 and
-CPython 3.x. In a future major release, support for CPython2 will be dropped,
-but until then if making modifications verify the code works as expected on
-both versions.
-
 Run ``./src/scripts/ci_build.py lint`` to run Pylint checks after any change.
 
 Template Language
diff --git a/doc/dev_ref/contributing.rst b/doc/dev_ref/contributing.rst
index 985e0479d..472d375f8 100644
--- a/doc/dev_ref/contributing.rst
+++ b/doc/dev_ref/contributing.rst
@@ -120,25 +120,23 @@ review/understanding of the change.
 Python
 ----------------------------------------
 
-Scripts should be in Python whenever possible.
+Scripts should be in Python 3 whenever possible.
 
 For configure.py (and helper scripts install.py, cleanup.py and build_docs.py)
-the target is stock (no modules outside the standard library) CPython 2.7 plus
-latest CPython 3.x. Support for CPython 2.6, PyPy, etc is great when viable (in
-the sense of not causing problems for 2.7 or 3.x, and not requiring huge blocks
-of version dependent code). As running this program successfully is required for
-a working build, making it as portable as possible is considered key.
+the target is stock (no modules outside the standard library) CPython 3.x.
+Support for PyPy, etc is great when viable (in the sense of not causing problems
+for 3.x, and not requiring huge blocks of version dependent code). As running
+this program successfully is required for a working build, making it as portable
+as possible is considered key.
 
-The python wrapper botan2.py targets CPython 2.7, 3.x, and latest PyPy. Note that
+The python wrapper botan2.py targets CPython 3.x, and latest PyPy. Note that
 a single file is used to avoid dealing with any of Python's various crazy module
 distribution issues.
 
 For random scripts not typically run by an end-user (codegen, visualization, and
-so on) there isn't any need to worry about 2.6 and even just running under
-Python2 xor Python3 is acceptable if needed. Here it's fine to depend on any
-useful modules such as graphviz or matplotlib, regardless if it is available
-from a stock CPython install. Since Python2 is now end of life, prefer Python3
-for new scripts of this sort.
+so on) there isn't any need to worry about platform independence. Here it's fine
+to depend on any useful modules such as graphviz or matplotlib, regardless if it
+is available from a stock CPython install.
 
 Build Tools and Hints
 ----------------------------------------
diff --git a/doc/dev_ref/release_process.rst b/doc/dev_ref/release_process.rst
index 1dd084264..ce4139abb 100644
--- a/doc/dev_ref/release_process.rst
+++ b/doc/dev_ref/release_process.rst
@@ -97,7 +97,7 @@ Build The Windows Installer
 
 On Windows, run ``configure.py`` to setup a build::
 
- $ python ./configure.py --cc=msvc --cpu=$ARCH --distribution-info=unmodified
+ $ python3 ./configure.py --cc=msvc --cpu=$ARCH --distribution-info=unmodified
 
 After completing the build (and running the tests), use `InnoSetup
 <http://www.jrsoftware.org/isinfo.php>`_ to create the installer.  A
diff --git a/src/python/botan2.py b/src/python/botan2.py
index 57e534220..7f66cb630 100755
--- a/src/python/botan2.py
+++ b/src/python/botan2.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 Python wrapper of the botan crypto library
@@ -10,7 +10,7 @@ https://botan.randombit.net
 Botan is released under the Simplified BSD License (see license.txt)
 
 This module uses the ctypes module and is usable by programs running
-under at least CPython 2.7, CPython 3.x, and PyPy
+under at least CPython 3.x, and PyPy
 
 It uses botan's ffi module, which exposes a C API. This version of the
 module requires FFI API version 20180713, which was introduced in
@@ -21,7 +21,7 @@ Botan 2.8
 from ctypes import CDLL, POINTER, byref, create_string_buffer, \
     c_void_p, c_size_t, c_uint8, c_uint32, c_uint64, c_int, c_uint, c_char_p
 
-from sys import version_info, platform
+from sys import platform
 from time import strptime, mktime, time as system_time
 from binascii import hexlify
 from datetime import datetime
@@ -504,38 +504,21 @@ def _ctype_str(s):
     if s is None:
         return None
     assert isinstance(s, str)
-    if version_info[0] < 3:
-        return s
-    else:
-        return s.encode('utf-8')
+    return s.encode('utf-8')
 
 def _ctype_to_str(s):
-    if version_info[0] < 3:
-        return s.encode('utf-8')
-    else:
-        return s.decode('utf-8')
+    return s.decode('utf-8')
 
 def _ctype_bits(s):
-    if version_info[0] < 3:
-        if isinstance(s, str):
-            return s
-        elif isinstance(s, unicode): # pylint: disable=undefined-variable
-            return s.decode('utf-8')
-        else:
-            raise Exception("Internal error - unexpected type %s provided to _ctype_bits" % (type(s).__name__))
+    if isinstance(s, bytes):
+        return s
+    elif isinstance(s, str):
+        return s.encode('utf-8')
     else:
-        if isinstance(s, bytes):
-            return s
-        elif isinstance(s, str):
-            return s.encode('utf-8')
-        else:
-            raise Exception("Internal error - unexpected type %s provided to _ctype_bits" % (type(s).__name__))
+        raise Exception("Internal error - unexpected type %s provided to _ctype_bits" % (type(s).__name__))
 
 def _ctype_bufout(buf):
-    if version_info[0] < 3:
-        return str(buf.raw)
-    else:
-        return buf.raw
+    return buf.raw
 
 def _hex_encode(buf):
     return hexlify(buf).decode('ascii')
@@ -572,7 +555,7 @@ def const_time_compare(x, y):
 #
 # RNG
 #
-class RandomNumberGenerator(object):
+class RandomNumberGenerator:
     # Can also use type "system"
     def __init__(self, rng_type='system'):
         self.__obj = c_void_p(0)
@@ -602,7 +585,7 @@ class RandomNumberGenerator(object):
 #
 # Block cipher
 #
-class BlockCipher(object):
+class BlockCipher:
     def __init__(self, algo):
 
         if isinstance(algo, c_void_p):
@@ -666,7 +649,7 @@ class BlockCipher(object):
 #
 # Hash function
 #
-class HashFunction(object):
+class HashFunction:
     def __init__(self, algo):
 
         if isinstance(algo, c_void_p):
@@ -710,7 +693,7 @@ class HashFunction(object):
 #
 # Message authentication codes
 #
-class MsgAuthCode(object):
+class MsgAuthCode:
     def __init__(self, algo):
         flags = c_uint32(0) # always zero in this API version
         self.__obj = c_void_p(0)
@@ -758,7 +741,7 @@ class MsgAuthCode(object):
         _DLL.botan_mac_final(self.__obj, out)
         return _ctype_bufout(out)
 
-class SymmetricCipher(object):
+class SymmetricCipher:
     def __init__(self, algo, encrypt=True):
         flags = 0 if encrypt else 1
         self.__obj = c_void_p(0)
@@ -932,7 +915,7 @@ def kdf(algo, secret, out_len, salt, label):
 #
 # Public key
 #
-class PublicKey(object): # pylint: disable=invalid-name
+class PublicKey: # pylint: disable=invalid-name
 
     def __init__(self, obj=c_void_p(0)):
         self.__obj = obj
@@ -1054,7 +1037,7 @@ class PublicKey(object): # pylint: disable=invalid-name
 #
 # Private Key
 #
-class PrivateKey(object):
+class PrivateKey:
 
     def __init__(self, obj=c_void_p(0)):
         self.__obj = obj
@@ -1196,7 +1179,7 @@ class PrivateKey(object):
         _DLL.botan_privkey_get_field(v.handle_(), self.__obj, _ctype_str(field_name))
         return int(v)
 
-class PKEncrypt(object):
+class PKEncrypt:
     def __init__(self, key, padding):
         self.__obj = c_void_p(0)
         flags = c_uint32(0) # always zero in this ABI
@@ -1213,7 +1196,7 @@ class PKEncrypt(object):
         return outbuf.raw[0:int(outbuf_sz.value)]
 
 
-class PKDecrypt(object):
+class PKDecrypt:
     def __init__(self, key, padding):
         self.__obj = c_void_p(0)
         flags = c_uint32(0) # always zero in this ABI
@@ -1229,7 +1212,7 @@ class PKDecrypt(object):
         _DLL.botan_pk_op_decrypt(self.__obj, outbuf, byref(outbuf_sz), _ctype_bits(msg), len(msg))
         return outbuf.raw[0:int(outbuf_sz.value)]
 
-class PKSign(object): # pylint: disable=invalid-name
+class PKSign: # pylint: disable=invalid-name
     def __init__(self, key, padding, der=False):
         self.__obj = c_void_p(0)
         flags = c_uint32(1) if der else c_uint32(0)
@@ -1248,7 +1231,7 @@ class PKSign(object): # pylint: disable=invalid-name
         _DLL.botan_pk_op_sign_finish(self.__obj, rng_obj.handle_(), outbuf, byref(outbuf_sz))
         return outbuf.raw[0:int(outbuf_sz.value)]
 
-class PKVerify(object):
+class PKVerify:
     def __init__(self, key, padding, der=False):
         self.__obj = c_void_p(0)
         flags = c_uint32(1) if der else c_uint32(0)
@@ -1266,7 +1249,7 @@ class PKVerify(object):
             return True
         return False
 
-class PKKeyAgreement(object):
+class PKKeyAgreement:
     def __init__(self, key, kdf_name):
         self.__obj = c_void_p(0)
         flags = c_uint32(0) # always zero in this ABI
@@ -1314,7 +1297,7 @@ def _load_buf_or_file(filename, buf, file_fn, buf_fn):
 #
 # X.509 certificates
 #
-class X509Cert(object): # pylint: disable=invalid-name
+class X509Cert: # pylint: disable=invalid-name
     def __init__(self, filename=None, buf=None):
         self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_cert_load_file, _DLL.botan_x509_cert_load)
 
@@ -1493,7 +1476,7 @@ class X509Cert(object): # pylint: disable=invalid-name
 #
 # X.509 Certificate revocation lists
 #
-class X509CRL(object):
+class X509CRL:
     def __init__(self, filename=None, buf=None):
         self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_crl_load_file, _DLL.botan_x509_crl_load)
 
@@ -1504,7 +1487,7 @@ class X509CRL(object):
         return self.__obj
 
 
-class MPI(object): # pylint: disable=too-many-public-methods
+class MPI: # pylint: disable=too-many-public-methods
 
     def __init__(self, initial_value=None, radix=None):
 
@@ -1720,7 +1703,7 @@ class MPI(object): # pylint: disable=too-many-public-methods
     def set_bit(self, bit):
         _DLL.botan_mp_set_bit(self.__obj, c_size_t(bit))
 
-class FormatPreservingEncryptionFE1(object):
+class FormatPreservingEncryptionFE1:
 
     def __init__(self, modulus, key, rounds=5, compat_mode=False):
         flags = c_uint32(1 if compat_mode else 0)
@@ -1740,7 +1723,7 @@ class FormatPreservingEncryptionFE1(object):
         _DLL.botan_fpe_decrypt(self.__obj, r.handle_(), _ctype_bits(tweak), len(tweak))
         return r
 
-class HOTP(object):
+class HOTP:
     def __init__(self, key, digest="SHA-1", digits=6):
         self.__obj = c_void_p(0)
         _DLL.botan_hotp_init(byref(self.__obj), key, len(key), _ctype_str(digest), digits)
@@ -1761,7 +1744,7 @@ class HOTP(object):
         else:
             return (False, counter)
 
-class TOTP(object):
+class TOTP:
     def __init__(self, key, digest="SHA-1", digits=6, timestep=30):
         self.__obj = c_void_p(0)
         _DLL.botan_totp_init(byref(self.__obj), key, len(key), _ctype_str(digest), digits, timestep)
@@ -1796,7 +1779,7 @@ def nist_key_unwrap(kek, wrapped):
     _DLL.botan_key_unwrap3394(wrapped, len(wrapped), kek, len(kek), output, byref(out_len))
     return output[0:int(out_len.value)]
 
-class Srp6ServerSession(object):
+class Srp6ServerSession:
     __obj = c_void_p(0)
 
     def __init__(self):
diff --git a/src/scripts/bench.py b/src/scripts/bench.py
index 1cc626366..471bd0f5a 100755
--- a/src/scripts/bench.py
+++ b/src/scripts/bench.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 Compare Botan with OpenSSL using their respective benchmark utils
@@ -28,7 +28,7 @@ def setup_logging(options):
     else:
         log_level = logging.INFO
 
-    class LogOnErrorHandler(logging.StreamHandler, object):
+    class LogOnErrorHandler(logging.StreamHandler):
         def emit(self, record):
             super(LogOnErrorHandler, self).emit(record)
             if record.levelno >= logging.ERROR:
@@ -135,7 +135,7 @@ def run_botan_bench(botan, runtime, buf_sizes, algo):
 
     return output
 
-class BenchmarkResult(object):
+class BenchmarkResult:
     def __init__(self, algo, buf_sizes, openssl_results, botan_results):
         self.algo = algo
         self.results = {}
diff --git a/src/scripts/build_docs.py b/src/scripts/build_docs.py
index b771db278..c5623ed71 100755
--- a/src/scripts/build_docs.py
+++ b/src/scripts/build_docs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Botan doc generation script
diff --git a/src/scripts/check.py b/src/scripts/check.py
old mode 100644
new mode 100755
index ea80d552d..b8c5882be
--- a/src/scripts/check.py
+++ b/src/scripts/check.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Implements the "make check" target
diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py
index 948076010..0e5d1a3cf 100755
--- a/src/scripts/ci_build.py
+++ b/src/scripts/ci_build.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 CI build script
@@ -418,16 +418,6 @@ def parse_args(args):
     parser.add_option('--pkcs11-lib', default=os.getenv('PKCS11_LIB'), metavar='LIB',
                       help='Set PKCS11 lib to use for testing')
 
-    parser.add_option('--with-python3', dest='use_python3', action='store_true', default=None,
-                      help='Enable using python3')
-    parser.add_option('--without-python3', dest='use_python3', action='store_false',
-                      help='Disable using python3')
-
-    parser.add_option('--with-pylint3', dest='use_pylint3', action='store_true', default=True,
-                      help='Enable using python3 pylint')
-    parser.add_option('--without-pylint3', dest='use_pylint3', action='store_false',
-                      help='Disable using python3 pylint')
-
     parser.add_option('--disable-werror', action='store_true', default=False,
                       help='Allow warnings to compile')
 
@@ -473,15 +463,7 @@ def main(args=None):
         print("Unknown target '%s'" % (target))
         return 2
 
-    if options.use_python3 is None:
-        use_python3 = have_prog('python3')
-    else:
-        use_python3 = options.use_python3
-
-    py_interp = 'python'
-    if use_python3:
-        py_interp = 'python3'
-
+    py_interp = 'python3'
     if options.cc_bin is None:
         if options.cc == 'gcc':
             options.cc_bin = 'g++'
@@ -519,10 +501,6 @@ def main(args=None):
         pylint_rc = '--rcfile=%s' % (os.path.join(root_dir, 'src/configs/pylint.rc'))
         pylint_flags = [pylint_rc, '--reports=no']
 
-        # Some disabled rules specific to Python3
-        # useless-object-inheritance: complains about code still useful in Python2
-        py3_flags = '--disable=useless-object-inheritance'
-
         py_scripts = [
             'configure.py',
             'src/python/botan2.py',
@@ -543,9 +521,7 @@ def main(args=None):
             'src/editors/sublime/build.py']
 
         full_paths = [os.path.join(root_dir, s) for s in py_scripts]
-
-        if use_python3 and options.use_pylint3:
-            cmds.append(['python3', '-m', 'pylint'] + pylint_flags + [py3_flags] + full_paths)
+        cmds.append([py_interp, '-m', 'pylint'] + pylint_flags + full_paths)
 
     else:
         config_flags, run_test_command, make_prefix, install_prefix = determine_flags(
@@ -617,14 +593,7 @@ def main(args=None):
         python_tests = os.path.join(root_dir, 'src/scripts/test_python.py')
 
         if target in ['shared', 'coverage']:
-
-            if options.os == 'windows':
-                if options.cpu == 'x86':
-                    # Python on AppVeyor is a 32-bit binary so only test for 32-bit
-                    cmds.append([py_interp, '-b', python_tests])
-            else:
-                if use_python3:
-                    cmds.append(['python3', '-b', python_tests])
+            cmds.append([py_interp, '-b', python_tests])
 
         if target in ['shared', 'static', 'bsi', 'nist']:
             cmds.append(make_cmd + ['install'])
diff --git a/src/scripts/ci_check_install.py b/src/scripts/ci_check_install.py
index c0a372762..c07356182 100755
--- a/src/scripts/ci_check_install.py
+++ b/src/scripts/ci_check_install.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 # coding=utf8
 
 """
diff --git a/src/scripts/cleanup.py b/src/scripts/cleanup.py
index 3e8142bab..aee514367 100755
--- a/src/scripts/cleanup.py
+++ b/src/scripts/cleanup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Implements the "make clean" target
diff --git a/src/scripts/comba.py b/src/scripts/comba.py
index c139b8464..3c052dbe6 100755
--- a/src/scripts/comba.py
+++ b/src/scripts/comba.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 
 import sys
 import datetime
diff --git a/src/scripts/create_corpus_zip.py b/src/scripts/create_corpus_zip.py
index b749b8d39..96971d316 100755
--- a/src/scripts/create_corpus_zip.py
+++ b/src/scripts/create_corpus_zip.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # (C) 2017 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
diff --git a/src/scripts/dist.py b/src/scripts/dist.py
index de4b1a4bb..5a7c5dfd1 100755
--- a/src/scripts/dist.py
+++ b/src/scripts/dist.py
@@ -1,9 +1,9 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Release script for botan (https://botan.randombit.net/)
 
-This script requires Python 2.7 or 3.6
+This script requires Python 3.6
 
 (C) 2011,2012,2013,2015,2016,2017 Jack Lloyd
 
@@ -299,7 +299,7 @@ def write_archive(version, output_basename, archive_type, rel_epoch, all_files,
     return output_archive
 
 def configure_logging(options):
-    class ExitOnErrorLogHandler(logging.StreamHandler, object):
+    class ExitOnErrorLogHandler(logging.StreamHandler):
         def emit(self, record):
             super(ExitOnErrorLogHandler, self).emit(record)
             # Exit script if and ERROR or worse occurred
diff --git a/src/scripts/ffi_decls.py b/src/scripts/ffi_decls.py
index b336e61f5..d7a998259 100755
--- a/src/scripts/ffi_decls.py
+++ b/src/scripts/ffi_decls.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 Automatically generate declarations for the FFI layer
diff --git a/src/scripts/install.py b/src/scripts/install.py
index 3e2f095ea..632270905 100755
--- a/src/scripts/install.py
+++ b/src/scripts/install.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Botan install script
diff --git a/src/scripts/macro_checks.py b/src/scripts/macro_checks.py
index 9b5eaf6f9..51a4faac1 100755
--- a/src/scripts/macro_checks.py
+++ b/src/scripts/macro_checks.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # (C) 2018 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
diff --git a/src/scripts/monty.py b/src/scripts/monty.py
index e08b7203c..3f4d170c7 100755
--- a/src/scripts/monty.py
+++ b/src/scripts/monty.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 
 import sys
 import datetime
diff --git a/src/scripts/oids.py b/src/scripts/oids.py
index dae1a4dc5..391432f42 100755
--- a/src/scripts/oids.py
+++ b/src/scripts/oids.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 (C) 2016 Jack Lloyd
diff --git a/src/scripts/run_clang_tidy.py b/src/scripts/run_clang_tidy.py
index 978815fa9..2d797b7c2 100755
--- a/src/scripts/run_clang_tidy.py
+++ b/src/scripts/run_clang_tidy.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 import subprocess
 import sys
diff --git a/src/scripts/run_tls_attacker.py b/src/scripts/run_tls_attacker.py
index ae05a2176..c2f226ed4 100755
--- a/src/scripts/run_tls_attacker.py
+++ b/src/scripts/run_tls_attacker.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # (C) 2017 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
diff --git a/src/scripts/run_tls_fuzzer.py b/src/scripts/run_tls_fuzzer.py
index 4122e5bd4..fd0a4e53e 100755
--- a/src/scripts/run_tls_fuzzer.py
+++ b/src/scripts/run_tls_fuzzer.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # (C) 2019 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
diff --git a/src/scripts/show_dependencies.py b/src/scripts/show_dependencies.py
index edf2d91e0..efce1c663 100755
--- a/src/scripts/show_dependencies.py
+++ b/src/scripts/show_dependencies.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 Show Botan module dependencies as a list or graph.
diff --git a/src/scripts/tls_scanner/tls_scanner.py b/src/scripts/tls_scanner/tls_scanner.py
index 3f9aa49d5..e7e89fda9 100755
--- a/src/scripts/tls_scanner/tls_scanner.py
+++ b/src/scripts/tls_scanner/tls_scanner.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/env python3
 
 # (C) 2017 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
@@ -9,11 +9,11 @@ import subprocess
 import re
 
 def format_report(client_output):
-    version_re = re.compile('TLS (v1\.[0-2]) using ([A-Z0-9_]+)')
+    version_re = re.compile('TLS (v1\.[0-3]) using ([A-Z0-9_]+)')
 
     version_match = version_re.search(client_output)
 
-    #print client_output
+    #print(client_output)
 
     if version_match:
         return "Established %s %s" % (version_match.group(1), version_match.group(2))
@@ -25,7 +25,7 @@ def scanner(args = None):
         args = sys.argv
 
     if len(args) != 2:
-        print "Error: Usage tls_scanner.py host_file"
+        print("Error: Usage tls_scanner.py host_file")
         return 2
 
     scanners = {}
@@ -41,21 +41,21 @@ def scanner(args = None):
     timeout = 10
 
     for url in scanners.keys():
-        print "waiting for", url
+        print("waiting for", url)
 
         for i in range(timeout):
             scanners[url].poll()
             if scanners[url].returncode != None:
                 break
-            #print "Waiting %d more seconds for %s" % (timeout-i, url)
+            #print("Waiting %d more seconds for %s" % (timeout-i, url))
             time.sleep(1)
 
         if scanners[url].returncode != None:
             output = scanners[url].stdout.read() + scanners[url].stderr.read()
-            report[url] = format_report(output)
+            report[url] = format_report(output.decode("utf-8"))
 
     for url in report.keys():
-        print url, ":", report[url]
+        print(url, ":", report[url])
 
     return 0
 
diff --git a/src/scripts/website.py b/src/scripts/website.py
index 9b745ab7e..8c45f816f 100755
--- a/src/scripts/website.py
+++ b/src/scripts/website.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 Generate the Botan website


Test patch:
diff --git a/src/scripts/test_all_configs.py b/src/scripts/test_all_configs.py
index bc91c6987..99db48c37 100755
--- a/src/scripts/test_all_configs.py
+++ b/src/scripts/test_all_configs.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 """
 This configures and builds with many different sub-configurations
diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py
index 4f4217b45..2d61a83d3 100755
--- a/src/scripts/test_cli.py
+++ b/src/scripts/test_cli.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 
 """
 (C) 2018,2019 Jack Lloyd
@@ -33,7 +33,7 @@ def run_socket_tests():
     # Connecting to the server port fails. Possibly a local firewall?
     return platform.system().lower() != "freebsd"
 
-class TestLogHandler(logging.StreamHandler, object):
+class TestLogHandler(logging.StreamHandler):
     def emit(self, record):
         # Do the default stuff first
         super(TestLogHandler, self).emit(record)
@@ -483,16 +483,10 @@ def cli_compress_tests(tmp_dir):
     if not os.access(output_file, os.R_OK):
         logging.error("Compression did not created expected output file")
 
-    is_py3 = sys.version_info[0] == 3
-
     output_hdr = open(output_file, 'rb').read(2)
 
-    if is_py3:
-        if output_hdr[0] != 0x1F or output_hdr[1] != 0x8B:
-            logging.error("Did not see expected gzip header")
-    else:
-        if ord(output_hdr[0]) != 0x1F or ord(output_hdr[1]) != 0x8B:
-            logging.error("Did not see expected gzip header")
+    if output_hdr[0] != 0x1F or output_hdr[1] != 0x8B:
+        logging.error("Did not see expected gzip header")
 
     os.unlink(input_file)
 
@@ -975,10 +969,7 @@ def cli_tls_http_server_tests(tmp_dir):
     if resp.status != 405:
         logging.error('Unexpected response status %d', resp.status)
 
-    if sys.version_info.major >= 3:
-        rc = tls_server.wait(5) # pylint: disable=too-many-function-args
-    else:
-        rc = tls_server.wait()
+    rc = tls_server.wait(5)
 
     if rc != 0:
         logging.error("Unexpected return code from https_server %d", rc)
@@ -1071,10 +1062,7 @@ def cli_tls_proxy_tests(tmp_dir):
         if body != server_response:
             logging.error('Unexpected response from server %s', body)
 
-    if sys.version_info.major >= 3:
-        rc = tls_proxy.wait(5) # pylint: disable=too-many-function-args
-    else:
-        rc = tls_proxy.wait()
+    rc = tls_proxy.wait(5)
 
     if rc != 0:
         logging.error('Unexpected return code %d', rc)
@@ -1087,11 +1075,7 @@ def cli_trust_root_tests(tmp_dir):
 
     dn_re = re.compile('(.+=\".+\")(,.+=\".+\")')
 
-    encoding_kwords = {}
-    if sys.version_info[0] == 3:
-        encoding_kwords['encoding'] = 'utf8'
-
-    for line in open(dn_file, **encoding_kwords):
+    for line in open(dn_file, encoding='utf8'):
         if dn_re.match(line) is None:
             logging.error("Unexpected DN line %s", line)
 
diff --git a/src/scripts/test_cli_crypt.py b/src/scripts/test_cli_crypt.py
index e7b9437d6..c12ce9a90 100755
--- a/src/scripts/test_cli_crypt.py
+++ b/src/scripts/test_cli_crypt.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 (C) 2015,2016,2017,2018 Simon Warta
diff --git a/src/scripts/test_fuzzers.py b/src/scripts/test_fuzzers.py
index 356a83779..9048cde64 100755
--- a/src/scripts/test_fuzzers.py
+++ b/src/scripts/test_fuzzers.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 
 # (C) 2017,2018 Jack Lloyd
 # Botan is released under the Simplified BSD License (see license.txt)
diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py
index 0f65f1c07..776ddb338 100644
--- a/src/scripts/test_python.py
+++ b/src/scripts/test_python.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 """
 (C) 2015,2017,2018,2019 Jack Lloyd
@@ -8,7 +8,17 @@ Botan is released under the Simplified BSD License (see license.txt)
 
 import unittest
 import binascii
-import botan2
+import os
+import platform
+
+# Starting with Python 3.8 DLL search locations are more restricted on Windows.
+# Hence, we need to explicitly add the current working directory before trying
+# to load the botan python wrapper.
+# See: https://docs.python.org/3/whatsnew/3.8.html#bpo-36085-whatsnew
+if platform.system() == "Windows" and hasattr(os, "add_dll_directory"):
+    os.add_dll_directory(os.getcwd())
+
+import botan2 # pylint: disable=wrong-import-position
 
 def hex_encode(buf):
     return binascii.hexlify(buf).decode('ascii')