From ade75eceb9f62bbdb9d1e75b20d902e09ad7c034 Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:15:48 +0200 Subject: [PATCH] Harden _parse_fukui's table-end detection xtb writes Fukui data only to stdout (verified: a clean-directory --vfukui run writes no dedicated file for it), so this parser is unavoidably stdout-scraping -- but it was only checking token count to detect the end of the table, so a coincidental 4-token line right after it would get silently misparsed as a data row. Now also checks the label matches "digits then letters" and the last 3 tokens parse as floats before accepting a row. --- ThermoScreening/calculator/xtb_cli.py | 12 ++++++-- tests/calculator/test_xtb_cli.py | 42 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/ThermoScreening/calculator/xtb_cli.py b/ThermoScreening/calculator/xtb_cli.py index fad9e0b..cc70b45 100644 --- a/ThermoScreening/calculator/xtb_cli.py +++ b/ThermoScreening/calculator/xtb_cli.py @@ -9,6 +9,7 @@ """ import os +import re import shutil import subprocess @@ -184,9 +185,14 @@ def _parse_fukui(output): tokens = line.split() if len(tokens) != 4: break - label, f_plus, f_minus, f_zero = tokens - symbol = label.lstrip("0123456789") - rows.append((symbol, float(f_plus), float(f_minus), float(f_zero))) + label_match = re.match(r"^(\d+)([A-Za-z]+)$", tokens[0]) + if label_match is None: + break + try: + f_plus, f_minus, f_zero = (float(token) for token in tokens[1:]) + except ValueError: + break + rows.append((label_match.group(2), f_plus, f_minus, f_zero)) return rows diff --git a/tests/calculator/test_xtb_cli.py b/tests/calculator/test_xtb_cli.py index 04b29a9..86dae65 100644 --- a/tests/calculator/test_xtb_cli.py +++ b/tests/calculator/test_xtb_cli.py @@ -110,6 +110,48 @@ def test_parse_fukui_raises_without_table(): xtb_cli._parse_fukui("no such table here") +def test_parse_fukui_stops_at_coincidental_four_token_line_with_bad_label(): + # a line right after the table with 4 tokens but a label that isn't + # "digits then letters" must not be misparsed as data -- token count + # alone isn't enough to detect the end of the table + output = ( + "Fukui functions:\n" + " # f(+) f(-) f(0)\n" + " 1C 0.024 0.024 0.024\n" + " abc 0.1 0.2 0.3\n" + " 8O 0.131 0.129 0.130\n" + ) + rows = xtb_cli._parse_fukui(output) + + assert rows == [("C", 0.024, 0.024, 0.024)] + + +def test_parse_fukui_stops_at_coincidental_four_token_line_with_bad_values(): + # a line with a valid-looking label but non-numeric values must also + # not be misparsed as data + output = ( + "Fukui functions:\n" + " # f(+) f(-) f(0)\n" + " 1C 0.024 0.024 0.024\n" + " 2C n/a n/a n/a\n" + " 8O 0.131 0.129 0.130\n" + ) + rows = xtb_cli._parse_fukui(output) + + assert rows == [("C", 0.024, 0.024, 0.024)] + + +def test_parse_fukui_handles_multi_digit_index_and_multi_letter_symbol(): + output = ( + "Fukui functions:\n" + " # f(+) f(-) f(0)\n" + " 14Cl 0.010 0.020 0.015\n" + ) + rows = xtb_cli._parse_fukui(output) + + assert rows == [("Cl", 0.010, 0.020, 0.015)] + + def test_run_xtb_fukui_builds_command_and_parses(monkeypatch, tmp_path): monkeypatch.chdir(tmp_path) monkeypatch.setenv("XTB_COMMAND", "/fake/xtb")