From dda57a599954ffda5e84fb144799cc4bc68ee2c8 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 18:37:59 +0100 Subject: [PATCH 1/3] Adds fallback search for 'python.exe' style shebangs. Fixes #91 --- src/manage/pathutils.py | 26 +++++++++++--------------- src/manage/scriptutils.py | 11 ++++++++--- tests/conftest.py | 5 +++++ tests/test_pathutils.py | 10 ++++++++++ tests/test_scriptutils.py | 8 +++++++- 5 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 tests/test_pathutils.py diff --git a/src/manage/pathutils.py b/src/manage/pathutils.py index da9ab4f..494faca 100644 --- a/src/manage/pathutils.py +++ b/src/manage/pathutils.py @@ -124,22 +124,18 @@ def match(self, pattern, full_match=False): if "*" not in p: return m.casefold() == p - allow_skip = False + must_start_with = not p.startswith("*") for bit in p.split("*"): - if bit: - if allow_skip: - allow_skip = False - try: - i = m.index(bit) - except ValueError: - return False - m = m[i + len(bit):] - elif m.startswith(bit): - m = m[len(bit):] - else: - return False - else: - allow_skip = True + if not bit: + continue + try: + i = m.index(bit) + except ValueError: + return False + if must_start_with and i != 0: + return False + m = m[i + len(bit):] + must_start_with = False return True diff --git a/src/manage/scriptutils.py b/src/manage/scriptutils.py index 99e99da..d4b9715 100644 --- a/src/manage/scriptutils.py +++ b/src/manage/scriptutils.py @@ -24,7 +24,7 @@ def _find_shebang_command(cmd, full_cmd): for i in cmd.get_installs(): if is_default and i.get("default"): return i - for a in i["alias"]: + for a in i.get("alias", ()): if sh_cmd.match(a["name"]): LOGGER.debug("Matched alias %s in %s", a["name"], i["id"]) return {**i, "executable": i["prefix"] / a["target"]} @@ -34,8 +34,13 @@ def _find_shebang_command(cmd, full_cmd): if sh_cmd.match(i["executable"]): LOGGER.debug("Matched executable %s in %s", i["executable"], i["id"]) return i - else: - raise LookupError + + # Fallback search for 'python.exe' shebangs + if sh_cmd.match("python*.exe"): + tag = sh_cmd.name[6:-4] + return cmd.get_install_to_run(f"PythonCore/{tag}") + + raise LookupError def _find_on_path(cmd, full_cmd): diff --git a/tests/conftest.py b/tests/conftest.py index 200be10..d059e71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -143,6 +143,11 @@ def __init__(self, installs=[]): def get_installs(self): return self.installs + def get_install_to_run(self, tag): + company, _, tag = (tag.replace("/", "\\")).rpartition("\\") + return [i for i in self.installs + if i["tag"] == tag and (not company or i["company"] == company)][0] + @pytest.fixture def fake_config(): diff --git a/tests/test_pathutils.py b/tests/test_pathutils.py new file mode 100644 index 0000000..65d7387 --- /dev/null +++ b/tests/test_pathutils.py @@ -0,0 +1,10 @@ +import pytest + +from manage.pathutils import Path, PurePath + +def test_path_match(): + p = Path("python3.12.exe") + assert p.match("*.exe") + assert p.match("python*") + assert p.match("python*.exe") + assert p.match("p*3.*.exe") diff --git a/tests/test_scriptutils.py b/tests/test_scriptutils.py index f33b5a6..05d3309 100644 --- a/tests/test_scriptutils.py +++ b/tests/test_scriptutils.py @@ -17,7 +17,7 @@ def _fake_install(v, **kwargs): return { - "company": "Test", + "company": kwargs.get("company", "Test"), "id": f"test-{v}", "tag": str(v), "version": str(v), @@ -29,6 +29,8 @@ 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.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"}]), ] @@ -46,6 +48,10 @@ def _fake_install(v, **kwargs): ("#! /usr/bin/env test1.0\n", "1.0"), ("#! /usr/bin/env test2.0\n", "2.0"), ("#! /usr/bin/env -S test2.0\n", "2.0"), + # Legacy handling specifically for "python" + ("#! /usr/bin/python1.3.1", "1.3.1"), + ("#! /usr/bin/env python1.3.1", "1.3.1"), + ("#! /usr/bin/python1.3.2", None), ]) def test_read_shebang(fake_config, tmp_path, script, expect): fake_config.installs.extend(INSTALLS) From 19259cb64df03d46966f6df6bf153a19930fd030 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 18:53:38 +0100 Subject: [PATCH 2/3] Improved match function --- src/manage/pathutils.py | 21 ++++++++++----------- tests/test_pathutils.py | 6 ++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/manage/pathutils.py b/src/manage/pathutils.py index 494faca..8e75d8e 100644 --- a/src/manage/pathutils.py +++ b/src/manage/pathutils.py @@ -124,19 +124,18 @@ def match(self, pattern, full_match=False): if "*" not in p: return m.casefold() == p - must_start_with = not p.startswith("*") + must_start_with = True for bit in p.split("*"): - if not bit: - continue - try: - i = m.index(bit) - except ValueError: - return False - if must_start_with and i != 0: - return False - m = m[i + len(bit):] + if bit: + try: + i = m.index(bit) + except ValueError: + return False + if must_start_with and i != 0: + return False + m = m[i + len(bit):] must_start_with = False - return True + return not m or p.endswith("*") class Path(PurePath): diff --git a/tests/test_pathutils.py b/tests/test_pathutils.py index 65d7387..0a72172 100644 --- a/tests/test_pathutils.py +++ b/tests/test_pathutils.py @@ -7,4 +7,10 @@ def test_path_match(): assert p.match("*.exe") assert p.match("python*") assert p.match("python*.exe") + assert p.match("*hon3.*") assert p.match("p*3.*.exe") + + assert not p.match("*.com") + assert not p.match("example*") + assert not p.match("example*.com") + assert not p.match("*ple*") From b4c983409701fb67c4a0ff0d3ec0ee1ea390ca95 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 12 May 2025 19:11:10 +0100 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- tests/conftest.py | 2 +- tests/test_pathutils.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index d059e71..eaf3b56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -144,7 +144,7 @@ def get_installs(self): return self.installs def get_install_to_run(self, tag): - company, _, tag = (tag.replace("/", "\\")).rpartition("\\") + company, _, tag = tag.replace("/", "\\").rpartition("\\") return [i for i in self.installs if i["tag"] == tag and (not company or i["company"] == company)][0] diff --git a/tests/test_pathutils.py b/tests/test_pathutils.py index 0a72172..5c6f62e 100644 --- a/tests/test_pathutils.py +++ b/tests/test_pathutils.py @@ -7,6 +7,7 @@ def test_path_match(): assert p.match("*.exe") assert p.match("python*") assert p.match("python*.exe") + assert p.match("python3.12*.exe") assert p.match("*hon3.*") assert p.match("p*3.*.exe")