Skip to content

Instantly share code, notes, and snippets.

@OdatNurd
Created March 13, 2020 19:28
Show Gist options
  • Select an option

  • Save OdatNurd/731e4daf5d4a41cc9fd9dd85f005fb0e to your computer and use it in GitHub Desktop.

Select an option

Save OdatNurd/731e4daf5d4a41cc9fd9dd85f005fb0e to your computer and use it in GitHub Desktop.
Collect minimum build version information for Sublime Text API endpoints
#!/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