Files
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

371 lines
14 KiB
Python

# -*- coding: utf-8 -*-
"""`liftexport.find_python_report`: an interpreter is chosen because it RUNS
and imports what the caller needs, not because its folder sorts first.
Gitea #16 (2026-09-05): `_python_dirs()` lists the newest install folder
first, `find_python()` took the first python.exe that existed, and a bare
Python 3.13 beat the Python 3.11 that had PyYAML. Panel Analysis' server
died at `import yaml` before printing its URL and every mark went NOT RUN
reading "did not report a URL in time". These pin the shape of the fix:
probe, first PASS wins, every rejection is named, and a hand-configured
interpreter that fails is reported rather than quietly replaced.
"""
import os
import re
import sys
import pytest
from vibe import liftexport as L
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
TOOLS = os.path.join(ROOT, 'tools')
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
EXE = L._PYTHON_NAMES[0]
@pytest.fixture(autouse=True)
def _clean(monkeypatch):
L._PROBE_OK.clear()
monkeypatch.delenv('WILLTOOLS_PYTHON', raising=False)
monkeypatch.setenv('PATH', '')
yield
L._PROBE_OK.clear()
def _install(folder, name=EXE):
folder.mkdir(parents=True, exist_ok=True)
exe = folder / name
exe.write_bytes(b'not really an interpreter\n')
return str(exe)
def _probe_table(verdicts):
"""A probe that answers from a table {exe: (ok, verdict)} and records
the order it was asked in."""
asked = []
def probe(exe, needs):
asked.append(exe)
return verdicts[exe]
probe.asked = asked
return probe
# ------------------------------------------------------------ parse / quote
def test_probe_line_with_nothing_missing_parses():
assert L._parse_probe('PY 3.11.4 MISSING ') == ((3, 11, 4), [])
def test_probe_line_with_two_missing_modules_parses():
assert L._parse_probe('PY 3.13.0 MISSING yaml,fitz\n') == (
(3, 13, 0), ['yaml', 'fitz'])
def test_garbage_output_is_not_a_version():
assert L._parse_probe('Python 3.11.4') == (None, [])
assert L._parse_probe('') == (None, [])
assert L._parse_probe(None) == (None, [])
def test_win_quote_follows_the_commandlinetoargvw_rule():
assert L._win_quote('yaml') == '"yaml"'
assert L._win_quote('a "b" c') == '"a \\"b\\" c"'
assert L._win_quote('C:\\dir\\') == '"C:\\dir\\\\"'
assert L._win_quote('x\\"y') == '"x\\\\\\"y"'
assert L._win_quote('line one\nline two') == '"line one\nline two"'
# --------------------------------------------------------------- real probe
def test_the_host_interpreter_passes_a_real_probe():
ok, verdict = L.probe_python(sys.executable, ())
assert ok, verdict
assert verdict.startswith('Python {0}.{1}.'.format(*sys.version_info[:2]))
def test_a_real_probe_names_the_module_it_cannot_import():
ok, verdict = L.probe_python(
sys.executable, ('no_such_module_willtools_gitea16',))
assert not ok
assert 'no_such_module_willtools_gitea16' in verdict
assert 'cannot import' in verdict
def test_a_missing_module_prints_its_pip_name_not_its_import_name():
ok, verdict = L.probe_python(
sys.executable, ('yaml_surely_absent_gitea16',))
assert not ok
# the pip name table: yaml -> pyyaml, fitz -> pymupdf; unknown as is
assert L._pip_name('yaml') == 'pyyaml'
assert L._pip_name('fitz') == 'pymupdf'
assert L._pip_name('yaml_surely_absent_gitea16') == \
'yaml_surely_absent_gitea16'
def test_a_file_that_is_not_an_interpreter_is_a_named_rejection(tmp_path):
fake = _install(tmp_path / 'Python313')
ok, verdict = L.probe_python(fake, ())
assert not ok
assert verdict # never an empty reason
def test_an_interpreter_below_the_floor_is_rejected(monkeypatch):
monkeypatch.setattr(L, '_run_probe',
lambda exe, needs: (0, 'PY 2.7.18 MISSING '))
ok, verdict = L.probe_python('C:/old/python.exe', ())
assert not ok
assert '2.7.18' in verdict and '3.9+' in verdict
# ---------------------------------------------------------------- caching
def test_a_pass_is_cached_so_a_second_call_does_not_spawn(monkeypatch):
calls = []
def run(exe, needs):
calls.append(exe)
return 0, 'PY 3.11.9 MISSING '
monkeypatch.setattr(L, '_run_probe', run)
assert L.probe_python('C:/py/python.exe', ('yaml',))[0]
assert L.probe_python('C:/py/python.exe', ('yaml',))[0]
assert calls == ['C:/py/python.exe']
def test_a_failure_is_never_cached_so_pip_install_fixes_the_next_press(
monkeypatch):
answers = [(1, 'PY 3.13.0 MISSING yaml'), (0, 'PY 3.13.0 MISSING ')]
monkeypatch.setattr(L, '_run_probe',
lambda exe, needs: answers.pop(0))
ok1, verdict1 = L.probe_python('C:/py/python.exe', ('yaml',))
ok2, verdict2 = L.probe_python('C:/py/python.exe', ('yaml',))
assert (ok1, ok2) == (False, True)
assert 'yaml' in verdict1 and 'pip install pyyaml' in verdict1
def test_the_cache_is_keyed_by_what_was_asked_for(monkeypatch):
monkeypatch.setattr(L, '_run_probe',
lambda exe, needs: (0, 'PY 3.11.9 MISSING '))
L.probe_python('C:/py/python.exe', ())
assert ('C:/py/python.exe', ('yaml',)) not in L._PROBE_OK
# --------------------------------------------------------------- selection
def test_gitea_16_the_older_install_with_the_package_wins(tmp_path,
monkeypatch):
"""The exact machine: Python313 sorts first and has no PyYAML,
Python311 has it. The 313 used to win on existence alone."""
newest = _install(tmp_path / 'Python313')
older = _install(tmp_path / 'Python311')
monkeypatch.setattr(L, '_python_dirs', lambda: [
os.path.dirname(newest), os.path.dirname(older)])
probe = _probe_table({
newest: (False, 'Python 3.13.0 cannot import yaml (pip install pyyaml)'),
older: (True, 'Python 3.11.9, imports yaml'),
})
exe, tried = L.find_python_report(None, ('yaml',), probe=probe)
assert exe == older
assert probe.asked == [newest, older]
assert tried == [
(newest, 'Python 3.13.0 cannot import yaml (pip install pyyaml)'),
(older, 'Python 3.11.9, imports yaml'),
]
def test_find_python_is_the_report_with_the_reasons_dropped(tmp_path,
monkeypatch):
exe_path = _install(tmp_path / 'Python311')
monkeypatch.setattr(L, '_python_dirs', lambda: [os.path.dirname(exe_path)])
monkeypatch.setattr(L, 'probe_python', lambda exe, needs: (True, 'ok'))
assert L.find_python(None, ('yaml',)) == exe_path
def test_no_candidate_passing_returns_none_and_every_rejection(tmp_path,
monkeypatch):
a = _install(tmp_path / 'Python313')
b = _install(tmp_path / 'Python311')
monkeypatch.setattr(L, '_python_dirs', lambda: [
os.path.dirname(a), os.path.dirname(b)])
probe = _probe_table({a: (False, 'cannot import yaml'),
b: (False, 'cannot import yaml')})
exe, tried = L.find_python_report(None, ('yaml',), probe=probe)
assert exe is None
assert [t[0] for t in tried] == [a, b]
text = L.describe_python_search(tried, ('yaml',))
assert a in text and b in text
assert 'cannot import yaml' in text
assert 'handling.python_exe' in text
def test_an_empty_search_says_nothing_was_found_and_how_to_fix_it():
text = L.describe_python_search([], ('yaml',))
assert 'no CPython 3.9+ was found' in text
assert 'handling.python_exe' in text
def test_the_store_alias_folder_is_never_probed(tmp_path, monkeypatch):
store = _install(tmp_path / 'WindowsApps')
real = _install(tmp_path / 'Python311')
monkeypatch.setattr(L, '_python_dirs', lambda: [
os.path.dirname(store), os.path.dirname(real)])
probe = _probe_table({real: (True, 'ok')})
exe, tried = L.find_python_report(None, (), probe=probe)
assert exe == real
assert store not in probe.asked
def test_the_same_interpreter_reached_twice_is_probed_once(tmp_path,
monkeypatch):
exe_path = _install(tmp_path / 'Python311')
folder = os.path.dirname(exe_path)
monkeypatch.setattr(L, '_python_dirs', lambda: [folder, folder])
probe = _probe_table({exe_path: (False, 'no')})
L.find_python_report(None, (), probe=probe)
assert probe.asked == [exe_path]
# ------------------------------------------------ a hand-configured path
def test_a_configured_path_that_fails_is_reported_not_replaced(tmp_path,
monkeypatch):
"""WILLTOOLS_PYTHON names an interpreter without the package; a working
one sits in the install folder. The configured one is the ONLY
candidate: the run must not proceed on an interpreter nobody chose."""
stated = _install(tmp_path / 'chosen')
other = _install(tmp_path / 'Python311')
monkeypatch.setenv('WILLTOOLS_PYTHON', stated)
monkeypatch.setattr(L, '_python_dirs', lambda: [os.path.dirname(other)])
probe = _probe_table({stated: (False, 'cannot import yaml'),
other: (True, 'ok')})
exe, tried = L.find_python_report(None, ('yaml',), probe=probe)
assert exe is None
assert probe.asked == [stated]
assert tried == [(stated, 'WILLTOOLS_PYTHON: cannot import yaml')]
def test_a_configured_path_that_passes_is_used(tmp_path, monkeypatch):
stated = _install(tmp_path / 'chosen')
monkeypatch.setenv('WILLTOOLS_PYTHON', stated)
probe = _probe_table({stated: (True, 'Python 3.11.9')})
exe, tried = L.find_python_report(None, (), probe=probe)
assert exe == stated
assert tried == [(stated, 'WILLTOOLS_PYTHON: Python 3.11.9')]
def test_a_configured_path_that_does_not_exist_stops_the_search(tmp_path,
monkeypatch):
other = _install(tmp_path / 'Python311')
monkeypatch.setenv('WILLTOOLS_PYTHON', str(tmp_path / 'gone' / EXE))
monkeypatch.setattr(L, '_python_dirs', lambda: [os.path.dirname(other)])
probe = _probe_table({other: (True, 'ok')})
exe, tried = L.find_python_report(None, (), probe=probe)
assert exe is None
assert probe.asked == []
assert len(tried) == 1 and 'does not exist' in tried[0][1]
assert 'WILLTOOLS_PYTHON' in tried[0][1]
def test_the_knowledge_layer_path_outranks_the_environment(tmp_path,
monkeypatch):
from_know = _install(tmp_path / 'know')
from_env = _install(tmp_path / 'env')
monkeypatch.setenv('WILLTOOLS_PYTHON', from_env)
know = {'handling': {'python_exe': from_know}}
probe = _probe_table({from_know: (True, 'ok'), from_env: (True, 'ok')})
exe, tried = L.find_python_report(know, (), probe=probe)
assert exe == from_know
assert tried[0][1].startswith('handling.python_exe:')
# ---------------------------------------------- setup_machine.py --check
def _step():
S = pytest.importorskip('setup_machine')
out = []
step = S.Step(out.append)
step.lines = out
return S, step
class _FakeLiftexport(object):
def __init__(self, exe, tried):
self._answer = (exe, tried)
self.needs_seen = None
def find_python_report(self, know, needs):
self.needs_seen = needs
return self._answer
describe_python_search = staticmethod(L.describe_python_search)
def test_setup_check_passes_when_the_chooser_finds_a_working_interpreter():
S, step = _step()
fake = _FakeLiftexport('C:/py311/python.exe',
[('C:/py311/python.exe', 'Python 3.11.9, imports yaml')])
assert S.verify_button_python(step, liftexport=fake)
assert fake.needs_seen == S.BUTTON_PYTHON_NEEDS
assert step.passed and not step.failures
assert 'C:/py311/python.exe' in step.lines[0]
def test_setup_check_fails_and_lists_every_rejected_interpreter():
"""The machine in Gitea #16: `sys.executable` imports yaml, so the
old dependency check passed, while the chooser picked a 3.13 that
could not. This step asks the chooser, so it fails here."""
S, step = _step()
fake = _FakeLiftexport(None, [
('C:/py313/python.exe', 'Python 3.13.0 cannot import yaml (pip install pyyaml)'),
])
assert not S.verify_button_python(step, liftexport=fake)
assert step.failures
text = '\n'.join(step.lines)
assert 'C:/py313/python.exe' in text
assert 'cannot import yaml' in text
assert 'handling.python_exe' in text
def test_setup_check_fails_loudly_when_liftexport_cannot_load(monkeypatch):
S, step = _step()
monkeypatch.setattr(S, '_load_liftexport', lambda: None)
assert not S.verify_button_python(step)
assert step.failures
def test_setup_check_runs_the_real_chooser_on_this_host(monkeypatch):
"""No fake: the real `find_python_report` under the real environment
of this host. Only asserts the shape -- a checkout with no CPython
on PATH is a legitimate FAIL, and this test may not fake one in."""
S, step = _step()
monkeypatch.setenv('PATH', os.path.dirname(sys.executable))
S.verify_button_python(step)
assert step.passed or step.failures
assert step.lines
def test_setup_check_asks_for_what_the_button_asks_for():
"""BUTTON_PYTHON_NEEDS and the button's PA_NEEDS are kept in step by
hand; this is the test that notices when one moves."""
S, _ = _step()
script = os.path.join(
ROOT, 'WillTools.extension', 'WillTools.tab', 'Design.panel',
'DesignHub.pulldown', 'PanelAnalysis.pushbutton', 'script.py')
with open(script, encoding='utf-8') as fh:
text = fh.read()
m = re.search(r"^PA_NEEDS = \((.*)\)$", text, re.M)
assert m, 'PanelAnalysis script.py no longer declares PA_NEEDS'
button_needs = tuple(x.strip().strip("'\"")
for x in m.group(1).split(',') if x.strip())
assert button_needs == S.BUTTON_PYTHON_NEEDS
def test_setup_check_is_part_of_run_verification():
S, _ = _step()
import inspect
assert 'verify_button_python' in inspect.getsource(S.run_verification)