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 | |
24 | import bt2.component | |
25 | ||
26 | ||
27 | def plugin_component_class(component_class): | |
28 | if not issubclass(component_class, bt2.component._UserComponent): | |
29 | raise TypeError('component class is not a subclass of a user component class') | |
30 | ||
31 | component_class._bt_plugin_component_class = None | |
32 | return component_class | |
33 | ||
34 | ||
61d96b89 FD |
35 | def register_plugin( |
36 | module_name, name, description=None, author=None, license=None, version=None | |
37 | ): | |
55bb57e0 PP |
38 | import sys |
39 | ||
40 | if module_name not in sys.modules: | |
61d96b89 FD |
41 | raise RuntimeError( |
42 | "cannot find module '{}' in loaded modules".format(module_name) | |
43 | ) | |
55bb57e0 PP |
44 | |
45 | utils._check_str(name) | |
46 | ||
47 | if description is not None: | |
48 | utils._check_str(description) | |
49 | ||
50 | if author is not None: | |
51 | utils._check_str(author) | |
52 | ||
53 | if license is not None: | |
54 | utils._check_str(license) | |
55 | ||
56 | if version is not None: | |
57 | if not _validate_version(version): | |
61d96b89 FD |
58 | raise ValueError( |
59 | 'wrong version: expecting a tuple: (major, minor, patch) or (major, minor, patch, extra)' | |
60 | ) | |
55bb57e0 | 61 | |
61d96b89 FD |
62 | sys.modules[module_name]._bt_plugin_info = _PluginInfo( |
63 | name, description, author, license, version | |
64 | ) | |
55bb57e0 PP |
65 | |
66 | ||
67 | def _validate_version(version): | |
68 | if version is None: | |
69 | return True | |
70 | ||
71 | if not isinstance(version, tuple): | |
72 | return False | |
73 | ||
74 | if len(version) < 3 or len(version) > 4: | |
75 | return False | |
76 | ||
77 | if not isinstance(version[0], int): | |
78 | return False | |
79 | ||
80 | if not isinstance(version[1], int): | |
81 | return False | |
82 | ||
83 | if not isinstance(version[2], int): | |
84 | return False | |
85 | ||
86 | if len(version) == 4: | |
87 | if not isinstance(version[3], str): | |
88 | return False | |
89 | ||
90 | return True | |
91 | ||
92 | ||
93 | class _PluginInfo: | |
94 | def __init__(self, name, description, author, license, version): | |
95 | self.name = name | |
96 | self.description = description | |
97 | self.author = author | |
98 | self.license = license | |
99 | self.version = version | |
100 | self.comp_class_addrs = None | |
101 | ||
102 | ||
103 | # called by the BT plugin system | |
104 | def _try_load_plugin_module(path): | |
105 | import importlib.machinery | |
106 | import inspect | |
107 | import hashlib | |
108 | ||
109 | if path is None: | |
110 | raise TypeError('missing path') | |
111 | ||
112 | # In order to load the module uniquely from its path, even from | |
113 | # different files which have the same basename, we hash the path | |
114 | # and prefix with `bt_plugin_`. This is its key in sys.modules. | |
115 | h = hashlib.sha256() | |
116 | h.update(path.encode()) | |
117 | module_name = 'bt_plugin_{}'.format(h.hexdigest()) | |
118 | ||
119 | # try loading the module: any raised exception is catched by the caller | |
120 | mod = importlib.machinery.SourceFileLoader(module_name, path).load_module() | |
121 | ||
122 | # we have the module: look for its plugin info first | |
123 | if not hasattr(mod, '_bt_plugin_info'): | |
124 | raise RuntimeError("missing '_bt_plugin_info' module attribute") | |
125 | ||
126 | plugin_info = mod._bt_plugin_info | |
127 | ||
128 | # search for user component classes | |
129 | def is_user_comp_class(obj): | |
130 | if not inspect.isclass(obj): | |
131 | return False | |
132 | ||
133 | if not hasattr(obj, '_bt_plugin_component_class'): | |
134 | return False | |
135 | ||
136 | return True | |
137 | ||
138 | comp_class_entries = inspect.getmembers(mod, is_user_comp_class) | |
139 | plugin_info.comp_class_addrs = [entry[1].addr for entry in comp_class_entries] | |
140 | return plugin_info |