Source code for ladybug_rhino.pythonpath

"""Functions for managing the setting of Rhino's IronPython path."""
import os
import io
import xml.etree.ElementTree

try:
    from ladybug.futil import nukedir, copy_file_tree
    from ladybug.config import folders as lb_folders
except ImportError as e:
    raise ImportError("Failed to import ladybug.\n{}".format(e))


# core library packages, which get copied or cleaned out of the Rhino scripts folder
PACKAGES = (
    'ladybug_rhino', 'ladybug_geometry', 'ladybug_geometry_polyskel',
    'ladybug', 'ladybug_display', 'ladybug_radiance', 'ladybug_comfort',
    'honeybee', 'honeybee_standards', 'honeybee_display', 'honeybee_energy',
    'honeybee_radiance', 'honeybee_radiance_folder',
    'honeybee_radiance_command', 'honeybee_radiance_postprocess',
    'dragonfly', 'dragonfly_energy', 'dragonfly_radiance', 'dragonfly_uwg',
    'lbt_recipes', 'pollination_handlers'
)
# Rhino versions that the plugins are compatible with
RHINO_VERSIONS = ('6.0', '7.0', '8.0')
# UUID that McNeel uses to identify the IronPython plugin
IRONPYTHON_ID = '814d908a-e25c-493d-97e9-ee3861957f49'


[docs] def create_python_package_dir(): """Get the default path where the ladybug_tools Python packages are installed. This method works both on Windows and Mac. If the folder is not found, this method will create the folder. """ py_install = os.path.join(lb_folders.ladybug_tools_folder, 'python') if os.name == 'nt': py_path = os.path.join(py_install, 'Lib', 'site-packages') else: py_ver = 'python3.7' \ if os.path.isdir(os.path.join(py_install, 'lib', 'python3.7')) \ else 'python3.10' py_path = os.path.join(py_install, 'lib', py_ver, 'site-packages') if not os.path.isdir(py_path): return os.makedirs(py_path) return py_path
[docs] def iron_python_search_path(python_package_dir, settings_file=None, destination_file=None): """Set Rhino to search for libraries in a given directory (on either OS). This is used as part of the installation process to ensure that Grasshopper looks for the core Python libraries in the ladybug_tools folder. The file will not be edited if the python_package_dir is already in the settings file. Args: python_package_dir: The path to a directory that contains the Ladybug Tools core libraries. settings_file: An optional XML settings file to which the python_package_dir will be added. If None, this method will search the current user's folder for all copies of this file for the installed Rhino versions. destination_file: Optional destination file to write out the edited settings file. If it is None, the settings_file will be overwritten. """ # make sure that the rhino scripts folder is clean to avoid namespace issues clean_rhino_scripts() # set the plugin to look for packages in ladybug_tools folder new_settings = [] if os.name == 'nt': # we are on Windows all_settings = [settings_file] if settings_file is not None else \ find_ironpython_settings_windows() for sf in all_settings: dest_settings = iron_python_search_path_windows( python_package_dir, sf, destination_file) new_settings.append(dest_settings) else: # we are on Mac, Linux, or some other unix-based system copy_packages_to_rhino_scripts(python_package_dir) return new_settings
[docs] def find_installed_rhino_versions_windows(): """Get a list of the compatible Rhino versions installed on this Windows machine.""" program_files = os.getenv('PROGRAMFILES') installed_vers = [] for ver in RHINO_VERSIONS: rhino_path = os.path.join( program_files, 'Rhino {}'.format(ver[0]), 'System', 'Rhino.exe') if os.path.isfile(rhino_path): installed_vers.append(ver) return installed_vers
[docs] def find_ironpython_settings_windows(): """Get a list of all settings XML files for the supported RHINO_VERSIONS.""" installed_set_files = [] appdata_roaming = os.getenv('APPDATA') for ver in find_installed_rhino_versions_windows(): # get the settings folder or create it if it doesn't exist plugin_folder = os.path.join( appdata_roaming, 'McNeel', 'Rhinoceros', ver, 'Plug-ins') settings_path = os.path.join( plugin_folder, 'IronPython ({})'.format(IRONPYTHON_ID), 'settings') if not os.path.isdir(settings_path): os.makedirs(settings_path) # append the default settings to the list of files to edit sf = os.path.join(settings_path, 'settings-Scheme__Default.xml') installed_set_files.append(sf) # get the search paths for any Rhino-inside instances if they exist for set_file in os.listdir(settings_path): if set_file.startswith('settings-Scheme') and \ set_file != 'settings-Scheme__Default.xml': sf = os.path.join(settings_path, set_file) installed_set_files.append(sf) return installed_set_files
[docs] def iron_python_search_path_windows(python_package_dir, settings_file, destination_file=None): """Set Rhino to search for libraries in a given directory (on Windows). This is used as part of the installation process to ensure that Grasshopper looks for the core Python libraries in the ladybug_tools folder. The file will not be edited if the python_package_dir is already in the settings file. Args: python_package_dir: The path to a directory that contains the Ladybug Tools core libraries. settings_file: An XML settings file to which the python_package_dir will be added. destination_file: Optional destination file to write out the edited settings file. If it is None, the settings_file will be overwritten. """ # open the settings file and find the search paths search_path_needed = True settings_key_needed = False existing_paths = None if os.path.isfile(settings_file): with io.open(settings_file, 'r', encoding='utf-8') as fp: set_data = fp.read() element = xml.etree.ElementTree.fromstring(set_data) settings = element.find('settings') if settings is not None: for entry in settings.iter('entry'): if 'SearchPaths' in list(entry.attrib.values()): existing_paths = entry.text else: # there's no settings key within the XML file; we must add it search_path_needed = False settings_key_needed = True else: contents = [ '<?xml version="1.0" encoding="utf-8"?>', '<settings id="2.0">', '<settings>', '</settings>', '</settings>' ] with open(settings_file, 'w') as fp: fp.write('\n'.join(contents)) # add the search paths if it was not found if destination_file is None: destination_file = settings_file if search_path_needed: if 'ladybug_tools' in python_package_dir and existing_paths is not None: existing_paths = filter_existing_paths(existing_paths) new_paths = '{};{}'.format(existing_paths, python_package_dir) \ if existing_paths is not None and existing_paths != '' \ else python_package_dir line_to_add = ' <entry key="SearchPaths">{}</entry>\n'.format(new_paths) with io.open(settings_file, 'r', encoding='utf-8') as fp: contents = fp.readlines() line_to_del = None for i, line in enumerate(contents): if '<entry key="SearchPaths">' in line: line_to_del = i elif '</settings>' in line: break contents.insert(i, line_to_add) if line_to_del is not None: del contents[line_to_del] with io.open(destination_file, 'w', encoding='utf-8') as fp: fp.write(''.join(contents)) elif settings_key_needed: lines_to_add = ' <settings>\n <entry key="SearchPaths">{}</entry>\n' \ ' </settings>\n'.format(python_package_dir) with io.open(settings_file, 'r', encoding='utf-8') as fp: contents = fp.readlines() for i, line in enumerate(contents): if '</settings>' in line: break contents.insert(i, lines_to_add) with io.open(destination_file, 'w', encoding='utf-8') as fp: fp.write(''.join(contents)) return destination_file
[docs] def filter_existing_paths(existing_paths): """Filter out any duplicate/unwanted search paths.""" paths_list = existing_paths.split(';') filt_paths = [p for p in paths_list if 'ladybug_tools' not in p and 'pollination' not in p] return ';'.join(filt_paths)
[docs] def find_installed_rhino_scripts(): """Get the path to the current user's Rhino scripts folder if it exists.""" installed_scripts = [] if os.name == 'nt': # we are on Windows appdata_roaming = os.getenv('APPDATA') for ver in find_installed_rhino_versions_windows(): scripts_folder = os.path.join(appdata_roaming, 'McNeel', 'Rhinoceros', ver, 'scripts') if os.path.isdir(scripts_folder): installed_scripts.append(scripts_folder) else: # we are on Mac home_folder = os.getenv('HOME') or os.path.expanduser('~') for ver in RHINO_VERSIONS: scripts_folder = os.path.join(home_folder, 'Library', 'Application Support', 'McNeel', 'Rhinoceros', ver, 'scripts') if not os.path.isdir(scripts_folder): os.makedirs(scripts_folder) installed_scripts.append(scripts_folder) return installed_scripts
[docs] def copy_packages_to_rhino_scripts(python_package_dir, directory=None): """Copy Ladybug tools packages into a directory. Args: python_package_dir: The path to a directory that contains the Ladybug Tools core libraries. directory: The directory into which the packages will be copied. If None, the function will search for all installed copies of the current user's Rhino scripts folder. """ folders = find_installed_rhino_scripts() if directory is None else [directory] for fold in folders: for pkg in PACKAGES: lib_folder = os.path.join(python_package_dir, pkg) dest_folder = os.path.join(fold, pkg) if os.path.isdir(lib_folder): copy_file_tree(lib_folder, dest_folder, True) print('Python packages copied to: {}'.format(dest_folder))
[docs] def clean_rhino_scripts(directory=None): """Remove installed Ladybug Tools packages from the old library directory. This function is usually run in order to avoid potential namespace conflicts. Args: directory: The directory to be cleaned. If None, the function will search for all installed copies of the current user's Rhino scripts folder. """ folders = find_installed_rhino_scripts() if directory is None else [directory] for fold in folders: for pkg in PACKAGES: lib_folder = os.path.join(fold, pkg) if os.path.isdir(lib_folder): nukedir(lib_folder, True) print('Python packages removed from: {}'.format(lib_folder))
[docs] def script_editor_search_path(python_package_dir=None): """Set the Rhino 8+ ScriptEditor to search for libraries (on either OS). Args: python_package_dir: The path to a directory that contains the Ladybug Tools core libraries. If None, it will be set to the current python_package_path of the ladybug.config module """ # check the python package directory and forego setting the path for Revit python_dir = python_package_dir if python_package_dir is not None \ else lb_folders.python_package_path if 'revit' in python_dir.lower(): # don't set Rhino paths to use Revit libraries return [] # add the pth files to set the ScriptEditor search paths installed_pth_files = [] for ver in find_installed_rhino_versions_windows(): if float(ver) >= 8: # we can add the path to the Script Editor # determine where the .pth files will be written (PROGRAMDATA or rhinocode) pth_files = [] prog_folder = os.getenv('PROGRAMDATA') data_folder = os.path.join( prog_folder, 'McNeel', 'Rhinoceros', ver, 'scripts') data_files = (os.path.join(data_folder, 'python-2_lbt.pth'), os.path.join(data_folder, 'python-3_lbt.pth')) user_folder = os.getenv('USERPROFILE') if os.name == 'nt' \ else os.path.expanduser('~') rh_code_folder = os.path.join(user_folder, '.rhinocode') rh_code_files = (os.path.join(rh_code_folder, 'python-2.pth'), os.path.join(rh_code_folder, 'python-3.pth')) if os.path.isfile(rh_code_files[0]): pth_files.extend(rh_code_files) # existing file to be corrected elif prog_folder is None or not os.access(prog_folder, os.W_OK): pth_files.extend(rh_code_files) # unable to use data folder if prog_folder is not None and os.access(prog_folder, os.W_OK): pth_files.extend(data_files) # the best place to have the .pth files # append the LBT folder to the Rhino Script Editor search paths for pth_f in pth_files: pth_folder = os.path.dirname(pth_f) if not os.path.isdir(pth_folder): os.makedirs(pth_folder) installed_pth_files.append(pth_f) file_contents = [] if os.path.isfile(pth_f): with open(pth_f, 'r') as pth_file: for line in pth_file: if 'pollination_revit' in line: continue if python_dir.replace('\\', '/') not in \ line.replace('\\', '/'): file_contents.append(line) file_contents.insert(0, '{}\n'.format(python_dir)) with open(pth_f, 'w') as pth_file: pth_file.write(''.join(file_contents)) return installed_pth_files