Skip to content

Commit

Permalink
Add --(no-)overwrite parameters to control override ability (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
shatakshiiii authored Oct 9, 2024
1 parent 9f0f4e8 commit 8eb5bcc
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 53 deletions.
16 changes: 16 additions & 0 deletions src/ansible_creator/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,22 @@ def _add_args_init_common(self, parser: ArgumentParser) -> None:
action="store_true",
help="Force re-initialize the specified directory.",
)
parser.add_argument(
"-o",
"--overwrite",
default=False,
dest="overwrite",
action="store_true",
help="Overwrite existing files or directories.",
)
parser.add_argument(
"-no",
"--no-overwrite",
default=False,
dest="no_overwrite",
action="store_true",
help="Flag that restricts overwriting operation.",
)

def _add_args_plugin_common(self, parser: ArgumentParser) -> None:
"""Add common plugin arguments to the parser.
Expand Down
4 changes: 4 additions & 0 deletions src/ansible_creator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Config:
subcommand: The subcommand to execute.
collection: The collection name to scaffold.
force: Whether to overwrite existing files.
overwrite: To overwrite files in an existing directory.
no_overwrite: To not overwrite files in an existing directory.
init_path: The path to initialize the project.
project: The type of project to scaffold.
collection_name: The name of the collection.
Expand All @@ -36,6 +38,8 @@ class Config:

collection: str = ""
force: bool = False
overwrite: bool = False
no_overwrite: bool = False
init_path: str | Path = "./"
project: str = ""
collection_name: str | None = None
Expand Down
51 changes: 38 additions & 13 deletions src/ansible_creator/subcommands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ansible_creator.exceptions import CreatorError
from ansible_creator.templar import Templar
from ansible_creator.types import TemplateData
from ansible_creator.utils import Copier, Walker
from ansible_creator.utils import Copier, Walker, ask_yes_no


if TYPE_CHECKING:
Expand Down Expand Up @@ -46,6 +46,8 @@ def __init__(
self._collection_name = config.collection_name or ""
self._init_path: Path = Path(config.init_path)
self._force = config.force
self._overwrite = config.overwrite
self._no_overwrite = config.no_overwrite
self._creator_version = config.creator_version
self._project = config.project
self._templar = Templar()
Expand Down Expand Up @@ -85,16 +87,7 @@ def init_exists(self) -> None:
if self._init_path.is_file():
msg = f"the path {self._init_path} already exists, but is a file - aborting"
raise CreatorError(msg)
if next(self._init_path.iterdir(), None):
# init-path exists and is not empty, but user did not request --force
if not self._force:
msg = (
f"The directory {self._init_path} is not empty.\n"
f"You can use --force to re-initialize this directory."
f"\nHowever it will delete ALL existing contents in it."
)
raise CreatorError(msg)

if next(self._init_path.iterdir(), None) and self._force:
# user requested --force, re-initializing existing directory
self.output.warning(
f"re-initializing existing directory {self._init_path}",
Expand All @@ -116,7 +109,12 @@ def unique_name_in_devfile(self) -> str:
return f"{final_name}-{final_uuid}"

def _scaffold(self) -> None:
"""Scaffold an ansible project."""
"""Scaffold an ansible project.
Raises:
CreatorError: When the destination directory contains files that will be overwritten and
the user chooses not to proceed.
"""
self.output.debug(msg=f"started copying {self._project} skeleton to destination")
template_data = TemplateData(
namespace=self._namespace,
Expand All @@ -138,6 +136,33 @@ def _scaffold(self) -> None:
copier = Copier(
output=self.output,
)
copier.copy_containers(paths)

if self._no_overwrite:
msg = "The flag `--no-overwrite` restricts overwriting."
if paths.has_conflicts():
msg += (
"\nThe destination directory contains files that can be overwritten."
"\nPlease re-run ansible-creator with --overwrite to continue."
)
raise CreatorError(msg)

if not paths.has_conflicts() or self._force or self._overwrite:
copier.copy_containers(paths)
self.output.note(f"{self._project} project created at {self._init_path}")
return

if not self._overwrite:
question = (
"Files in the destination directory will be overwritten. Do you want to proceed?"
)
answer = ask_yes_no(question)
if answer:
copier.copy_containers(paths)
else:
msg = (
"The destination directory contains files that will be overwritten."
" Please re-run ansible-creator with --overwrite to continue."
)
raise CreatorError(msg)

self.output.note(f"{self._project} project created at {self._init_path}")
18 changes: 17 additions & 1 deletion src/ansible_creator/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import yaml

from ansible_creator.constants import SKIP_DIRS, SKIP_FILES_TYPES
from ansible_creator.output import Color


if TYPE_CHECKING:
Expand Down Expand Up @@ -101,7 +102,7 @@ def conflict(self) -> str:
if self.dest.is_file():
dest_content = self.dest.read_text("utf8")
if self.content != dest_content:
return f"{self.dest} will be overwritten!"
return f"{self.dest} already exists"
else:
return f"{self.dest} already exists and is a directory!"

Expand Down Expand Up @@ -376,3 +377,18 @@ def copy_containers(self: Copier, paths: FileList) -> None:

elif path.source.is_file():
self._copy_file(path)


def ask_yes_no(question: str) -> bool:
"""Ask a question and return the answer.
Args:
question: The question to ask.
Returns:
The answer as a boolean.
"""
answer = ""
while answer not in ["y", "n"]:
answer = input(f"{Color.BRIGHT_WHITE}{question} (y/n){Color.END}: ").lower()
return answer == "y"
22 changes: 10 additions & 12 deletions tests/integration/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,17 @@ def test_run_init_basic(cli: CliRunCallable, tmp_path: Path) -> None:

assert result.returncode != 0

# this is required to handle random line breaks in CI, especially with macos runners
mod_stderr = "".join([line.strip() for line in result.stderr.splitlines()])
assert (
re.search(
rf"Error:\s*The\s*directory\s*{final_dest}/testorg/testcol\s*is\s*not\s*empty.",
mod_stderr,
)
is not None
)
assert "You can use --force to re-initialize this directory." in result.stderr
assert "However it will delete ALL existing contents in it." in result.stderr

# override existing collection with force=true
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --force")
assert result.returncode == 0
assert re.search("Warning: re-initializing existing directory", result.stdout) is not None

# override existing collection with override=true
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --overwrite")
assert result.returncode == 0
assert re.search(f"Note: collection project created at {tmp_path}", result.stdout) is not None

# use no-override=true
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --no-overwrite")
assert result.returncode != 0
assert re.search("The flag `--no-overwrite` restricts overwriting.", result.stderr) is not None
12 changes: 12 additions & 0 deletions tests/units/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "testorg.testcol",
"init_path": "./",
"force": False,
"no_overwrite": False,
"overwrite": False,
"project": "collection", # default value
},
],
Expand All @@ -76,6 +78,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "weather.demo",
"init_path": f"{Path.home()}/my-ansible-project",
"force": False,
"no_overwrite": False,
"overwrite": False,
"project": "playbook",
},
],
Expand Down Expand Up @@ -104,6 +108,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "testorg.testcol",
"init_path": f"{Path.home()}",
"force": True,
"no_overwrite": False,
"overwrite": False,
"project": "collection", # default value
},
],
Expand Down Expand Up @@ -134,6 +140,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "weather.demo",
"init_path": f"{Path.home()}/my-ansible-project",
"force": True,
"no_overwrite": False,
"overwrite": False,
"project": "playbook",
},
],
Expand All @@ -152,6 +160,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "foo.bar",
"init_path": "/test/test",
"force": False,
"no_overwrite": False,
"overwrite": False,
"json": False,
"log_append": "true",
"log_file": "test.log",
Expand All @@ -175,6 +185,8 @@ def test_configuration_class(output: Output) -> None:
"collection": "foo.bar",
"init_path": "/test/test",
"force": False,
"no_overwrite": False,
"overwrite": False,
"json": False,
"log_append": "true",
"log_file": "test.log",
Expand Down
92 changes: 66 additions & 26 deletions tests/units/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class ConfigDict(TypedDict):
init_path: Path to initialize the project.
project: The type of project to scaffold.
force: Force overwrite of existing directory.
overwrite: To overwrite files in an existing directory.
no_overwrite: To not overwrite files in an existing directory.
"""

creator_version: str
Expand All @@ -40,6 +42,8 @@ class ConfigDict(TypedDict):
init_path: str
project: str
force: bool
overwrite: bool
no_overwrite: bool


@pytest.fixture(name="cli_args")
Expand All @@ -61,6 +65,8 @@ def fixture_cli_args(tmp_path: Path, output: Output) -> ConfigDict:
"init_path": str(tmp_path / "testorg" / "testcol"),
"project": "",
"force": False,
"overwrite": False,
"no_overwrite": False,
}


Expand Down Expand Up @@ -109,14 +115,14 @@ def mock_unique_name_in_devfile(self: Init) -> str:
coll_name = self._collection_name
return f"{coll_namespace}.{coll_name}"

# Apply the mock
monkeypatch.setattr(
Init,
"unique_name_in_devfile",
mock_unique_name_in_devfile,
)

init.run()
with pytest.MonkeyPatch.context() as mp:
# Apply the mock
mp.setattr(
Init,
"unique_name_in_devfile",
mock_unique_name_in_devfile,
)
init.run()
result = capsys.readouterr().out

# check stdout
Expand All @@ -127,15 +133,32 @@ def mock_unique_name_in_devfile(self: Init) -> str:
diff = has_differences(dcmp=cmp, errors=[])
assert diff == [], diff

# fail to override existing collection with force=false (default)
# expect a CreatorError when the response to overwrite is no.
monkeypatch.setattr("builtins.input", lambda _: "n")
fail_msg = (
f"The directory {tmp_path}/testorg/testcol is not empty."
"\nYou can use --force to re-initialize this directory."
"\nHowever it will delete ALL existing contents in it."
"The destination directory contains files that will be overwritten."
" Please re-run ansible-creator with --overwrite to continue."
)
with pytest.raises(CreatorError, match=fail_msg):
with pytest.raises(
CreatorError,
match=fail_msg,
):
init.run()

# expect a warning followed by collection project creation msg
# when response to overwrite is yes.
monkeypatch.setattr("builtins.input", lambda _: "y")
init.run()
result = capsys.readouterr().out
assert (
re.search(
"already exists",
result,
)
is not None
), result
assert re.search("Note: collection project created at", result) is not None, result

# override existing collection with force=true
cli_args["force"] = True
init = Init(
Expand Down Expand Up @@ -175,14 +198,14 @@ def mock_unique_name_in_devfile(self: Init) -> str:
coll_name = self._collection_name
return f"{coll_namespace}.{coll_name}"

# Apply the mock
monkeypatch.setattr(
Init,
"unique_name_in_devfile",
mock_unique_name_in_devfile,
)

init.run()
with pytest.MonkeyPatch.context() as mp:
# Apply the mock
mp.setattr(
Init,
"unique_name_in_devfile",
mock_unique_name_in_devfile,
)
init.run()
result = capsys.readouterr().out

# check stdout
Expand All @@ -196,15 +219,32 @@ def mock_unique_name_in_devfile(self: Init) -> str:
diff = has_differences(dcmp=cmp, errors=[])
assert diff == [], diff

# fail to override existing playbook directory with force=false (default)
# expect a CreatorError when the response to overwrite is no.
monkeypatch.setattr("builtins.input", lambda _: "n")
fail_msg = (
f"The directory {tmp_path}/new_project is not empty."
"\nYou can use --force to re-initialize this directory."
"\nHowever it will delete ALL existing contents in it."
"The destination directory contains files that will be overwritten."
" Please re-run ansible-creator with --overwrite to continue."
)
with pytest.raises(CreatorError, match=fail_msg):
with pytest.raises(
CreatorError,
match=fail_msg,
):
init.run()

# expect a warning followed by playbook project creation msg
# when response to overwrite is yes.
monkeypatch.setattr("builtins.input", lambda _: "y")
init.run()
result = capsys.readouterr().out
assert (
re.search(
"already exists",
result,
)
is not None
), result
assert re.search("Note: playbook project created at", result) is not None, result

# override existing playbook directory with force=true
cli_args["force"] = True
init = Init(
Expand Down
Loading

0 comments on commit 8eb5bcc

Please sign in to comment.