Skip to content

Commit

Permalink
feat: run commands to get settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Ned Batchelder committed Oct 9, 2023
1 parent 8cc5926 commit ff87ae3
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/scriv/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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("\n")
value = out
return value

def read_file_value(self, file_name: str) -> str:
Expand Down
16 changes: 16 additions & 0 deletions src/scriv/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ def run_simple_command(cmd: Union[str, List[str]]) -> str:
if not ok:
return ""
return out.strip()


def run_shell_command(cmd: str) -> CmdResult:
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
6 changes: 3 additions & 3 deletions tests/faker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
"""
Expand All @@ -22,9 +22,9 @@ 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:
"""
Expand Down
28 changes: 28 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,31 @@ 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:]))
)
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><", re.escape('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

0 comments on commit ff87ae3

Please sign in to comment.