Repository: testing-cabal/testtools (language: python)

Issue:
`testtools` fails to import with Python 3.12
At the moment, `import testtools` fails because of the eager import of `distutils` via `testtools.distutilscmd`, which provides the deprecated `TestCommand`, since `distutils` is removed from the Python 3.12 standard library.

`setuptools` still packages `distutils`, so a temporary workaround could be to add `setuptools` as a runtime dependency until `TestCommand` can be removed, or potentially the import of `TestCommand` in the root could be conditional on Python version?

Happy to provide a patch in either direction, if it would help.


Discussion / hints:
We've marked distutils support as deprecated (see NEWS); however, we haven't done any major release since then.

We could try to import from setuptools first, and fall back to importing from distutils if that doesn't work? That way at least we don't add setuptools as a runtime dependency for existing users on python < 3.12.
Yeah, I saw the deprecation - if the preferred solution is just to wait til the next major release to support Python 3.12 then that's a fine answer to my query.

Setuptools doesn't re-expose `setuptools.distutils` publicly - the `setuptools` Python wheel puts in a shim to the import system via its `_distutils_hack` package that causes `import distutils` to pull in the private `setuptools._distutils` if there's no other `distutils` in the import path.

Could it be acceptable to add the dependency on `setuptools; python_version>=3.12` to avoid burdening prior Python versions with the extra requirement?
AFAIK we just import Command from distutils, so I was proposing just doing:

```python

try:
    from setuptools.command import Command
except ModuleNotFoundError:
    from distutils.command import Command
```

and then indeed adding a ``setuptools; python_version>=3.12`` dependency.

Fix patch:
diff --git a/setup.cfg b/setup.cfg
index b988065c..b73fa3e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -25,6 +25,11 @@ classifier =
     Topic :: Software Development :: Libraries :: Python Modules
     Topic :: Software Development :: Testing
 
+[options]
+install_requires =
+    # Purely to support the deprecated `TestCommand` until 3.0.
+    setuptools; python_version>='3.12'
+
 [extras]
 test =
   testscenarios


Test patch:
diff --git a/testtools/distutilscmd.py b/testtools/distutilscmd.py
index eedc4ff2..daf761fa 100644
--- a/testtools/distutilscmd.py
+++ b/testtools/distutilscmd.py
@@ -5,8 +5,12 @@
 import sys
 import warnings
 
-from distutils.core import Command
-from distutils.errors import DistutilsOptionError
+try:
+    from setuptools import Command
+    from setuptools.errors import OptionError
+except ModuleNotFoundError:
+    from distutils.core import Command
+    from distutils.errors import DistutilsOptionError as OptionError
 
 from testtools.run import TestProgram, TestToolsTestRunner
 
@@ -47,12 +51,12 @@ def initialize_options(self):
     def finalize_options(self):
         if self.test_suite is None:
             if self.test_module is None:
-                raise DistutilsOptionError(
+                raise OptionError(
                     "You must specify a module or a suite to run tests from")
             else:
                 self.test_suite = self.test_module+".test_suite"
         elif self.test_module:
-            raise DistutilsOptionError(
+            raise OptionError(
                 "You may specify a module or a suite, but not both")
         self.test_args = [self.test_suite]
         if self.verbose: