Skip to content

Commit eeb71aa

Browse files
committed
Uses windowed setting in shebang processing to avoid using the console.
Fixes #216
1 parent 3ce3db9 commit eeb71aa

File tree

3 files changed

+75
-22
lines changed

3 files changed

+75
-22
lines changed

src/manage/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ def get_install_to_run(self, tag=None, script=None, *, windowed=False):
659659
if script and not tag:
660660
from .scriptutils import find_install_from_script
661661
try:
662-
return find_install_from_script(self, script)
662+
return find_install_from_script(self, script, windowed=windowed)
663663
except LookupError:
664664
pass
665665
from .installs import get_install_to_run

src/manage/scriptutils.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class NoShebang(Exception):
1212
pass
1313

1414

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

25+
# Internal logic error, but non-fatal, if it has no value
26+
assert windowed is not None
27+
2528
for i in cmd.get_installs():
2629
if is_default and i.get("default"):
27-
if is_wdefault:
30+
if is_wdefault or windowed:
2831
target = [t for t in i.get("run-for", []) if t.get("windowed")]
2932
if target:
3033
return {**i, "executable": i["prefix"] / target[0]["target"]}
3134
return {**i, "executable": i["prefix"] / i["executable"]}
3235
for a in i.get("alias", ()):
3336
if sh_cmd.match(a["name"]):
3437
LOGGER.debug("Matched alias %s in %s", a["name"], i["id"])
38+
if windowed and not a.get("windowed"):
39+
for a2 in i.get("alias", ()):
40+
if a2.get("windowed"):
41+
LOGGER.debug("Substituting alias %s for windowed=1", a2["name"])
42+
return {**i, "executable": i["prefix"] / a2["target"]}
3543
return {**i, "executable": i["prefix"] / a["target"]}
3644
if sh_cmd.full_match(PurePath(i["executable"]).name):
3745
LOGGER.debug("Matched executable name %s in %s", i["executable"], i["id"])
@@ -69,15 +77,15 @@ def _find_on_path(cmd, full_cmd):
6977
}
7078

7179

72-
def _parse_shebang(cmd, line):
80+
def _parse_shebang(cmd, line, *, windowed=None):
7381
# For /usr[/local]/bin, we look for a matching alias name.
7482
shebang = re.match(r"#!\s*/usr/(?:local/)?bin/(?!env\b)([^\\/\s]+).*", line)
7583
if shebang:
7684
# Handle the /usr[/local]/bin/python cases
7785
full_cmd = shebang.group(1)
7886
LOGGER.debug("Matching shebang: %s", full_cmd)
7987
try:
80-
return _find_shebang_command(cmd, full_cmd)
88+
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
8189
except LookupError:
8290
LOGGER.warn("A shebang '%s' was found, but could not be matched "
8391
"to an installed runtime.", full_cmd)
@@ -93,7 +101,7 @@ def _parse_shebang(cmd, line):
93101
# First do regular install lookup for /usr/bin/env shebangs
94102
full_cmd = shebang.group(1)
95103
try:
96-
return _find_shebang_command(cmd, full_cmd)
104+
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
97105
except LookupError:
98106
pass
99107
# If not, warn and do regular PATH search
@@ -125,7 +133,7 @@ def _parse_shebang(cmd, line):
125133
# A regular lookup will handle the case where the entire shebang is
126134
# a valid alias.
127135
try:
128-
return _find_shebang_command(cmd, full_cmd)
136+
return _find_shebang_command(cmd, full_cmd, windowed=windowed)
129137
except LookupError:
130138
pass
131139
if cmd.shebang_can_run_anything or cmd.shebang_can_run_anything_silently:
@@ -149,7 +157,7 @@ def _parse_shebang(cmd, line):
149157
raise NoShebang
150158

151159

152-
def _read_script(cmd, script, encoding):
160+
def _read_script(cmd, script, encoding, *, windowed=None):
153161
try:
154162
f = open(script, "r", encoding=encoding, errors="replace")
155163
except OSError as ex:
@@ -158,7 +166,7 @@ def _read_script(cmd, script, encoding):
158166
first_line = next(f, "").rstrip()
159167
if first_line.startswith("#!"):
160168
try:
161-
return _parse_shebang(cmd, first_line)
169+
return _parse_shebang(cmd, first_line, windowed=windowed)
162170
except LookupError:
163171
raise LookupError(script) from None
164172
except NoShebang:
@@ -176,12 +184,12 @@ def _read_script(cmd, script, encoding):
176184
raise LookupError(script)
177185

178186

179-
def find_install_from_script(cmd, script):
187+
def find_install_from_script(cmd, script, *, windowed=False):
180188
try:
181-
return _read_script(cmd, script, "utf-8-sig")
189+
return _read_script(cmd, script, "utf-8-sig", windowed=windowed)
182190
except NewEncoding as ex:
183191
encoding = ex.args[0]
184-
return _read_script(cmd, script, encoding)
192+
return _read_script(cmd, script, encoding, windowed=windowed)
185193

186194

187195
def _maybe_quote(a):

tests/test_scriptutils.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ def _fake_install(v, **kwargs):
2828
}
2929

3030
INSTALLS = [
31-
_fake_install("1.0", alias=[{"name": "test1.0.exe", "target": "./test-binary-1.0.exe"}]),
32-
_fake_install("1.1", alias=[{"name": "test1.1.exe", "target": "./test-binary-1.1.exe"}]),
31+
_fake_install("1.0", alias=[{"name": "test1.0.exe", "target": "./test-binary-1.0.exe"},
32+
{"name": "testw1.0.exe", "target": "./test-binary-w-1.0.exe", "windowed": 1}]),
33+
_fake_install("1.1", alias=[{"name": "test1.1.exe", "target": "./test-binary-1.1.exe"},
34+
{"name": "testw1.1.exe", "target": "./test-binary-w-1.1.exe", "windowed": 1}]),
3335
_fake_install("1.3.1", company="PythonCore"),
3436
_fake_install("1.3.2", company="PythonOther"),
3537
_fake_install("2.0", alias=[{"name": "test2.0.exe", "target": "./test-binary-2.0.exe"}]),
@@ -64,12 +66,52 @@ def test_read_shebang(fake_config, tmp_path, script, expect):
6466
script = script.encode()
6567
script_py.write_bytes(script)
6668
try:
67-
actual = find_install_from_script(fake_config, script_py)
69+
actual = find_install_from_script(fake_config, script_py, windowed=False)
6870
assert expect == actual
6971
except LookupError:
7072
assert not expect
7173

7274

75+
@pytest.mark.parametrize("script, expect, windowed", [
76+
("#! /usr/bin/test1.0\n", "test-binary-1.0.exe", False),
77+
("#! /usr/bin/test1.0\n", "test-binary-w-1.0.exe", True),
78+
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", False),
79+
("#! /usr/bin/testw1.0\n", "test-binary-w-1.0.exe", True),
80+
# No windowed option for 2.0, so picks the regular executable
81+
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", False),
82+
("#! /usr/bin/test2.0\n", "test-binary-2.0.exe", True),
83+
("#! /usr/bin/testw2.0\n", None, False),
84+
("#! /usr/bin/testw2.0\n", None, True),
85+
("#!test1.0.exe\n", "test-binary-1.0.exe", False),
86+
("#!test1.0.exe\n", "test-binary-w-1.0.exe", True),
87+
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", False),
88+
("#!testw1.0.exe\n", "test-binary-w-1.0.exe", True),
89+
("#!test1.1.exe\n", "test-binary-1.1.exe", False),
90+
("#!test1.1.exe\n", "test-binary-w-1.1.exe", True),
91+
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", False),
92+
("#!testw1.1.exe\n", "test-binary-w-1.1.exe", True),
93+
# Matching executable name won't be overridden by windowed setting
94+
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", False),
95+
("#!test-binary-1.1.exe\n", "test-binary-1.1.exe", True),
96+
("#! /usr/bin/env test1.0\n", "test-binary-1.0.exe", False),
97+
("#! /usr/bin/env test1.0\n", "test-binary-w-1.0.exe", True),
98+
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", False),
99+
("#! /usr/bin/env testw1.0\n", "test-binary-w-1.0.exe", True),
100+
])
101+
def test_read_shebang_windowed(fake_config, tmp_path, script, expect, windowed):
102+
fake_config.installs.extend(INSTALLS)
103+
104+
script_py = tmp_path / "test-script.py"
105+
if isinstance(script, str):
106+
script = script.encode()
107+
script_py.write_bytes(script)
108+
try:
109+
actual = find_install_from_script(fake_config, script_py, windowed=windowed)
110+
assert actual["executable"].match(expect)
111+
except LookupError:
112+
assert not expect
113+
114+
73115
def test_default_py_shebang(fake_config, tmp_path):
74116
inst = _fake_install("1.0", company="PythonCore", prefix=PurePath("C:\\TestRoot"), default=True)
75117
inst["run-for"] = [
@@ -78,14 +120,17 @@ def test_default_py_shebang(fake_config, tmp_path):
78120
]
79121
fake_config.installs[:] = [inst]
80122

123+
def t(n):
124+
return _find_shebang_command(fake_config, n, windowed=False)
125+
81126
# Finds the install's default executable
82-
assert _find_shebang_command(fake_config, "python")["executable"].match("test-binary-1.0.exe")
83-
assert _find_shebang_command(fake_config, "py")["executable"].match("test-binary-1.0.exe")
84-
assert _find_shebang_command(fake_config, "python1.0")["executable"].match("test-binary-1.0.exe")
127+
assert t("python")["executable"].match("test-binary-1.0.exe")
128+
assert t("py")["executable"].match("test-binary-1.0.exe")
129+
assert t("python1.0")["executable"].match("test-binary-1.0.exe")
85130
# Finds the install's run-for executable with windowed=1
86-
assert _find_shebang_command(fake_config, "pythonw")["executable"].match("pythonw.exe")
87-
assert _find_shebang_command(fake_config, "pyw")["executable"].match("pythonw.exe")
88-
assert _find_shebang_command(fake_config, "pythonw1.0")["executable"].match("pythonw.exe")
131+
assert t("pythonw")["executable"].match("pythonw.exe")
132+
assert t("pyw")["executable"].match("pythonw.exe")
133+
assert t("pythonw1.0")["executable"].match("pythonw.exe")
89134

90135

91136

@@ -104,7 +149,7 @@ def test_read_coding_comment(fake_config, tmp_path, script, expect):
104149
script = script.encode()
105150
script_py.write_bytes(script)
106151
try:
107-
_read_script(fake_config, script_py, "utf-8-sig")
152+
_read_script(fake_config, script_py, "utf-8-sig", windowed=False)
108153
except NewEncoding as enc:
109154
assert enc.args[0] == expect
110155
except LookupError:

0 commit comments

Comments
 (0)