From eb17cddd2123e818b01b1babccb6c8e7b612c2ed Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 29 Oct 2026 13:47:45 +0100 Subject: [PATCH] fix(config): open nano on the terminal in config edit pkgmgr config edit ran nano through run_command, which pipes stdout and stderr to capture them, so nano aborted with 'Standard output is not a terminal'. The editor now runs with subprocess.call and the inherited terminal, and a failing editor exits with its code. Co-Authored-By: Claude Opus 5.5 --- src/pkgmgr/cli/commands/config.py | 6 ++-- tests/unit/pkgmgr/cli/commands/test_config.py | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/unit/pkgmgr/cli/commands/test_config.py diff --git a/src/pkgmgr/cli/commands/config.py b/src/pkgmgr/cli/commands/config.py index 289a5e0..a34e394 100644 --- a/src/pkgmgr/cli/commands/config.py +++ b/src/pkgmgr/cli/commands/config.py @@ -4,6 +4,7 @@ from __future__ import annotations import os import shutil +import subprocess import sys from pathlib import Path from typing import Any @@ -14,7 +15,6 @@ from pkgmgr.actions.config.add import interactive_add from pkgmgr.actions.config.init import config_init from pkgmgr.actions.config.show import show_config from pkgmgr.cli.context import CLIContext -from pkgmgr.core.command.run import run_command from pkgmgr.core.config.save import save_user_config from pkgmgr.core.repository.resolve import resolve_repos @@ -108,7 +108,9 @@ def handle_config(args, ctx: CLIContext) -> None: return if args.subcommand == "edit": - run_command(f"nano {user_config_path}") + returncode = subprocess.call(["nano", os.path.expanduser(user_config_path)]) + if returncode != 0: + sys.exit(returncode) return if args.subcommand == "init": diff --git a/tests/unit/pkgmgr/cli/commands/test_config.py b/tests/unit/pkgmgr/cli/commands/test_config.py new file mode 100644 index 0000000..b09d586 --- /dev/null +++ b/tests/unit/pkgmgr/cli/commands/test_config.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import types +import unittest +from unittest.mock import patch + +from pkgmgr.cli.commands import config as config_cmd + + +class ConfigEditTests(unittest.TestCase): + def _edit(self, path: str, returncode: int): + with patch( + "subprocess.Popen", + side_effect=AssertionError("the editor's output must not be captured"), + ), patch("subprocess.call", return_value=returncode) as call: + config_cmd.handle_config( + types.SimpleNamespace(subcommand="edit"), + types.SimpleNamespace(user_config_path=path), + ) + return call + + def test_nano_inherits_the_terminal(self): + call = self._edit("/cfg/config.yaml", 0) + + call.assert_called_once_with(["nano", "/cfg/config.yaml"]) + + def test_a_failing_editor_exits_with_its_code(self): + with self.assertRaises(SystemExit) as raised: + self._edit("/cfg/config.yaml", 3) + + self.assertEqual(raised.exception.code, 3) + + +if __name__ == "__main__": + unittest.main()