Beispiel #1
0
    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
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #6
0
    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])
Beispiel #7
0
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)
Beispiel #8
0
    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}"
            )
Beispiel #10
0
    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)
Beispiel #11
0
    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}
Beispiel #12
0
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
Beispiel #13
0
    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
        ]
Beispiel #14
0
    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)
Beispiel #15
0
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
    """
    )