jenkins-bot submitted this change.

View Change

Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
[IMPR] Remove distutils

Python 3.10 deprecated distutils and it will be removed in 3.12. They advise
that we use packaging instead...

https://docs.python.org/3.10/whatsnew/3.10.html#distutils-deprecated

We mostly used distutils for version parsing, for which packaging provides
a simple replacement...

https://stackoverflow.com/a/11887885
https://packaging.pypa.io/en/latest/version.html#packaging.version.parse

Setuptools contains a copy of packaging under 'pkg_resources.extern.packaging'
and pkg_resources.parse_version() is effectively an alias of
packaging.version.parse(). Since we already have a depency on Setuptools
this uses its copy.

As for get_python_lib() sysconfig can provide the same...

>>> import distutils.sysconfig
>>> distutils.sysconfig.get_python_lib(standard_lib=True)
'/usr/lib/python3.8'

>>> import sysconfig
>>> sysconfig.get_paths()['stdlib']
'/usr/lib/python3.8'

Bug: T274696
Change-Id: I6245895d4249d32cfae34043eed489cc94ddea2e
---
M pywikibot/comms/eventstreams.py
M pywikibot/data/mysql.py
M pywikibot/tools/__init__.py
M pywikibot/version.py
M tests/tools_tests.py
5 files changed, 76 insertions(+), 24 deletions(-)

diff --git a/pywikibot/comms/eventstreams.py b/pywikibot/comms/eventstreams.py
index 662598a..6964089 100644
--- a/pywikibot/comms/eventstreams.py
+++ b/pywikibot/comms/eventstreams.py
@@ -12,11 +12,11 @@
#
# Distributed under the terms of the MIT license.
#
-from distutils.version import LooseVersion
-from functools import partial
import json
import socket

+from functools import partial
+from pkg_resources import parse_version
from typing import Optional

from requests import __version__ as requests_version
@@ -31,7 +31,7 @@
from pywikibot import config, debug, Timestamp, Site, warning
from pywikibot.tools import deprecated_args

-if LooseVersion(requests_version) < LooseVersion('2.20.1'):
+if parse_version(requests_version) < parse_version('2.20.1'):
raise ImportError(
'requests >= 2.20.1 is required for EventStreams;\n'
"install it with 'pip install \"requests>=2.20.1\"'\n")
diff --git a/pywikibot/data/mysql.py b/pywikibot/data/mysql.py
index 9ab934b..f3caa95 100644
--- a/pywikibot/data/mysql.py
+++ b/pywikibot/data/mysql.py
@@ -4,7 +4,8 @@
#
# Distributed under the terms of the MIT license.
#
-from distutils.version import LooseVersion
+import pkg_resources
+
from typing import Optional

import pywikibot
@@ -62,7 +63,10 @@
charset='utf8',
defer_connect=query == 'test', # for tests
**credentials)
- if LooseVersion(pymysql.__version__) < LooseVersion('1.0.0'):
+
+ pymysql_version = pkg_resources.parse_version(pymysql.__version__)
+
+ if pymysql_version < pkg_resources.parse_version('1.0.0'):
from contextlib import closing
connection = closing(connection)

diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index f891c09..77a5494 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -10,6 +10,7 @@
import inspect
import itertools
import os
+import pkg_resources
import queue
import re
import stat
@@ -23,7 +24,6 @@
from collections import defaultdict
from contextlib import suppress
from datetime import datetime
-from distutils.version import LooseVersion, Version
from functools import wraps
from importlib import import_module
from inspect import getfullargspec
@@ -78,24 +78,25 @@


def has_module(module, version=None):
- """Check whether a module can be imported."""
+ """Check if a module can be imported."""
try:
m = import_module(module)
except ImportError:
- pass
+ return False
else:
- if version is None:
- return True
- try:
- module_version = LooseVersion(m.__version__)
- except AttributeError:
- pass
- else:
- if module_version >= LooseVersion(version):
- return True
- warn('Module version {} is lower than requested version {}'
- .format(module_version, version), ImportWarning)
- return False
+ if version:
+ if not hasattr(m, '__version__'):
+ return False
+
+ required_version = pkg_resources.parse_version(version)
+ module_version = pkg_resources.parse_version(m.__version__)
+
+ if module_version < required_version:
+ warn('Module version {} is lower than requested version {}'
+ .format(module_version, required_version), ImportWarning)
+ return False
+
+ return True


def empty_iterator():
@@ -501,7 +502,7 @@
return first_upper(username)


-class MediaWikiVersion(Version):
+class MediaWikiVersion:

"""
Version object to allow comparing 'wmf' versions with normal ones.
@@ -526,6 +527,15 @@
MEDIAWIKI_VERSION = re.compile(
r'(\d+(?:\.\d+)+)(-?wmf\.?(\d+)|alpha|beta(\d+)|-?rc\.?(\d+)|.*)?$')

+ def __init__(self, version_str):
+ """
+ Initializer.
+
+ @param version_str: version to parse
+ @type version: str
+ """
+ self.parse(version_str)
+
@classmethod
def from_generator(cls, generator):
"""Create instance using the generator string."""
@@ -583,6 +593,21 @@
return -1
return 0

+ def __eq__(self, other):
+ return self._cmp(other) == 0
+
+ def __lt__(self, other):
+ return self._cmp(other) < 0
+
+ def __le__(self, other):
+ return self._cmp(other) <= 0
+
+ def __gt__(self, other):
+ return self._cmp(other) > 0
+
+ def __ge__(self, other):
+ return self._cmp(other) >= 0
+

class ThreadedGenerator(threading.Thread):

diff --git a/pywikibot/version.py b/pywikibot/version.py
index 20242f5..a71772b 100644
--- a/pywikibot/version.py
+++ b/pywikibot/version.py
@@ -11,11 +11,11 @@
import socket
import subprocess
import sys
+import sysconfig
import time
import xml.dom.minidom

from contextlib import closing, suppress
-from distutils.sysconfig import get_python_lib
from importlib import import_module
from io import BytesIO
from typing import Optional
@@ -415,7 +415,7 @@
if not modules:
modules = sys.modules.keys()

- std_lib_dir = pathlib.Path(get_python_lib(standard_lib=True))
+ std_lib_dir = pathlib.Path(sysconfig.get_paths()['stdlib'])

root_packages = {key.split('.')[0] for key in modules}

diff --git a/tests/tools_tests.py b/tests/tools_tests.py
index ea0c7a3..cda8191 100644
--- a/tests/tools_tests.py
+++ b/tests/tools_tests.py
@@ -16,7 +16,7 @@
from importlib import import_module

from pywikibot import tools
-from pywikibot.tools import classproperty
+from pywikibot.tools import classproperty, has_module, suppress_warnings

from tests import join_xml_data_path, mock
from tests.aspects import require_modules, TestCase
@@ -742,6 +742,29 @@
self.assertEqual(result, 'HelloWorld')


+class TestHasModule(TestCase):
+
+ """Unit test class for has_module."""
+
+ net = False
+
+ def test_when_present(self):
+ """Test when the module is available."""
+ self.assertTrue(has_module('setuptools'))
+ self.assertTrue(has_module('setuptools', '1.0'))
+
+ def test_when_missing(self):
+ """Test when the module is unavailable."""
+ self.assertFalse(has_module('no-such-module'))
+
+ @suppress_warnings(
+ r'^Module version .* is lower than requested version 99999$',
+ ImportWarning)
+ def test_when_insufficient_version(self):
+ """Test when the module is older than what we need."""
+ self.assertFalse(has_module('setuptools', '99999'))
+
+
if __name__ == '__main__': # pragma: no cover
with suppress(SystemExit):
unittest.main()

To view, visit change 679612. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I6245895d4249d32cfae34043eed489cc94ddea2e
Gerrit-Change-Number: 679612
Gerrit-PatchSet: 3
Gerrit-Owner: Damian <atagar1@gmail.com>
Gerrit-Reviewer: Xqt <info@gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged