Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/manage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ def get_install_to_run(self, tag=None, script=None, *, windowed=False):
if script and not tag:
from .scriptutils import find_install_from_script
try:
return find_install_from_script(self, script)
return find_install_from_script(self, script, windowed=windowed)
except LookupError:
pass
from .installs import get_install_to_run
Expand Down
30 changes: 19 additions & 11 deletions src/manage/scriptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class NoShebang(Exception):
pass


def _find_shebang_command(cmd, full_cmd):
def _find_shebang_command(cmd, full_cmd, *, windowed=None):
sh_cmd = PurePath(full_cmd)
# HACK: Assuming alias/executable suffix is '.exe' here
# (But correctly assuming we can't use with_suffix() or .stem)
Expand All @@ -22,16 +22,24 @@ def _find_shebang_command(cmd, full_cmd):
is_wdefault = sh_cmd.match("pythonw.exe") or sh_cmd.match("pyw.exe")
is_default = is_wdefault or sh_cmd.match("python.exe") or sh_cmd.match("py.exe")

# Internal logic error, but non-fatal, if it has no value
assert windowed is not None

for i in cmd.get_installs():
if is_default and i.get("default"):
if is_wdefault:
if is_wdefault or windowed:
target = [t for t in i.get("run-for", []) if t.get("windowed")]
if target:
return {**i, "executable": i["prefix"] / target[0]["target"]}
return {**i, "executable": i["prefix"] / i["executable"]}
for a in i.get("alias", ()):
if sh_cmd.match(a["name"]):
LOGGER.debug("Matched alias %s in %s", a["name"], i["id"])
if windowed and not a.get("windowed"):
for a2 in i.get("alias", ()):
if a2.get("windowed"):
LOGGER.debug("Substituting alias %s for windowed=1", a2["name"])
return {**i, "executable": i["prefix"] / a2["target"]}
return {**i, "executable": i["prefix"] / a["target"]}
if sh_cmd.full_match(PurePath(i["executable"]).name):
LOGGER.debug("Matched executable name %s in %s", i["executable"], i["id"])
Expand Down Expand Up @@ -69,15 +77,15 @@ def _find_on_path(cmd, full_cmd):
}


def _parse_shebang(cmd, line):
def _parse_shebang(cmd, line, *, windowed=None):
# For /usr[/local]/bin, we look for a matching alias name.
shebang = re.match(r"#!\s*/usr/(?:local/)?bin/(?!env\b)([^\\/\s]+).*", line)
if shebang:
# Handle the /usr[/local]/bin/python cases
full_cmd = shebang.group(1)
LOGGER.debug("Matching shebang: %s", full_cmd)
try:
return _find_shebang_command(cmd, full_cmd)
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
except LookupError:
LOGGER.warn("A shebang '%s' was found, but could not be matched "
"to an installed runtime.", full_cmd)
Expand All @@ -93,7 +101,7 @@ def _parse_shebang(cmd, line):
# First do regular install lookup for /usr/bin/env shebangs
full_cmd = shebang.group(1)
try:
return _find_shebang_command(cmd, full_cmd)
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
except LookupError:
pass
# If not, warn and do regular PATH search
Expand Down Expand Up @@ -125,7 +133,7 @@ def _parse_shebang(cmd, line):
# A regular lookup will handle the case where the entire shebang is
# a valid alias.
try:
return _find_shebang_command(cmd, full_cmd)
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
except LookupError:
pass
if cmd.shebang_can_run_anything or cmd.shebang_can_run_anything_silently:
Expand All @@ -149,7 +157,7 @@ def _parse_shebang(cmd, line):
raise NoShebang


def _read_script(cmd, script, encoding):
def _read_script(cmd, script, encoding, *, windowed=None):
try:
f = open(script, "r", encoding=encoding, errors="replace")
except OSError as ex:
Expand All @@ -158,7 +166,7 @@ def _read_script(cmd, script, encoding):
first_line = next(f, "").rstrip()
if first_line.startswith("#!"):
try:
return _parse_shebang(cmd, first_line)
return _parse_shebang(cmd, first_line, windowed=windowed)
except LookupError:
raise LookupError(script) from None
except NoShebang:
Expand All @@ -176,12 +184,12 @@ def _read_script(cmd, script, encoding):
raise LookupError(script)


def find_install_from_script(cmd, script):
def find_install_from_script(cmd, script, *, windowed=False):
try:
return _read_script(cmd, script, "utf-8-sig")
return _read_script(cmd, script, "utf-8-sig", windowed=windowed)
except NewEncoding as ex:
encoding = ex.args[0]
return _read_script(cmd, script, encoding)
return _read_script(cmd, script, encoding, windowed=windowed)


def _maybe_quote(a):
Expand Down
65 changes: 55 additions & 10 deletions tests/test_scriptutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def _fake_install(v, **kwargs):
}

INSTALLS = [
_fake_install("1.0", alias=[{"name": "test1.0.exe", "target": "./test-binary-1.0.exe"}]),
_fake_install("1.1", alias=[{"name": "test1.1.exe", "target": "./test-binary-1.1.exe"}]),
_fake_install("1.0", alias=[{"name": "test1.0.exe", "target": "./test-binary-1.0.exe"},
{"name": "testw1.0.exe", "target": "./test-binary-w-1.0.exe", "windowed": 1}]),
_fake_install("1.1", alias=[{"name": "test1.1.exe", "target": "./test-binary-1.1.exe"},
{"name": "testw1.1.exe", "target": "./test-binary-w-1.1.exe", "windowed": 1}]),
_fake_install("1.3.1", company="PythonCore"),
_fake_install("1.3.2", company="PythonOther"),
_fake_install("2.0", alias=[{"name": "test2.0.exe", "target": "./test-binary-2.0.exe"}]),
Expand Down Expand Up @@ -64,12 +66,52 @@ def test_read_shebang(fake_config, tmp_path, script, expect):
script = script.encode()
script_py.write_bytes(script)
try:
actual = find_install_from_script(fake_config, script_py)
actual = find_install_from_script(fake_config, script_py, windowed=False)
assert expect == actual
except LookupError:
assert not expect


@pytest.mark.parametrize("script, expect, windowed", [
("#! /usr/bin/test1.0\n", "test-binary-1.0.exe", False),
("#! /usr/bin/test1.0\n", "test-binary-w-1.0.exe", True),
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", False),
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", True),
# No windowed option for 2.0, so picks the regular executable
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", False),
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", True),
("#! /usr/bin/testw2.0\n", None, False),
("#! /usr/bin/testw2.0\n", None, True),
("#!test1.0.exe\n", "test-binary-1.0.exe", False),
("#!test1.0.exe\n", "test-binary-w-1.0.exe", True),
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", False),
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", True),
("#!test1.1.exe\n", "test-binary-1.1.exe", False),
("#!test1.1.exe\n", "test-binary-w-1.1.exe", True),
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", False),
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", True),
# Matching executable name won't be overridden by windowed setting
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", False),
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", True),
("#! /usr/bin/env test1.0\n", "test-binary-1.0.exe", False),
("#! /usr/bin/env test1.0\n", "test-binary-w-1.0.exe", True),
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", False),
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", True),
])
def test_read_shebang_windowed(fake_config, tmp_path, script, expect, windowed):
fake_config.installs.extend(INSTALLS)

script_py = tmp_path / "test-script.py"
if isinstance(script, str):
script = script.encode()
script_py.write_bytes(script)
try:
actual = find_install_from_script(fake_config, script_py, windowed=windowed)
assert actual["executable"].match(expect)
except LookupError:
assert not expect


def test_default_py_shebang(fake_config, tmp_path):
inst = _fake_install("1.0", company="PythonCore", prefix=PurePath("C:\\TestRoot"), default=True)
inst["run-for"] = [
Expand All @@ -78,14 +120,17 @@ def test_default_py_shebang(fake_config, tmp_path):
]
fake_config.installs[:] = [inst]

def t(n):
return _find_shebang_command(fake_config, n, windowed=False)

# Finds the install's default executable
assert _find_shebang_command(fake_config, "python")["executable"].match("test-binary-1.0.exe")
assert _find_shebang_command(fake_config, "py")["executable"].match("test-binary-1.0.exe")
assert _find_shebang_command(fake_config, "python1.0")["executable"].match("test-binary-1.0.exe")
assert t("python")["executable"].match("test-binary-1.0.exe")
assert t("py")["executable"].match("test-binary-1.0.exe")
assert t("python1.0")["executable"].match("test-binary-1.0.exe")
# Finds the install's run-for executable with windowed=1
assert _find_shebang_command(fake_config, "pythonw")["executable"].match("pythonw.exe")
assert _find_shebang_command(fake_config, "pyw")["executable"].match("pythonw.exe")
assert _find_shebang_command(fake_config, "pythonw1.0")["executable"].match("pythonw.exe")
assert t("pythonw")["executable"].match("pythonw.exe")
assert t("pyw")["executable"].match("pythonw.exe")
assert t("pythonw1.0")["executable"].match("pythonw.exe")



Expand All @@ -104,7 +149,7 @@ def test_read_coding_comment(fake_config, tmp_path, script, expect):
script = script.encode()
script_py.write_bytes(script)
try:
_read_script(fake_config, script_py, "utf-8-sig")
_read_script(fake_config, script_py, "utf-8-sig", windowed=False)
except NewEncoding as enc:
assert enc.args[0] == expect
except LookupError:
Expand Down
Loading