Skip to content

Commit 04d38aa

Browse files
committed
Refactor to use FileOperation class insead of tuple
Signed-off-by: Piotr <piotrzan@gmail.com>
1 parent e793665 commit 04d38aa

File tree

4 files changed

+182
-157
lines changed

4 files changed

+182
-157
lines changed

killercoda_cli/cli.py

+142-125
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
11
#!/usr/bin/env python3
2-
import os
32
import difflib
43
import json
4+
import os
55
import subprocess
66
import sys
7-
from typing import Union, List, Tuple, Literal
7+
from typing import List, Optional
8+
9+
10+
class FileOperation:
11+
"""
12+
# Define a type hint for the different types of file operations that can be performed.
13+
# This Union type allows for specifying the operation (as a string literal indicating the type of action),
14+
# and the required arguments for each operation type:
15+
# - 'makedirs': Create a new directory; requires the path of the directory.
16+
# - 'write_file': Write content to a file; requires the file path and the content to write.
17+
# - 'chmod': Change the file mode; requires the file path and the new mode (as an integer).
818
9-
# Define a type hint for the different types of file operations that can be performed.
10-
# This Union type allows for specifying the operation (as a string literal indicating the type of action),
11-
# and the required arguments for each operation type:
12-
# - 'makedirs': Create a new directory; requires the path of the directory.
13-
# - 'write_file': Write content to a file; requires the file path and the content to write.
14-
# - 'chmod': Change the file mode; requires the file path and the new mode (as an integer).
15-
FileOperation = Union[
16-
Tuple[Literal["makedirs"], str],
17-
Tuple[Literal["write_file"], str, str],
18-
Tuple[Literal["chmod"], str, int],
19-
]
19+
"""
20+
21+
def __init__(
22+
self,
23+
operation: str,
24+
path: str,
25+
content: Optional[str] = None,
26+
mode: Optional[int] = None,
27+
):
28+
self.operation = operation
29+
self.path = path
30+
self.content = content
31+
self.mode = mode
32+
33+
def __eq__(self, other):
34+
if not isinstance(other, FileOperation):
35+
# don't attempt to compare against unrelated types
36+
return NotImplemented
37+
38+
return (
39+
self.operation == other.operation
40+
and self.path == other.path
41+
and self.content == other.content
42+
and self.mode == other.mode
43+
)
2044

2145

2246
# TODO:(piotr1215) fallback if tree is not installed
@@ -133,37 +157,56 @@ def calculate_renaming_operations(renaming_plan):
133157
Calculate the file operations required to execute the renaming plan.
134158
135159
Args:
136-
renaming_plan (list): A list of tuples representing the renaming operations required.
160+
renaming_plan (list): A list representing the renaming operations required, where each item
161+
is a tuple of the form (old_name, new_name).
137162
138163
Returns:
139-
list: A list of file operations (as tuples) to be performed for renaming.
164+
list: A list of FileOperation objects to be performed for renaming.
140165
"""
141-
# Execute the renaming plan
142166
file_operations = []
143167
for old_name, new_name in renaming_plan:
144-
# Make the new directory if it doesn't exist
145-
file_operations.append(("makedirs", new_name))
146-
# If it's a directory, we need to check for background.sh and foreground.sh
168+
# Create the new directory if it doesn't exist
169+
file_operations.append(FileOperation("makedirs", new_name))
170+
171+
# If it's a directory, we need to check for and move necessary files
147172
if os.path.isdir(old_name):
173+
# Paths for background and foreground scripts
174+
old_background = os.path.join(old_name, "background.sh")
175+
new_background = os.path.join(new_name, "background.sh")
176+
old_foreground = os.path.join(old_name, "foreground.sh")
177+
new_foreground = os.path.join(new_name, "foreground.sh")
178+
148179
# Check and move background.sh if it exists
149-
old_background = f"{old_name}/background.sh"
150-
new_background = f"{new_name}/background.sh"
151180
if os.path.isfile(old_background):
152-
file_operations.append(("rename", old_background, new_background))
181+
file_operations.append(
182+
FileOperation("rename", old_background, content=new_background)
183+
)
184+
153185
# Check and move foreground.sh if it exists
154-
old_foreground = f"{old_name}/foreground.sh"
155-
new_foreground = f"{new_name}/foreground.sh"
156186
if os.path.isfile(old_foreground):
157-
file_operations.append(("rename", old_foreground, new_foreground))
187+
file_operations.append(
188+
FileOperation("rename", old_foreground, content=new_foreground)
189+
)
190+
158191
# Rename the step markdown file
159-
old_step_md = f"{old_name}/step{old_name.replace('step', '')}.md"
160-
new_step_md = f"{new_name}/step{new_name.replace('step', '')}.md"
192+
old_step_md = os.path.join(
193+
old_name, f"step{old_name.replace('step', '')}.md"
194+
)
195+
new_step_md = os.path.join(
196+
new_name, f"step{new_name.replace('step', '')}.md"
197+
)
161198
if os.path.isfile(old_step_md):
162-
file_operations.append(("rename", old_step_md, new_step_md))
199+
file_operations.append(
200+
FileOperation("rename", old_step_md, content=new_step_md)
201+
)
202+
163203
else:
164-
# If it's just a markdown file without a directory
204+
# If it's just a markdown file without a directory, prepare to rename it
165205
new_step_md = f"{new_name}.md"
166-
file_operations.append(("rename", old_name, new_step_md))
206+
file_operations.append(
207+
FileOperation("rename", old_name, content=new_step_md)
208+
)
209+
167210
return file_operations
168211

169212

@@ -184,25 +227,27 @@ def calculate_new_step_file_operations(
184227
List[FileOperation]: A list of file operations that, when executed, will set up
185228
the new step's directory, markdown file, and script files.
186229
"""
187-
# Add the new step folder and files
188230
new_step_folder = f"step{insert_step_num}"
189231
new_step_md = f"{new_step_folder}/step{insert_step_num}.md"
190232
new_step_background = f"{new_step_folder}/background.sh"
191233
new_step_foreground = f"{new_step_folder}/foreground.sh"
192234

193-
file_operations: List[FileOperation] = [("makedirs", new_step_folder)]
194-
195-
# Write the step markdown file
196-
file_operations.append(("write_file", new_step_md, f"# {step_title}\n"))
197-
198-
# Write a simple echo command to the background and foreground scripts
199-
script_content = f'#!/bin/sh\necho "{step_title} script"\n'
200-
201-
file_operations.append(("write_file", new_step_background, script_content))
202-
file_operations.append(("write_file", new_step_foreground, script_content))
203-
204-
file_operations.append(("chmod", new_step_background, 0o755))
205-
file_operations.append(("chmod", new_step_foreground, 0o755))
235+
file_operations = [
236+
FileOperation("makedirs", new_step_folder),
237+
FileOperation("write_file", new_step_md, content=f"# {step_title}\n"),
238+
FileOperation(
239+
"write_file",
240+
new_step_background,
241+
content=f'#!/bin/sh\necho "{step_title} script"\n',
242+
),
243+
FileOperation(
244+
"write_file",
245+
new_step_foreground,
246+
content=f'#!/bin/sh\necho "{step_title} script"\n',
247+
),
248+
FileOperation("chmod", new_step_background, mode=0o755),
249+
FileOperation("chmod", new_step_foreground, mode=0o755),
250+
]
206251

207252
return file_operations
208253

@@ -272,6 +317,22 @@ def display_help():
272317
print(help_text)
273318

274319

320+
def execute_file_operations(file_operations):
321+
print("Debug file operations before execution:") # Debugging line
322+
for op in file_operations:
323+
print(op) # Debugging line
324+
for operation in file_operations:
325+
if operation.operation == "makedirs":
326+
os.makedirs(operation.path, exist_ok=True)
327+
elif operation.operation == "write_file":
328+
with open(operation.path, "w") as file:
329+
file.write(operation.content)
330+
elif operation.operation == "chmod":
331+
os.chmod(operation.path, operation.mode)
332+
elif operation.operation == "rename":
333+
os.rename(operation.path, operation.content)
334+
335+
275336
def main():
276337
"""
277338
This function orchestrates the entire process of adding a new step to the scenario,
@@ -281,90 +342,46 @@ def main():
281342
and applies those changes to the file system and the index.json file.
282343
Finally, it outputs the changes to the directory structure for the user to review.
283344
"""
284-
if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]:
285-
display_help()
286-
sys.exit()
287-
# Check for the presence of an 'index.json' file
288-
old_tree_structure = get_tree_structure()
289-
directory_items = os.listdir(".")
290-
steps_dict = get_current_steps_dict(directory_items)
291-
if not steps_dict:
292-
print(
293-
"No step files or directories found. Please run this command in a directory containing step files or directories."
345+
try:
346+
if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help"]:
347+
display_help()
348+
return
349+
old_tree_structure = get_tree_structure()
350+
directory_items = os.listdir(".")
351+
steps_dict = get_current_steps_dict(directory_items)
352+
if "index.json" not in directory_items:
353+
print(
354+
"The 'index.json' file is missing. Please ensure it is present in the current directory."
355+
)
356+
return
357+
step_title_input = input("Enter the title for the new step: ")
358+
highest_step_num = max(steps_dict.keys(), default=0)
359+
step_number_input = input(
360+
f"Enter the step number to insert the new step at (1-{highest_step_num+1}): "
294361
)
295-
sys.exit(1)
296-
if "index.json" not in os.listdir("."):
297-
print(
298-
"The 'index.json' file is missing. Please ensure it is present in the current directory."
362+
step_title, insert_step_num = get_user_input(
363+
steps_dict, step_title_input, step_number_input
299364
)
300-
sys.exit(1)
301-
if not steps_dict:
302-
print(
303-
"No step files or directories found. Please run this command in a directory containing step files or directories."
365+
renaming_plan = plan_renaming(steps_dict, insert_step_num)
366+
renaming_operations = calculate_renaming_operations(renaming_plan)
367+
new_step_operations = calculate_new_step_file_operations(
368+
insert_step_num, step_title
304369
)
305-
sys.exit(1)
306-
step_title_input = input("Enter the title for the new step: ")
307-
highest_step_num = max(steps_dict.keys(), default=0)
308-
while True:
309-
try:
310-
step_number_input = input(
311-
f"Enter the step number to insert the new step at (1-{highest_step_num+1}): "
312-
)
313-
insert_step_num = int(step_number_input)
314-
if 1 <= insert_step_num <= highest_step_num + 1:
315-
break
316-
else:
317-
print(
318-
f"Please enter a valid step number between 1 and {highest_step_num+1}."
319-
)
320-
except ValueError:
321-
print("That's not a valid number. Please try again.")
322-
step_title, insert_step_num = get_user_input(
323-
steps_dict, step_title_input, step_number_input
324-
)
325-
renaming_plan = plan_renaming(steps_dict, insert_step_num)
326-
327-
# Calculate the file operations for the renaming plan
328-
file_operations = calculate_renaming_operations(renaming_plan)
329-
# Execute the file operations
330-
for operation in file_operations:
331-
if operation[0] == "makedirs":
332-
os.makedirs(operation[1], exist_ok=True)
333-
elif operation[0] == "rename":
334-
os.rename(operation[1], operation[2])
335-
336-
# Calculate the file operations for the new step
337-
new_step_operations = calculate_new_step_file_operations(
338-
insert_step_num, step_title
339-
)
340-
# Execute the file operations for the new step
341-
for operation in new_step_operations:
342-
if operation[0] == "makedirs":
343-
os.makedirs(operation[1], exist_ok=True)
344-
elif operation[0] == "write_file":
345-
with open(operation[1], "w") as file:
346-
file.write(operation[2])
347-
elif operation[0] == "chmod":
348-
os.chmod(operation[1], operation[2])
349-
350-
# Read the current index.json data
351-
index_file_path = "index.json"
352-
with open(index_file_path, "r") as index_file:
353-
current_index_data = json.load(index_file)
354-
355-
# Calculate the updates to the index.json data
356-
updated_index_data = calculate_index_json_updates(
357-
insert_step_num, step_title, current_index_data
358-
)
359-
360-
# Write the updated index.json data back to the file
361-
with open(index_file_path, "w") as index_file:
362-
json.dump(updated_index_data, index_file, ensure_ascii=False, indent=4)
363-
new_tree_structure = get_tree_structure()
364-
# Print out the new file structure for confirmation
365-
tree_diff = generate_diff(old_tree_structure, new_tree_structure)
366-
print("\nFile structure changes:")
367-
print(tree_diff, end="")
370+
index_file_path = "index.json"
371+
with open(index_file_path, "r") as index_file:
372+
current_index_data = json.load(index_file)
373+
updated_index_data = calculate_index_json_updates(
374+
insert_step_num, step_title, current_index_data
375+
)
376+
execute_file_operations(renaming_operations + new_step_operations)
377+
with open(index_file_path, "w") as index_file:
378+
json.dump(updated_index_data, index_file, ensure_ascii=False, indent=4)
379+
new_tree_structure = get_tree_structure()
380+
tree_diff = generate_diff(old_tree_structure, new_tree_structure)
381+
print("\nFile structure changes:")
382+
print(tree_diff, end="")
383+
except Exception as e:
384+
print(f"An error occurred: {e}")
368385

369386

370387
if __name__ == "__main__":

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ path = ".venv-test"
5050
dependencies = ["coverage"]
5151

5252
[tool.hatch.envs.test.scripts]
53-
unit = "coverage run --source=killercoda_cli -m unittest discover tests"
53+
unit = "coverage run --source=killercoda_cli -m unittest discover -v tests"
5454
coverage-report = "coverage xml"
5555

5656
[tool.hatch.envs.docs]

0 commit comments

Comments
 (0)