Skip to content

Instantly share code, notes, and snippets.

@jbrzozoski
Last active February 27, 2023 20:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbrzozoski/daffd76ce3c449858ba4e654c3a43b4c to your computer and use it in GitHub Desktop.
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
#!/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"""')
@jbrzozoski
Copy link
Author

I tweaked this on Feb 2023 to output YAML by default since I'm finding that even easier to read and diff.

@jbrzozoski
Copy link
Author

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