Skip to content

Commit

Permalink
Merge pull request #248 from LarrysGIT/main
Browse files Browse the repository at this point in the history
Extend Windows system executable extensions
  • Loading branch information
jaraco committed Apr 20, 2024
2 parents 6c1cb08 + d12c13e commit 6a7ecb7
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 29 deletions.
51 changes: 32 additions & 19 deletions distutils/spawn.py
Expand Up @@ -7,6 +7,8 @@
"""

import os
import pathlib
import platform
import subprocess
import sys

Expand Down Expand Up @@ -68,19 +70,19 @@ def spawn(cmd, search_path=1, verbose=0, dry_run=0, env=None): # noqa: C901
raise DistutilsExecError(f"command {cmd!r} failed with exit code {exitcode}")


def find_executable(executable, path=None):
"""Tries to find 'executable' in the directories listed in 'path'.
A string listing directories separated by 'os.pathsep'; defaults to
os.environ['PATH']. Returns the complete filename or None if not found.
def _executable_candidates(executable: pathlib.Path):
"""
Given an executable, yields common executable variants.
"""
_, ext = os.path.splitext(executable)
if (sys.platform == 'win32') and (ext != '.exe'):
executable = executable + '.exe'
yield executable
if platform.system() != 'Windows':
return
exts = os.environ.get('PATHEXT').split(os.pathsep)
unique = (ext for ext in exts if executable.suffix.casefold() != ext.casefold())
yield from map(executable.with_suffix, unique)

if os.path.isfile(executable):
return executable

def _search_paths(path):
if path is None:
path = os.environ.get('PATH', None)
if path is None:
Expand All @@ -94,12 +96,23 @@ def find_executable(executable, path=None):

# PATH='' doesn't match, whereas PATH=':' looks in the current directory
if not path:
return None

paths = path.split(os.pathsep)
for p in paths:
f = os.path.join(p, executable)
if os.path.isfile(f):
# the file exists, we have a shot at spawn working
return f
return None
return ()

return map(pathlib.Path, path.split(os.pathsep))


def find_executable(executable, path=None):
"""Tries to find 'executable' in the directories listed in 'path'.
A string listing directories separated by 'os.pathsep'; defaults to
os.environ['PATH']. Returns the complete filename or None if not found.
"""
if os.path.isfile(executable):
return executable

found = (
os.fspath(exe)
for p in _search_paths(path)
for exe in filter(pathlib.Path.is_file, _executable_candidates(p / executable))
)
return next(found, None)
59 changes: 49 additions & 10 deletions distutils/tests/test_spawn.py
@@ -1,6 +1,8 @@
"""Tests for distutils.spawn."""

import os
import platform
import random
import stat
import sys
import unittest.mock as mock
Expand Down Expand Up @@ -45,25 +47,20 @@ def test_spawn(self):
spawn([exe]) # should work without any error

def test_find_executable(self, tmp_path):
program_noeext = 'program'
# Give the temporary program an ".exe" suffix for all.
# It's needed on Windows and not harmful on other platforms.
program = program_noeext + ".exe"

program_path = tmp_path / program
program_path.write_text("", encoding='utf-8')
program_path.chmod(stat.S_IXUSR)
program_path = self._make_executable(tmp_path, '.exe')
program = program_path.name
program_noeext = program_path.with_suffix('').name
filename = str(program_path)
tmp_dir = path.Path(tmp_path)

# test path parameter
rv = find_executable(program, path=tmp_dir)
assert rv == filename

if sys.platform == 'win32':
if sys.platform == 'win32': # pragma: no cover
# test without ".exe" extension
rv = find_executable(program_noeext, path=tmp_dir)
assert rv == filename
assert os.path.samefile(rv, filename)

# test find in the current directory
with tmp_dir:
Expand Down Expand Up @@ -121,7 +118,49 @@ def test_find_executable(self, tmp_path):
rv = find_executable(program)
assert rv == filename

def test_find_executable_alt_ext(self, tmp_path, windows_pathext):
"""
On Windows with PATHEXT set, also executables matching those.
"""
ext = random.choice(os.environ['PATHEXT'].split(os.pathsep))
program = self._make_executable(tmp_path, ext)
rv = find_executable(program.with_suffix('').name, path=str(tmp_path))
assert rv == str(program)

@staticmethod
def _make_executable(tmp_path, ext):
# Give the temporary program a suffix regardless of platform.
# It's needed on Windows and not harmful on others.
program = tmp_path.joinpath('program').with_suffix(ext)
program.write_text("", encoding='utf-8')
program.chmod(stat.S_IXUSR)
return program

def test_spawn_missing_exe(self):
with pytest.raises(DistutilsExecError) as ctx:
spawn(['does-not-exist'])
assert "command 'does-not-exist' failed" in str(ctx.value)


@pytest.fixture
def windows_pathext(monkeypatch):
"""
Set PATHEXT as if on Windows.
"""
monkeypatch.setattr(platform, 'system', lambda: 'Windows')
typical_exts = [
'.COM',
'.EXE',
'.BAT',
'.CMD',
'.VBS',
'.VBE',
'.JS',
'.JSE',
'.WSF',
'.WSH',
'.MSC',
'.PY',
'.PYW',
]
monkeypatch.setenv('PATHEXT', os.pathsep.join(typical_exts))

0 comments on commit 6a7ecb7

Please sign in to comment.