Skip to content

Commit abd5d57

Browse files
committed
feat: raise InteractiveSessionError when prompting in non-interactive environment
1 parent 1b4bbb7 commit abd5d57

File tree

3 files changed

+75
-7
lines changed

3 files changed

+75
-7
lines changed

copier/errors.py

+7
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,10 @@ class MissingSettingsWarning(UserWarning, CopierWarning):
147147

148148
class MissingFileWarning(UserWarning, CopierWarning):
149149
"""I still couldn't find what I'm looking for."""
150+
151+
152+
class InteractiveSessionError(UserMessageError):
153+
"""An interactive session is required to run this program."""
154+
155+
def __init__(self, message: str) -> None:
156+
super().__init__(f"Interactive session required: {message}")

copier/main.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@
3232
from jinja2.loaders import FileSystemLoader
3333
from pathspec import PathSpec
3434
from plumbum import ProcessExecutionError, colors
35-
from plumbum.cli.terminal import ask
3635
from plumbum.machines import local
3736
from pydantic import ConfigDict, PositiveInt
3837
from pydantic.dataclasses import dataclass
3938
from pydantic_core import to_jsonable_python
40-
from questionary import unsafe_prompt
39+
from questionary import confirm, unsafe_prompt
4140

4241
from .errors import (
4342
CopierAnswersInterrupt,
4443
ExtensionNotFoundError,
44+
InteractiveSessionError,
4545
UnsafeTemplateError,
4646
UserMessageError,
4747
YieldTagInFileError,
@@ -466,7 +466,11 @@ def _solve_render_conflict(self, dst_relpath: Path) -> bool:
466466
file_=sys.stderr,
467467
)
468468
return True
469-
return bool(ask(f" Overwrite {dst_relpath}?", default=True))
469+
try:
470+
answer = confirm(f" Overwrite {dst_relpath}?", default=True).unsafe_ask()
471+
except EOFError as err:
472+
raise InteractiveSessionError("Consider using `--overwrite`") from err
473+
return bool(answer)
470474

471475
def _render_allowed(
472476
self,
@@ -587,10 +591,15 @@ def _ask(self) -> None: # noqa: C901
587591
if new_answer is MISSING:
588592
raise ValueError(f'Question "{var_name}" is required')
589593
else:
590-
new_answer = unsafe_prompt(
591-
[question.get_questionary_structure()],
592-
answers={question.var_name: question.get_default()},
593-
)[question.var_name]
594+
try:
595+
new_answer = unsafe_prompt(
596+
[question.get_questionary_structure()],
597+
answers={question.var_name: question.get_default()},
598+
)[question.var_name]
599+
except EOFError as err:
600+
raise InteractiveSessionError(
601+
"Use `--defaults` and/or `--data`/`--data-file`"
602+
) from err
594603
except KeyboardInterrupt as err:
595604
raise CopierAnswersInterrupt(
596605
self.answers, question, self.template

tests/test_prompt.py

+52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import platform
4+
import subprocess
35
import sys
46
from pathlib import Path
57
from typing import Any, Dict, List, Mapping, Protocol, Union
@@ -997,3 +999,53 @@ def test_secret_validator(
997999
tui.sendline(default)
9981000
tui.expect_exact("******")
9991001
tui.expect_exact(pexpect.EOF)
1002+
1003+
1004+
# Related to https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1243#issuecomment-706668723)
1005+
@pytest.mark.xfail(
1006+
platform.system() == "Windows",
1007+
reason="prompt-toolkit in subprocess call fails on Windows",
1008+
)
1009+
def test_interactive_session_required_for_question_prompt(
1010+
question_tree: QuestionTreeFixture,
1011+
) -> None:
1012+
"""Answering a question prompt requires an interactive session."""
1013+
src, dst = question_tree(type="str")
1014+
process = subprocess.run(
1015+
(*COPIER_PATH, "copy", str(src), str(dst)),
1016+
stdin=subprocess.PIPE, # Prevents interactive input
1017+
capture_output=True,
1018+
timeout=10,
1019+
)
1020+
assert process.returncode == 1
1021+
assert (
1022+
b"Interactive session required: Use `--defaults` and/or `--data`/`--data-file`"
1023+
) in process.stderr
1024+
1025+
1026+
# Related to https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1243#issuecomment-706668723)
1027+
@pytest.mark.xfail(
1028+
platform.system() == "Windows",
1029+
reason="prompt-toolkit in subprocess call fails on Windows",
1030+
)
1031+
def test_interactive_session_required_for_overwrite_prompt(
1032+
tmp_path_factory: pytest.TempPathFactory,
1033+
) -> None:
1034+
"""Overwriting a file without `--overwrite` flag requires an interactive session."""
1035+
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
1036+
build_file_tree(
1037+
{
1038+
(src / "foo.txt.jinja"): "bar",
1039+
(dst / "foo.txt"): "baz",
1040+
}
1041+
)
1042+
process = subprocess.run(
1043+
(*COPIER_PATH, "copy", str(src), str(dst)),
1044+
stdin=subprocess.PIPE, # Prevents interactive input
1045+
capture_output=True,
1046+
timeout=10,
1047+
)
1048+
assert process.returncode == 1
1049+
assert (
1050+
b"Interactive session required: Consider using `--overwrite`" in process.stderr
1051+
)

0 commit comments

Comments
 (0)