Coverage for src/deepdraw/script/config.py: 86%

88 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-11-30 15:00 +0100

1# SPDX-FileCopyrightText: Copyright © 2023 Idiap Research Institute <contact@idiap.ch> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7import importlib.metadata 

8import inspect 

9import typing 

10 

11import click 

12 

13from clapper.click import AliasedGroup, verbosity_option 

14from clapper.logging import setup 

15 

16logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s") 

17 

18 

19def _retrieve_entry_points( 

20 group: str, 

21) -> typing.Iterable[importlib.metadata.EntryPoint]: 

22 """Wraps various entry-point retrieval mechanisms. 

23 

24 For Python 3.9 and 3.10, 

25 :py:func:`importlib.metadata.entry_points()` 

26 returns a dictionary keyed by entry-point group names. From Python 

27 3.10 

28 onwards, one may pass the ``group`` keyword to that function to 

29 enable 

30 pre-filtering, or use the ``select()`` method on the returned value, 

31 which 

32 is no longer a dictionary. 

33 

34 For anything before Python 3.8, you must use the backported library 

35 ``importlib_metadata``. 

36 """ 

37 import sys 

38 

39 if sys.version_info[:2] < (3, 10): 

40 all_entry_points = importlib.metadata.entry_points() 

41 return all_entry_points.get(group, []) # Python 3.9 

42 

43 # Python 3.10 and above 

44 return importlib.metadata.entry_points().select(group=group) 

45 

46 

47@click.group(cls=AliasedGroup) 

48def config(): 

49 """Commands for listing, describing and copying configuration resources.""" 

50 pass 

51 

52 

53@config.command( 

54 epilog="""Examples: 

55 

56\b 

57 1. Lists all configuration resources (type: deepdraw.config) installed: 

58 

59 .. code:: sh 

60 

61 deepdraw config list 

62 

63 

64\b 

65 2. Lists all configuration resources and their descriptions (notice this may 

66 be slow as it needs to load all modules once): 

67 

68 .. code:: sh 

69 

70 deepdraw config list -v 

71""" 

72) 

73@verbosity_option(logger=logger) 

74def list(verbose) -> None: 

75 """Lists configuration files installed.""" 

76 entry_points = _retrieve_entry_points("deepdraw.config") 

77 entry_point_dict = {k.name: k for k in entry_points} 

78 

79 # all modules with configuration resources 

80 modules = {k.module.rsplit(".", 1)[0] for k in entry_point_dict.values()} 

81 keep_modules: set[str] = set() 

82 for k in sorted(modules): 

83 if k not in keep_modules and not any( 

84 k.startswith(element) for element in keep_modules 

85 ): 

86 keep_modules.add(k) 

87 modules = keep_modules 

88 

89 # sort data entries by originating module 

90 entry_points_by_module: dict[str, dict[str, typing.Any]] = {} 

91 for k in modules: 

92 entry_points_by_module[k] = {} 

93 for name, ep in entry_point_dict.items(): 

94 if ep.module.startswith(k): 

95 entry_points_by_module[k][name] = ep 

96 

97 for config_type in sorted(entry_points_by_module): 

98 # calculates the longest config name so we offset the printing 

99 longest_name_length = max( 

100 len(k) for k in entry_points_by_module[config_type].keys() 

101 ) 

102 

103 # set-up printing options 

104 print_string = " %%-%ds %%s" % (longest_name_length,) 

105 # 79 - 4 spaces = 75 (see string above) 

106 description_leftover = 75 - longest_name_length 

107 

108 print(f"module: {config_type}") 

109 for name in sorted(entry_points_by_module[config_type]): 

110 ep = entry_point_dict[name] 

111 

112 if verbose >= 1: 

113 module = ep.load() 

114 doc = inspect.getdoc(module) 

115 if doc is not None: 

116 summary = doc.split("\n\n")[0] 

117 else: 

118 summary = "<DOCSTRING NOT AVAILABLE>" 

119 else: 

120 summary = "" 

121 

122 summary = ( 

123 (summary[: (description_leftover - 3)] + "...") 

124 if len(summary) > (description_leftover - 3) 

125 else summary 

126 ) 

127 

128 print(print_string % (name, summary)) 

129 

130 

131@config.command( 

132 epilog="""Examples: 

133 

134\b 

135 1. Describes the Montgomery dataset configuration: 

136 

137 .. code:: sh 

138 

139 deepdraw config describe montgomery 

140 

141 

142\b 

143 2. Describes the Montgomery dataset configuration and lists its 

144 contents: 

145 

146 .. code:: sh 

147 

148 deepdraw config describe montgomery -v 

149 

150""" 

151) 

152@click.argument( 

153 "name", 

154 required=True, 

155 nargs=-1, 

156) 

157@verbosity_option(logger=logger) 

158def describe(name, verbose) -> None: 

159 """Describes a specific configuration file.""" 

160 entry_points = _retrieve_entry_points("deepdraw.config") 

161 entry_point_dict = {k.name: k for k in entry_points} 

162 

163 for k in name: 

164 if k not in entry_point_dict: 

165 logger.error("Cannot find configuration resource '%s'", k) 

166 continue 

167 ep = entry_point_dict[k] 

168 print(f"Configuration: {ep.name}") 

169 print(f"Python Module: {ep.module}") 

170 print("") 

171 mod = ep.load() 

172 

173 if verbose >= 1: 

174 fname = inspect.getfile(mod) 

175 print("Contents:") 

176 with open(fname) as f: 

177 print(f.read()) 

178 else: # only output documentation 

179 print("Documentation:") 

180 print(inspect.getdoc(mod)) 

181 

182 

183@config.command( 

184 epilog="""Examples: 

185 

186\b 

187 1. Makes a copy of one of the stock configuration files locally, so it can be 

188 adapted: 

189 

190 .. code:: sh 

191 

192 $ deepdraw config copy montgomery -vvv newdataset.py 

193 

194""" 

195) 

196@click.argument( 

197 "source", 

198 required=True, 

199 nargs=1, 

200) 

201@click.argument( 

202 "destination", 

203 required=True, 

204 nargs=1, 

205) 

206@verbosity_option(logger=logger, expose_value=False) 

207def copy(source, destination) -> None: 

208 """Copy a specific configuration resource so it can be modified locally.""" 

209 import shutil 

210 

211 entry_points = _retrieve_entry_points("deepdraw.config") 

212 entry_point_dict = {k.name: k for k in entry_points} 

213 

214 if source not in entry_point_dict: 

215 logger.error("Cannot find configuration resource '%s'", source) 

216 return 

217 

218 ep = entry_point_dict[source] 

219 mod = ep.load() 

220 src_name = inspect.getfile(mod) 

221 logger.info(f"cp {src_name} -> {destination}") 

222 shutil.copyfile(src_name, destination)