import re import ast # We want to grab all of the ptAttributes initialized at the start of every # script. We could use the Abstract Syntax Tree parser... except that if we # try to use that on a script written for a substantially different version of # Python (e.g., parsing Path of the Shell scripts in the Py 3.4 interpreter), # we'll get a nice *boom*. I don't want to write a full parser, so we'll # compromise. # # Here's what we're going to do: # Using a regex we'll collect all of the assignments that occur using the # ptAttrib initializers, and then process them. Some arguments can contain # lists and tuples. We can't use literal_eval since the function call can also # contain keyword arguments, which from its point of view is an expression. # Writing a full parser in Python's regex is not going to happen, so we'll lump # them all together and throw them at an AST visitor to do the heavy lifting # and give us back a nice data structure. # This is the regex we'll be using to find all of the ptAttrib assignments. # It captures commented-out assignments too, but the AST parser will wisely # recognize them as comments and won't trouble us with them. ptAttribFunction = "(#*\w+?\s*?=\s*?ptAttrib[^()]+?\s*?\(.+\).*\s*?)" funcregex = re.compile(ptAttribFunction) class PlasmaAttributeVisitor(ast.NodeVisitor): def __init__(self): self._attributes = dict() def visit_Module(self, node): # Filter out anything that isn't an assignment, and make a nice list. assigns = [x for x in node.body if isinstance(x, ast.Assign)] for assign in assigns: # We only want: # - assignments with targets # - that are taking a function call (the ptAttrib Constructor) # - whose name starts with ptAttrib if (len(assign.targets) == 1 and isinstance(assign.value, ast.Call) and hasattr(assign.value.func, "id") and assign.value.func.id.startswith("ptAttrib")): # Start pulling apart that delicious information ptVar = assign.targets[0].id ptType = assign.value.func.id ptArgs = [] for arg in assign.value.args: value = self.visit(arg) ptArgs.append(value) # Some scripts use dynamic ptAttribs (see: dsntKILightMachine) # which only have an index. We don't want those. if len(ptArgs) > 1: # Add the common arguments as named items. self._attributes[ptArgs[0]] = {"name": ptVar, "type": ptType, "desc": ptArgs[1]} # Add the class-specific arguments under the 'args' item. if ptArgs[2:]: self._attributes[ptArgs[0]]["args"] = ptArgs[2:] # Add the keyword arguments, if any. if assign.value.keywords: for keyword in assign.value.keywords: self._attributes[ptArgs[0]][keyword.arg] = self.visit(keyword.value) return self.generic_visit(node) def visit_Name(self, node): return(node.id) def visit_Num(self, node): return(node.n) def visit_Str(self, node): return(node.s) def visit_List(self, node): elts = [] for x in node.elts: elts.append(self.visit(x)) return elts def visit_Tuple(self, node): elts = [] for x in node.elts: elts.append(self.visit(x)) return tuple(elts) def generic_visit(self, node): ast.NodeVisitor.generic_visit(self, node) def get_attributes(scriptFile): """Scan the file for assignments matching our regex, let our visitor parse them, and return the file's ptAttribs, if any.""" attribs = None with open(scriptFile) as script: results = funcregex.findall(script.read()) if results: # We'll fake the ptAttribs being all alone in a module... assigns = ast.parse("\n".join(results)) v = PlasmaAttributeVisitor() v.visit(assigns) if v._attributes: attribs = v._attributes return attribs if __name__ == "__main__": import glob import json import os import sys if len(sys.argv) != 2: print("Specify a path containing Plasma Python!") else: readpath = sys.argv[1] files = glob.glob(os.path.join(readpath, "*.py")) ptAttribs = {} for scriptFile in files: attribs = get_attributes(scriptFile) if attribs: ptAttribs[os.path.basename(scriptFile)] = attribs jsonout = open("attribs.json", "w") jsonout.write(json.dumps(ptAttribs, sort_keys=True, indent=2))