|
|
|
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):
|
|
|
|
# Workaround for old Cyan scripts: replace variables named "true" or "false"
|
|
|
|
# with the respective constant values True or False.
|
|
|
|
if node.id.lower() in {"true", "false"}:
|
|
|
|
return ast.literal_eval(node.id.capitalize())
|
|
|
|
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 visit_NameConstant(self, node):
|
|
|
|
return node.value
|
|
|
|
|
|
|
|
def visit_UnaryOp(self, node):
|
|
|
|
if type(node.op) == ast.USub:
|
|
|
|
return -self.visit(node.operand)
|
|
|
|
elif type(node.op) == ast.UAdd:
|
|
|
|
return self.visit(node.operand)
|
|
|
|
|
|
|
|
def generic_visit(self, node):
|
|
|
|
ast.NodeVisitor.generic_visit(self, node)
|
|
|
|
|
|
|
|
|
|
|
|
def get_attributes_from_file(filepath):
|
|
|
|
"""Scan the file for assignments matching our regex, let our visitor parse them, and return the
|
|
|
|
file's ptAttribs, if any."""
|
|
|
|
with open(str(filepath)) as script:
|
|
|
|
return get_attributes_from_str(script.read())
|
|
|
|
|
|
|
|
def get_attributes_from_str(code):
|
|
|
|
results = funcregex.findall(code)
|
|
|
|
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:
|
|
|
|
return v._attributes
|
|
|
|
return {}
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
import json
|
|
|
|
from pathlib import Path
|
|
|
|
import sys
|
|
|
|
|
|
|
|
if len(sys.argv) != 2:
|
|
|
|
print("Specify a path containing Plasma Python!")
|
|
|
|
else:
|
|
|
|
readpath = sys.argv[1]
|
|
|
|
files = Path(readpath).glob("*.py")
|
|
|
|
ptAttribs = {}
|
|
|
|
for scriptFile in files:
|
|
|
|
attribs = get_attributes_from_file(scriptFile)
|
|
|
|
if attribs:
|
|
|
|
ptAttribs[scriptFile.stem] = attribs
|
|
|
|
|
|
|
|
jsonout = open("attribs.json", "w")
|
|
|
|
jsonout.write(json.dumps(ptAttribs, sort_keys=True, indent=2))
|