Skip to content

Commit 8eb5bcc

Browse files
authored
Add --(no-)overwrite parameters to control override ability (#298)
1 parent 9f0f4e8 commit 8eb5bcc

File tree

8 files changed

+210
-53
lines changed

8 files changed

+210
-53
lines changed

src/ansible_creator/arg_parser.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,22 @@ def _add_args_init_common(self, parser: ArgumentParser) -> None:
196196
action="store_true",
197197
help="Force re-initialize the specified directory.",
198198
)
199+
parser.add_argument(
200+
"-o",
201+
"--overwrite",
202+
default=False,
203+
dest="overwrite",
204+
action="store_true",
205+
help="Overwrite existing files or directories.",
206+
)
207+
parser.add_argument(
208+
"-no",
209+
"--no-overwrite",
210+
default=False,
211+
dest="no_overwrite",
212+
action="store_true",
213+
help="Flag that restricts overwriting operation.",
214+
)
199215

200216
def _add_args_plugin_common(self, parser: ArgumentParser) -> None:
201217
"""Add common plugin arguments to the parser.

src/ansible_creator/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Config:
2424
subcommand: The subcommand to execute.
2525
collection: The collection name to scaffold.
2626
force: Whether to overwrite existing files.
27+
overwrite: To overwrite files in an existing directory.
28+
no_overwrite: To not overwrite files in an existing directory.
2729
init_path: The path to initialize the project.
2830
project: The type of project to scaffold.
2931
collection_name: The name of the collection.
@@ -36,6 +38,8 @@ class Config:
3638

3739
collection: str = ""
3840
force: bool = False
41+
overwrite: bool = False
42+
no_overwrite: bool = False
3943
init_path: str | Path = "./"
4044
project: str = ""
4145
collection_name: str | None = None

src/ansible_creator/subcommands/init.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from ansible_creator.exceptions import CreatorError
1212
from ansible_creator.templar import Templar
1313
from ansible_creator.types import TemplateData
14-
from ansible_creator.utils import Copier, Walker
14+
from ansible_creator.utils import Copier, Walker, ask_yes_no
1515

1616

1717
if TYPE_CHECKING:
@@ -46,6 +46,8 @@ def __init__(
4646
self._collection_name = config.collection_name or ""
4747
self._init_path: Path = Path(config.init_path)
4848
self._force = config.force
49+
self._overwrite = config.overwrite
50+
self._no_overwrite = config.no_overwrite
4951
self._creator_version = config.creator_version
5052
self._project = config.project
5153
self._templar = Templar()
@@ -85,16 +87,7 @@ def init_exists(self) -> None:
8587
if self._init_path.is_file():
8688
msg = f"the path {self._init_path} already exists, but is a file - aborting"
8789
raise CreatorError(msg)
88-
if next(self._init_path.iterdir(), None):
89-
# init-path exists and is not empty, but user did not request --force
90-
if not self._force:
91-
msg = (
92-
f"The directory {self._init_path} is not empty.\n"
93-
f"You can use --force to re-initialize this directory."
94-
f"\nHowever it will delete ALL existing contents in it."
95-
)
96-
raise CreatorError(msg)
97-
90+
if next(self._init_path.iterdir(), None) and self._force:
9891
# user requested --force, re-initializing existing directory
9992
self.output.warning(
10093
f"re-initializing existing directory {self._init_path}",
@@ -116,7 +109,12 @@ def unique_name_in_devfile(self) -> str:
116109
return f"{final_name}-{final_uuid}"
117110

118111
def _scaffold(self) -> None:
119-
"""Scaffold an ansible project."""
112+
"""Scaffold an ansible project.
113+
114+
Raises:
115+
CreatorError: When the destination directory contains files that will be overwritten and
116+
the user chooses not to proceed.
117+
"""
120118
self.output.debug(msg=f"started copying {self._project} skeleton to destination")
121119
template_data = TemplateData(
122120
namespace=self._namespace,
@@ -138,6 +136,33 @@ def _scaffold(self) -> None:
138136
copier = Copier(
139137
output=self.output,
140138
)
141-
copier.copy_containers(paths)
139+
140+
if self._no_overwrite:
141+
msg = "The flag `--no-overwrite` restricts overwriting."
142+
if paths.has_conflicts():
143+
msg += (
144+
"\nThe destination directory contains files that can be overwritten."
145+
"\nPlease re-run ansible-creator with --overwrite to continue."
146+
)
147+
raise CreatorError(msg)
148+
149+
if not paths.has_conflicts() or self._force or self._overwrite:
150+
copier.copy_containers(paths)
151+
self.output.note(f"{self._project} project created at {self._init_path}")
152+
return
153+
154+
if not self._overwrite:
155+
question = (
156+
"Files in the destination directory will be overwritten. Do you want to proceed?"
157+
)
158+
answer = ask_yes_no(question)
159+
if answer:
160+
copier.copy_containers(paths)
161+
else:
162+
msg = (
163+
"The destination directory contains files that will be overwritten."
164+
" Please re-run ansible-creator with --overwrite to continue."
165+
)
166+
raise CreatorError(msg)
142167

143168
self.output.note(f"{self._project} project created at {self._init_path}")

src/ansible_creator/utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import yaml
1616

1717
from ansible_creator.constants import SKIP_DIRS, SKIP_FILES_TYPES
18+
from ansible_creator.output import Color
1819

1920

2021
if TYPE_CHECKING:
@@ -101,7 +102,7 @@ def conflict(self) -> str:
101102
if self.dest.is_file():
102103
dest_content = self.dest.read_text("utf8")
103104
if self.content != dest_content:
104-
return f"{self.dest} will be overwritten!"
105+
return f"{self.dest} already exists"
105106
else:
106107
return f"{self.dest} already exists and is a directory!"
107108

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

377378
elif path.source.is_file():
378379
self._copy_file(path)
380+
381+
382+
def ask_yes_no(question: str) -> bool:
383+
"""Ask a question and return the answer.
384+
385+
Args:
386+
question: The question to ask.
387+
388+
Returns:
389+
The answer as a boolean.
390+
"""
391+
answer = ""
392+
while answer not in ["y", "n"]:
393+
answer = input(f"{Color.BRIGHT_WHITE}{question} (y/n){Color.END}: ").lower()
394+
return answer == "y"

tests/integration/test_init.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,17 @@ def test_run_init_basic(cli: CliRunCallable, tmp_path: Path) -> None:
127127

128128
assert result.returncode != 0
129129

130-
# this is required to handle random line breaks in CI, especially with macos runners
131-
mod_stderr = "".join([line.strip() for line in result.stderr.splitlines()])
132-
assert (
133-
re.search(
134-
rf"Error:\s*The\s*directory\s*{final_dest}/testorg/testcol\s*is\s*not\s*empty.",
135-
mod_stderr,
136-
)
137-
is not None
138-
)
139-
assert "You can use --force to re-initialize this directory." in result.stderr
140-
assert "However it will delete ALL existing contents in it." in result.stderr
141-
142130
# override existing collection with force=true
143131
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --force")
144132
assert result.returncode == 0
145133
assert re.search("Warning: re-initializing existing directory", result.stdout) is not None
134+
135+
# override existing collection with override=true
136+
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --overwrite")
137+
assert result.returncode == 0
138+
assert re.search(f"Note: collection project created at {tmp_path}", result.stdout) is not None
139+
140+
# use no-override=true
141+
result = cli(f"{CREATOR_BIN} init testorg.testcol --init-path {tmp_path} --no-overwrite")
142+
assert result.returncode != 0
143+
assert re.search("The flag `--no-overwrite` restricts overwriting.", result.stderr) is not None

tests/units/test_basic.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def test_configuration_class(output: Output) -> None:
5353
"collection": "testorg.testcol",
5454
"init_path": "./",
5555
"force": False,
56+
"no_overwrite": False,
57+
"overwrite": False,
5658
"project": "collection", # default value
5759
},
5860
],
@@ -76,6 +78,8 @@ def test_configuration_class(output: Output) -> None:
7678
"collection": "weather.demo",
7779
"init_path": f"{Path.home()}/my-ansible-project",
7880
"force": False,
81+
"no_overwrite": False,
82+
"overwrite": False,
7983
"project": "playbook",
8084
},
8185
],
@@ -104,6 +108,8 @@ def test_configuration_class(output: Output) -> None:
104108
"collection": "testorg.testcol",
105109
"init_path": f"{Path.home()}",
106110
"force": True,
111+
"no_overwrite": False,
112+
"overwrite": False,
107113
"project": "collection", # default value
108114
},
109115
],
@@ -134,6 +140,8 @@ def test_configuration_class(output: Output) -> None:
134140
"collection": "weather.demo",
135141
"init_path": f"{Path.home()}/my-ansible-project",
136142
"force": True,
143+
"no_overwrite": False,
144+
"overwrite": False,
137145
"project": "playbook",
138146
},
139147
],
@@ -152,6 +160,8 @@ def test_configuration_class(output: Output) -> None:
152160
"collection": "foo.bar",
153161
"init_path": "/test/test",
154162
"force": False,
163+
"no_overwrite": False,
164+
"overwrite": False,
155165
"json": False,
156166
"log_append": "true",
157167
"log_file": "test.log",
@@ -175,6 +185,8 @@ def test_configuration_class(output: Output) -> None:
175185
"collection": "foo.bar",
176186
"init_path": "/test/test",
177187
"force": False,
188+
"no_overwrite": False,
189+
"overwrite": False,
178190
"json": False,
179191
"log_append": "true",
180192
"log_file": "test.log",

tests/units/test_init.py

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class ConfigDict(TypedDict):
3131
init_path: Path to initialize the project.
3232
project: The type of project to scaffold.
3333
force: Force overwrite of existing directory.
34+
overwrite: To overwrite files in an existing directory.
35+
no_overwrite: To not overwrite files in an existing directory.
3436
"""
3537

3638
creator_version: str
@@ -40,6 +42,8 @@ class ConfigDict(TypedDict):
4042
init_path: str
4143
project: str
4244
force: bool
45+
overwrite: bool
46+
no_overwrite: bool
4347

4448

4549
@pytest.fixture(name="cli_args")
@@ -61,6 +65,8 @@ def fixture_cli_args(tmp_path: Path, output: Output) -> ConfigDict:
6165
"init_path": str(tmp_path / "testorg" / "testcol"),
6266
"project": "",
6367
"force": False,
68+
"overwrite": False,
69+
"no_overwrite": False,
6470
}
6571

6672

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

112-
# Apply the mock
113-
monkeypatch.setattr(
114-
Init,
115-
"unique_name_in_devfile",
116-
mock_unique_name_in_devfile,
117-
)
118-
119-
init.run()
118+
with pytest.MonkeyPatch.context() as mp:
119+
# Apply the mock
120+
mp.setattr(
121+
Init,
122+
"unique_name_in_devfile",
123+
mock_unique_name_in_devfile,
124+
)
125+
init.run()
120126
result = capsys.readouterr().out
121127

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

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

148+
# expect a warning followed by collection project creation msg
149+
# when response to overwrite is yes.
150+
monkeypatch.setattr("builtins.input", lambda _: "y")
151+
init.run()
152+
result = capsys.readouterr().out
153+
assert (
154+
re.search(
155+
"already exists",
156+
result,
157+
)
158+
is not None
159+
), result
160+
assert re.search("Note: collection project created at", result) is not None, result
161+
139162
# override existing collection with force=true
140163
cli_args["force"] = True
141164
init = Init(
@@ -175,14 +198,14 @@ def mock_unique_name_in_devfile(self: Init) -> str:
175198
coll_name = self._collection_name
176199
return f"{coll_namespace}.{coll_name}"
177200

178-
# Apply the mock
179-
monkeypatch.setattr(
180-
Init,
181-
"unique_name_in_devfile",
182-
mock_unique_name_in_devfile,
183-
)
184-
185-
init.run()
201+
with pytest.MonkeyPatch.context() as mp:
202+
# Apply the mock
203+
mp.setattr(
204+
Init,
205+
"unique_name_in_devfile",
206+
mock_unique_name_in_devfile,
207+
)
208+
init.run()
186209
result = capsys.readouterr().out
187210

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

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

234+
# expect a warning followed by playbook project creation msg
235+
# when response to overwrite is yes.
236+
monkeypatch.setattr("builtins.input", lambda _: "y")
237+
init.run()
238+
result = capsys.readouterr().out
239+
assert (
240+
re.search(
241+
"already exists",
242+
result,
243+
)
244+
is not None
245+
), result
246+
assert re.search("Note: playbook project created at", result) is not None, result
247+
208248
# override existing playbook directory with force=true
209249
cli_args["force"] = True
210250
init = Init(

0 commit comments

Comments
 (0)