Last active
February 27, 2023 20:09
-
-
Save jbrzozoski/daffd76ce3c449858ba4e654c3a43b4c to your computer and use it in GitHub Desktop.
Script to use as a textconv tool for Ignition view.json files under git
This file contains 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 | |
""" | |
Utility script to help diff/log Perspective view.json files under git. | |
The git "textconv" feature is intended to convert binary files to a | |
rough text approximation, so that viewing diffs or logs of those files | |
will provide an idea of what changed. This script can be used as a | |
textconv on Ignition Perspective view.json files to extract JSON-encoded | |
script blocks into something that almost resembles proper Python | |
functions, and allows easier diffing of those scripts under git. | |
To make use of this, put this file somewhere in your path and mark it executable. Then run this git command: | |
git config --global diff.perspective_view.textconv view_textconv | |
And then in the top directory of your git repository you want it active on, run this command: | |
echo "view.json diff=perspective_view" >> .gitattributes | |
""" | |
import sys | |
import json | |
import ruamel.yaml as yaml | |
def str_presenter(dumper, data): | |
if '\n' in data or '\t' in data: # check for complex or multiline strings | |
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|') | |
return dumper.represent_scalar('tag:yaml.org,2002:str', data) | |
yaml.add_representer(str, str_presenter) | |
OUTPUT_YAML = True | |
def print_script_function(fname, script, params=[]): | |
print('def {}({}):'.format(fname, ', '.join(params))) | |
print('{}'.format(script)) | |
print('# END {}\n'.format(fname)) | |
def process_property_configs(curr_path, propConfig): | |
for propname, propconfig in propConfig.items(): | |
if 'binding' in propconfig and 'transforms' in propconfig['binding']: | |
for tnum, \ | |
transform in enumerate(propconfig['binding']['transforms']): | |
if transform['type'] == 'script': | |
fname = '{}[\'{}\'].BINDING[{}]'.format(curr_path, | |
propname, tnum) | |
print_script_function(fname, transform['code']) | |
transform['code'] = fname | |
if 'onChange' in propconfig: | |
fname = '{}[\'{}\'].ON_CHANGE'.format(curr_path, propname) | |
print_script_function(fname, propconfig['onChange']['script']) | |
propconfig['onChange']['script'] = fname | |
def process_events(curr_path, events): | |
if events is None: | |
return | |
for category_name, cat_events in events.items(): | |
for evt_name, evt_cfgs in cat_events.items(): | |
# Weird gotcha that sometimes this item is not in a list when | |
# there's only one, but is in a list if there's more than 1. | |
# Fix it by putting the solo item in a list of 1 item. | |
if type(evt_cfgs) != list: | |
evt_cfgs = [evt_cfgs] | |
for ecnum, ecfg in enumerate(evt_cfgs): | |
if ecfg['type'] == 'script': | |
fname = '{}.EVENT.{}.{}[{}]'.format(curr_path, | |
category_name, | |
evt_name, ecnum) | |
print_script_function(fname, ecfg['config']['script']) | |
ecfg['config']['script'] = fname | |
def process_component_custom_scripts(curr_path, scripts): | |
for cmethod in scripts['customMethods']: | |
fname = '{}.CUSTOM_METHOD.{}'.format(curr_path, cmethod['name']) | |
print_script_function(fname, cmethod['script'], | |
params=cmethod['params']) | |
cmethod['script'] = fname | |
if scripts.get('extensionFunctions') is not None: | |
for extfunc in scripts['extensionFunctions']: | |
fname = '{}.EXTENSION_FUNCTION.{}'.format(curr_path, | |
extfunc['name']) | |
print_script_function(fname, extfunc['script']) | |
extfunc['script'] = fname | |
for mhandler in scripts['messageHandlers']: | |
fname = '{}.MESSAGE_HANDLER.{}'.format(curr_path, | |
mhandler['messageType']) | |
print_script_function(fname, mhandler['script']) | |
mhandler['script'] = fname | |
def process_perspective_component(path_prefix, component): | |
# Make sure we know the current components name and path | |
cname = component['meta']['name'] | |
curr_path = '{}.{}'.format(path_prefix, cname) | |
# Print the scripts at this level | |
if 'propConfig' in component: | |
process_property_configs(curr_path, component['propConfig']) | |
if 'events' in component: | |
process_events(curr_path, component['events']) | |
if 'scripts' in component: | |
process_component_custom_scripts(curr_path, component['scripts']) | |
# Recurse into any children as well | |
if 'children' in component: | |
for child in component['children']: | |
process_perspective_component(curr_path, child) | |
def process_full_view(view): | |
curr_path = 'view' | |
# Print the scripts at this level | |
if 'propConfig' in view: | |
process_property_configs(curr_path, view['propConfig']) | |
if 'events' in view: | |
process_events(curr_path, view['events']) | |
# Recurse into any children as well | |
process_perspective_component(curr_path, view['root']) | |
with open(sys.argv[1]) as f: | |
view = json.load(f) | |
process_full_view(view) | |
if OUTPUT_YAML: | |
print('view_yaml = """') | |
yaml.dump(view, sys.stdout) | |
print('\n"""') | |
else: | |
print('view_json = """') | |
json.dump(view, sys.stdout, sort_keys=True, indent=4) | |
print('\n"""') |
And a couple days later in Feb I tweaked it to use ruamel instead of pyyaml and to output complex strings in escaped blocks...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I tweaked this on Feb 2023 to output YAML by default since I'm finding that even easier to read and diff.