From dc09cee3af3d78e271933198d4510afe90c85d56 Mon Sep 17 00:00:00 2001 From: Kendon Bell Date: Tue, 31 Mar 2026 19:07:52 +1300 Subject: [PATCH 1/3] Use active runtime for helper Python processes --- castervoice/asynch/hmc/h_launch.py | 4 +- castervoice/asynch/hud_support.py | 4 +- castervoice/lib/navigation.py | 8 +-- castervoice/lib/settings.py | 53 +++++++++++++++--- .../rules/ccr/recording_rules/bringme.py | 29 ++++++++-- tests/lib/test_settings.py | 54 +++++++++++++++++++ 6 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 tests/lib/test_settings.py diff --git a/castervoice/asynch/hmc/h_launch.py b/castervoice/asynch/hmc/h_launch.py index 9a2f1c5b1..1709d1e61 100644 --- a/castervoice/asynch/hmc/h_launch.py +++ b/castervoice/asynch/hmc/h_launch.py @@ -26,12 +26,12 @@ def launch(hmc_type, data=None): def _get_instructions(hmc_type): if hmc_type == settings.QTTYPE_SETTINGS: return [ - settings.SETTINGS["paths"]["PYTHONW"], + settings.runtime_hidden_console_binary(), settings.SETTINGS["paths"]["SETTINGS_WINDOW_PATH"] ] else: return [ - settings.SETTINGS["paths"]["PYTHONW"], + settings.runtime_hidden_console_binary(), settings.SETTINGS["paths"]["HOMUNCULUS_PATH"], hmc_type ] diff --git a/castervoice/asynch/hud_support.py b/castervoice/asynch/hud_support.py index 8792d18c5..5f7dfc7a6 100644 --- a/castervoice/asynch/hud_support.py +++ b/castervoice/asynch/hud_support.py @@ -19,7 +19,7 @@ def start_hud(): try: hud.ping() except Exception: - subprocess.Popen([settings.SETTINGS["paths"]["PYTHONW"], + subprocess.Popen([settings.runtime_hidden_console_binary(), settings.SETTINGS["paths"]["HUD_PATH"]]) @@ -129,4 +129,4 @@ def handle_message(self, items): printer.out("Hud not available. \n{}".format(e)) raise("") # pylint: disable=raising-bad-type else: - raise("") # pylint: disable=raising-bad-type \ No newline at end of file + raise("") # pylint: disable=raising-bad-type diff --git a/castervoice/lib/navigation.py b/castervoice/lib/navigation.py index f55e9296e..1d834986b 100644 --- a/castervoice/lib/navigation.py +++ b/castervoice/lib/navigation.py @@ -73,25 +73,25 @@ def mouse_alternates(cls, mode, monitor=1, rough=True): ls.scan(bbox, rough) tscan = ls.get_update() args = [ - settings.settings(["paths", "PYTHONW"]), + settings.runtime_hidden_console_binary(), settings.settings(["paths", "LEGION_PATH"]), "-t", tscan[0], "-m", str(monitor) ] elif mode == "rainbow": args = [ - settings.settings(["paths", "PYTHONW"]), + settings.runtime_hidden_console_binary(), settings.settings(["paths", "RAINBOW_PATH"]), "-g", "r", "-m", str(monitor) ] elif mode == "douglas": args = [ - settings.settings(["paths", "PYTHONW"]), + settings.runtime_hidden_console_binary(), settings.settings(["paths", "DOUGLAS_PATH"]), "-g", "d", "-m", str(monitor) ] elif mode == "sudoku": args = [ - settings.settings(["paths", "PYTHONW"]), + settings.runtime_hidden_console_binary(), settings.settings(["paths", "SUDOKU_PATH"]), "-g", "s", "-m", str(monitor) ] diff --git a/castervoice/lib/settings.py b/castervoice/lib/settings.py index 186f071a0..6c83e6995 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -42,26 +42,37 @@ WSR = False _BASE_PATH = None _USER_DIR = None +_USER_DIR_REPORTED = False _SETTINGS_PATH = None +def _hidden_console_binary_for(executable): + executable_path = Path(executable) + if sys.platform != "win32": + return str(executable_path) + if executable_path.name.lower() == "pythonw.exe": + return str(executable_path) + hidden_console_binary = executable_path.with_name("pythonw.exe") + if hidden_console_binary.is_file(): + return str(hidden_console_binary) + return str(executable_path) + + def _get_platform_information(): """Return a dictionary containing platform-specific information.""" import sysconfig system_information = {"platform": sysconfig.get_platform()} system_information.update({"python version": sys.version_info}) binary_path = str(Path(sys.exec_prefix).joinpath(sys.exec_prefix).joinpath("bin")) - hidden_console_binary = str(Path(sys.executable)) main_binary = str(Path(sys.executable)) if sys.platform == "win32": if sys.prefix == sys.base_prefix: main_binary = str(Path(sys.exec_prefix).joinpath("python.exe")) - hidden_console_binary = str(Path(sys.exec_prefix).joinpath("pythonw.exe")) else: # Virtual environment detected # TODO: MacOS and Linux? main_binary = str(Path(sys.prefix) / "Scripts" / "python.exe") - hidden_console_binary = str(Path(sys.prefix) / "Scripts" / "pythonw.exe") + hidden_console_binary = _hidden_console_binary_for(main_binary) system_information.update({"binary path": binary_path}) system_information.update({"main binary": main_binary}) system_information.update({"hidden console binary": hidden_console_binary}) @@ -73,6 +84,35 @@ def get_filename(): return _SETTINGS_PATH +def detected_user_dir(): + configured_user_dir = os.getenv("CASTER_USER_DIR") + if configured_user_dir is not None: + return configured_user_dir + return user_data_dir(appname="caster", appauthor=False) + + +def report_user_dir(): + global _USER_DIR, _USER_DIR_REPORTED + if _USER_DIR is None: + _USER_DIR = detected_user_dir() + if not _USER_DIR_REPORTED: + printer.out("Caster User Directory: {}".format(_USER_DIR)) + _USER_DIR_REPORTED = True + return _USER_DIR + + +def runtime_hidden_console_binary(): + runtime_binary = "" + if SYSTEM_INFORMATION is not None: + runtime_binary = SYSTEM_INFORMATION.get("hidden console binary", "") + if runtime_binary and os.path.isfile(runtime_binary): + return runtime_binary + configured_binary = settings(["paths", "PYTHONW"], "") + if configured_binary and os.path.isfile(configured_binary): + return configured_binary + return _hidden_console_binary_for(sys.executable) + + def _validate_engine_path(): ''' Validates path 'Engine Path' in settings.toml @@ -471,10 +511,7 @@ def initialize(): # calculate prerequisites SYSTEM_INFORMATION = _get_platform_information() _BASE_PATH = str(Path(__file__).resolve().parent.parent) - if os.getenv("CASTER_USER_DIR") is not None: - _USER_DIR = os.getenv("CASTER_USER_DIR") - else: - _USER_DIR = user_data_dir(appname="caster", appauthor=False) + _USER_DIR = detected_user_dir() _SETTINGS_PATH = str(Path(_USER_DIR).joinpath("settings/settings.toml")) # Kick everything off. @@ -488,4 +525,4 @@ def initialize(): if _debugger_path not in sys.path and os.path.isdir(_debugger_path): sys.path.append(_debugger_path) - printer.out("Caster User Directory: {}".format(_USER_DIR)) + report_user_dir() diff --git a/castervoice/rules/ccr/recording_rules/bringme.py b/castervoice/rules/ccr/recording_rules/bringme.py index 58cb92f52..abcd99a55 100644 --- a/castervoice/rules/ccr/recording_rules/bringme.py +++ b/castervoice/rules/ccr/recording_rules/bringme.py @@ -18,6 +18,27 @@ from castervoice.lib.merge.state.short import R +def _base_path_dir(): + configured_base_path = settings.settings(["paths", "BASE_PATH"], "") + if configured_base_path: + return Path(configured_base_path) + return Path(__file__).resolve().parents[3] + + +def _user_dir_path(): + configured_user_dir = settings.settings(["paths", "USER_DIR"], "") + if configured_user_dir: + return Path(configured_user_dir) + return Path(settings.detected_user_dir()) + + +def _bringme_config_path(): + configured_bringme_path = settings.settings(["paths", "SM_BRINGME_PATH"], "") + if configured_bringme_path: + return configured_bringme_path + return str(_user_dir_path().joinpath("settings", "sm_bringme.toml")) + + class BringRule(BaseSelfModifyingRule): """ BringRule adds entries to a 2-layered map which can be described as @@ -34,14 +55,14 @@ class BringRule(BaseSelfModifyingRule): _explorer_context = AppContext("explorer.exe") | contexts.DIALOGUE_CONTEXT _terminal_context = contexts.TERMINAL_CONTEXT # Paths - _terminal_path = settings.settings(["paths", "TERMINAL_PATH"]) + _terminal_path = settings.settings(["paths", "TERMINAL_PATH"], "") _explorer_path = str(Path("C:\\Windows\\explorer.exe")) - _source_dir = Path(settings.settings(["paths", "BASE_PATH"])).parents[0] - _user_dir = settings.settings(["paths", "USER_DIR"]) + _source_dir = _base_path_dir().parent + _user_dir = _user_dir_path() _home_dir = Path.home() def __init__(self, **kwargs): - super(BringRule, self).__init__(settings.settings(["paths", "SM_BRINGME_PATH"]), **kwargs) + super(BringRule, self).__init__(_bringme_config_path(), **kwargs) def _initialize(self): """ diff --git a/tests/lib/test_settings.py b/tests/lib/test_settings.py new file mode 100644 index 000000000..00f95c693 --- /dev/null +++ b/tests/lib/test_settings.py @@ -0,0 +1,54 @@ +from unittest import TestCase +from unittest.mock import patch + +from castervoice.lib import settings + + +class TestSettings(TestCase): + + def setUp(self): + self.addCleanup(self._reset_settings_state) + self._reset_settings_state() + + def _reset_settings_state(self): + settings.SETTINGS = None + settings.SYSTEM_INFORMATION = None + settings._BASE_PATH = None + settings._USER_DIR = None + settings._USER_DIR_REPORTED = False + settings._SETTINGS_PATH = None + + def test_runtime_python_paths_removed_from_defaults(self): + with patch.object(settings, "_BASE_PATH", "C:/Caster/castervoice"), \ + patch.object(settings, "_USER_DIR", "C:/Users/Main/AppData/Local/caster"), \ + patch.object(settings, "SYSTEM_INFORMATION", {"hidden console binary": "C:/Python/pythonw.exe"}), \ + patch.object(settings, "_validate_engine_path", return_value=""), \ + patch("castervoice.lib.settings.os.path.isfile", return_value=False): + defaults = settings._get_defaults() + + self.assertNotIn("WSR_RUNTIME_PYTHON_PATH", defaults["paths"]) + self.assertNotIn("KALDI_RUNTIME_PYTHON_PATH", defaults["paths"]) + + def test_runtime_hidden_console_binary_prefers_active_runtime(self): + runtime_pythonw = "C:/Caster/.venv/Scripts/pythonw.exe" + settings.SYSTEM_INFORMATION = {"hidden console binary": runtime_pythonw} + settings.SETTINGS = {"paths": {"PYTHONW": "C:/Legacy/pythonw.exe"}} + + with patch("castervoice.lib.settings.os.path.isfile", side_effect=lambda path: path == runtime_pythonw): + self.assertEqual(runtime_pythonw, settings.runtime_hidden_console_binary()) + + def test_detected_user_dir_prefers_environment_override(self): + with patch("castervoice.lib.settings.os.getenv", return_value="C:/Users/Main/CasterData"), \ + patch("castervoice.lib.settings.user_data_dir") as user_data_dir: + self.assertEqual("C:/Users/Main/CasterData", settings.detected_user_dir()) + + user_data_dir.assert_not_called() + + def test_report_user_dir_uses_default_location_once(self): + with patch("castervoice.lib.settings.os.getenv", return_value=None), \ + patch("castervoice.lib.settings.user_data_dir", return_value="C:/Users/Main/AppData/Local/caster"), \ + patch("castervoice.lib.settings.printer.out") as printer_out: + self.assertEqual("C:/Users/Main/AppData/Local/caster", settings.report_user_dir()) + self.assertEqual("C:/Users/Main/AppData/Local/caster", settings.report_user_dir()) + + printer_out.assert_called_once_with("Caster User Directory: C:/Users/Main/AppData/Local/caster") From 278c4a3baf5dc84f823d67f7647a2eec0a9cffd1 Mon Sep 17 00:00:00 2001 From: Kendon Bell Date: Wed, 1 Apr 2026 12:32:39 +1300 Subject: [PATCH 2/3] Stop persisting PYTHONW runtime paths --- castervoice/lib/settings.py | 6 ------ tests/lib/test_settings.py | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/castervoice/lib/settings.py b/castervoice/lib/settings.py index 6c83e6995..daef31e5d 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -107,9 +107,6 @@ def runtime_hidden_console_binary(): runtime_binary = SYSTEM_INFORMATION.get("hidden console binary", "") if runtime_binary and os.path.isfile(runtime_binary): return runtime_binary - configured_binary = settings(["paths", "PYTHONW"], "") - if configured_binary and os.path.isfile(configured_binary): - return configured_binary return _hidden_console_binary_for(sys.executable) @@ -354,9 +351,6 @@ def _get_defaults(): "CONFIGDEBUGTXT_PATH": str(Path(_USER_DIR).joinpath("data/configdebug.txt")), - # PYTHON - "PYTHONW": - SYSTEM_INFORMATION["hidden console binary"], }, # Speech recognition engine settings diff --git a/tests/lib/test_settings.py b/tests/lib/test_settings.py index 00f95c693..02edc4ca4 100644 --- a/tests/lib/test_settings.py +++ b/tests/lib/test_settings.py @@ -28,6 +28,7 @@ def test_runtime_python_paths_removed_from_defaults(self): self.assertNotIn("WSR_RUNTIME_PYTHON_PATH", defaults["paths"]) self.assertNotIn("KALDI_RUNTIME_PYTHON_PATH", defaults["paths"]) + self.assertNotIn("PYTHONW", defaults["paths"]) def test_runtime_hidden_console_binary_prefers_active_runtime(self): runtime_pythonw = "C:/Caster/.venv/Scripts/pythonw.exe" @@ -37,6 +38,14 @@ def test_runtime_hidden_console_binary_prefers_active_runtime(self): with patch("castervoice.lib.settings.os.path.isfile", side_effect=lambda path: path == runtime_pythonw): self.assertEqual(runtime_pythonw, settings.runtime_hidden_console_binary()) + def test_runtime_hidden_console_binary_ignores_configured_fallback(self): + runtime_pythonw = "C:/Caster/.venv/Scripts/pythonw.exe" + settings.SETTINGS = {"paths": {"PYTHONW": "C:/Legacy/pythonw.exe"}} + + with patch("castervoice.lib.settings._hidden_console_binary_for", return_value=runtime_pythonw), \ + patch("castervoice.lib.settings.os.path.isfile", return_value=False): + self.assertEqual(runtime_pythonw, settings.runtime_hidden_console_binary()) + def test_detected_user_dir_prefers_environment_override(self): with patch("castervoice.lib.settings.os.getenv", return_value="C:/Users/Main/CasterData"), \ patch("castervoice.lib.settings.user_data_dir") as user_data_dir: From 8dc8993b59215920f7d96f3065e9a93ad8c315e4 Mon Sep 17 00:00:00 2001 From: Kendon Bell Date: Tue, 7 Apr 2026 11:15:52 +1200 Subject: [PATCH 3/3] Inline user directory reporting --- castervoice/lib/settings.py | 13 +------------ tests/lib/test_settings.py | 10 ---------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/castervoice/lib/settings.py b/castervoice/lib/settings.py index daef31e5d..6bddb76cf 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -42,7 +42,6 @@ WSR = False _BASE_PATH = None _USER_DIR = None -_USER_DIR_REPORTED = False _SETTINGS_PATH = None @@ -91,16 +90,6 @@ def detected_user_dir(): return user_data_dir(appname="caster", appauthor=False) -def report_user_dir(): - global _USER_DIR, _USER_DIR_REPORTED - if _USER_DIR is None: - _USER_DIR = detected_user_dir() - if not _USER_DIR_REPORTED: - printer.out("Caster User Directory: {}".format(_USER_DIR)) - _USER_DIR_REPORTED = True - return _USER_DIR - - def runtime_hidden_console_binary(): runtime_binary = "" if SYSTEM_INFORMATION is not None: @@ -519,4 +508,4 @@ def initialize(): if _debugger_path not in sys.path and os.path.isdir(_debugger_path): sys.path.append(_debugger_path) - report_user_dir() + printer.out("Caster User Directory: {}".format(_USER_DIR)) diff --git a/tests/lib/test_settings.py b/tests/lib/test_settings.py index 02edc4ca4..810ead34b 100644 --- a/tests/lib/test_settings.py +++ b/tests/lib/test_settings.py @@ -15,7 +15,6 @@ def _reset_settings_state(self): settings.SYSTEM_INFORMATION = None settings._BASE_PATH = None settings._USER_DIR = None - settings._USER_DIR_REPORTED = False settings._SETTINGS_PATH = None def test_runtime_python_paths_removed_from_defaults(self): @@ -52,12 +51,3 @@ def test_detected_user_dir_prefers_environment_override(self): self.assertEqual("C:/Users/Main/CasterData", settings.detected_user_dir()) user_data_dir.assert_not_called() - - def test_report_user_dir_uses_default_location_once(self): - with patch("castervoice.lib.settings.os.getenv", return_value=None), \ - patch("castervoice.lib.settings.user_data_dir", return_value="C:/Users/Main/AppData/Local/caster"), \ - patch("castervoice.lib.settings.printer.out") as printer_out: - self.assertEqual("C:/Users/Main/AppData/Local/caster", settings.report_user_dir()) - self.assertEqual("C:/Users/Main/AppData/Local/caster", settings.report_user_dir()) - - printer_out.assert_called_once_with("Caster User Directory: C:/Users/Main/AppData/Local/caster")