| Total Complexity | 44 |
| Total Lines | 226 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like module_renamer.commands.analyze_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 = ( |
||
| 13 | "\n" |
||
| 14 | "Unfortunately, you moved two objects with the same name on different paths.\n" |
||
| 15 | "This situation could be catastrophic while running the script for rename the imports. \n" |
||
| 16 | "These are the imports that have conflict: \n " |
||
| 17 | " -> {} \n" |
||
| 18 | ) |
||
| 19 | |||
| 20 | INFORMATIVE_CONFLICT_MSG = ( |
||
| 21 | "Do you want to generate the file without this import? \n" |
||
| 22 | "Otherwise this script will be aborted." |
||
| 23 | ) |
||
| 24 | |||
| 25 | Import = namedtuple("Import", ["module", "name"]) |
||
| 26 | |||
| 27 | |||
| 28 | @contextmanager |
||
| 29 | def checkout_branch(repo, branch_name): |
||
| 30 | """ |
||
| 31 | Change the current git branch for the branch informed on branch_name |
||
| 32 | |||
| 33 | :param git.Repo repo: The git repository of the project. |
||
| 34 | |||
| 35 | :param str branch_name: Name of the branch that Git should move into |
||
| 36 | """ |
||
| 37 | |||
| 38 | if (repo.is_dirty() or len(repo.untracked_files)) != 0: |
||
| 39 | raise ClickException("The repository is dirty, please clean it first ") |
||
| 40 | |||
| 41 | current_branch = repo.active_branch.name |
||
| 42 | repo.branches[branch_name].checkout() |
||
| 43 | |||
| 44 | try: |
||
| 45 | yield |
||
| 46 | finally: |
||
| 47 | getattr(repo.heads, current_branch).checkout() |
||
| 48 | |||
| 49 | |||
| 50 | def analyze_modifications(project_path, compare_with, branch, output_file): |
||
| 51 | """ |
||
| 52 | Track modifications between two different branches. |
||
| 53 | The output will be a list written directly to a file. |
||
| 54 | """ |
||
| 55 | |||
| 56 | full_project_path = os.path.join(project_path) |
||
| 57 | repo = Repo(project_path) |
||
| 58 | |||
| 59 | origin_branch = compare_with |
||
| 60 | |||
| 61 | if branch: |
||
| 62 | work_branch = branch |
||
| 63 | else: |
||
| 64 | work_branch = repo.active_branch.name |
||
| 65 | |||
| 66 | if origin_branch == work_branch: |
||
| 67 | raise ClickException( |
||
| 68 | "Origin and Working branch are the same. " |
||
| 69 | "Please, change your activate branch to where you made you changes on the code, " |
||
| 70 | "or use the option --branch and --compare-with ." |
||
| 71 | ) |
||
| 72 | |||
| 73 | file_counter = total_of_py_files_on_project(full_project_path) |
||
| 74 | |||
| 75 | with checkout_branch(repo, origin_branch): |
||
| 76 | import_list_from_origin = {imp for imp in get_imports(full_project_path, file_counter)} |
||
| 77 | |||
| 78 | with checkout_branch(repo, work_branch): |
||
| 79 | import_list_from_working = {imp for imp in get_imports(full_project_path, file_counter)} |
||
| 80 | |||
| 81 | list_with_modified_imports = generate_list_with_modified_imports(import_list_from_origin, |
||
| 82 | import_list_from_working) |
||
| 83 | write_list_to_file(list_with_modified_imports, output_file) |
||
| 84 | |||
| 85 | |||
| 86 | def write_list_to_file(list_with_modified_imports, file_name): |
||
| 87 | """ |
||
| 88 | Write the list of modified imports on a python file, the python file per default will be named |
||
| 89 | "list_output.py" but can be changed by passing the argument --output_file |
||
| 90 | |||
| 91 | The name of the list (inside the file) cannot be changed since it will be used later on the |
||
| 92 | script for renaming the project. |
||
| 93 | """ |
||
| 94 | echo('Generating the file {0}'.format(file_name)) |
||
| 95 | |||
| 96 | with open(file_name, 'w') as file: |
||
| 97 | file.write("imports_to_move = ") |
||
| 98 | file.writelines(repr(list(list_with_modified_imports))) |
||
| 99 | |||
| 100 | FormatFile(file_name, in_place=True) |
||
| 101 | |||
| 102 | |||
| 103 | def generate_list_with_modified_imports(import_list_from_origin, import_list_from_working): |
||
|
1 ignored issue
–
show
|
|||
| 104 | """ |
||
| 105 | This methods looks for imports that keep the same name but has has different modules path |
||
| 106 | |||
| 107 | :return: A list with unique elements that has the same name but different modules path |
||
| 108 | :rtype: set |
||
| 109 | """ |
||
| 110 | |||
| 111 | origin_filtered, working_filtered = _filter_import(import_list_from_origin, |
||
| 112 | import_list_from_working) |
||
| 113 | |||
| 114 | moved_imports = _find_moved_imports(origin_filtered, working_filtered) |
||
| 115 | |||
| 116 | list_with_modified_imports = _check_for_conflicts(moved_imports) |
||
| 117 | |||
| 118 | return list_with_modified_imports |
||
| 119 | |||
| 120 | |||
| 121 | def _filter_import(origin_import_list, working_import_list): |
||
| 122 | """ |
||
| 123 | Create a new set with elements present origin_list or working_list but not on both, |
||
| 124 | this helps to filter classes that could have same name but different modules. |
||
| 125 | """ |
||
| 126 | difference = origin_import_list.symmetric_difference(working_import_list) |
||
| 127 | origin_filtered = origin_import_list.intersection(difference) |
||
| 128 | working_filtered = working_import_list.intersection(difference) |
||
| 129 | return origin_filtered, working_filtered |
||
| 130 | |||
| 131 | |||
| 132 | def _find_moved_imports(list_with_import_from_origin_branch, list_with_import_from_working_branch): |
||
| 133 | """ |
||
| 134 | Return a list with tuples where the first element is the origin and the second element is |
||
| 135 | the branch with the modifications |
||
| 136 | """ |
||
| 137 | list_with_modified_imports = { |
||
| 138 | (origin.module + "." + origin.name, working.module + "." + working.name) |
||
| 139 | for origin in list_with_import_from_origin_branch |
||
| 140 | for working in list_with_import_from_working_branch |
||
| 141 | if working.name is not '*' |
||
| 142 | if origin.name == working.name |
||
| 143 | if origin.module != working.module |
||
| 144 | } |
||
| 145 | return list_with_modified_imports |
||
| 146 | |||
| 147 | |||
| 148 | def _check_for_conflicts(list_with_modified_imports): |
||
| 149 | """ |
||
| 150 | Checks if there is more than one object with the same module path. |
||
| 151 | |||
| 152 | If any conflict is found, the script will display the list of conflicts for the user |
||
| 153 | to decide either abort the script or create the list without the conflicted module path. |
||
| 154 | """ |
||
| 155 | import_from = [modified_import[0] for modified_import in list(list_with_modified_imports)] |
||
| 156 | imports_with_conflict = [class_name |
||
| 157 | for class_name, number_of_occurrences in Counter(import_from).items() |
||
| 158 | if number_of_occurrences > 1] |
||
| 159 | if len(imports_with_conflict) > 0: |
||
| 160 | echo(CONFLICT_MSG.format('\n -> '.join(imports_with_conflict))) |
||
| 161 | |||
| 162 | if confirm(INFORMATIVE_CONFLICT_MSG, abort=True): |
||
| 163 | list_with_modified_imports = [modified_import for modified_import in |
||
| 164 | list_with_modified_imports |
||
| 165 | if modified_import[0] not in imports_with_conflict |
||
| 166 | if modified_import[1] not in imports_with_conflict] |
||
| 167 | return list_with_modified_imports |
||
| 168 | |||
| 169 | |||
| 170 | def get_imports(project_path, file_counter): |
||
| 171 | # type: (str) -> Import |
||
| 172 | """ |
||
| 173 | Look for all .py files on the given project path and return the import statements found on |
||
| 174 | each file. |
||
| 175 | |||
| 176 | Note.: I inserted the TQDM here because was the only way that I could have an accurate |
||
| 177 | progress bar, feel free to share any thoughts or tips on how to improve this progress bar =) |
||
| 178 | |||
| 179 | :type project_path: str |
||
| 180 | :rtype: commands.utils.Import |
||
| 181 | """ |
||
| 182 | with tqdm(total=file_counter, unit='files', leave=False, desc=project_path) as pbar: |
||
| 183 | for file_path in walk_on_py_files(project_path): |
||
| 184 | pbar.update() |
||
| 185 | with open(file_path, mode='r') as file: |
||
| 186 | file_content = ast.parse(file.read(), file_path) |
||
| 187 | |||
| 188 | yield from _classify_imports(file_content) |
||
| 189 | |||
| 190 | |||
| 191 | def _classify_imports(file_content): |
||
| 192 | for node in ast.iter_child_nodes(file_content): |
||
| 193 | if isinstance(node, ast.Import): |
||
| 194 | module = '' |
||
| 195 | elif isinstance(node, ast.ImportFrom): |
||
| 196 | # node.module can be None when the following statement is used: from . import foo |
||
| 197 | if node.module is not None: |
||
| 198 | module = node.module |
||
| 199 | else: |
||
| 200 | module = '' |
||
| 201 | else: |
||
| 202 | continue |
||
| 203 | |||
| 204 | for name_node in node.names: |
||
| 205 | yield Import(module, name_node.name) |
||
| 206 | |||
| 207 | |||
| 208 | def walk_on_py_files(folder): |
||
| 209 | """ |
||
| 210 | Walk through each python files in a directory |
||
| 211 | """ |
||
| 212 | for dir_path, _, files in os.walk(folder): |
||
| 213 | for filename in fnmatch.filter(files, '*.py'): |
||
| 214 | yield os.path.abspath(os.path.join(dir_path, filename)) |
||
| 215 | |||
| 216 | |||
| 217 | def total_of_py_files_on_project(project_path): |
||
| 218 | """ |
||
| 219 | Helper function to find the total number of py files that needs to be iterated. |
||
| 220 | :return: Total number of py files |
||
| 221 | """ |
||
| 222 | file_counter = 0 |
||
| 223 | for _ in walk_on_py_files(project_path): |
||
| 224 | file_counter += 1 |
||
| 225 | return file_counter |
||
| 226 |
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.