Commit | Line | Data |
---|---|---|
55bb57e0 PP |
1 | # The MIT License (MIT) |
2 | # | |
3 | # Copyright (c) 2017 Philippe Proulx <pproulx@efficios.com> | |
4 | # | |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy | |
6 | # of this software and associated documentation files (the "Software"), to deal | |
7 | # in the Software without restriction, including without limitation the rights | |
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
9 | # copies of the Software, and to permit persons to whom the Software is | |
10 | # furnished to do so, subject to the following conditions: | |
11 | # | |
12 | # The above copyright notice and this permission notice shall be included in | |
13 | # all copies or substantial portions of the Software. | |
14 | # | |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
21 | # THE SOFTWARE. | |
22 | ||
23 | from bt2 import utils | |
3fb99a22 | 24 | from bt2 import component as bt2_component |
9105c467 PP |
25 | import sys |
26 | ||
27 | ||
28 | # Python plugin path to `_PluginInfo` (cache) | |
29 | _plugin_infos = {} | |
55bb57e0 PP |
30 | |
31 | ||
32 | def plugin_component_class(component_class): | |
3fb99a22 | 33 | if not issubclass(component_class, bt2_component._UserComponent): |
55bb57e0 PP |
34 | raise TypeError('component class is not a subclass of a user component class') |
35 | ||
36 | component_class._bt_plugin_component_class = None | |
37 | return component_class | |
38 | ||
39 | ||
cfbd7cf3 FD |
40 | def register_plugin( |
41 | module_name, name, description=None, author=None, license=None, version=None | |
42 | ): | |
55bb57e0 | 43 | if module_name not in sys.modules: |
cfbd7cf3 FD |
44 | raise RuntimeError( |
45 | "cannot find module '{}' in loaded modules".format(module_name) | |
46 | ) | |
55bb57e0 PP |
47 | |
48 | utils._check_str(name) | |
49 | ||
50 | if description is not None: | |
51 | utils._check_str(description) | |
52 | ||
53 | if author is not None: | |
54 | utils._check_str(author) | |
55 | ||
56 | if license is not None: | |
57 | utils._check_str(license) | |
58 | ||
59 | if version is not None: | |
60 | if not _validate_version(version): | |
cfbd7cf3 FD |
61 | raise ValueError( |
62 | 'wrong version: expecting a tuple: (major, minor, patch) or (major, minor, patch, extra)' | |
63 | ) | |
55bb57e0 | 64 | |
cfbd7cf3 FD |
65 | sys.modules[module_name]._bt_plugin_info = _PluginInfo( |
66 | name, description, author, license, version | |
67 | ) | |
55bb57e0 PP |
68 | |
69 | ||
70 | def _validate_version(version): | |
71 | if version is None: | |
72 | return True | |
73 | ||
74 | if not isinstance(version, tuple): | |
75 | return False | |
76 | ||
77 | if len(version) < 3 or len(version) > 4: | |
78 | return False | |
79 | ||
80 | if not isinstance(version[0], int): | |
81 | return False | |
82 | ||
83 | if not isinstance(version[1], int): | |
84 | return False | |
85 | ||
86 | if not isinstance(version[2], int): | |
87 | return False | |
88 | ||
89 | if len(version) == 4: | |
90 | if not isinstance(version[3], str): | |
91 | return False | |
92 | ||
93 | return True | |
94 | ||
95 | ||
96 | class _PluginInfo: | |
97 | def __init__(self, name, description, author, license, version): | |
98 | self.name = name | |
99 | self.description = description | |
100 | self.author = author | |
101 | self.license = license | |
102 | self.version = version | |
103 | self.comp_class_addrs = None | |
104 | ||
105 | ||
106 | # called by the BT plugin system | |
107 | def _try_load_plugin_module(path): | |
9105c467 PP |
108 | if path in _plugin_infos: |
109 | # do not load module and create plugin info twice for this path | |
110 | return _plugin_infos[path] | |
111 | ||
55bb57e0 PP |
112 | import importlib.machinery |
113 | import inspect | |
114 | import hashlib | |
115 | ||
116 | if path is None: | |
117 | raise TypeError('missing path') | |
118 | ||
119 | # In order to load the module uniquely from its path, even from | |
120 | # different files which have the same basename, we hash the path | |
121 | # and prefix with `bt_plugin_`. This is its key in sys.modules. | |
122 | h = hashlib.sha256() | |
123 | h.update(path.encode()) | |
124 | module_name = 'bt_plugin_{}'.format(h.hexdigest()) | |
9105c467 | 125 | assert module_name not in sys.modules |
55bb57e0 PP |
126 | # try loading the module: any raised exception is catched by the caller |
127 | mod = importlib.machinery.SourceFileLoader(module_name, path).load_module() | |
128 | ||
129 | # we have the module: look for its plugin info first | |
130 | if not hasattr(mod, '_bt_plugin_info'): | |
131 | raise RuntimeError("missing '_bt_plugin_info' module attribute") | |
132 | ||
133 | plugin_info = mod._bt_plugin_info | |
134 | ||
135 | # search for user component classes | |
136 | def is_user_comp_class(obj): | |
137 | if not inspect.isclass(obj): | |
138 | return False | |
139 | ||
140 | if not hasattr(obj, '_bt_plugin_component_class'): | |
141 | return False | |
142 | ||
143 | return True | |
144 | ||
145 | comp_class_entries = inspect.getmembers(mod, is_user_comp_class) | |
146 | plugin_info.comp_class_addrs = [entry[1].addr for entry in comp_class_entries] | |
9105c467 | 147 | _plugin_infos[path] = plugin_info |
55bb57e0 | 148 | return plugin_info |