def install_plugin(self, plugin_file_path: Path, dest_path: Path) -> str: """ Extract the content of the zip file into dest_path. If the installation occurs successfully the name of the installed plugin will be returned. The following checks will be executed to validate the consistency of the inputs: 1. The destination Path should be one of the paths informed during the initialization of HookMan (plugins_dirs field). 2. The plugins_dirs cannot have two plugins with the same name. :plugin_file_path: The Path for the ``.hmplugin`` :dest_path: The destination to where the plugin should be placed. """ plugin_file_zip = ZipFile(plugin_file_path) PluginInfo.validate_plugin_file(plugin_file_zip=plugin_file_zip) if dest_path not in self.plugins_dirs: raise InvalidDestinationPathError( f"Invalid destination path, {dest_path} is not one of " f"the paths that were informed when the HookMan " f"object was initialized: {self.plugins_dirs}.") plugin_name = Path(plugin_file_zip.filename).stem.replace( '-linux64', '').replace('-win64', '') plugins_dirs = [x for x in dest_path.iterdir() if x.is_dir()] if plugin_name in [x.name for x in plugins_dirs]: raise PluginAlreadyInstalledError("Plugin already installed") plugin_destination_folder = dest_path / plugin_name plugin_destination_folder.mkdir(parents=True) plugin_file_zip.extractall(plugin_destination_folder) return plugin_name
def test_load_config_content(datadir, mocker): mocker.patch.object(PluginInfo, '_get_hooks_implemented', return_value=['a']) hooks_available = {'friction_factor': 'acme_v1_friction_factor', 'env_temperature': 'acme_v1_env_temperature'} plugin_yaml_file = datadir / 'assets/plugin.yaml' config_file_content = PluginInfo(plugin_yaml_file, hooks_available) assert config_file_content is not None with pytest.raises(FileNotFoundError): PluginInfo(datadir / 'NonValid', hooks_available)
def test_get_shared_libs_path(datadir, mocker): mocker.patch('sys.platform', 'linux') expected_path = datadir / 'artifacts/libname_of_the_shared_lib.so' plugin_config = PluginInfo(datadir / 'assets/plugin.yaml', hooks_available=None) assert plugin_config.shared_lib_path == expected_path mocker.patch('sys.platform', 'win32') expected_path = datadir / 'artifacts/name_of_the_shared_lib.dll' plugin_config = PluginInfo(datadir / 'assets/plugin.yaml', hooks_available=None) assert plugin_config.shared_lib_path == expected_path
def test_get_shared_libs_path(datadir, mocker, mock_plugin_id_from_dll): mocker.patch("sys.platform", "linux") expected_path = datadir / "artifacts/libname_of_the_shared_lib.so" plugin_config = PluginInfo(datadir / "assets/plugin.yaml", hooks_available=None) assert plugin_config.shared_lib_path == expected_path mocker.patch("sys.platform", "win32") expected_path = datadir / "artifacts/name_of_the_shared_lib.dll" plugin_config = PluginInfo(datadir / "assets/plugin.yaml", hooks_available=None) assert plugin_config.shared_lib_path == expected_path
def test_load_config_content(datadir, mocker, mock_plugin_id_from_dll): mocker.patch.object(PluginInfo, "_get_hooks_implemented", return_value=["a"]) hooks_available = { "friction_factor": "acme_v1_friction_factor", "env_temperature": "acme_v1_env_temperature", } plugin_yaml_file = datadir / "assets/plugin.yaml" config_file_content = PluginInfo(plugin_yaml_file, hooks_available) assert config_file_content is not None with pytest.raises(FileNotFoundError): PluginInfo(datadir / "NonValid", hooks_available)
def _bind_libs_functions_on_hook_caller(self, shared_lib_path: Path, hook_caller): """ Load the shared_lib_path from the plugin and bind methods that are implemented on the hook_caller. """ plugin_dll = ctypes.cdll.LoadLibrary(str(shared_lib_path)) hooks_to_bind = {} for hook_name, full_hook_name in self.hooks_available.items(): if PluginInfo.is_implemented_on_plugin(plugin_dll, full_hook_name): func_address = PluginInfo.get_function_address( plugin_dll, full_hook_name) hooks_to_bind[f'set_{hook_name}_function'] = func_address for hook in hooks_to_bind: cpp_func = getattr(hook_caller, hook) cpp_func(hooks_to_bind[hook])
def test_plugin_id_conflict(simple_plugin, datadir): yaml_file = simple_plugin['path'] / 'assets/plugin.yaml' assert PluginInfo(yaml_file, None) import sys shared_lib_name = f"simple_plugin.dll" if sys.platform == 'win32' else f"libsimple_plugin.so" shared_lib_executable = simple_plugin[ 'path'] / f'artifacts/{shared_lib_name}' acme_lib_name = shared_lib_name.replace("simple_plugin", "ACME") acme_lib = simple_plugin['path'] / f'artifacts/{acme_lib_name}' shared_lib_executable.rename(acme_lib) new_content = yaml_file.read_text().replace('simple_plugin', 'ACME') yaml_file.write_text(new_content) expected_msg = ( 'Error, the plugin_id inside plugin.yaml is "ACME" ' f'while the plugin_id inside the {acme_lib_name} is simple_plugin') with pytest.raises(RuntimeError, match=expected_msg): PluginInfo(yaml_file, None)
def _validate_plugin_config_file(self, plugin_config_file: Path): """ Check if the given plugin_file is valid, by creating a instance of PluginInfo. All checks are made in the __init__ """ plugin_file_content = PluginInfo(plugin_config_file, hooks_available=None) semantic_version_re = re.compile(r"^(\d+)\.(\d+)\.(\d+)") # Ex.: 1.0.0 version = semantic_version_re.match(plugin_file_content.version) if not version: raise ValueError( f"Version attribute does not follow semantic version, got {plugin_file_content.version!r}" )
def _validate_plugin_config_file(cls, plugin_config_file: Path, artifacts_dir: Path): """ Check if the given plugin_file is valid, currently the only check that this method do is to verify if the shared_lib is available """ plugin_file_content = PluginInfo(plugin_config_file, hooks_available=None) if not artifacts_dir.joinpath( plugin_file_content.shared_lib_name).is_file(): raise SharedLibraryNotFoundError( f"{plugin_file_content.shared_lib_name} could not be found in {artifacts_dir}" )
def generate_plugin_package(self, package_name: str, plugin_dir: Union[Path, str], dst_path: Path = None): """ Creates a .hmplugin file using the name provided on package_name argument. The file `.hmplugin` will be created with the content from the folder assets and artifacts. In order to successfully creates a plugin, at least the following files should be present: - plugin.yml - shared library (.ddl or .so) - readme.md Per default, the package will be created inside the folder plugin_dir, however it's possible to give another path filling the dst argument """ plugin_dir = Path(plugin_dir) if dst_path is None: dst_path = plugin_dir assets_dir = plugin_dir / "assets" artifacts_dir = plugin_dir / "artifacts" python_dir = plugin_dir / "src" / "python" self._validate_package_folder(artifacts_dir, assets_dir) self._validate_plugin_config_file(assets_dir / "plugin.yaml") version = PluginInfo(assets_dir / "plugin.yaml", hooks_available=None).version if sys.platform == "win32": shared_lib_extension = "*.dll" hmplugin_path = dst_path / f"{package_name}-{version}-win64.hmplugin" else: shared_lib_extension = "*.so" hmplugin_path = dst_path / f"{package_name}-{version}-linux64.hmplugin" with ZipFile(hmplugin_path, "w") as zip_file: for file in assets_dir.rglob("*"): zip_file.write(filename=file, arcname=file.relative_to(plugin_dir)) for file in artifacts_dir.rglob(shared_lib_extension): zip_file.write(filename=file, arcname=file.relative_to(plugin_dir)) for file in python_dir.rglob("*"): dst_filename = Path( "artifacts" / file.relative_to(plugin_dir / "src/python")) zip_file.write(filename=file, arcname=dst_filename)
def _get_plugin(plugin_name): plugin_dir = datadir / f"plugins/{plugin_name}/" from shutil import copytree copytree(src=plugins_folder / plugin_name, dst=plugin_dir) from hookman.plugin_config import PluginInfo version = PluginInfo(plugin_dir / "assets/plugin.yaml", hooks_available=None).version name = f"{plugin_name}-{version}" import sys hm_plugin_name = ( f"{name}-win64.hmplugin" if sys.platform == "win32" else f"{name}-linux64.hmplugin" ) plugin_zip_path = plugins_zip_folder / hm_plugin_name return {"path": plugin_dir, "specs": acme_hook_specs, "zip": plugin_zip_path}
def test_generate_plugin_package(acme_hook_specs_file, tmpdir, mock_plugin_id_from_dll): hg = HookManGenerator(hook_spec_file_path=acme_hook_specs_file) plugin_id = 'acme' hg.generate_plugin_template( caption='acme', plugin_id='acme', author_email='acme1', author_name='acme2', dst_path=Path(tmpdir) ) plugin_dir = Path(tmpdir) / 'acme' artifacts_dir = plugin_dir / 'artifacts' artifacts_dir.mkdir() import sys shared_lib_name = f"{plugin_id}.dll" if sys.platform == 'win32' else f"lib{plugin_id}.so" shared_lib_path = artifacts_dir / shared_lib_name shared_lib_path.write_text('') hg.generate_plugin_package( package_name='acme', plugin_dir=plugin_dir, ) from hookman.plugin_config import PluginInfo version = PluginInfo(Path(tmpdir / 'acme/assets/plugin.yaml'), None).version win_plugin_name = f"{plugin_id}-{version}-win64.hmplugin" linux_plugin_name = f"{plugin_id}-{version}-linux64.hmplugin" hm_plugin_name = win_plugin_name if sys.platform == 'win32' else linux_plugin_name compressed_plugin = plugin_dir / hm_plugin_name assert compressed_plugin.exists() from zipfile import ZipFile plugin_file_zip = ZipFile(compressed_plugin) list_of_files = [file.filename for file in plugin_file_zip.filelist] assert 'assets/plugin.yaml' in list_of_files assert 'assets/README.md' in list_of_files assert f'artifacts/{shared_lib_name}' in list_of_files
def get_plugins_available( self, ignored_plugins: Sequence[str] = ()) -> Optional[List[PluginInfo]]: """ Return a list of :ref:`plugin-info-api-section` that are available on ``plugins_dirs`` Optionally you can pass a list of plugins that should be ignored. The :ref:`plugin-info-api-section` is a object that holds all information related to the plugin. """ plugin_config_files = hookman_utils.find_config_files( self.plugins_dirs) plugins_available = [ PluginInfo(plugin_file, self.hooks_available) for plugin_file in plugin_config_files ] return [ plugin_info for plugin_info in plugins_available if plugin_info.name not in ignored_plugins ]
def generate_plugin_package( self, package_name: str, plugin_dir: Union[Path, str], dst_path: Path = None, extras_defaults: Optional[Dict[str, str]] = None, ): """ Creates a .hmplugin file using the name provided on package_name argument. The file `.hmplugin` will be created with the content from the folder assets and artifacts. In order to successfully creates a plugin, at least the following files should be present: - plugin.yml - shared library (.ddl or .so) - readme.md Per default, the package will be created inside the folder plugin_dir, however it's possible to give another path filling the dst argument :param Dict[str,str] extras_defaults: (key, value) entries to be added to "extras" if not defined by the original input yaml. """ plugin_dir = Path(plugin_dir) if dst_path is None: dst_path = plugin_dir assets_dir = plugin_dir / "assets" artifacts_dir = plugin_dir / "artifacts" python_dir = plugin_dir / "src" / "python" self._validate_package_folder(artifacts_dir, assets_dir) self._validate_plugin_config_file(assets_dir / "plugin.yaml") plugin_info = PluginInfo(assets_dir / "plugin.yaml", hooks_available=None) if sys.platform == "win32": shared_lib_extension = "*.dll" hmplugin_path = dst_path / f"{package_name}-{plugin_info.version}-win64.hmplugin" else: shared_lib_extension = "*.so" hmplugin_path = dst_path / f"{package_name}-{plugin_info.version}-linux64.hmplugin" contents = (assets_dir / "plugin.yaml").read_text() if extras_defaults is not None: import strictyaml contents_dict = strictyaml.load(contents, PLUGIN_CONFIG_SCHEMA) extras = extras_defaults.copy() extras.update(contents_dict.data.get("extras", {})) contents_dict["extras"] = dict(sorted(extras.items())) contents = contents_dict.as_yaml() with ZipFile(hmplugin_path, "w") as zip_file: for file in assets_dir.rglob("*"): if file.name == "plugin.yaml": zip_file.writestr(str(file.relative_to(plugin_dir)), data=contents) else: zip_file.write(filename=file, arcname=file.relative_to(plugin_dir)) for file in artifacts_dir.rglob(shared_lib_extension): zip_file.write(filename=file, arcname=file.relative_to(plugin_dir)) for file in python_dir.rglob("*"): dst_filename = Path("artifacts" / file.relative_to(plugin_dir / "src/python")) zip_file.write(filename=file, arcname=dst_filename)
def test_generate_plugin_package(acme_hook_specs_file, tmpdir, mock_plugin_id_from_dll): hg = HookManGenerator(hook_spec_file_path=acme_hook_specs_file) plugin_id = "acme" hg.generate_plugin_template( caption="acme", plugin_id="acme", author_email="acme1", author_name="acme2", dst_path=Path(tmpdir), extras={"key": "override", "key2": "value2"}, ) plugin_dir = Path(tmpdir) / "acme" artifacts_dir = plugin_dir / "artifacts" artifacts_dir.mkdir() import sys shared_lib_name = f"{plugin_id}.dll" if sys.platform == "win32" else f"lib{plugin_id}.so" shared_lib_path = artifacts_dir / shared_lib_name shared_lib_path.write_text("") hg.generate_plugin_package( package_name="acme", plugin_dir=plugin_dir, extras_defaults={"key": "default", "key3": "default"}, ) from hookman.plugin_config import PluginInfo version = PluginInfo(Path(tmpdir / "acme/assets/plugin.yaml"), None).version win_plugin_name = f"{plugin_id}-{version}-win64.hmplugin" linux_plugin_name = f"{plugin_id}-{version}-linux64.hmplugin" hm_plugin_name = win_plugin_name if sys.platform == "win32" else linux_plugin_name compressed_plugin = plugin_dir / hm_plugin_name assert compressed_plugin.exists() from zipfile import ZipFile plugin_file_zip = ZipFile(compressed_plugin) list_of_files = [file.filename for file in plugin_file_zip.filelist] assert "assets/plugin.yaml" in list_of_files assert "assets/README.md" in list_of_files assert f"artifacts/{shared_lib_name}" in list_of_files with plugin_file_zip.open("assets/plugin.yaml", "r") as f: contents = f.read().decode("utf-8") from textwrap import dedent assert contents == dedent( """\ author: acme2 caption: acme email: acme1 id: acme version: 1.0.0 extras: key: override key2: value2 key3: default """ )