Skip to content

Commit b85b49f

Browse files
committed
Add some updates after review
- Make the version be a object of Version; - Make the version property be optional in remove_plugin; - Use the rsplit to obtain the correct name of plugin from its folder;
1 parent 36668e2 commit b85b49f

8 files changed

Lines changed: 87 additions & 31 deletions

File tree

hookman/hookman_generator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def generate_plugin_package(
330330
contents_dict["requirements"] = dict(sorted(requirements.items()))
331331
contents = contents_dict.as_yaml()
332332

333-
hmplugin_base_name_components = [package_name, plugin_info.version]
333+
hmplugin_base_name_components = [package_name, plugin_info.version.base_version]
334334
if package_name_suffix is not None:
335335
hmplugin_base_name_components.append(package_name_suffix)
336336

@@ -404,12 +404,13 @@ def _validate_plugin_config_file(self, plugin_config_file: Path) -> None:
404404
All checks are made in the __init__
405405
"""
406406
plugin_file_content = PluginInfo(plugin_config_file, hooks_available=None)
407+
content_version = plugin_file_content.version.base_version
407408
semantic_version_re = re.compile(r"^(\d+)\.(\d+)\.(\d+)") # Ex.: 1.0.0 or 2025.1.0
408-
version = semantic_version_re.match(plugin_file_content.version)
409+
version = semantic_version_re.match(content_version)
409410

410411
if not version:
411412
raise ValueError(
412-
f"Version attribute does not follow the calendar or semantic versioning, got {plugin_file_content.version!r}"
413+
f"Version attribute does not follow the calendar or semantic versioning, got {content_version!r}"
413414
)
414415

415416
def _hook_specs_header_content(self, plugin_id: str) -> str:

hookman/hooks.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from zipfile import ZipFile
99

1010
from dataclasses import dataclass
11+
from packaging.version import Version
1112

1213
from hookman import hookman_utils
1314
from hookman.exceptions import InvalidDestinationPathError
@@ -18,10 +19,10 @@
1819
@dataclass(frozen=True)
1920
class InstalledPluginInfo:
2021
"""
21-
Responsible to stores the information about the installed plugin.
22+
Responsible to store the information about an installed plugin.
2223
"""
2324
id: str
24-
version: str
25+
version: Version
2526

2627
class HookSpecs:
2728
"""
@@ -107,7 +108,7 @@ def install_plugin(self, plugin_file_path: Path, dest_path: Path) -> InstalledPl
107108
108109
1. The destination Path should be one of the paths informed during the initialization of HookMan (plugins_dirs field).
109110
110-
2. The plugins_dirs cannot have two plugins with the same name.
111+
2. The plugins_dirs cannot have two plugins with the same name and version.
111112
112113
:param: plugin_file_path:
113114
The Path for the ``.hmplugin``
@@ -127,8 +128,9 @@ def install_plugin(self, plugin_file_path: Path, dest_path: Path) -> InstalledPl
127128

128129
yaml_content = plugin_file_zip.open("assets/plugin.yaml").read().decode("utf-8")
129130

130-
plugin_id: str = PluginInfo._load_yaml_file(yaml_content)["id"]
131-
plugin_version: str = PluginInfo._load_yaml_file(yaml_content)["version"]
131+
yaml_data = PluginInfo._load_yaml_file(yaml_content)
132+
plugin_id: str = yaml_data["id"]
133+
plugin_version: str = yaml_data["version"]
132134
plugin_id_version = f"{plugin_id}-{plugin_version}"
133135

134136
plugins_dirs = [x for x in dest_path.iterdir() if x.is_dir()]
@@ -139,7 +141,7 @@ def install_plugin(self, plugin_file_path: Path, dest_path: Path) -> InstalledPl
139141
plugin_destination_folder = dest_path / plugin_id_version
140142
plugin_destination_folder.mkdir(parents=True)
141143
plugin_file_zip.extractall(plugin_destination_folder)
142-
return InstalledPluginInfo(version=plugin_version, id=plugin_id)
144+
return InstalledPluginInfo(version=Version(plugin_version), id=plugin_id)
143145

144146
def _move_to_trash(self, root_dir, name):
145147
"""
@@ -172,20 +174,29 @@ def _try_clear_trash(self, root_dir):
172174
with suppress(OSError):
173175
filename.unlink()
174176

175-
def remove_plugin(self, caption: str, version: str) -> None:
177+
def remove_plugin(self, caption: str, version: Version | None = None) -> None:
176178
"""
177-
This method receives the name of the plugin as input, and will remove completely the plugin from ``plugin_dirs``.
179+
This method receives the name and version of plugin as input, and will remove completely the
180+
plugin from ``plugin_dirs``.
178181
179182
:param caption:
180183
Name of the plugin to be removed.
181184
182185
:param version:
183-
Version of the plugin to be removed.
186+
Optional parameter used to remove a specific version of plugin. Case it is not specified,
187+
all versions of a given plugin will be removed.
184188
"""
185189
for plugin in self.get_plugins_available():
186-
if plugin.id == caption and plugin.version == version:
187-
plugin_dir = plugin.yaml_location.parents[1]
188-
root_dir = plugin_dir.parent
190+
plugin_dir = plugin.yaml_location.parents[1]
191+
root_dir = plugin_dir.parent
192+
remove_plugin = False
193+
194+
if version is None and caption in plugin_dir.name:
195+
remove_plugin = True
196+
elif plugin.id == caption and version == plugin.version:
197+
remove_plugin = True
198+
199+
if remove_plugin:
189200
self._move_to_trash(root_dir, plugin_dir.name)
190201
self._try_clear_trash(root_dir)
191202
break

hookman/plugin_config.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import sys
33
from collections.abc import Sequence
44
from pathlib import Path
5+
from textwrap import dedent
56
from zipfile import ZipFile
67

78
from attr import define
89
from attr import field
9-
from strictyaml import Map
10+
from packaging.version import Version
11+
from strictyaml import Map, YAMLValidationError
1012
from strictyaml import MapPattern
1113
from strictyaml import Optional
1214
from strictyaml import Str
@@ -44,7 +46,7 @@ class PluginInfo:
4446
caption: str = field(init=False)
4547
shared_lib_name: str = field(init=False)
4648
shared_lib_path: Path = field(init=False)
47-
version: str = field(init=False)
49+
version: Version = field(init=False)
4850
requirements: dict[str, str] = field(init=False)
4951
extras: dict = field(init=False)
5052
id: str = field(init=False)
@@ -62,7 +64,7 @@ def __attrs_post_init__(self) -> None:
6264
self.author = plugin_config_file_content["author"]
6365
self.caption = plugin_config_file_content["caption"]
6466
self.email = plugin_config_file_content["email"]
65-
self.version = plugin_config_file_content["version"]
67+
self.version = Version(plugin_config_file_content["version"])
6668
self.requirements = plugin_config_file_content.get("requirements", {})
6769
self.extras = plugin_config_file_content.get("extras", {})
6870

@@ -92,8 +94,8 @@ def _get_plugin_id_from_dll(self, plugin_id_from_plugin_yaml: str) -> str:
9294
plugin_id_from_shared_lib = plugin_dll.get_plugin_id().decode("utf-8")
9395
if plugin_id_from_shared_lib != plugin_id_from_plugin_yaml:
9496
msg = (
95-
f'Error, the id inside plugin.yaml is "{plugin_id_from_plugin_yaml}" '
96-
f"while the id inside the {self.shared_lib_name} is {plugin_id_from_shared_lib}"
97+
f'Error, the plugin_id inside plugin.yaml is "{plugin_id_from_plugin_yaml}" '
98+
f"while the plugin_id inside the {self.shared_lib_name} is {plugin_id_from_shared_lib}"
9799
)
98100
raise RuntimeError(msg)
99101
return plugin_id_from_shared_lib
@@ -133,7 +135,12 @@ def is_implemented_on_plugin(cls, plugin_dll: ctypes.CDLL, hook_name: str) -> bo
133135
def _load_yaml_file(cls, yaml_content: str) -> YAML:
134136
import strictyaml
135137

136-
plugin_config_file_content = strictyaml.load(yaml_content, PLUGIN_CONFIG_SCHEMA).data
138+
try:
139+
plugin_config_file_content = strictyaml.load(yaml_content, PLUGIN_CONFIG_SCHEMA).data
140+
except YAMLValidationError:
141+
current_plugin_schema = "\n".join(f"{key} : {value}" for key, value in PLUGIN_CONFIG_SCHEMA._validator.items())
142+
raise ValueError(f"The plugin.yaml does not follow the PLUGIN_CONFIG_SCHEMA: {current_plugin_schema}")
143+
137144
if sys.platform == "win32":
138145
plugin_config_file_content["shared_lib_name"] = (
139146
f"{plugin_config_file_content['id']}.dll"

tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def generate_build_files(ctx):
6363

6464
# Copy all the plugins to the build dir
6565
for plugin_dir in plugins_dirs:
66-
plugin_name, _ = plugin_dir.name.split("-")
66+
plugin_name, _ = plugin_dir.name.rsplit("-", maxsplit=1)
6767
plugin_dir_build = project_dir_for_build / f"plugin/{plugin_dir.name}"
6868
shutil.copytree(src=plugin_dir, dst=plugin_dir_build)
6969
(plugin_dir_build / "src/hook_specs.h").write_text(
@@ -169,7 +169,7 @@ def _package_plugins(ctx):
169169

170170
for plugin in plugins_dirs:
171171
(plugin / "artifacts").mkdir()
172-
name, _ = plugin.name.split("-")
172+
name, _ = plugin.name.rsplit("-",maxsplit=1)
173173
if sys.platform == "win32":
174174
shutil.copy2(src=artifacts_dir / f"{name}.dll", dst=plugin / "artifacts")
175175
else:

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ def _get_plugin(plugin_name):
5252
from hookman.plugin_config import PluginInfo
5353

5454
version = PluginInfo(plugin_dir / "assets/plugin.yaml", hooks_available=None).version
55-
name, version = plugin_name.split("-")
55+
name, version = plugin_name.rsplit("-",maxsplit=1)
5656
import sys
5757

5858
hm_plugin_name = (
59-
f"{name}-{version}-win64.hmplugin" if sys.platform == "win32" else f"{name}-{version}-linux64.hmplugin"
59+
f"{plugin_name}-win64.hmplugin" if sys.platform == "win32" else f"{plugin_name}-linux64.hmplugin"
6060
)
6161
plugin_zip_path = plugins_zip_folder / hm_plugin_name
6262

tests/test_hookman_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def test_generate_plugin_package(
242242

243243
from hookman.plugin_config import PluginInfo
244244

245-
version = PluginInfo(Path(tmpdir / "acme/assets/plugin.yaml"), None).version
245+
version = PluginInfo(Path(tmpdir / "acme/assets/plugin.yaml"), None).version.base_version
246246

247247
base_plugin_name_components = [plugin_id, version]
248248
if package_name_extra is not None:

tests/test_hooks.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from hookman.hooks import HookMan
44
from hookman.hooks import HookSpecs
55
from hookman.hooks import PluginInfo
6-
6+
from packaging.version import Version
77

88
def _get_plugin_id_set(plugin_info_list):
99
"""
@@ -233,7 +233,10 @@ def test_remove_plugin(datadir, simple_plugin, simple_plugin_2):
233233

234234
assert _get_plugin_id_set(hm.get_plugins_available()) == {"simple_plugin", "simple_plugin_2"}
235235
assert _get_names_inside_folder(datadir / "plugins") == {"simple_plugin-1.0.0", "simple_plugin_2-1.0.0"}
236-
hm.remove_plugin("simple_plugin_2","1.0.0")
236+
hm.remove_plugin("simple_plugin_2", Version("1.0.0"))
237237
assert _get_plugin_id_set(hm.get_plugins_available()) == {"simple_plugin"}
238238
assert _get_names_inside_folder(datadir / "plugins") == {"simple_plugin-1.0.0", ".trash"}
239239
assert _get_names_inside_folder(datadir / "plugins" / ".trash") == set()
240+
241+
hm.remove_plugin("simple_plugin")
242+
assert _get_plugin_id_set(hm.get_plugins_available()) == set()

tests/test_plugin_config.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import pytest
2+
from pytest_mock import MockerFixture
23

34
from hookman.plugin_config import PluginInfo
4-
5+
from pathlib import Path
6+
from textwrap import dedent
7+
from strictyaml import YAMLValidationError
8+
import re
59

610
def test_load_config_content(datadir, mocker, mock_plugin_id_from_dll):
711
mocker.patch.object(PluginInfo, "_get_hooks_implemented", return_value=["a"])
@@ -50,8 +54,38 @@ def test_plugin_id_conflict(simple_plugin, datadir):
5054
yaml_file.write_text(new_content)
5155

5256
expected_msg = (
53-
'Error, the id inside plugin.yaml is "ACME" '
54-
f"while the id inside the {acme_lib_name} is simple_plugin"
57+
'Error, the plugin_id inside plugin.yaml is "ACME" '
58+
f"while the plugin_id inside the {acme_lib_name} is simple_plugin"
5559
)
5660
with pytest.raises(RuntimeError, match=expected_msg):
5761
PluginInfo(yaml_file, None)
62+
63+
def testPluginInfoInvalidSchema(tmp_path: Path, mocker: MockerFixture) -> None:
64+
invalid_yaml_content = f"""\
65+
caption: 'Plugin'
66+
version: '1.0.0'
67+
author: 'ESSS'
68+
developer: 'Developer 1'
69+
email: 'alfasim-dev@esss.co'
70+
id: 'plugin'
71+
"""
72+
73+
invalid_yaml_file = tmp_path / "config.yaml"
74+
invalid_yaml_file.write_text(invalid_yaml_content)
75+
76+
mocker.patch.object(PluginInfo,'_check_if_shared_lib_exists', autospec=True)
77+
mocker.patch.object(PluginInfo,'_get_plugin_id_from_dll', autospc=True, return_value='plugin')
78+
79+
current_schema = dedent("""\
80+
caption : Str()
81+
version : Str()
82+
author : Str()
83+
email : Str()
84+
id : Str()
85+
Optional("requirements") : MapPattern(Str(), Str())
86+
Optional("extras") : MapPattern(Str(), Str())
87+
""").strip()
88+
expected_message = f"The plugin.yaml does not follow the PLUGIN_CONFIG_SCHEMA: {current_schema}"
89+
with pytest.raises(ValueError, match=re.escape(expected_message)):
90+
info = PluginInfo(invalid_yaml_file)
91+

0 commit comments

Comments
 (0)