From eb16a343280796919024c367b16009ea46144c27 Mon Sep 17 00:00:00 2001 From: Anay Garodia <76836303+AnayGarodia@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:04:52 -0700 Subject: [PATCH] =?UTF-8?q?feat(cli):=20add=20--version=20option=20?= =?UTF-8?q?=F0=9F=A4=96=F0=9F=A4=96=F0=9F=A4=96=20(#563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): add version flag * fix(cli): parse args before acquiring single-instance lock so --version works while app is running Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/vocalinux/main.py | 16 +++++++++++--- tests/test_main_args_deps.py | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py index 9a2892b..124ef1f 100644 --- a/src/vocalinux/main.py +++ b/src/vocalinux/main.py @@ -8,6 +8,8 @@ import atexit import logging import sys +from .version import __version__ + # Configure logging logging.basicConfig( level=logging.INFO, @@ -21,7 +23,13 @@ logger = logging.getLogger(__name__) def parse_arguments(): """Parse command line arguments.""" - parser = argparse.ArgumentParser(description="Vocalinux") + parser = argparse.ArgumentParser(prog="vocalinux", description="Vocalinux") + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Show the installed Vocalinux version and exit", + ) parser.add_argument("--debug", action="store_true", help="Enable debug logging") # default model, language and engine are loaded from default config # due to priority of args over config @@ -214,6 +222,10 @@ def check_appindicator_support(): def main(): """Main entry point for the application.""" + # Parse arguments first so flags like --version work even when + # another instance already holds the single-instance lock + args = parse_arguments() + # Check for single instance BEFORE any initialization from . import single_instance @@ -241,8 +253,6 @@ def main(): # Register cleanup to release lock on exit atexit.register(single_instance.release_lock) - args = parse_arguments() - # Configure debug logging if requested if args.debug: logging.getLogger().setLevel(logging.DEBUG) diff --git a/tests/test_main_args_deps.py b/tests/test_main_args_deps.py index e2d67a7..99ff9f2 100644 --- a/tests/test_main_args_deps.py +++ b/tests/test_main_args_deps.py @@ -8,6 +8,8 @@ Covers normal startup, debug mode, version flag, keyboard interrupt, and excepti import os import sys import unittest +from contextlib import redirect_stdout +from io import StringIO from unittest.mock import MagicMock, call, patch import pytest @@ -21,6 +23,14 @@ def _restore_sys_modules(): added = set(sys.modules.keys()) - set(saved.keys()) for k in added: del sys.modules[k] + # Also drop the stale attribute from the parent package so that + # mock.patch and a later re-import resolve to the same fresh module + parent, _, child = k.rpartition(".") + if parent and parent in sys.modules: + try: + delattr(sys.modules[parent], child) + except AttributeError: + pass for k, v in saved.items(): if k not in sys.modules or sys.modules[k] is not v: sys.modules[k] = v @@ -50,6 +60,20 @@ class TestParseArguments(unittest.TestCase): args = parse_arguments() assert args.debug is True + def test_parse_args_version_flag(self): + """Test that --version prints the package version and exits.""" + from vocalinux.version import __version__ + + output = StringIO() + with patch.object(sys, "argv", ["vocalinux", "--version"]): + from vocalinux.main import parse_arguments + + with redirect_stdout(output), self.assertRaises(SystemExit) as exit_context: + parse_arguments() + + assert exit_context.exception.code == 0 + assert output.getvalue() == f"vocalinux {__version__}\n" + def test_parse_args_model_argument(self): """Test parsing with model argument.""" with patch.object(sys, "argv", ["vocalinux", "--model", "small"]): @@ -282,6 +306,25 @@ class TestMainFunction(unittest.TestCase): main() assert exc_info.value.code == 1 + def test_main_version_flag_with_lock_held(self): + """Test that --version works even when another instance holds the lock.""" + from vocalinux.main import main + from vocalinux.version import __version__ + + mock_single_instance = MagicMock() + mock_single_instance.acquire_lock.return_value = False + + output = StringIO() + with patch.dict(sys.modules, {"vocalinux.single_instance": mock_single_instance}): + with patch.object(sys, "argv", ["vocalinux", "--version"]): + with redirect_stdout(output): + with pytest.raises(SystemExit) as exc_info: + main() + + assert exc_info.value.code == 0 + assert output.getvalue() == f"vocalinux {__version__}\n" + mock_single_instance.acquire_lock.assert_not_called() + @patch("vocalinux.main.check_display_available") @patch("vocalinux.main.logging") @patch("vocalinux.main.check_dependencies")