Skip to content

Commit 1b3bc61

Browse files
authored
gh-83180: Made launcher treat shebang 'python' tags as low priority so that active virtual environments are preferred (GH-108101)
1 parent 6139bf5 commit 1b3bc61

File tree

4 files changed

+71
-12
lines changed

4 files changed

+71
-12
lines changed

Doc/using/windows.rst

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -867,17 +867,18 @@ For example, if the first line of your script starts with
867867
868868
#! /usr/bin/python
869869
870-
The default Python will be located and used. As many Python scripts written
871-
to work on Unix will already have this line, you should find these scripts can
872-
be used by the launcher without modification. If you are writing a new script
873-
on Windows which you hope will be useful on Unix, you should use one of the
874-
shebang lines starting with ``/usr``.
870+
The default Python or an active virtual environment will be located and used.
871+
As many Python scripts written to work on Unix will already have this line,
872+
you should find these scripts can be used by the launcher without modification.
873+
If you are writing a new script on Windows which you hope will be useful on
874+
Unix, you should use one of the shebang lines starting with ``/usr``.
875875

876876
Any of the above virtual commands can be suffixed with an explicit version
877877
(either just the major version, or the major and minor version).
878878
Furthermore the 32-bit version can be requested by adding "-32" after the
879879
minor version. I.e. ``/usr/bin/python3.7-32`` will request usage of the
880-
32-bit python 3.7.
880+
32-bit Python 3.7. If a virtual environment is active, the version will be
881+
ignored and the environment will be used.
881882

882883
.. versionadded:: 3.7
883884

@@ -891,6 +892,13 @@ minor version. I.e. ``/usr/bin/python3.7-32`` will request usage of the
891892
not provably i386/32-bit". To request a specific environment, use the new
892893
:samp:`-V:{TAG}` argument with the complete tag.
893894

895+
.. versionchanged:: 3.13
896+
897+
Virtual commands referencing ``python`` now prefer an active virtual
898+
environment rather than searching :envvar:`PATH`. This handles cases where
899+
the shebang specifies ``/usr/bin/env python3`` but :file:`python3.exe` is
900+
not present in the active environment.
901+
894902
The ``/usr/bin/env`` form of shebang line has one further special property.
895903
Before looking for installed Python interpreters, this form will search the
896904
executable :envvar:`PATH` for a Python executable matching the name provided

Lib/test/test_launcher.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,25 @@ def test_literal_shebang_invalid_template(self):
717717
f"{expect} arg1 {script}",
718718
data["stdout"].strip(),
719719
)
720+
721+
def test_shebang_command_in_venv(self):
722+
stem = "python-that-is-not-on-path"
723+
724+
# First ensure that our test name doesn't exist, and the launcher does
725+
# not match any installed env
726+
with self.script(f'#! /usr/bin/env {stem} arg1') as script:
727+
data = self.run_py([script], expect_returncode=103)
728+
729+
with self.fake_venv() as (venv_exe, env):
730+
# Put a real Python (ourselves) on PATH as a distraction.
731+
# The active VIRTUAL_ENV should be preferred when the name isn't an
732+
# exact match.
733+
env["PATH"] = f"{Path(sys.executable).parent};{os.environ['PATH']}"
734+
735+
with self.script(f'#! /usr/bin/env {stem} arg1') as script:
736+
data = self.run_py([script], env=env)
737+
self.assertEqual(data["stdout"].strip(), f"{venv_exe} arg1 {script}")
738+
739+
with self.script(f'#! /usr/bin/env {Path(sys.executable).stem} arg1') as script:
740+
data = self.run_py([script], env=env)
741+
self.assertEqual(data["stdout"].strip(), f"{sys.executable} arg1 {script}")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Changes the :ref:`launcher` to prefer an active virtual environment when the
2+
launched script has a shebang line using a Unix-like virtual command, even
3+
if the command requests a specific version of Python.

PC/launcher2.c

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
195195
}
196196

197197

198+
bool
199+
split_parent(wchar_t *buffer, size_t bufferLength)
200+
{
201+
return SUCCEEDED(PathCchRemoveFileSpec(buffer, bufferLength));
202+
}
203+
204+
198205
int
199206
_compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
200207
{
@@ -414,8 +421,8 @@ typedef struct {
414421
// if true, treats 'tag' as a non-PEP 514 filter
415422
bool oldStyleTag;
416423
// if true, ignores 'tag' when a high priority environment is found
417-
// gh-92817: This is currently set when a tag is read from configuration or
418-
// the environment, rather than the command line or a shebang line, and the
424+
// gh-92817: This is currently set when a tag is read from configuration,
425+
// the environment, or a shebang, rather than the command line, and the
419426
// only currently possible high priority environment is an active virtual
420427
// environment
421428
bool lowPriorityTag;
@@ -794,6 +801,8 @@ searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
794801
}
795802
}
796803

804+
debug(L"# Search PATH for %s\n", filename);
805+
797806
wchar_t pathVariable[MAXLEN];
798807
int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
799808
if (!n) {
@@ -1031,8 +1040,11 @@ checkShebang(SearchInfo *search)
10311040
debug(L"Shebang: %s\n", shebang);
10321041

10331042
// Handle shebangs that we should search PATH for
1043+
int executablePathWasSetByUsrBinEnv = 0;
10341044
exitCode = searchPath(search, shebang, shebangLength);
1035-
if (exitCode != RC_NO_SHEBANG) {
1045+
if (exitCode == 0) {
1046+
executablePathWasSetByUsrBinEnv = 1;
1047+
} else if (exitCode != RC_NO_SHEBANG) {
10361048
return exitCode;
10371049
}
10381050

@@ -1067,21 +1079,22 @@ checkShebang(SearchInfo *search)
10671079
search->tagLength = commandLength;
10681080
// If we had 'python3.12.exe' then we want to strip the suffix
10691081
// off of the tag
1070-
if (search->tagLength > 4) {
1082+
if (search->tagLength >= 4) {
10711083
const wchar_t *suffix = &search->tag[search->tagLength - 4];
10721084
if (0 == _comparePath(suffix, 4, L".exe", -1)) {
10731085
search->tagLength -= 4;
10741086
}
10751087
}
10761088
// If we had 'python3_d' then we want to strip the '_d' (any
10771089
// '.exe' is already gone)
1078-
if (search->tagLength > 2) {
1090+
if (search->tagLength >= 2) {
10791091
const wchar_t *suffix = &search->tag[search->tagLength - 2];
10801092
if (0 == _comparePath(suffix, 2, L"_d", -1)) {
10811093
search->tagLength -= 2;
10821094
}
10831095
}
10841096
search->oldStyleTag = true;
1097+
search->lowPriorityTag = true;
10851098
search->executableArgs = &command[commandLength];
10861099
search->executableArgsLength = shebangLength - commandLength;
10871100
if (search->tag && search->tagLength) {
@@ -1095,6 +1108,11 @@ checkShebang(SearchInfo *search)
10951108
}
10961109
}
10971110

1111+
// Didn't match a template, but we found it on PATH
1112+
if (executablePathWasSetByUsrBinEnv) {
1113+
return 0;
1114+
}
1115+
10981116
// Unrecognised executables are first tried as command aliases
10991117
commandLength = 0;
11001118
while (commandLength < shebangLength && !isspace(shebang[commandLength])) {
@@ -1765,7 +1783,15 @@ virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result)
17651783
return 0;
17661784
}
17671785

1768-
if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
1786+
DWORD attr = GetFileAttributesW(buffer);
1787+
if (INVALID_FILE_ATTRIBUTES == attr && search->lowPriorityTag) {
1788+
if (!split_parent(buffer, MAXLEN) || !join(buffer, MAXLEN, L"python.exe")) {
1789+
return 0;
1790+
}
1791+
attr = GetFileAttributesW(buffer);
1792+
}
1793+
1794+
if (INVALID_FILE_ATTRIBUTES == attr) {
17691795
debug(L"Python executable %s missing from virtual env\n", buffer);
17701796
return 0;
17711797
}

0 commit comments

Comments
 (0)