Source code for ladybug_rhino.versioning.diff

"""Functions for managing components differences and syncing component versions."""
import os

try:
    import System.Drawing
except ImportError:
    raise ImportError("Failed to import System.")

try:
    import Grasshopper.Kernel as gh
except ImportError:
    raise ImportError("Failed to import Grasshopper.")

from ..grasshopper import give_warning
from .userobject import UO_FOLDER, FOLDER_MAP


# list of valid change tags for export
CHANGE_TAGS = ('fix', 'release', 'feat', 'perf', 'docs', 'ignore')


[docs]def validate_change_type(change_type): """Check that a change type is a valid tag.""" assert change_type in CHANGE_TAGS, 'Invalid change_type "{}". Choose from ' \ 'the following:\n{}'.format(change_type, ' '.join(CHANGE_TAGS)) return change_type
[docs]def current_userobject_version(component): """Get the current installed version of a component. Args: component: A Grasshopper Python component with the same name as an installed user object. If no matching user object is found, this method returns None. """ # load component from the folder where it should be assert component.Category in FOLDER_MAP, \ 'Unknown category: %s. Add category to folder_dict.' % component.Category fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], 'user_objects', '%s.ghuser' % component.Name) if os.path.isfile(fp): # if the component was found, parse out the version uo = gh.GH_UserObject(fp).InstantiateObject() # uo.Message returns None so we have to find it the old school way! for lc, line in enumerate(uo.Code.split('\n')): if lc > 200: # this should never happen for valid userobjects raise ValueError( 'Failed to find version from UserObject for %s' % uo.Name ) if line.strip().startswith("ghenv.Component.Message"): return line.split("=")[1].strip()[1:-1] else: # there is no currently installed version of this userobject return None
[docs]def parse_version(semver_str): """Parse semantic version string into (major, minor, patch) tuple. Args: semver_str: Text for a component version (eg. "1.0.1"). """ try: major, minor, patch = [int(d) for d in semver_str.strip().split('.')] except ValueError: raise ValueError( '\nInvalid version format: %s\nYou must follow major.minor.patch format ' 'with 3 integer values' % semver_str ) return major, minor, patch
[docs]def validate_version(current_version, new_version, change_type): """Validate that a version tag conforms to currently-installed version difference. Args: current_version: Semantic version string for the currently installed version. new_version: Semantic version string for the new component version. change_type: Text tag for the change type (eg. "fix"). """ if current_version is None: # this is the first time that this component is created; give it a pass print(' New component. No change in version: %s' % current_version) return True x, y, z = parse_version(current_version) parse_version(new_version) # just make sure the semantic version format is valid msg = '\nFor a \'%s\' the component version should change to %s not %s.' \ '\nFix the version or select the correct change type and try again.' if change_type == 'ignore': valid_version = new_version elif change_type == 'fix': valid_version = '.'.join(str(i) for i in (x, y, z + 1)) elif change_type == 'feat' or change_type == 'perf': valid_version = '.'.join(str(i) for i in (x, y + 1, 0)) elif change_type == 'release': valid_version = '.'.join(str(i) for i in (x + 1, 0, 0)) elif change_type == 'docs': valid_version = '.'.join(str(i) for i in (x, y, z)) else: raise ValueError('Invalid change_type: %s' % change_type) assert valid_version == new_version, msg % (change_type, valid_version, new_version) if current_version == new_version: print(' No change in version: %s' % current_version) else: print(' Version is changed from %s to %s.' % (current_version, new_version))
[docs]def has_version_changed(user_object, component): """Check if the version of a component has changed from a corresponding user object. Note that this method only works for user objects that have been dropped on the canvas. For comparing the version with a user object that hasn't been loaded from the component server by dropping it on the canvas, the current_userobject_version method should be used. Args: user_object: A Grasshopper user object component instance. component: The Grasshopper component object for which the version is being checked. """ return not user_object.Message == component.Message
[docs]def update_port(p1, p2): """Update one port based on another. Args: p1: The first port object, which is the one used for updating. p2: The second port object, which will be updated base on p1. """ if hasattr(p1, 'TypeHint'): # input if p1.Name != p2.Name: p2.NickName = p1.NickName p2.Name = p1.Name if p1.TypeHint.TypeName != p2.TypeHint.TypeName: p2.TypeHint = p1.TypeHint if str(p1.Access) != str(p2.Access): p2.Access = p1.Access return True else: # output if p1.Name != p2.Name: p2.NickName = p1.NickName p2.Name = p1.Name return True
[docs]def update_ports(c1, c2): """Update all of the ports of one component based on another. Args: c1: The first component object, which is the one used for updating. c2: The second component object, which will be updated base on c1. """ for i in range(c1.Params.Input.Count): if not update_port(c1.Params.Input[i], c2.Params.Input[i]): return True for i in range(c1.Params.Output.Count): if not update_port(c1.Params.Output[i], c2.Params.Output[i]): return True return False
[docs]def input_output_changed(user_object, component): """Check if any of inputs or outputs have changed between two components. Args: user_object: A Grasshopper user object component instance. component: The Grasshopper component object for which the version is being checked. """ if user_object.Params.Input.Count != component.Params.Input.Count: return True elif user_object.Params.Output.Count != component.Params.Output.Count: return True return update_ports(user_object, component)
[docs]def insert_new_user_object(user_object, component, doc): """Insert a new user object next to an existing component in the Grasshopper doc. Args: user_object: A Grasshopper user object component instance. component: The outdated component where the userobject will be inserted next to. doc: The Grasshopper document object. """ # use component to find the location x = component.Attributes.Pivot.X + 30 y = component.Attributes.Pivot.Y - 20 # insert the new one user_object.Attributes.Pivot = System.Drawing.PointF(x, y) doc.AddObject(user_object, False, 0)
[docs]def mark_component(doc, component, note=None): """Put a circular red group around a component and label it with a note. Args: doc: The Grasshopper document object. component: A Grasshopper component object on the canvas to be circled. note: Text for the message to be displayed on the circle. If None, a default message about input/output change wil be used. """ note = note or 'There is a change in the input or output! ' \ 'Replace this component manually.' grp = gh.Special.GH_Group() grp.CreateAttributes() grp.Border = gh.Special.GH_GroupBorder.Blob grp.AddObject(component.InstanceGuid) grp.Colour = System.Drawing.Color.IndianRed # way to pick a racist color name, .NET grp.NickName = note doc.AddObject(grp, False) return True
[docs]def sync_component(component, syncing_component): """Sync a component on the canvas with its corresponding installed version. This includes identifying if the component by name in the user object folder, injecting the code from that user object into the component, and (if the component inputs or outputs have changed) circling the component in red + dropping the new user object next to the component. Args: component: A Grasshopper component object on the canvas to be synced. syncing_component: An object for the component that is doing the syncing. This will be used to give warnings and access the Grasshopper doc. Typically, this can be accessed through the ghenv.Component call. """ # identify the correct user object sub-folder to which the component belongs ghuser_file = '%s.ghuser' % component.Name if str(component.Name).startswith(('LB', 'HB', 'DF')): fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], 'user_objects', ghuser_file) elif str(component.Name).startswith(('Ladybug', 'Honeybee', 'HoneybeePlus')): category = str(component.Name).split('_')[0] fp = os.path.join(UO_FOLDER, category, ghuser_file) else: # unidentified plugin; see if we can find it in the root fp = os.path.join(UO_FOLDER, ghuser_file) # check to see if the user object is installed if not os.path.isfile(fp): # check if it's a component with a name change alt_fp = fp.replace('Vizualize', 'Visualize') alt_fp = fp.replace('Mofidier', 'Modifier') alt_fp = fp.replace('Abolute', 'Absolute') alt_fp = alt_fp.replace('gbXML', 'gbXML OSM IDF') if os.path.isfile(alt_fp): fp = alt_fp elif component.Category in FOLDER_MAP: # see if category has a folder fp = os.path.join(UO_FOLDER, FOLDER_MAP[component.Category], ghuser_file) if not os.path.isfile(fp): # see if the component is in the root fp = os.path.join(UO_FOLDER, ghuser_file) if not os.path.isfile(fp): # all hope is lost; component not installed warning = 'Failed to find the userobject for %s' % component.Name give_warning(syncing_component, warning) return False # the the instance of the user object from the file uo = gh.GH_UserObject(fp).InstantiateObject() # check to see if the version of the userobject has changed if not has_version_changed(uo, component): return False # the version has changed, let's update the code component.Code = uo.Code doc = syncing_component.OnPingDocument() # get the Grasshopper document # define the callback function and update the solution def call_back(document): component.ExpireSolution(False) doc.ScheduleSolution(2, gh.GH_Document.GH_ScheduleDelegate(call_back)) # check if the inputs or outputs have changed if input_output_changed(uo, component): insert_new_user_object(uo, component, doc) mark_component(doc, component) # mark component with a warning to the user return 'Cannot update %s. Replace manually.' % component.Name return 'Updated %s' % component.Name