Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
20 changes: 14 additions & 6 deletions Doc/using/windows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -867,17 +867,18 @@ For example, if the first line of your script starts with

#! /usr/bin/python

The default Python will be located and used. As many Python scripts written
to work on Unix will already have this line, you should find these scripts can
be used by the launcher without modification. If you are writing a new script
on Windows which you hope will be useful on Unix, you should use one of the
shebang lines starting with ``/usr``.
The default Python or an active virtual environment will be located and used.
As many Python scripts written to work on Unix will already have this line,
you should find these scripts can be used by the launcher without modification.
If you are writing a new script on Windows which you hope will be useful on
Unix, you should use one of the shebang lines starting with ``/usr``.

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

.. versionadded:: 3.7

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

.. versionchanged:: 3.13

Virtual commands referencing ``python`` now prefer an active virtual
environment rather than searching :envvar:`PATH`. This handles cases where
the shebang specifies ``/usr/bin/env python3`` but :file:`python3.exe` is
not present in the active environment.

The ``/usr/bin/env`` form of shebang line has one further special property.
Before looking for installed Python interpreters, this form will search the
executable :envvar:`PATH` for a Python executable matching the name provided
Expand Down
10 changes: 8 additions & 2 deletions Lib/test/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ def test_search_path(self):
[script, "-postarg"],
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
)
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())

def test_search_path_exe(self):
# Leave the .exe on the name to ensure we don't add it a second time
Expand All @@ -634,7 +634,7 @@ def test_search_path_exe(self):
[script, "-postarg"],
env={"PATH": f"{Path(sys.executable).parent};{os.getenv('PATH')}"},
)
self.assertEqual(f"{sys.executable} -prearg {script} -postarg", data["stdout"].strip())
self.assertEqual(f"X.Y.exe -prearg {script} -postarg", data["stdout"].strip())

def test_recursive_search_path(self):
stem = self.get_py_exe().stem
Expand Down Expand Up @@ -717,3 +717,9 @@ def test_literal_shebang_invalid_template(self):
f"{expect} arg1 {script}",
data["stdout"].strip(),
)

def test_shebang_command_in_venv(self):
with self.fake_venv() as (venv_exe, env):
with self.script('#! /usr/bin/env pythonanythingatall arg1') as script:
data = self.run_py([script], env=env)
self.assertEqual(data["stdout"].strip(), f"{venv_exe} arg1 {script}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Changes the :ref:`launcher` to prefer an active virtual environment when the
launched script has a shebang line using a Unix-like virtual command, even
if the command requests a specific version of Python.
38 changes: 33 additions & 5 deletions PC/launcher2.c
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
}


bool
split_parent(wchar_t *buffer, size_t bufferLength)
{
return SUCCEEDED(PathCchRemoveFileSpec(buffer, bufferLength));
}


int
_compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
{
Expand Down Expand Up @@ -414,8 +421,8 @@ typedef struct {
// if true, treats 'tag' as a non-PEP 514 filter
bool oldStyleTag;
// if true, ignores 'tag' when a high priority environment is found
// gh-92817: This is currently set when a tag is read from configuration or
// the environment, rather than the command line or a shebang line, and the
// gh-92817: This is currently set when a tag is read from configuration,
// the environment, or a shebang, rather than the command line, and the
// only currently possible high priority environment is an active virtual
// environment
bool lowPriorityTag;
Expand Down Expand Up @@ -794,6 +801,8 @@ searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
}
}

debug(L"# Search PATH for %s\n", filename);

wchar_t pathVariable[MAXLEN];
int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
if (!n) {
Expand Down Expand Up @@ -1031,8 +1040,12 @@ checkShebang(SearchInfo *search)
debug(L"Shebang: %s\n", shebang);

// Handle shebangs that we should search PATH for
int executablePathWasSetByUsrBinEnv = 0;
exitCode = searchPath(search, shebang, shebangLength);
if (exitCode != RC_NO_SHEBANG) {
if (exitCode == 0) {
// We might need to clear executable path later if we match a template
executablePathWasSetByUsrBinEnv = 1;
} else if (exitCode != RC_NO_SHEBANG) {
return exitCode;
}

Expand Down Expand Up @@ -1075,13 +1088,20 @@ checkShebang(SearchInfo *search)
}
// If we had 'python3_d' then we want to strip the '_d' (any
// '.exe' is already gone)
if (search->tagLength > 2) {
if (search->tagLength >= 2) {
const wchar_t *suffix = &search->tag[search->tagLength - 2];
if (0 == _comparePath(suffix, 2, L"_d", -1)) {
search->tagLength -= 2;
}
}
search->oldStyleTag = true;
search->lowPriorityTag = true;
if (executablePathWasSetByUsrBinEnv) {
// If it was allocated, it's in a free list, so just clear it
debug(L"# Shebang template made us forget executablePath %s\n",
search->executablePath);
search->executablePath = NULL;
}
search->executableArgs = &command[commandLength];
search->executableArgsLength = shebangLength - commandLength;
if (search->tag && search->tagLength) {
Expand Down Expand Up @@ -1765,7 +1785,15 @@ virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result)
return 0;
}

if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
DWORD attr = GetFileAttributesW(buffer);
if (INVALID_FILE_ATTRIBUTES == attr && search->lowPriorityTag) {
if (!split_parent(buffer, MAXLEN) || !join(buffer, MAXLEN, L"python.exe")) {
return 0;
}
attr = GetFileAttributesW(buffer);
}

if (INVALID_FILE_ATTRIBUTES == attr) {
debug(L"Python executable %s missing from virtual env\n", buffer);
return 0;
}
Expand Down