Are you dealing with a large script full of unused methods? This script will iteratively execute vulture to try and guess which methods are unreachable and remove them. You can whitelist specific functions and test the script at different levels of depth. Should work on all versions of python 2 and 3 supported by vulture.
Last active
April 29, 2020 10:37
-
-
Save PadLex/5fea22fb0033fe6d323bac322492ac5d to your computer and use it in GitHub Desktop.
This script will automatically delete unused functions from a python script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import re | |
| import os | |
| import shutil | |
| import warnings | |
| # Method definitions # | |
| def get_indentation(line): | |
| for i, char in enumerate(line): | |
| if not char.isspace(): | |
| return i | |
| def find_method_definition_end(script, def_line_number): | |
| # Deal with multi-line function definitions | |
| i = 0 | |
| while True: | |
| line = script[def_line_number + i] | |
| select_between = r'%s(.*?)%s' | |
| # remove strings | |
| line = re.sub(select_between % ('\'', '\''), '', line) | |
| line = re.sub(select_between % ('\"', '\"'), '', line) | |
| # remove comments | |
| line = re.sub(select_between % ('#', '$'), '', line) | |
| if ':' in line: | |
| if i > 0: | |
| print(def_line_number + i) | |
| return def_line_number + i | |
| i += 1 | |
| def find_function_ends(script, element): | |
| start = element["line"] - 1 # Start of the definition. def count(one, \n | |
| # Verify that the expected start of the definition corresponds with the actual start. | |
| regex = r"\s*def\s*%s\s*\(" % element["name"] | |
| if not re.match(regex, script[start]): | |
| raise Exception("Error: line number is not corresponding. " | |
| "The file may have been modified after running vulture") | |
| colon_start = find_method_definition_end(script, start) # Find the end of the definition. two, three): \n | |
| # Deal with one-line methods | |
| colon_indentation = get_indentation(script[colon_start]) | |
| next_indentation = get_indentation(script[colon_start + 1]) | |
| if colon_indentation == start and next_indentation <= colon_indentation: | |
| end = start | |
| return start, end | |
| start_indentation = next_indentation | |
| count_empty = 0 | |
| for i in range(colon_start + 1, len(script)-1): | |
| line = script[i] | |
| if '/t' in line: # Tabs warning | |
| warnings.warn("PEP 8 disallows the use of tabs. " | |
| "The script will still work so long as you don't combine tabs with spaces.") | |
| if line.isspace(): # Ignore empty lines | |
| count_empty += 1 | |
| continue | |
| new_indentation = get_indentation(line) # Detect end of function | |
| if new_indentation < start_indentation: | |
| end = i - 1 - count_empty | |
| return start, end | |
| if not line.isspace(): | |
| count_empty = 0 | |
| raise Exception("Error: could not find end of %s %s starting at line %d" | |
| % (element["type"], element["name"], element["line"])) | |
| def remove_between(script, marked_for_deletion): | |
| clean_script = [] | |
| previous_end = 0 | |
| for del_range in marked_for_deletion: | |
| start = del_range[0] | |
| end = del_range[1] + 1 | |
| clean_script.extend(script[previous_end: start]) | |
| print(previous_end, start) | |
| previous_end = end | |
| clean_script.extend(script[previous_end:]) | |
| return clean_script | |
| def remove_unused(script_path, output_path, vulture_output_path): # Execute one removal cycle | |
| finished = True | |
| # Identify unused objects from vulture output | |
| unused_elements = [] | |
| with open(vulture_output_path, 'r') as input_file: | |
| contents = input_file.readlines() | |
| for line in contents: | |
| split = line.split() | |
| element = { | |
| "line": int(split[0].split(':')[1]), | |
| "type": split[2], | |
| "name": split[3].replace('\'', '') | |
| } | |
| unused_elements.append(element) | |
| # Read script | |
| with open(script_path, 'r') as script_file: | |
| script = script_file.readlines() | |
| # Find start end end of methods | |
| marked_for_deletion = [] | |
| for element in unused_elements: | |
| if element["type"] == "function": | |
| start, end = find_function_ends(script, element) | |
| marked_for_deletion.append((start, end)) | |
| finished = False | |
| # Generate script without unused elements and write changes to disk | |
| clean_script = remove_between(script, marked_for_deletion) | |
| with open(output_path, 'w') as output_file: | |
| output_file.writelines(clean_script) | |
| # ToDo remove debug | |
| print(contents) | |
| print(unused_elements) | |
| print(marked_for_deletion) | |
| return finished | |
| if __name__ == "__main__": | |
| # Initial parameters # | |
| # Relative path to the script you want to parse | |
| # Probably the only variable you want to change | |
| script_path = "laser.py" | |
| if not os.path.isfile(script_path): | |
| raise ValueError("File does not exist: %s" % script_path) | |
| if script_path[-3:] != ".py": | |
| raise ValueError("Error: %s does not end with .py" % script_path) | |
| # Whether or not to automatically execute vulture | |
| # If you intend to manually generate the unused.txt file set thi to False | |
| auto_call_vulture = True | |
| # Whitelist methods which should be ignored by vulture | |
| # Add any false positives here | |
| whitelist = ["effect"] | |
| # Directory name to contain each iteration of the script | |
| # Every iteration will have fewer and fewer unused methods but will be more likely to break your script | |
| cycle_directory = "cycles" | |
| if os.path.exists(cycle_directory): | |
| shutil.rmtree(cycle_directory) | |
| os.mkdir(cycle_directory) | |
| # Code starts here # | |
| # Copy script as iteration 0 | |
| i0_path = script_path[:-3] + "_i0.py" | |
| i0_path = os.path.join(cycle_directory, i0_path) | |
| shutil.copy(script_path, i0_path) | |
| script_path = i0_path | |
| whitelist_str = "--ignore-names " + ",".join(whitelist) if len(whitelist) > 0 else '' | |
| unused_path = os.path.join(cycle_directory, "unused.txt") | |
| # Iteratively removed unused functions until vulture cannot find any | |
| finished = False | |
| n = 0 | |
| while not finished: | |
| script_path = script_path[:-4] + "%d.py" % n | |
| output_path = script_path[:-4] + "%d.py" % (n + 1) | |
| print('\n', script_path, output_path) | |
| # Execute vulture and write output to cycles/unused.txt | |
| if auto_call_vulture: | |
| os.system("vulture %s %s > %s" % (whitelist_str, script_path, unused_path)) | |
| finished = remove_unused(script_path, output_path, unused_path) | |
| if not auto_call_vulture: | |
| finished = True | |
| n += 1 # Useful comment |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment