Created
March 13, 2020 19:28
-
-
Save OdatNurd/731e4daf5d4a41cc9fd9dd85f005fb0e to your computer and use it in GitHub Desktop.
Collect minimum build version information for Sublime Text API endpoints
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
| #!/usr/bin/env python3 | |
| import os | |
| import ast | |
| import glob | |
| import zipfile | |
| # Classes that are part of the API in general; the source files contain some | |
| # helper classes that are just noise | |
| _classes = [ | |
| # From the sublime module | |
| "Edit", "Phantom", "PhantomSet", "Region", "Selection", "Settings", "View", | |
| "Window", "Sheet", "HistoricPosition", "TextChange", "TextSheet", | |
| "ImageSheet", "HtmlSheet", "Html", "CompletionList", "CompletionItem", | |
| # From the sublime_plugin module | |
| "ApplicationCommand", "EventListener", "ListInputHandler", "TextCommand", | |
| "TextInputHandler", "ViewEventListener", "WindowCommand", | |
| "BackInputHandler", | |
| # These are special base classes; we don't want to spit them out directly | |
| # but we do want to remember they exist because they provide the API to | |
| # their base classes. The members for event listeners comes from the | |
| # 'all_callbacks' dictionary at the top level of sublime_plugin.py | |
| # "Command", "CommandInputHandler", | |
| ] | |
| def _get_class_methods(cls_node, base=None): | |
| """ | |
| Return a sorted list of the methods in a given class node. The methods in | |
| the base list (if any) are also included. Private methods are excluded and | |
| the list is sorted lexically. | |
| """ | |
| def keep(method): | |
| n = method.name | |
| if n.startswith("__"): | |
| return True | |
| return not (n.startswith("_") or n.endswith("_")) | |
| methods = list(base or []) | |
| for node in ast.iter_child_nodes(cls_node): | |
| if isinstance(node, ast.FunctionDef) and keep(node): | |
| methods.append(node.name) | |
| return sorted(methods) | |
| def get_plugin_specials(module): | |
| """ | |
| Capture the methods for special classes that are not fully defined in the | |
| module files themselves. This returns empty lists if invoked from a file | |
| that doesn't contain the appropriate classes. | |
| """ | |
| cmd_methods = [] | |
| input_methods = [] | |
| events = [] | |
| for node in ast.iter_child_nodes(module): | |
| # Pick up command class and input handler methods; these classes are | |
| # not directly exposed in the API (except to use as base classes) but | |
| # subclasses inherit them. | |
| if isinstance(node, ast.ClassDef): | |
| if node.name == "Command": | |
| cmd_methods = _get_class_methods(node) | |
| elif node.name == "CommandInputHandler": | |
| input_methods = _get_class_methods(node) | |
| # Find event handlers by getting the keys from the global all_callbacks | |
| # variable. NOTE: Not all of these apply to ViewEventListener (e.g. | |
| # on_new) so external work is needed to cull those after the fact. | |
| # | |
| # NOTE: Starting in the 4xxx build series, there is now a global | |
| # view_event_listener_excluded_callbacks set that indicates the events | |
| # excluded in view event listeners. However that doesn't help if you're | |
| # running this over the 3xxx series too, since that doesn't include | |
| # this and they end up showing up in that version anyway. | |
| elif isinstance(node, ast.Assign): | |
| for target in [n for n in node.targets if isinstance(n, ast.Name)]: | |
| if target.id == "all_callbacks": | |
| events = [key.s for key in node.value.keys] | |
| return cmd_methods, input_methods, events | |
| def add_result(results, build, module_name, obj_name, method_name=None): | |
| key = "%s.%s" % (module_name, obj_name) | |
| if method_name: | |
| key += ".%s" % method_name | |
| if key not in results: | |
| results[key] = build | |
| # build_list = results.get(key) | |
| # if build_list is None: | |
| # build_list = [] | |
| # results[key] = build_list | |
| # build_list.append(build) | |
| # print(key,build) | |
| def module_report(build, results, module, name, inc_funcs, inc_class): | |
| """ | |
| Display a report containing the functions and/or classes in the given | |
| module. Parameters indicate if module level functions and/or classes should | |
| be extracted for the report. | |
| """ | |
| cmd_methods, input_methods, events = get_plugin_specials(module) | |
| def base_methods(cls_name): | |
| """ | |
| Get the methods from known base classes | |
| """ | |
| if cls_name.endswith("Command"): | |
| return cmd_methods | |
| elif cls_name.endswith("InputHandler"): | |
| return input_methods | |
| elif cls_name.endswith("EventListener"): | |
| return events | |
| return [] | |
| for node in ast.iter_child_nodes(module): | |
| # Capture functions | |
| if isinstance(node, ast.FunctionDef) and inc_funcs: | |
| add_result(results, build, name, node.name) | |
| # Capture classes | |
| elif isinstance(node, ast.ClassDef) and inc_class: | |
| if node.name in _classes: | |
| methods = _get_class_methods(node, base_methods(node.name)) | |
| for method in methods: | |
| add_result(results, build, name, node.name, method) | |
| def load_module(build, results, handle, filename, inc_funcs=True, inc_class=True): | |
| """ | |
| Given an open handle to the named file, load the file into an AST tree and | |
| generate a report of the contents, including functions or classes as | |
| requested. | |
| """ | |
| module_name = os.path.splitext(os.path.basename(filename))[0] | |
| tree = ast.parse(handle.read(), filename) | |
| module_report(build, results, tree, module_name, inc_funcs, inc_class) | |
| def handle_raw_file(build, filename, inc_funcs=True, inc_class=True): | |
| """ | |
| Handle a single input file by loading it, parsing it into an AST, and then | |
| dumping the module it contains out. | |
| """ | |
| with open(filename) as handle: | |
| load_module(build, handle, filename, inc_funcs, inc_class) | |
| def process_archive_member(build, results, zipFile, entry, inc_funcs=True, inc_class=True): | |
| try: | |
| load_module(build, results, zipFile.open(entry), entry, inc_funcs, inc_class) | |
| except KeyError: | |
| pass | |
| def handle_archive(archive_name, results=None): | |
| """ | |
| Handle both of the appropriate files from the given zip file version of a | |
| portable install of Sublime Text. | |
| """ | |
| results = results or {} | |
| # The build is second to last in the file name when the build is an x64 | |
| # build. The builds in the 4xxx series have a different filename style | |
| # (at least up until the time of this writing), so we need to adjust for | |
| # that. | |
| base = os.path.splitext(os.path.basename(archive_name))[0] | |
| try: | |
| build = base.split()[-2] | |
| except IndexError: | |
| build = base.split('_')[-2] | |
| with zipfile.ZipFile(archive_name, "r") as zFile: | |
| process_archive_member(build, results, zFile, "sublime.py") | |
| process_archive_member(build, results, zFile, "sublime_plugin.py", inc_funcs=False) | |
| process_archive_member(build, results, zFile, "Lib/python33/sublime.py") | |
| process_archive_member(build, results, zFile, "Lib/python33/sublime_plugin.py", inc_funcs=False) | |
| process_archive_member(build, results, zFile, "Lib/python38/sublime.py") | |
| process_archive_member(build, results, zFile, "Lib/python38/sublime_plugin.py", inc_funcs=False) | |
| return results; | |
| if __name__ == "__main__": | |
| results = None | |
| # results = handle_archive("Text/windows/Sublime Text Build 3211 x64.zip", results) | |
| # results = handle_archive("Text/windows/sublime_text_build_4067_x64.zip", results) | |
| for archive in sorted(glob.glob("Text/windows/*.zip")): | |
| results = handle_archive(archive, results) | |
| for key in sorted(results.keys()): | |
| print("%s,%s" % (key, results[key])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment