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..6bddb76cf 100644 --- a/castervoice/lib/settings.py +++ b/castervoice/lib/settings.py @@ -45,23 +45,33 @@ _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 +83,22 @@ 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 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 + return _hidden_console_binary_for(sys.executable) + + def _validate_engine_path(): ''' Validates path 'Engine Path' in settings.toml @@ -314,9 +340,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 @@ -471,10 +494,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. 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..810ead34b --- /dev/null +++ b/tests/lib/test_settings.py @@ -0,0 +1,53 @@ +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._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"]) + self.assertNotIn("PYTHONW", 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_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: + self.assertEqual("C:/Users/Main/CasterData", settings.detected_user_dir()) + + user_data_dir.assert_not_called()