Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2 lml.loader 

3 ~~~~~~~~~~~~~~~~~~~ 

4 

5 Plugin discovery module. It supports plugins installed via pip tools 

6 and pyinstaller. :func:`~lml.loader.scan_plugins` is expected to be 

7 called in the main package of yours at an earliest time of convenience. 

8 

9 :copyright: (c) 2017-2020 by Onni Software Ltd. 

10 :license: New BSD License, see LICENSE for more details 

11""" 

12import re 

13import logging 

14import pkgutil 

15import warnings 

16from itertools import chain 

17 

18from lml.utils import do_import 

19 

20log = logging.getLogger(__name__) 

21 

22 

23def scan_plugins( 

24 prefix, 

25 pyinstaller_path, 

26 black_list=None, 

27 white_list=None, 

28 plugin_name_patterns=None, 

29): 

30 """ 

31 Implicitly discover plugins via pkgutil and pyinstaller path 

32 

33 Parameters 

34 ----------------- 

35 

36 prefix:string 

37 module prefix. This prefix should become the prefix of the module name 

38 of all plugins. 

39 

40 In the tutorial, robotchef-britishcuisine is a plugin package 

41 of robotchef and its module name is 'robotchef_britishcuisine'. When 

42 robotchef call scan_plugins to load its cuisine plugins, it specifies 

43 its prefix as "robotchef_". All modules that starts with 'robotchef_' 

44 will be auto-loaded: robotchef_britishcuisine, robotchef_chinesecuisine, 

45 etc. 

46 

47 pyinstaller_path:string 

48 used in pyinstaller only. When your end developer would package 

49 your main library and its plugins using pyinstaller, this path 

50 helps pyinstaller to find the plugins. 

51 

52 black_list:list 

53 a list of module names that should be skipped. 

54 

55 white_list:list 

56 a list of modules that comes with your main module. If you have a 

57 built-in module, the module name should be inserted into the list. 

58 

59 For example, robot_cuisine is a built-in module inside robotchef. It 

60 is listed in white_list. 

61 """ 

62 __plugin_name_patterns = "^%s.+$" % prefix 

63 warnings.warn( 

64 "Deprecated! since version 0.0.3. Please use scan_plugins_regex!" 

65 ) 

66 scan_plugins_regex( 

67 plugin_name_patterns=__plugin_name_patterns, 

68 pyinstaller_path=pyinstaller_path, 

69 black_list=black_list, 

70 white_list=white_list, 

71 ) 

72 

73 

74def scan_plugins_regex( 

75 plugin_name_patterns=None, 

76 pyinstaller_path=None, 

77 black_list=None, 

78 white_list=None, 

79): 

80 

81 """ 

82 Implicitly discover plugins via pkgutil and pyinstaller path using 

83 regular expression 

84 

85 Parameters 

86 ----------------- 

87 

88 plugin_name_patterns: python regular expression 

89 it is used to match all your plugins, either it is a prefix, 

90 a suffix, some text in the middle or all. 

91 

92 pyinstaller_path:string 

93 used in pyinstaller only. When your end developer would package 

94 your main library and its plugins using pyinstaller, this path 

95 helps pyinstaller to find the plugins. 

96 

97 black_list:list 

98 a list of module names that should be skipped. 

99 

100 white_list:list 

101 a list of modules that comes with your main module. If you have a 

102 built-in module, the module name should be inserted into the list. 

103 

104 For example, robot_cuisine is a built-in module inside robotchef. It 

105 is listed in white_list. 

106 """ 

107 log.debug("scanning for plugins...") 

108 if black_list is None: 

109 black_list = [] 

110 

111 if white_list is None: 

112 white_list = [] 

113 

114 # scan pkgutil.iter_modules 

115 module_names = ( 

116 module_info[1] 

117 for module_info in pkgutil.iter_modules() 

118 if module_info[2] and re.match(plugin_name_patterns, module_info[1]) 

119 ) 

120 

121 # scan pyinstaller 

122 module_names_from_pyinstaller = scan_from_pyinstaller( 

123 plugin_name_patterns, pyinstaller_path 

124 ) 

125 

126 all_modules = chain( 

127 module_names, module_names_from_pyinstaller, white_list 

128 ) 

129 # loop through modules and find our plug ins 

130 for module_name in all_modules: 

131 

132 if module_name in black_list: 

133 log.debug("ignored " + module_name) 

134 continue 

135 

136 try: 

137 do_import(module_name) 

138 except ImportError as e: 

139 log.debug(module_name) 

140 log.debug(e) 

141 continue 

142 log.debug("scanning done") 

143 

144 

145# load modules to work based with and without pyinstaller 

146# from: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py 

147# see: https://github.com/pyinstaller/pyinstaller/issues/1905 

148# load modules using iter_modules() 

149# (should find all plug ins in normal build, but not pyinstaller) 

150def scan_from_pyinstaller(plugin_name_patterns, path): 

151 """ 

152 Discover plugins from pyinstaller 

153 """ 

154 table_of_content = set() 

155 for a_toc in ( 

156 importer.toc 

157 for importer in map(pkgutil.get_importer, path) 

158 if hasattr(importer, "toc") 

159 ): 

160 table_of_content |= a_toc 

161 

162 for module_name in table_of_content: 

163 if "." in module_name: 

164 continue 

165 if re.match(plugin_name_patterns, module_name): 

166 yield module_name