| Total Complexity | 43 | 
| Total Lines | 207 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like module_renamer.commands.track_modifications often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | import ast  | 
            ||
| 
                                                                                                    
                         1 ignored issue 
                            –
                            show
                         | 
                |||
| 2 | import fnmatch  | 
            ||
| 3 | import os  | 
            ||
| 4 | from collections import Counter, namedtuple  | 
            ||
| 5 | from contextlib import contextmanager  | 
            ||
| 6 | |||
| 7 | from click import ClickException, confirm, echo  | 
            ||
| 8 | from git import Repo  | 
            ||
| 9 | from tqdm import tqdm  | 
            ||
| 10 | from yapf.yapflib.yapf_api import FormatFile  | 
            ||
| 11 | |||
| 12 | CONFLICT_MSG = "\nUnfortunately, you moved two objects with the same name on " \  | 
            ||
| 13 | "different paths.\nThis situation could be catastrophic while running " \  | 
            ||
| 14 | "the script for rename the imports. \n" \  | 
            ||
| 15 |                "These are the imports that have conflict: \n -> {} \n" | 
            ||
| 16 | |||
| 17 | INFORMATIVE_CONFLICT_MSG = 'Do you want to generate the file without this import? ' \  | 
            ||
| 18 | 'Otherwise this script will be aborted.'  | 
            ||
| 19 | Import = namedtuple("Import", ["module", "name"]) | 
            ||
| 20 | |||
| 21 | |||
| 22 | @contextmanager  | 
            ||
| 23 | def branch_checkout(repo, branch_name):  | 
            ||
| 24 | # type: (git.Repo, str) -> None  | 
            ||
| 25 | """  | 
            ||
| 26 | Change the current git branch for the branch informed on branch_name  | 
            ||
| 27 | |||
| 28 | :param repo: A git.Repo object that represents the git repository from the project  | 
            ||
| 29 | :type repo: git.Repo  | 
            ||
| 30 | |||
| 31 | :param branch_name: Name of the branch that Git should move intoN  | 
            ||
| 32 | :type branch_name: str  | 
            ||
| 33 | """  | 
            ||
| 34 | if (repo.is_dirty() or len(repo.untracked_files)) != 0:  | 
            ||
| 35 |         raise ClickException("The repository is dirty, please clean it first ") | 
            ||
| 36 | |||
| 37 | current_branch = repo.active_branch.name  | 
            ||
| 38 | getattr(repo.heads, branch_name).checkout()  | 
            ||
| 39 | |||
| 40 | try:  | 
            ||
| 41 | yield  | 
            ||
| 42 | finally:  | 
            ||
| 43 | getattr(repo.heads, current_branch).checkout()  | 
            ||
| 44 | |||
| 45 | |||
| 46 | def track_modifications(**kwargs):  | 
            ||
| 47 | """  | 
            ||
| 48 | Command to track all modifications made between two different branches. The output will be a  | 
            ||
| 49 | list written directly to a file (which will later be used by the script to rename the imports)  | 
            ||
| 50 | |||
| 51 | :param kwargs:  | 
            ||
| 52 | """  | 
            ||
| 53 | project_path = os.path.join(kwargs['project_path'])  | 
            ||
| 54 | repo = Repo(project_path)  | 
            ||
| 55 | |||
| 56 | origin_branch = kwargs["origin_branch"]  | 
            ||
| 57 | |||
| 58 | if kwargs['work_branch']:  | 
            ||
| 59 | work_branch = kwargs['work_branch']  | 
            ||
| 60 | else:  | 
            ||
| 61 | work_branch = repo.active_branch.name  | 
            ||
| 62 | |||
| 63 | if origin_branch == work_branch:  | 
            ||
| 64 | raise ClickException(  | 
            ||
| 65 | "Origin and Working branch are the same. "  | 
            ||
| 66 | "Please, change your activate branch to where you made you changes on the code, "  | 
            ||
| 67 | "or use the option --origin_branch and --work_branch ."  | 
            ||
| 68 | )  | 
            ||
| 69 | file_counter = total_of_py_files_on_project(project_path)  | 
            ||
| 70 | |||
| 71 | with branch_checkout(repo, origin_branch):  | 
            ||
| 72 |         origin_import_list = {imp for imp in get_imports(project_path, file_counter)} | 
            ||
| 73 | |||
| 74 | with branch_checkout(repo, work_branch):  | 
            ||
| 75 |         working_import_list = {imp for imp in get_imports(project_path, file_counter)} | 
            ||
| 76 | |||
| 77 | modified_imports = generate_list_with_modified_imports(origin_import_list, working_import_list)  | 
            ||
| 78 | write_list_to_file(modified_imports, kwargs['output_file'])  | 
            ||
| 79 | |||
| 80 | |||
| 81 | def write_list_to_file(list_with_modified_imports, file_name):  | 
            ||
| 82 | """  | 
            ||
| 83 | Write the list of modified imports on a python file, the python file per default will be named  | 
            ||
| 84 | "list_output.py" but can be changed by passing the argument --output_file  | 
            ||
| 85 | |||
| 86 | The name of the list (inside the file) cannot be changed since it will be used later on the  | 
            ||
| 87 | script for renaming the project.  | 
            ||
| 88 | """  | 
            ||
| 89 |     echo('Generating the file {0}'.format(file_name)) | 
            ||
| 90 | |||
| 91 | with open(file_name, 'w') as file:  | 
            ||
| 92 |         file.write("imports_to_move = ") | 
            ||
| 93 | file.writelines(repr(list(list_with_modified_imports)))  | 
            ||
| 94 | |||
| 95 | FormatFile(file_name, in_place=True)  | 
            ||
| 96 | |||
| 97 | |||
| 98 | def generate_list_with_modified_imports(origin_import_list, working_import_list):  | 
            ||
| 
                                                                                                    
                         1 ignored issue 
                            –
                            show
                         | 
                |||
| 99 | """  | 
            ||
| 100 | This methods looks for imports that keep the same name but has has different modules path  | 
            ||
| 101 | |||
| 102 | :return: A list with unique elements that has the same name but different modules path  | 
            ||
| 103 | :rtype: set  | 
            ||
| 104 | """  | 
            ||
| 105 | |||
| 106 | origin_filtered, working_filtered = _filter_import(origin_import_list, working_import_list)  | 
            ||
| 107 | |||
| 108 | moved_imports = _find_moved_imports(origin_filtered, working_filtered)  | 
            ||
| 109 | |||
| 110 | list_with_modified_imports = _check_for_conflicts(moved_imports)  | 
            ||
| 111 | |||
| 112 | return list_with_modified_imports  | 
            ||
| 113 | |||
| 114 | |||
| 115 | def _filter_import(origin_import_list, working_import_list):  | 
            ||
| 116 | """  | 
            ||
| 117 | Create a new set with elements present origin_list or working_list but not on both,  | 
            ||
| 118 | this helps to filter classes that could have same name but different modules.  | 
            ||
| 119 | """  | 
            ||
| 120 | difference = origin_import_list.symmetric_difference(working_import_list)  | 
            ||
| 121 | origin_filtered = origin_import_list.intersection(difference)  | 
            ||
| 122 | working_filtered = working_import_list.intersection(difference)  | 
            ||
| 123 | return origin_filtered, working_filtered  | 
            ||
| 124 | |||
| 125 | |||
| 126 | def _find_moved_imports(origin_filtered, working_filtered):  | 
            ||
| 127 |     list_with_modified_imports = { | 
            ||
| 128 | (origin.module + "." + origin.name, working.module + "." + working.name)  | 
            ||
| 129 | for origin in origin_filtered  | 
            ||
| 130 | for working in working_filtered  | 
            ||
| 131 | if working.name is not '*'  | 
            ||
| 132 | if origin.name == working.name  | 
            ||
| 133 | if origin.module != working.module  | 
            ||
| 134 | }  | 
            ||
| 135 | return list_with_modified_imports  | 
            ||
| 136 | |||
| 137 | |||
| 138 | def _check_for_conflicts(list_with_modified_imports):  | 
            ||
| 139 | import_from = [modified_import[0] for modified_import in list(list_with_modified_imports)]  | 
            ||
| 140 | imports_with_conflict = [class_name  | 
            ||
| 141 | for class_name, number_of_occurrences in Counter(import_from).items()  | 
            ||
| 142 | if number_of_occurrences > 1]  | 
            ||
| 143 | if len(imports_with_conflict) > 0:  | 
            ||
| 144 |         echo(CONFLICT_MSG.format('\n -> '.join(imports_with_conflict))) | 
            ||
| 145 | |||
| 146 | if confirm(INFORMATIVE_CONFLICT_MSG, abort=True):  | 
            ||
| 147 | list_with_modified_imports = [modified_import for modified_import in  | 
            ||
| 148 | list_with_modified_imports  | 
            ||
| 149 | if modified_import[0] not in imports_with_conflict  | 
            ||
| 150 | if modified_import[1] not in imports_with_conflict]  | 
            ||
| 151 | return list_with_modified_imports  | 
            ||
| 152 | |||
| 153 | |||
| 154 | def get_imports(project_path, file_counter):  | 
            ||
| 155 | # type: (str) -> Import  | 
            ||
| 156 | """  | 
            ||
| 157 | Look for all .py files on the given project path and return the import statements found on  | 
            ||
| 158 | each file.  | 
            ||
| 159 | |||
| 160 | Note.: I inserted the TQDM here because was the only way that I could have an accurate  | 
            ||
| 161 | progress bar, feel free to share any thoughts or tips on how to improve this progress bar =)  | 
            ||
| 162 | |||
| 163 | :type project_path: str  | 
            ||
| 164 | :rtype: commands.utils.Import  | 
            ||
| 165 | """  | 
            ||
| 166 | with tqdm(total=file_counter, unit='files', leave=False, desc=project_path) as pbar:  | 
            ||
| 167 | for file_path in walk_on_py_files(project_path):  | 
            ||
| 168 | pbar.update()  | 
            ||
| 169 | with open(file_path, mode='r') as file:  | 
            ||
| 170 | file_content = ast.parse(file.read(), file_path)  | 
            ||
| 171 | |||
| 172 | for node in ast.iter_child_nodes(file_content):  | 
            ||
| 173 | if isinstance(node, ast.Import):  | 
            ||
| 174 | module = ''  | 
            ||
| 175 | elif isinstance(node, ast.ImportFrom):  | 
            ||
| 176 | # node.module can be None on the following situation  | 
            ||
| 177 | # from . import foo  | 
            ||
| 178 | if node.module is not None:  | 
            ||
| 179 | module = node.module  | 
            ||
| 180 | else:  | 
            ||
| 181 | module = ''  | 
            ||
| 182 | else:  | 
            ||
| 183 | continue  | 
            ||
| 184 | |||
| 185 | for n in node.names:  | 
            ||
| 186 | yield Import(module, n.name)  | 
            ||
| 187 | |||
| 188 | |||
| 189 | def walk_on_py_files(folder):  | 
            ||
| 190 | """  | 
            ||
| 191 | Walk through each python files in a directory  | 
            ||
| 192 | """  | 
            ||
| 193 | for dir_path, _, files in os.walk(folder):  | 
            ||
| 194 | for filename in fnmatch.filter(files, '*.py'):  | 
            ||
| 195 | yield os.path.abspath(os.path.join(dir_path, filename))  | 
            ||
| 196 | |||
| 197 | |||
| 198 | def total_of_py_files_on_project(project_path):  | 
            ||
| 199 | """  | 
            ||
| 200 | Helper function to find the total number of py files that needs to be iterated.  | 
            ||
| 201 | :return: Total number of py files  | 
            ||
| 202 | """  | 
            ||
| 203 | file_counter = 0  | 
            ||
| 204 | for _ in walk_on_py_files(project_path):  | 
            ||
| 205 | file_counter += 1  | 
            ||
| 206 | return file_counter  | 
            ||
| 207 | 
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.