Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --(no-)overwrite parameters to control override ability #298

Merged
merged 18 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading