Add Python 3.10 support and tests (#9077)

Fixes https://github.com/certbot/certbot/issues/9058.

The changes to the CI config are equivalent to the ones made in https://github.com/certbot/certbot/pull/8460.

Other than ignoring some warnings raised by botocore, the main additional work that had to be done here was switching away from using `distutils.version.LooseVersion` since the entire `distutils` module was deprecated in Python 3.10. To do that, I took a few different approaches:

* If the version strings being parsed are from Python packages such as Certbot or setuptools, I switched to using [pkg_resources.parse_version](https://setuptools.pypa.io/en/latest/pkg_resources.html#parsing-utilities) from `setuptools`. This functionality has been available since [setuptools 8.0 from 2014](https://setuptools.pypa.io/en/latest/history.html#id865).
* If the version strings being parsed are not from Python packages, I added code equivalent to `distutils.version.LooseVersion` in `certbot.util.parse_loose_version`.
* The code for `CERTBOT_PIP_NO_BINARY` can be completely removed since that variable isn't used or referenced anywhere in this repo.

* add python 3.10 support

* make some version changes

* don't use looseversion in setup.py

* switch to pkg_resources

* deprecate get_strict_version

* fix route53 tests

* remove unused CERTBOT_PIP_NO_BINARY code

* stop using distutils in letstest

* add unit tests

* more changelog entries
This commit is contained in:
Brad Warren
2021-11-08 15:55:32 -08:00
committed by GitHub
parent b1edda8a65
commit e8265dbf9c
35 changed files with 158 additions and 61 deletions
+6 -2
View File
@@ -6,11 +6,15 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Added
*
* Support for Python 3.10 was added to Certbot and all of its components.
* The function certbot.util.parse_loose_version was added to parse version
strings in the same way as the now deprecated distutils.version.LooseVersion
class from the Python standard library.
### Changed
*
* The function certbot.util.get_strict_version was deprecated and will be
removed in a future release.
### Fixed
+3 -2
View File
@@ -12,6 +12,7 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
import parsedatetime
import pkg_resources
import pytz
import certbot
@@ -33,7 +34,7 @@ logger = logging.getLogger(__name__)
ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
README = "README"
CURRENT_VERSION = util.get_strict_version(certbot.__version__)
CURRENT_VERSION = pkg_resources.parse_version(certbot.__version__)
BASE_PRIVKEY_MODE = 0o600
@@ -457,7 +458,7 @@ class RenewableCert(interfaces.RenewableCert):
conf_version = self.configuration.get("version")
if (conf_version is not None and
util.get_strict_version(conf_version) > CURRENT_VERSION):
pkg_resources.parse_version(conf_version) > CURRENT_VERSION):
logger.info(
"Attempting to parse the version %s renewal configuration "
"file found at %s with version %s of Certbot. This might not "
+35 -6
View File
@@ -1,10 +1,7 @@
"""Utilities for all Certbot."""
# distutils.version under virtualenv confuses pylint
# For more info, see: https://github.com/PyCQA/pylint/issues/73
import argparse
import atexit
import collections
import distutils.version
import errno
import logging
import platform
@@ -14,6 +11,7 @@ import subprocess
import sys
from typing import Dict
from typing import IO
from typing import List
from typing import Text
from typing import Tuple
from typing import Union
@@ -61,7 +59,7 @@ _INITIAL_PID = os.getpid()
# program exits before the lock is cleaned up, it is automatically
# released, but the file isn't deleted.
_LOCKS: Dict[str, lock.LockFile] = {}
_VERSION_COMPONENT_RE = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
def env_no_snap_for_external_calls():
"""
@@ -612,8 +610,13 @@ def get_strict_version(normalized):
:rtype: distutils.version.StrictVersion
"""
# strict version ending with "a" and a number designates a pre-release
return distutils.version.StrictVersion(normalized.replace(".dev", "a"))
warnings.warn("certbot.util.get_strict_version is deprecated and will be "
"removed in a future release.", DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
import distutils.version
# strict version ending with "a" and a number designates a pre-release
return distutils.version.StrictVersion(normalized.replace(".dev", "a"))
def is_staging(srv):
@@ -639,6 +642,32 @@ def atexit_register(func, *args, **kwargs):
atexit.register(_atexit_call, func, *args, **kwargs)
def parse_loose_version(version_string):
"""Parses a version string into its components.
This code and the returned tuple is based on the now deprecated
distutils.version.LooseVersion class from the Python standard library.
Two LooseVersion classes and two lists as returned by this function should
compare in the same way. See
https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/version.py#L205-L347.
:param str version_string: version string
:returns: list of parsed version string components
:rtype: list
"""
components: List[Union[int, str]]
components = [x for x in _VERSION_COMPONENT_RE.split(version_string)
if x and x != '.']
for i, obj in enumerate(components):
try:
components[i] = int(obj)
except ValueError:
pass
return components
def _atexit_call(func, *args, **kwargs):
if _INITIAL_PID == os.getpid():
func(*args, **kwargs)
+3 -2
View File
@@ -1,9 +1,9 @@
import codecs
from distutils.version import LooseVersion
import os
import re
import sys
from pkg_resources import parse_version
from setuptools import __version__ as setuptools_version
from setuptools import find_packages
from setuptools import setup
@@ -11,7 +11,7 @@ from setuptools import setup
min_setuptools_version='39.0.1'
# This conditional isn't necessary, but it provides better error messages to
# people who try to install this package with older versions of setuptools.
if LooseVersion(setuptools_version) < LooseVersion(min_setuptools_version):
if parse_version(setuptools_version) < parse_version(min_setuptools_version):
raise RuntimeError(f'setuptools {min_setuptools_version}+ is required')
# Workaround for https://bugs.python.org/issue8876, see
@@ -132,6 +132,7 @@ setup(
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
'Topic :: System :: Installation/Setup',
+46
View File
@@ -592,6 +592,19 @@ class OsInfoTest(unittest.TestCase):
self.assertEqual(cbutil.get_python_os_info(), ("testdist", "42"))
class GetStrictVersionTest(unittest.TestCase):
"""Test for certbot.util.get_strict_version."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.util import get_strict_version
return get_strict_version(*args, **kwargs)
def test_it(self):
with self.assertWarnsRegex(DeprecationWarning, "get_strict_version"):
self._call("1.2.3")
class AtexitRegisterTest(unittest.TestCase):
"""Tests for certbot.util.atexit_register."""
def setUp(self):
@@ -624,5 +637,38 @@ class AtexitRegisterTest(unittest.TestCase):
atexit_func(*args[1:], **kwargs)
class ParseLooseVersionTest(unittest.TestCase):
"""Test for certbot.util.parse_loose_version.
These tests are based on the original tests for
distutils.version.LooseVersion at
https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/tests/test_version.py#L58-L81.
"""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.util import parse_loose_version
return parse_loose_version(*args, **kwargs)
def test_less_than(self):
comparisons = (('1.5.1', '1.5.2b2'),
('3.4j', '1996.07.12'),
('2g6', '11g'),
('0.960923', '2.2beta29'),
('1.13++', '5.5.kw'))
for v1, v2 in comparisons:
self.assertLess(self._call(v1), self._call(v2))
def test_equal(self):
self.assertEqual(self._call('8.02'), self._call('8.02'))
def test_greater_than(self):
comparisons = (('161', '3.10a'),
('3.2.pl0', '3.1.1.6'))
for v1, v2 in comparisons:
self.assertGreater(self._call(v1), self._call(v2))
if __name__ == "__main__":
unittest.main() # pragma: no cover