Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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