Repository: common-workflow-language/cwltool

Issue:
replace distutils.spawn usage
Distutils is deprecated since 2014 and will be removed from the Python standard library in Python 3.12

https://www.python.org/dev/peps/pep-0632/

`distutils.spawn.find_executable` → `shutil.which`
`distutils.spawn.spawn` → `subprocess.run`

https://github.com/common-workflow-language/cwltool/search?q=distutils


Discussion:
@mr-c I would like to work on this issue.

Resolving patch:
diff --git a/cwltool/docker.py b/cwltool/docker.py
--- a/cwltool/docker.py
+++ b/cwltool/docker.py
@@ -8,7 +8,6 @@
 import subprocess  # nosec
 import sys
 import threading
-from distutils import spawn
 from io import StringIO  # pylint: disable=redefined-builtin
 from typing import Callable, Dict, List, MutableMapping, Optional, Set, Tuple, cast
 
@@ -235,7 +234,7 @@ def get_from_requirements(
         force_pull: bool,
         tmp_outdir_prefix: str,
     ) -> Optional[str]:
-        if not spawn.find_executable("docker"):
+        if not shutil.which("docker"):
             raise WorkflowException("docker executable is not available")
 
         if self.get_image(
diff --git a/cwltool/singularity.py b/cwltool/singularity.py
--- a/cwltool/singularity.py
+++ b/cwltool/singularity.py
@@ -5,7 +5,6 @@
 import re
 import shutil
 import sys
-from distutils import spawn
 from subprocess import (  # nosec
     DEVNULL,
     PIPE,
@@ -267,7 +266,7 @@ def get_from_requirements(
 
         (e.g. hello-world-latest.{img,sif}).
         """
-        if not bool(spawn.find_executable("singularity")):
+        if not bool(shutil.which("singularity")):
             raise WorkflowException("singularity executable is not available")
 
         if not self.get_image(cast(Dict[str, str], r), pull_image, force_pull):


Test patch:
diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py
--- a/tests/test_dependencies.py
+++ b/tests/test_dependencies.py
@@ -1,5 +1,5 @@
 import os
-from distutils import spawn
+from shutil import which
 
 import pytest
 
@@ -35,9 +35,7 @@ def test_bioconda() -> None:
     assert error_code == 0, stderr
 
 
-@pytest.mark.skipif(
-    not spawn.find_executable("modulecmd"), reason="modulecmd not installed"
-)
+@pytest.mark.skipif(not which("modulecmd"), reason="modulecmd not installed")
 def test_modules() -> None:
     wflow = get_data("tests/random_lines.cwl")
     job = get_data("tests/random_lines_job.json")
diff --git a/tests/test_docker.py b/tests/test_docker.py
--- a/tests/test_docker.py
+++ b/tests/test_docker.py
@@ -1,4 +1,5 @@
-from distutils import spawn
+"""Tests for docker engine."""
+from shutil import which
 from pathlib import Path
 
 from cwltool.main import main
@@ -35,7 +36,7 @@ def test_docker_iwdr() -> None:
             "hello",
         ]
     )
-    docker_installed = bool(spawn.find_executable("docker"))
+    docker_installed = bool(which("docker"))
     if docker_installed:
         assert result_code == 0
     else:
diff --git a/tests/test_singularity.py b/tests/test_singularity.py
--- a/tests/test_singularity.py
+++ b/tests/test_singularity.py
@@ -1,4 +1,5 @@
-import distutils.spawn
+"""Tests to find local Singularity image."""
+import shutil
 import os
 import sys
 from pathlib import Path
@@ -70,7 +71,7 @@ def test_singularity_iwdr() -> None:
             "hello",
         ]
     )
-    singularity_installed = bool(distutils.spawn.find_executable("singularity"))
+    singularity_installed = bool(shutil.which("singularity"))
     if singularity_installed:
         assert result_code == 0
     else:
diff --git a/tests/util.py b/tests/util.py
--- a/tests/util.py
+++ b/tests/util.py
@@ -1,5 +1,5 @@
 import contextlib
-import distutils.spawn  # pylint: disable=no-name-in-module,import-error
+import shutil  # pylint: disable=no-name-in-module,import-error
 import functools
 import io
 import os
@@ -53,29 +53,28 @@ def get_data(filename: str) -> str:
 
 
 needs_docker = pytest.mark.skipif(
-    not bool(distutils.spawn.find_executable("docker")),
+    not bool(shutil.which("docker")),
     reason="Requires the docker executable on the system path.",
 )
 
 needs_singularity = pytest.mark.skipif(
-    not bool(distutils.spawn.find_executable("singularity")),
+    not bool(shutil.which("singularity")),
     reason="Requires the singularity executable on the system path.",
 )
 
 needs_singularity_2_6 = pytest.mark.skipif(
-    not bool(distutils.spawn.find_executable("singularity") and is_version_2_6()),
+    not bool(shutil.which("singularity") and is_version_2_6()),
     reason="Requires that version 2.6.x of singularity executable version is on the system path.",
 )
 
 needs_singularity_3_or_newer = pytest.mark.skipif(
-    (not bool(distutils.spawn.find_executable("singularity")))
-    or (not is_version_3_or_newer()),
+    (not bool(shutil.which("singularity"))) or (not is_version_3_or_newer()),
     reason="Requires that version 3.x of singularity executable version is on the system path.",
 )
 
 
 windows_needs_docker = pytest.mark.skipif(
-    onWindows() and not bool(distutils.spawn.find_executable("docker")),
+    onWindows() and not bool(shutil.which("docker")),
     reason="Running this test on MS Windows requires the docker executable "
     "on the system path.",
 )