lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lockss/turtles/plugin.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
3
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
4
4
  #
5
5
  # Redistribution and use in source and binary forms, with or without
6
6
  # modification, are permitted provided that the following conditions are met:
@@ -28,66 +28,67 @@
28
28
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
- from pathlib import Path
32
- import xml.etree.ElementTree
33
- import zipfile
31
+ """
32
+ Module to represent a LOCKSS plugin.
33
+ """
34
34
 
35
- import java_manifest
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
36
+ from __future__ import annotations
36
37
 
37
- from lockss.turtles.util import _path
38
+ from collections.abc import Callable
39
+ from pathlib import Path
40
+ from typing import Any, AnyStr, IO, Optional
41
+ import xml.etree.ElementTree as ET
42
+ from zipfile import ZipFile
38
43
 
44
+ import java_manifest
45
+ from lockss.pybasic.fileutil import path
39
46
 
40
- class Plugin(object):
47
+ from .util import PathOrStr
41
48
 
42
- @staticmethod
43
- def from_jar(jar_path):
44
- jar_path = _path(jar_path) # in case it's a string
45
- plugin_id = Plugin.id_from_jar(jar_path)
46
- plugin_fstr = str(Plugin.id_to_file(plugin_id))
47
- with zipfile.ZipFile(jar_path, 'r') as zip_file:
48
- with zip_file.open(plugin_fstr, 'r') as plugin_file:
49
- return Plugin(plugin_file, plugin_fstr)
50
49
 
51
- @staticmethod
52
- def from_path(path):
53
- path = _path(path) # in case it's a string
54
- with open(path, 'r') as input_file:
55
- return Plugin(input_file, path)
50
+ #: A type alias for plugin identifiers.
51
+ PluginIdentifier = str
56
52
 
57
- @staticmethod
58
- def file_to_id(plugin_fstr):
59
- return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
60
53
 
61
- @staticmethod
62
- def id_from_jar(jar_path):
63
- jar_path = _path(jar_path) # in case it's a string
64
- manifest = java_manifest.from_jar(jar_path)
65
- for entry in manifest:
66
- if entry.get('Lockss-Plugin') == 'true':
67
- name = entry.get('Name')
68
- if name is None:
69
- raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
70
- return Plugin.file_to_id(name)
71
- else:
72
- raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
54
+ class Plugin(object):
55
+ """
56
+ An object to represent a LOCKSS plugin.
57
+ """
73
58
 
74
- @staticmethod
75
- def id_to_dir(plugin_id):
76
- return Plugin.id_to_file(plugin_id).parent
59
+ def __init__(self, plugin_file: IO[AnyStr], plugin_path: PathOrStr) -> None:
60
+ """
61
+ Constructor.
77
62
 
78
- @staticmethod
79
- def id_to_file(plugin_id):
80
- return Path(f'{plugin_id.replace(".", "/")}.xml')
63
+ Other exceptions than ``RuntimeError`` may be raised if the plugin
64
+ definition file cannot be parsed by ``xml.etree.ElementTree``.
81
65
 
82
- def __init__(self, plugin_file, plugin_path):
66
+ :param plugin_file: An open file-like object that can read the plugin
67
+ definition file.
68
+ :type plugin_file: IO[AnyStr]
69
+ :param plugin_path: A string (or Path) representing the hierarchical
70
+ path of the plugin definition file (as a real file,
71
+ or as a file entry in a JAR file).
72
+ :type plugin_path: PathOrStr
73
+ :raises RuntimeError: If the plugin definition file parses as XML but
74
+ the top-level element is not <map>.
75
+ """
83
76
  super().__init__()
84
77
  self._path = plugin_path
85
- self._parsed = xml.etree.ElementTree.parse(plugin_file).getroot()
78
+ self._parsed = ET.parse(plugin_file).getroot()
86
79
  tag = self._parsed.tag
87
80
  if tag != 'map':
88
81
  raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
89
82
 
90
- def get_aux_packages(self):
83
+ def get_aux_packages(self) -> list[str]:
84
+ """
85
+ Returns the (possibly empty) list of auxiliary code packages declared by
86
+ the plugin (``plugin_aux_packages``).
87
+
88
+ :return: A non-null list of strings representing auxiliary code
89
+ packages.
90
+ :rtype: list[str]
91
+ """
91
92
  key = 'plugin_aux_packages'
92
93
  lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
93
94
  if lst is None or len(lst) < 1:
@@ -96,25 +97,181 @@ class Plugin(object):
96
97
  raise ValueError(f'plugin declares {len(lst)} entries for {key}')
97
98
  return [x.text for x in lst[0].findall('string')]
98
99
 
99
- def get_identifier(self):
100
+ def get_identifier(self) -> Optional[PluginIdentifier]:
101
+ """
102
+ Get this plugin's identifier (``plugin_identifier``).
103
+
104
+ :return: A plugin identifier, or None if missing.
105
+ :rtype: Optional[PluginIdentifier]
106
+ :raises ValueError: If the plugin definition contains more than one.
107
+ """
100
108
  return self._only_one('plugin_identifier')
101
109
 
102
- def get_name(self):
110
+ def get_name(self) -> Optional[str]:
111
+ """
112
+ Get this plugin's name (``plugin_name``).
113
+
114
+ :return: A plugin name, or None if missing.
115
+ :rtype: Optional[str]
116
+ :raises ValueError: If the plugin definition contains more than one.
117
+ """
103
118
  return self._only_one('plugin_name')
104
119
 
105
- def get_parent_identifier(self):
120
+ def get_parent_identifier(self) -> Optional[PluginIdentifier]:
121
+ """
122
+ Get this plugin's parent identifier (``plugin_parent``).
123
+
124
+ :return: A parent plugin identifier, or None if this plugin has no
125
+ parent.
126
+ :rtype: Optional[PluginIdentifier]
127
+ :raises ValueError: If the plugin definition contains more than one.
128
+ """
106
129
  return self._only_one('plugin_parent')
107
130
 
108
- def get_parent_version(self):
131
+ def get_parent_version(self) -> Optional[int]:
132
+ """
133
+ Get this plugin's parent version (``plugin_parent_version``).
134
+
135
+ :return: A parent plugin version, or None if this plugin has no
136
+ parent.
137
+ :rtype: Optional[int]
138
+ :raises ValueError: If the plugin definition contains more than one.
139
+ """
109
140
  return self._only_one('plugin_parent_version', int)
110
141
 
111
- def get_version(self):
142
+ def get_version(self) -> Optional[int]:
143
+ """
144
+ Get this plugin's version (``plugin_version``).
145
+
146
+ :return: A plugin version, or None if missing.
147
+ :rtype: Optional[int]
148
+ :raises ValueError: If the plugin definition contains more than one.
149
+ """
112
150
  return self._only_one('plugin_version', int)
113
151
 
114
- def _only_one(self, key, result=str):
152
+ def _only_one(self, key: str, result: Callable[[str], Any]=str) -> Optional[Any]:
153
+ """
154
+ Retrieves the value of a given key in the plugin definition, optionally
155
+ coerced into a representation (by default simply a string).
156
+
157
+ :param key: A plugin key.
158
+ :param key: str
159
+ :param result: A functor that takes a string and returns the desired
160
+ representation; by default this is the string constructor
161
+ ``str``, meaning by default the string values are
162
+ returned unchanged.
163
+ :param result: Callable[[str], Any]
164
+ :return: The value for the given key, coerced through the given functor,
165
+ or None if the plugin definition does not contain any entry
166
+ with the given key.
167
+ :rtype: Optional[Any]
168
+ :raises ValueError: If the plugin definition contains more than one
169
+ entry with the given key.
170
+ """
115
171
  lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
116
172
  if lst is None or len(lst) < 1:
117
173
  return None
118
174
  if len(lst) > 1:
119
175
  raise ValueError(f'plugin declares {len(lst)} entries for {key}')
120
176
  return result(lst[0])
177
+
178
+ @staticmethod
179
+ def from_jar(jar_path_or_str: PathOrStr) -> Plugin:
180
+ """
181
+ Instantiates a Plugin object from the given plugin JAR file.
182
+
183
+ :param jar_path_or_str: The path to a plugin JAR.
184
+ :type jar_path_or_str: PathOrStr
185
+ :return: A Plugin object.
186
+ :rtype: Plugin
187
+ """
188
+ jar_path = path(jar_path_or_str)
189
+ plugin_id = Plugin.id_from_jar(jar_path)
190
+ plugin_fstr = str(Plugin.id_to_file(plugin_id))
191
+ with ZipFile(jar_path, 'r') as zip_file:
192
+ with zip_file.open(plugin_fstr, 'r') as plugin_file:
193
+ return Plugin(plugin_file, plugin_fstr)
194
+
195
+ @staticmethod
196
+ def from_path(path_or_str: PathOrStr) -> Plugin:
197
+ """
198
+ Instantiates a Plugin object from the given plugin file.
199
+
200
+ :param path_or_str: The path to a plugin file.
201
+ :type path_or_str: PathOrStr
202
+ :return: A Plugin object.
203
+ :rtype: Plugin
204
+ """
205
+ fpath = path(path_or_str)
206
+ with fpath.open('r') as input_file:
207
+ return Plugin(input_file, fpath)
208
+
209
+ @staticmethod
210
+ def file_to_id(plugin_fstr: str) -> PluginIdentifier:
211
+ """
212
+ Converts a plugin file path (ending in ``.xml``) to the implied plugin
213
+ identifier (e.g. ``org/myproject/plugin/MyPlugin.xml`` implies
214
+ ``org.myproject.plugin.MyPlugin``).
215
+
216
+ See also ``id_to_file``.
217
+
218
+ :param plugin_fstr: A string file path.
219
+ :type plugin_fstr: str
220
+ :return: A plugin identifier.
221
+ :rtype: PluginIdentifier
222
+ """
223
+ return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
224
+
225
+ @staticmethod
226
+ def id_from_jar(jar_path_or_str: PathOrStr) -> PluginIdentifier:
227
+ """
228
+ Extracts the plugin identifier from a plugin JAR's manifest file.
229
+
230
+ :param jar_path_or_str: The path to a plugin JAR.
231
+ :type jar_path_or_str: PathOrStr
232
+ :return: The plugin identifier extracted from the given plugin JAR's
233
+ manifest file.
234
+ :rtype: PluginIdentifier
235
+ :raises Exception: If the JAR's manifest file has no entry with
236
+ ``Lockss-Plugin`` equal to ``true`` and ``Name``
237
+ equal to the packaged plugin's identifier.
238
+ """
239
+ jar_path = path(jar_path_or_str)
240
+ manifest = java_manifest.from_jar(jar_path)
241
+ for entry in manifest:
242
+ if entry.get('Lockss-Plugin') == 'true':
243
+ name = entry.get('Name')
244
+ if name is None:
245
+ raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
246
+ return Plugin.file_to_id(name)
247
+ else:
248
+ raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
249
+
250
+ @staticmethod
251
+ def id_to_dir(plugin_id: PluginIdentifier) -> Path:
252
+ """
253
+ Returns the path of the directory containing the given plugin identifier
254
+ (for example ``org/myproject/plugin`` for
255
+ ``org.myproject.plugin.MyPlugin``).
256
+
257
+ :param plugin_id: A plugin identifier.
258
+ :type plugin_id: PluginIdentifier
259
+ :return: The directory path containing the given plugin identifier.
260
+ :rtype: Path
261
+ """
262
+ return Plugin.id_to_file(plugin_id).parent
263
+
264
+ @staticmethod
265
+ def id_to_file(plugin_id: PluginIdentifier) -> Path:
266
+ """
267
+ Returns the path of the definition file corresponding to the given
268
+ plugin identifier (for example ``org/myproject/plugin/MyPlugin.xml`` for
269
+ ``org.myproject.plugin.MyPlugin``).
270
+
271
+ :param plugin_id: A plugin identifier.
272
+ :type plugin_id: PluginIdentifier
273
+ :return: The path of the definition file corresponding to the given
274
+ plugin identifier.
275
+ :rtype: Path
276
+ """
277
+ return Path(f'{plugin_id.replace(".", "/")}.xml')