11"""Create cli using the typer library."""
2+
23import re
34import typer
45import pathlib
5- from snappylapy .constants import directory_names
6+ from enum import Enum
7+ from snappylapy ._utils_directories import DirectoryNamesUtil
8+ from snappylapy .constants import DIRECTORY_NAMES
69
710app = typer .Typer (
811 no_args_is_help = True ,
912 help = """
10- The CLI provides commands to initialize the repo and to clear test results and snapshots.
11- In the future the future the CLI will be expanded with review and update commands .
13+ The CLI provides commands to initialize the repo and to update or clear test results and snapshots.
14+ In the future the future the CLI will be expanded with review.
1215 """ ,
1316)
1417
@@ -24,39 +27,48 @@ def init() -> None:
2427 # Check if already in .gitignore
2528 with gitignore_path .open ("r" ) as file :
2629 lines = file .readlines ()
27- regex = re .compile (
28- rf"^{ re .escape (directory_names .test_results_dir_name )} (/|$)" )
30+ regex = re .compile (rf"^{ re .escape (DIRECTORY_NAMES .test_results_dir_name )} (/|$)" )
2931 if any (regex .match (line ) for line in lines ):
3032 typer .echo ("Already in .gitignore." )
3133 return
3234 # Add to .gitignore to top of file
33- line_to_add = f"# Ignore test results from snappylapy\n { directory_names .test_results_dir_name } /\n \n "
35+ line_to_add = f"# Ignore test results from snappylapy\n { DIRECTORY_NAMES .test_results_dir_name } /\n \n "
3436 with gitignore_path .open ("w" ) as file :
3537 file .write (line_to_add )
3638 file .writelines (lines )
37- typer .echo (
38- f"Added { directory_names .test_results_dir_name } / to .gitignore." )
39+ typer .echo (f"Added { DIRECTORY_NAMES .test_results_dir_name } / to .gitignore." )
3940
4041
4142@app .command ()
42- def clear (force : bool = typer .Option (
43- False ,
44- "--force" ,
45- "-f" ,
46- help = "Force deletion without confirmation" ,
47- )) -> None :
43+ def clear (
44+ force : bool = typer .Option (
45+ False ,
46+ "--force" ,
47+ "-f" ,
48+ help = "Force deletion without confirmation" ,
49+ ),
50+ ) -> None :
4851 """Clear all test results and snapshots, recursively, using pathlib."""
49- list_of_files_to_delete = get_files_to_delete ()
52+ directories_to_delete = DirectoryNamesUtil ().get_all_directories_created_by_snappylapy ()
53+ list_of_files_to_delete = DirectoryNamesUtil ().get_all_file_paths_created_by_snappylapy ()
5054 if not list_of_files_to_delete :
5155 typer .echo ("No files to delete." )
5256 return
5357 if not force :
54- # Ask for confirmation
58+ typer .echo ("Deleting files:" )
59+ for file in list_of_files_to_delete :
60+ typer .echo (f"- { file } " )
61+
5562 typer .secho (
56- "\n Are you sure you want to delete all test results and snapshots?" ,
57- fg = typer .colors .BRIGHT_BLUE )
58- response = typer .prompt (
59- "Type 'yes' to confirm, anything else to abort." )
63+ f"Deleting { len (list_of_files_to_delete )} files from { len (directories_to_delete )} directories:" ,
64+ fg = typer .colors .BRIGHT_BLUE ,
65+ )
66+ for directory in directories_to_delete :
67+ typer .echo (f"- { directory } " )
68+
69+ # Ask for confirmation
70+ typer .secho ("\n Are you sure you want to delete all test results and snapshots?" , fg = typer .colors .BRIGHT_BLUE )
71+ response = typer .prompt ("Type 'yes' to confirm, anything else to abort." , default = "no" )
6072 if response .lower () != "yes" :
6173 typer .echo ("Aborted." )
6274 return
@@ -65,19 +77,29 @@ def clear(force: bool = typer.Option(
6577 typer .echo (f"Deleted { len (list_of_files_to_delete )} files." )
6678
6779
68- def get_files_to_delete () -> list [pathlib .Path ]:
69- """Get list of files to delete."""
70- list_of_files_to_delete : list [pathlib .Path ] = []
71- for dir_name in [
72- directory_names .test_results_dir_name ,
73- directory_names .snapshot_dir_name ,
74- ]:
75- for root_dir in pathlib .Path ().rglob (dir_name ):
76- for file in root_dir .iterdir ():
77- if file .is_file ():
78- list_of_files_to_delete .append (file )
79- typer .echo (f"Found file to delete: { file } " )
80- return list_of_files_to_delete
80+ @app .command ()
81+ def update () -> None :
82+ """Update the snapshot files by copying the test results, to the snapshot directory."""
83+ files_test_results = DirectoryNamesUtil ().get_all_file_paths_test_results ()
84+ if not files_test_results :
85+ typer .echo ("No files to update." )
86+ return
87+ file_statuses = check_file_statuses (files_test_results )
88+ files_to_update = [file for file , status in file_statuses .items () if status != FileStatus .UNCHANGED ]
89+ count_up_to_date_files = len (files_test_results ) - len (files_to_update )
90+ if not files_to_update :
91+ typer .echo (f"All snapshot files are up to date. { count_up_to_date_files } files are up to date." )
92+ return
93+
94+ typer .echo (
95+ f"Found { len (files_to_update )} files to update."
96+ + (f" { count_up_to_date_files } files are up to date." if count_up_to_date_files > 0 else "" ),
97+ )
98+ for file in files_to_update :
99+ snapshot_file = file .parent .parent / DIRECTORY_NAMES .snapshot_dir_name / file .name
100+ snapshot_file .parent .mkdir (parents = True , exist_ok = True )
101+ snapshot_file .write_bytes (file .read_bytes ())
102+ typer .echo (f"Updated snapshot: { snapshot_file } " )
81103
82104
83105def delete_files (list_of_files_to_delete : list [pathlib .Path ]) -> None :
@@ -87,12 +109,40 @@ def delete_files(list_of_files_to_delete: list[pathlib.Path]) -> None:
87109 file .unlink ()
88110 # Delete directories
89111 for dir_name in [
90- directory_names .test_results_dir_name ,
91- directory_names .snapshot_dir_name ,
112+ DIRECTORY_NAMES .test_results_dir_name ,
113+ DIRECTORY_NAMES .snapshot_dir_name ,
92114 ]:
93115 for root_dir in pathlib .Path ().rglob (dir_name ):
94116 root_dir .rmdir ()
95117
96118
119+ class FileStatus (Enum ):
120+ """Enum to represent the status of a file."""
121+
122+ NOT_FOUND = "not_found"
123+ CHANGED = "changed"
124+ UNCHANGED = "unchanged"
125+
126+
127+ def check_file_statuses (
128+ file_paths : list [pathlib .Path ],
129+ ) -> dict [pathlib .Path , FileStatus ]:
130+ """Check the status of files in the snapshot directory."""
131+ file_statuses : dict [pathlib .Path , FileStatus ] = {}
132+ for file_path in file_paths :
133+ snapshot_file = file_path .parent .parent / DIRECTORY_NAMES .snapshot_dir_name / file_path .name
134+ if not snapshot_file .exists ():
135+ file_statuses [file_path ] = FileStatus .NOT_FOUND
136+ elif snapshot_file .stat ().st_size != file_path .stat ().st_size :
137+ # TODO: This is not foolproof, does not catch content swaps and byte flips.
138+ file_statuses [file_path ] = FileStatus .CHANGED
139+ elif snapshot_file .read_bytes () != file_path .read_bytes ():
140+ # TODO: Expensive call, store hashes instead in a data file.
141+ file_statuses [file_path ] = FileStatus .CHANGED
142+ else :
143+ file_statuses [file_path ] = FileStatus .UNCHANGED
144+ return file_statuses
145+
146+
97147if __name__ == "__main__" :
98148 app ()
0 commit comments