From 5947e4a28eb67ad3b115895bacfb2171e7529106 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Oct 2023 14:41:15 -0400 Subject: [PATCH] feat: run commands to get settings --- src/scriv/config.py | 10 ++++++++++ src/scriv/shell.py | 19 +++++++++++++++++++ tests/faker.py | 8 +++++--- tests/test_config.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/scriv/config.py b/src/scriv/config.py index e388d8d..0416c56 100644 --- a/src/scriv/config.py +++ b/src/scriv/config.py @@ -13,6 +13,7 @@ from .exceptions import ScrivException from .literals import find_literal from .optional import tomllib +from .shell import run_shell_command logger = logging.getLogger(__name__) @@ -363,6 +364,7 @@ def resolve_value(self, value: str) -> str: Prefixes: "file:" read the content from a file. "literal:" read a literal string from a file. + "command:" read the output of a shell command. """ value = value.replace("${config:format}", self._options.format) @@ -392,6 +394,14 @@ def resolve_value(self, value: str) -> str: + f"{value!r}" ) value = found + elif value.startswith("command:"): + cmd = value.partition(":")[2].strip() + ok, out = run_shell_command(cmd) + if not ok: + raise ScrivException(f"Command {cmd!r} failed:\n{out}") + if out.count("\n") == 1: + out = out.rstrip("\r\n") + value = out return value def read_file_value(self, file_name: str) -> str: diff --git a/src/scriv/shell.py b/src/scriv/shell.py index e2cdc2c..7edd605 100644 --- a/src/scriv/shell.py +++ b/src/scriv/shell.py @@ -46,3 +46,22 @@ def run_simple_command(cmd: Union[str, List[str]]) -> str: if not ok: return "" return out.strip() + + +def run_shell_command(cmd: str) -> CmdResult: + """ + Run a command line with a shell. + """ + logger.debug(f"Running shell command {cmd!r}") + proc = subprocess.run( + cmd, + shell=True, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output = proc.stdout.decode("utf-8") + logger.debug( + f"Command exited with {proc.returncode} status. Output: {output!r}" + ) + return proc.returncode == 0, output diff --git a/tests/faker.py b/tests/faker.py index b0657a1..27ab3f9 100644 --- a/tests/faker.py +++ b/tests/faker.py @@ -11,7 +11,7 @@ class FakeRunCommand: """ - A fake implementation of run_command. + A fake implementation of run_command and similar functions. Add handlers for commands with `add_handler`. """ @@ -22,9 +22,11 @@ def __init__(self, mocker): self.mocker = mocker self.patch_module("scriv.shell") - def patch_module(self, mod_name: str) -> None: + def patch_module( + self, mod_name: str, func_name: str = "run_command" + ) -> None: """Replace ``run_command`` in `mod_name` with our fake.""" - self.mocker.patch(f"{mod_name}.run_command", self) + self.mocker.patch(f"{mod_name}.{func_name}", self) def add_handler(self, argv0: str, handler: CmdHandler) -> None: """ diff --git a/tests/test_config.py b/tests/test_config.py index 5fb1fd6..80e3bf7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -344,3 +344,37 @@ def test_no_toml_installed_no_settings(self, temp_dir): with without_module(scriv.config, "tomllib"): config = Config.read() assert config.categories[0] == "Removed" + + +def test_command_running(fake_run_command): + # Any setting can be the output of a command. + fake_run_command.patch_module("scriv.config", "run_shell_command") + fake_run_command.add_handler( + "showtext", lambda argv: (True, " ".join(argv[1:]) + "\n") + ) + text = Config(output_file="command: showtext Xyzzy 2 3").output_file + assert text == "Xyzzy 2 3" + + +def test_real_command_running(): + text = Config(output_file="command: echo Xyzzy 2 3").output_file + assert text == "Xyzzy 2 3" + + +@pytest.mark.parametrize( + "bad_cmd, msg_rx", + [ + ( + "xyzzyplugh", + "Couldn't read 'output_file' setting: Command 'xyzzyplugh' failed:", + ), + ( + "'hello!2><", + "Couldn't read 'output_file' setting: Command \"'hello!2><\" failed:" + ), + ], +) +def test_bad_command(fake_run_command, bad_cmd, msg_rx): + # Any setting can be the output of a command. + with pytest.raises(ScrivException, match=msg_rx): + _ = Config(output_file=f"command: {bad_cmd}").output_file