Source code for ladybug_rhino.versioning.change

"""Functions for changing the installed version of Ladybug Tools."""
import os
import json
import subprocess

try:
    from ladybug.config import folders
    from ladybug.futil import nukedir, copy_file_tree, \
        download_file_by_name, unzip_file
except ImportError as e:
    raise ImportError('\nFailed to import ladybug:\n\t{}'.format(e))

from ..config import folders as lbr_folders
from ..config import rhino_version
from ..pythonpath import iron_python_search_path, create_python_package_dir, \
    clean_rhino_scripts

# find the location where the Grasshopper user objects are stored
UO_DIRECTORY = lbr_folders.uo_folder
GHA_DIRECTORY = lbr_folders.gha_folder


[docs]def get_gem_directory(): """Get the directory where measures distributed with Ladybug Tools are installed.""" measure_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'measures') if not os.path.isdir(measure_folder): os.makedirs(measure_folder) return measure_folder
[docs]def get_standards_directory(): """Get the directory where Honeybee standards are installed.""" hb_folder = os.path.join(folders.ladybug_tools_folder, 'resources', 'standards') if not os.path.isdir(hb_folder): os.makedirs(hb_folder) return hb_folder
[docs]def remove_dist_info_files(directory): """Remove all of the PyPI .dist-info folders from a given directory. Args: directory: A directory containing .dist-info folders to delete. """ for fold in os.listdir(directory): if fold.endswith('.dist-info'): nukedir(os.path.join(directory, fold), rmdir=True)
[docs]def get_config_dict(): """Get a dictionary of the ladybug configurations. This is needed in order to put the configurations back after update. """ with open(folders.config_file, 'r') as cfg: config_dict = json.load(cfg) return config_dict
[docs]def set_config_dict(config_dict): """Set the configurations using a dictionary. Args: config_dict: A dictionary of configuration paths. """ with open(folders.config_file, 'w') as fp: json.dump(config_dict, fp, indent=4)
[docs]def full_access_permission(directory): """Give a directory any all of its files full permissions. Args: directory: A directory containing for which full access will be given. """ for root, dirs, files in os.walk(directory): for d in dirs: os.chmod(os.path.join(root, d), 0o777) for f in files: os.chmod(os.path.join(root, f), 0o777)
[docs]def update_libraries_pip(python_exe, package_name, version=None, target=None): """Change python libraries to be of a specific version using pip. Args: python_exe: The path to the Python executable to be used for installation. package_name: The name of the PyPI package to install. version: An optional string for the version of the package to install. If None, the library will be updated to the latest version with -U. target: An optional target directory into which the package will be installed. """ # build up the command using the inputs if version is not None: package_name = '{}=={}'.format(package_name, version) cmds = [python_exe, '-m', 'pip', 'install', package_name] if version is None: cmds.append('-U') if target is not None: cmds.extend(['--target', target, '--upgrade']) # execute the command and print any errors print('Installing "{}" version via pip'.format(package_name)) use_shell = True if os.name == 'nt' else False process = subprocess.Popen( cmds, shell=use_shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = process.communicate() stdout, stderr = output error_msg = 'Package "{}" may not have been updated correctly\n' \ 'or its usage in the plugin may have changed. See pip stderr below:\n' \ '{}'.format(package_name, stderr) return error_msg
[docs]def download_repo_github(repo, target_directory, version=None): """Download a repo of a particular version from from github. Args: repo: The name of a repo to be downloaded (eg. 'lbt-grasshopper'). target_directory: The directory where the library should be downloaded. version: The version of the repository to download. If None, the most recent version will be downloaded. (Default: None) """ # download files if version is None: url = 'https://github.com/ladybug-tools/{}/archive/master.zip'.format(repo) else: url = 'https://github.com/ladybug-tools/{}/archive/v{}.zip'.format(repo, version) zip_file = os.path.join(target_directory, '%s.zip' % repo) print('Downloading "{}" github repository to: {}'.format(repo, target_directory)) try: download_file_by_name(url, target_directory, zip_file) except ValueError: msg = 'Access is denied to: {}\nMake sure that you are running this command as' \ ' an Administrator by right-clicking on\nRhino and selecting "Run As ' \ 'Administrator" before opening Grasshopper and\n running this ' \ 'component.'.format(target_directory) print(msg) raise ValueError(msg) # unzip the file unzip_file(zip_file, target_directory) # try to clean up the downloaded zip file try: os.remove(zip_file) except Exception: print('Failed to remove downloaded zip file: {}.'.format(zip_file)) # return the directory where the unzipped files live if version is None: return os.path.join(target_directory, '{}-master'.format(repo)) else: return os.path.join(target_directory, '{}-{}'.format(repo, version))
[docs]def latest_github_version(repo, target_directory): """Get the latest version tag of a particular repo on github. Args: repo: The name of a repo to be downloaded (eg. 'lbt-grasshopper'). target_directory: The directory where the HTML Tags page should be downloaded. """ # download the latest page with all of the tags url = 'https://github.com/ladybug-tools/{}/tags'.format(repo) tag_file = os.path.join(target_directory, '%s.html' % repo) try: download_file_by_name(url, target_directory, tag_file) except ValueError: msg = 'Access is denied to: {}\nMake sure that you are running this command as' \ ' an Administrator.'.format(target_directory) print(msg) raise ValueError(msg) # parse the page to find the correct tag version_str, key_word = None, '/ladybug-tools/lbt-grasshopper/releases/tag/' try: with open(tag_file) as tf: for row in tf: if key_word in row: version_str = row.split(key_word)[-1].split('"')[0].replace('v', '') break except UnicodeDecodeError: pass # machine lacks the encoding to read the HTML # try to clean up the downloaded html file try: os.remove(tag_file) except Exception: print('Failed to remove downloaded HTML file: {}.'.format(tag_file)) return version_str
[docs]def update_requirements_version(uo_folder, lbt_version): """Update the version of lbt_grasshopper in the user object requirements.txt file. Args: uo_folder: The directory where the user objects currently exist. lbt_version: The version of LBT-grasshopper to be updated in the requirements.txt file. """ req_file = os.path.join(uo_folder, 'requirements.txt') all_rows = [] if os.path.isfile(req_file): with open(req_file) as rf: for row in rf: if row.startswith('lbt-grasshopper=='): all_rows.append('lbt-grasshopper=={}\n'.format(lbt_version)) else: all_rows.append(row) else: all_rows.append('lbt-grasshopper=={}\n'.format(lbt_version)) with open(req_file, 'w') as rf: rf.write(''.join(all_rows))
[docs]def parse_lbt_gh_versions(lbt_gh_folder): """Parse versions of compatible libs from a clone of the lbt-grasshopper repo. Args: lbt_gh_folder: Path to the clone of the lbt-grasshopper repo Returns: A dictionary of library versions formatted like so (but with actual version numbers in place of '0.0.0'). { 'lbt-dragonfly' = '0.0.0', 'ladybug-rhino' = '0.0.0', 'lbt-recipes' = '0.0.0', 'honeybee-openstudio-gem' = '0.0.0', 'lbt-measures' = '0.0.0', 'honeybee-standards' = '0.0.0', 'honeybee-energy-standards' = '0.0.0', 'ladybug-grasshopper': '0.0.0', 'honeybee-grasshopper-core': '0.0.0', 'honeybee-grasshopper-radiance': '0.0.0', 'honeybee-grasshopper-energy': '0.0.0', 'dragonfly-grasshopper': '0.0.0', 'ladybug-grasshopper-dotnet': '0.0.0' } """ # set the names of the libraries to collect and the version dict version_dict = { 'lbt-dragonfly': None, 'ladybug-rhino': None, 'lbt-recipes': None, 'honeybee-standards': None, 'honeybee-energy-standards': None, 'honeybee-openstudio-gem': None, 'lbt-measures': None, 'ladybug-grasshopper': None, 'honeybee-grasshopper-core': None, 'honeybee-grasshopper-radiance': None, 'honeybee-grasshopper-energy': None, 'dragonfly-grasshopper': None, 'ladybug-grasshopper-dotnet': None } libs_to_collect = list(version_dict.keys()) def search_versions(version_file): """Search for version numbers within a .txt file.""" with open(version_file) as ver_file: for row in ver_file: try: library, version = row.strip().split('==') if library in libs_to_collect: version_dict[library] = version except Exception: # not a row with a ladybug tools library pass # search files for versions requirements = os.path.join(lbt_gh_folder, 'requirements.txt') dev_requirements = os.path.join(lbt_gh_folder, 'dev-requirements.txt') ruby_requirements = os.path.join(lbt_gh_folder, 'ruby-requirements.txt') search_versions(requirements) search_versions(dev_requirements) search_versions(ruby_requirements) return version_dict
[docs]def change_installed_version(version_to_install=None): """Change the currently installed version of Ladybug Tools. This requires an internet connection and will update all core libraries and Grasshopper components to the specified version_to_install. Args: version_to_install: An optional text string for the version of the LBT plugin to be installed. The input should contain only integers separated by two periods (eg. 1.0.0). If None, the Ladybug Tools plugin shall be updated to the latest available version. The version specified here does not need to be newer than the current installation and can be older but grasshopper plugin versions less than 0.3.0 are not supported. A list of all versions of the Grasshopper plugin can be found here - https://github.com/ladybug-tools/lbt-grasshopper/releases """ # ensure that Python has been installed in the ladybug_tools folder py_exe, py_lib = folders.python_exe_path, folders.python_package_path assert py_exe is not None and py_lib is not None, \ 'No valid Python installation was found at: {}.\nThis is a requirement in ' \ 'order to continue with installation'.format( os.path.join(folders.ladybug_tools_folder, 'python')) # get the compatible versions of all the dependencies temp_folder = os.path.join(folders.ladybug_tools_folder, 'temp') lbt_gh_folder = download_repo_github( 'lbt-grasshopper', temp_folder, version_to_install) ver_dict = parse_lbt_gh_versions(lbt_gh_folder) if version_to_install is None: version_to_install = latest_github_version('lbt-grasshopper', temp_folder) ver_dict['lbt-grasshopper'] = version_to_install # install the core libraries print('Installing Ladybug Tools core Python libraries.') config_dict = get_config_dict() # load configs so they can be put back after update df_ver = ver_dict['lbt-dragonfly'] stderr = update_libraries_pip(py_exe, 'lbt-dragonfly', df_ver) if os.path.isdir(os.path.join(py_lib, 'lbt_dragonfly-{}.dist-info'.format(df_ver))): print('Ladybug Tools core Python libraries successfully installed!\n ') else: print(stderr) set_config_dict(config_dict) # restore the previous configurations # install the lbt_recipes package print('Installing Ladybug Tools Recipes.') rec_ver = ver_dict['lbt-recipes'] stderr = update_libraries_pip(py_exe, 'lbt-recipes', rec_ver) if os.path.isdir(os.path.join(py_lib, 'lbt_recipes-{}.dist-info'.format(rec_ver))): print('Ladybug Tools Recipes successfully installed!\n ') else: print(stderr) # install the library needed for interaction with Rhino print('Installing ladybug-rhino Python library.') rh_ver = ver_dict['ladybug-rhino'] stderr = update_libraries_pip(py_exe, 'ladybug-rhino', rh_ver) if os.path.isdir(os.path.join(py_lib, 'ladybug_rhino-{}.dist-info'.format(rh_ver))): print('Ladybug-rhino Python library successfully installed!\n ') else: print(stderr) # make sure libraries are copied to the rhino scripts folder on Mac if os.name != 'nt': if rhino_version[0] < 7: # remove and replace all packages iron_python_search_path(create_python_package_dir()) else: # just remove any possible conflicting packages clean_rhino_scripts() # install the grasshopper components print('Installing Ladybug Tools Grasshopper components.') gh_ver = ver_dict['lbt-grasshopper'] stderr = update_libraries_pip(py_exe, 'lbt-grasshopper', gh_ver, UO_DIRECTORY) lb_gh_ver = ver_dict['ladybug-grasshopper'] lb_gh_info = 'ladybug_grasshopper-{}.dist-info'.format(lb_gh_ver) if os.path.isdir(os.path.join(UO_DIRECTORY, lb_gh_info)): print('Ladybug Tools Grasshopper components successfully installed!\n ') remove_dist_info_files(UO_DIRECTORY) # remove the .dist-info files full_access_permission(UO_DIRECTORY) else: print(stderr) if version_to_install is not None: update_requirements_version(UO_DIRECTORY, version_to_install) # install the .gha Grasshopper components gha_location = os.path.join(GHA_DIRECTORY, 'ladybug_grasshopper_dotnet') if os.path.isdir(gha_location): msg = '.gha files already exist in your Components folder and cannot be ' \ 'deleted while Grasshopper is open.\nThese .gha files rarely change ' \ 'and so it is not critical that they be updated. However, if you ' \ 'want to be sure that you have the latest version installed, then close ' \ 'Grasshopper, delete the ladybug_grasshopper_dotnet folder at\n{}\nand ' \ 'rerun this versioner component to get the new .gha files.\n'.format( gha_location) print(msg) else: gha_ver = ver_dict['ladybug-grasshopper-dotnet'] stderr = update_libraries_pip( py_exe, 'ladybug-grasshopper-dotnet', gha_ver, GHA_DIRECTORY) package_dir = os.path.join( GHA_DIRECTORY, 'ladybug_grasshopper_dotnet-{}.dist-info'.format(gha_ver)) if os.path.isdir(package_dir): print('Ladybug Tools .gha Grasshopper components successfully installed!\n ') remove_dist_info_files(GHA_DIRECTORY) # remove the dist-info files full_access_permission(GHA_DIRECTORY) else: print(stderr) # install the honeybee-openstudio ruby gem gem_ver = ver_dict['honeybee-openstudio-gem'] print('Installing Honeybee-OpenStudio gem version {}.'.format(gem_ver)) gem_dir = get_gem_directory() base_folder = download_repo_github('honeybee-openstudio-gem', gem_dir, gem_ver) source_folder = os.path.join(base_folder, 'lib') lib_folder = os.path.join(gem_dir, 'honeybee_openstudio_gem', 'lib') print('Copying "honeybee_openstudio_gem" source code to {}\n '.format(lib_folder)) copy_file_tree(source_folder, lib_folder) nukedir(base_folder, True) # install the lbt-measures ruby gem mea_ver = ver_dict['lbt-measures'] print('Installing Ladybug Tools Measures version {}.'.format(mea_ver)) base_folder = download_repo_github('lbt-measures', gem_dir, mea_ver) source_folder = os.path.join(base_folder, 'lib') print('Copying "lbt_measures" source code to {}\n '.format(gem_dir)) copy_file_tree(source_folder, gem_dir) nukedir(base_folder, True) # always update the honeybee-energy-standards package print('Installing Honeybee energy standards.') stand_dir = get_standards_directory() hes_ver = ver_dict['honeybee-energy-standards'] if os.path.isdir(os.path.join(stand_dir, 'honeybee_energy_standards')): nukedir(os.path.join(stand_dir, 'honeybee_energy_standards'), True) stderr = update_libraries_pip( py_exe, 'honeybee-energy-standards', hes_ver, stand_dir) hes_info = 'honeybee_energy_standards-{}.dist-info'.format(hes_ver) if os.path.isdir(os.path.join(stand_dir, hes_info)): print('Honeybee energy standards successfully installed!\n ') remove_dist_info_files(stand_dir) # remove the dist-info files full_access_permission(stand_dir) else: print(stderr) # delete the temp folder and give a completion message nukedir(temp_folder, True) version = 'LATEST' if version_to_install is None else version_to_install success_msg = 'Change to Version {} Successful!'.format(version) restart_msg = 'RESTART RHINO to load the new components + library.' sync_msg = 'The "LB Sync Grasshopper File" component can be used\n' \ 'to sync Grasshopper definitions with your new installation.' for msg in (success_msg, restart_msg, sync_msg): print(msg)