Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions pre_commit/languages/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ def _find_by_py_launcher(
return None


def _impl_exe_name() -> str:
if sys.implementation.name == 'cpython': # pragma: cpython cover
return 'python'
else: # pragma: cpython no cover
return sys.implementation.name # pypy mostly


def _find_by_sys_executable() -> str | None:
def _norm(path: str) -> str | None:
_, exe = os.path.split(path.lower())
Expand All @@ -100,18 +107,25 @@ def _norm(path: str) -> str | None:

@functools.lru_cache(maxsize=1)
def get_default_version() -> str: # pragma: no cover (platform dependent)
# First attempt from `sys.executable` (or the realpath)
exe = _find_by_sys_executable()
if exe:
return exe

# Next try the `pythonX.X` executable
exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
if find_executable(exe):
return exe

if _find_by_py_launcher(exe):
return exe
v_major = f'{sys.version_info[0]}'
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'

# attempt the likely implementation exe
for potential in (v_minor, v_major):
exe = f'{_impl_exe_name()}{potential}'
if find_executable(exe):
return exe

# next try `sys.executable` (or the realpath)
maybe_exe = _find_by_sys_executable()
if maybe_exe:
return maybe_exe

# maybe on windows we can find it via py launcher?
if sys.platform == 'win32': # pragma: win32 cover
exe = f'python{v_minor}'
if _find_by_py_launcher(exe):
return exe

# We tried!
return C.DEFAULT
Expand Down
67 changes: 67 additions & 0 deletions tests/languages/python_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pre_commit.prefix import Prefix
from pre_commit.util import make_executable
from pre_commit.util import win_exe
from testing.auto_namedtuple import auto_namedtuple
from testing.language_helpers import run_language


Expand All @@ -34,6 +35,72 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir):
assert python._read_pyvenv_cfg(pyvenv_cfg) == expected


def _get_default_version(
*,
impl: str,
exe: str,
found: set[str],
version: tuple[int, int],
) -> str:
sys_exe = f'/fake/path/{exe}'
sys_impl = auto_namedtuple(name=impl)
sys_ver = auto_namedtuple(major=version[0], minor=version[1])

def find_exe(s):
if s in found:
return f'/fake/path/found/{exe}'
else:
return None

with (
mock.patch.object(sys, 'implementation', sys_impl),
mock.patch.object(sys, 'executable', sys_exe),
mock.patch.object(sys, 'version_info', sys_ver),
mock.patch.object(python, 'find_executable', find_exe),
):
return python.get_default_version.__wrapped__()


def test_default_version_sys_executable_found():
ret = _get_default_version(
impl='cpython',
exe='python3.12',
found={'python3.12'},
version=(3, 12),
)
assert ret == 'python3.12'


def test_default_version_picks_specific_when_found():
ret = _get_default_version(
impl='cpython',
exe='python3',
found={'python3', 'python3.12'},
version=(3, 12),
)
assert ret == 'python3.12'


def test_default_version_picks_pypy_versioned_exe():
ret = _get_default_version(
impl='pypy',
exe='python',
found={'pypy3.12', 'python3'},
version=(3, 12),
)
assert ret == 'pypy3.12'


def test_default_version_picks_pypy_unversioned_exe():
ret = _get_default_version(
impl='pypy',
exe='python',
found={'pypy3', 'python3'},
version=(3, 12),
)
assert ret == 'pypy3'


def test_norm_version_expanduser():
home = os.path.expanduser('~')
if sys.platform == 'win32': # pragma: win32 cover
Expand Down