Coverage for /home/mattis/projects/websites/dighl/edictor/src/edictor/cli.py: 82%

113 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-07 06:52 +0200

1""" 

2Commandline Interface of EDICTOR 

3""" 

4 

5import webbrowser 

6from http.server import HTTPServer 

7import argparse 

8from pathlib import Path 

9import codecs 

10import json 

11import threading 

12 

13from edictor.util import DATA 

14 

15try: 

16 import lingpy 

17except ImportError: 

18 lingpy = False 

19 

20 

21class CommandMeta(type): 

22 """ 

23 A metaclass which keeps track of subclasses, if they have all-lowercase names. 

24 """ 

25 

26 __instances = [] 

27 

28 def __init__(self, name, bases, dct): # noqa 

29 super(CommandMeta, self).__init__(name, bases, dct) 

30 if name == name.lower(): 

31 self.__instances.append(self) 

32 

33 def __iter__(self): 

34 return iter(self.__instances) 

35 

36 

37class Command(metaclass=CommandMeta): 

38 """Base class for subcommands of the EDICTOR command line interface.""" 

39 

40 help = None 

41 

42 @classmethod 

43 def subparser(cls, parser): 

44 """Hook to define subcommand arguments.""" 

45 return 

46 

47 def __call__(self, args): 

48 """Hook to run the subcommand.""" 

49 raise NotImplementedError 

50 

51 

52def add_option(parser, name_, default_, help_, short_opt=None, **kw): 

53 names = ["--" + name_] 

54 if short_opt: 

55 names.append("-" + short_opt) 

56 

57 if isinstance(default_, bool): 

58 kw["action"] = "store_false" if default_ else "store_true" 

59 elif isinstance(default_, int): 

60 kw["type"] = int 

61 elif isinstance(default_, float): 

62 kw["type"] = float 

63 kw["default"] = default_ 

64 kw["help"] = help_ 

65 parser.add_argument(*names, **kw) 

66 

67 

68def _cmd_by_name(name): 

69 for cmd in Command: 

70 if cmd.__name__ == name: 

71 return cmd() 

72 

73 

74class server(Command): 

75 """ 

76 Run EDICTOR 3 in a local server and access the application through your webbrowser. 

77 """ 

78 

79 @classmethod 

80 def subparser(cls, p): 

81 """ 

82 EDICTOR 3 in local server. 

83 """ 

84 add_option( 

85 p, 

86 "port", 

87 9999, 

88 "Port where the local server will serve the application.", 

89 short_opt="p", 

90 ) 

91 add_option( 

92 p, 

93 "browser", 

94 "firefox", 

95 "Select the webbrowser to open the application.", 

96 short_opt="b" 

97 ) 

98 add_option( 

99 p, 

100 "config", 

101 "config.json", 

102 "Name of the configuration file to be used.", 

103 short_opt="c" 

104 ) 

105 add_option( 

106 p, 

107 "no-window", 

108 False, 

109 "Do not open a window of the application.", 

110 ) 

111 

112 def __call__(self, args): 

113 """ 

114 EDICTOR 3 Server Application. 

115 """ 

116 DATA["config"] = args.config 

117 from edictor.server import Handler 

118 httpd = HTTPServer(("", args.port), Handler) 

119 print("Serving EDICTOR 3 at port {0}...".format(args.port)) 

120 url = "http://localhost:" + str(args.port) + "/" 

121 if not args.no_window: # pragma: no cover 

122 try: 

123 webbrowser.get(args.browser).open_new_tab(url) 

124 except: # noqa 

125 try: 

126 webbrowser.open(url) 

127 except: # noqa 

128 print("Could not open webbrowser, please open locally " 

129 "at http://localhost:" + str(args.port) + "/") 

130 httpd.serve_forever() 

131 

132 

133 

134class fetch(Command): 

135 """ 

136 Download a wordlist from an EDICTOR application. 

137 """ 

138 @classmethod 

139 def subparser(cls, p): 

140 add_option( 

141 p, 

142 "dataset", 

143 None, 

144 "Name of the remote dataset you want to access.", 

145 short_opt="d" 

146 ) 

147 add_option( 

148 p, 

149 "name", 

150 "dummy.tsv", 

151 "Name of the file where you want to store the data.", 

152 short_opt="n" 

153 ) 

154 

155 def __call__(self, args): 

156 """ 

157 Download data. 

158 """ 

159 from edictor.wordlist import fetch_wordlist 

160 data = fetch_wordlist( 

161 args.dataset 

162 ) 

163 with codecs.open(args.name, "w", "utf-8") as f: 

164 f.write(data) 

165 

166 

167class wordlist(Command): 

168 """ 

169 Convert a dataset to EDICTOR's SQLITE and TSV formats (requires LingPy). 

170 """ 

171 

172 @classmethod 

173 def subparser(cls, p): 

174 add_option( 

175 p, 

176 "dataset", 

177 Path("cldf", "cldf-metadata.json"), 

178 "Path to the CLDF metadata file.", 

179 short_opt="d", 

180 ) 

181 add_option( 

182 p, 

183 "preprocessing", 

184 None, 

185 "path to the module to preprocess the data", 

186 short_opt="p", 

187 ) 

188 add_option( 

189 p, 

190 "namespace", 

191 '{"language_id": "doculect", "concept_name": "concept",' 

192 '"value": "value", "form": "form", "segments": "tokens",' 

193 '"comment": "note"}', 

194 "namespace and columns you want to extract", 

195 ) 

196 add_option( 

197 p, "name", "dummy", "name of the dataset you want to create", short_opt="n" 

198 ) 

199 add_option(p, "addon", None, "expand the namespace", short_opt="a") 

200 add_option(p, "sqlite", False, "convert to SQLITE format") 

201 add_option( 

202 p, "custom", None, "custom field where arguments can be passed in JSON form" 

203 ) 

204 

205 def __call__(self, args): 

206 

207 from edictor.wordlist import get_wordlist 

208 import importlib.util 

209 

210 namespace = json.loads(args.namespace) 

211 if args.addon: 

212 for row in args.addon.split(","): 

213 s, t = row.split(":") 

214 namespace[s] = t 

215 

216 columns = [x for x in list(namespace)] 

217 if args.preprocessing: 

218 spec = importlib.util.spec_from_file_location("prep", args.preprocessing) 

219 prep = importlib.util.module_from_spec(spec) 

220 spec.loader.exec_module(prep) 

221 preprocessing = prep.run 

222 else: 

223 preprocessing = None 

224 if args.custom: 

225 custom_args = json.loads(args.custom) 

226 else: 

227 custom_args = None 

228 get_wordlist( 

229 args.dataset, 

230 args.name, 

231 columns=columns, 

232 namespace=namespace, 

233 preprocessing=preprocessing, 

234 lexibase=args.sqlite, 

235 custom_args=custom_args, 

236 ) 

237 

238 

239def get_parser(): 

240 # basic parser for lingpy 

241 parser = argparse.ArgumentParser( 

242 description=main.__doc__, 

243 formatter_class=argparse.ArgumentDefaultsHelpFormatter 

244 ) 

245 

246 subparsers = parser.add_subparsers(dest="subcommand") 

247 for cmd in Command: 

248 if not lingpy and cmd.__name__ == "wordlist": 

249 continue 

250 subparser = subparsers.add_parser( 

251 cmd.__name__, 

252 help=(cmd.__doc__ or "").strip().split("\n")[0], 

253 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 

254 ) 

255 cmd.subparser(subparser) 

256 cmd.help = subparser.format_help() 

257 

258 return parser 

259 

260 

261def main(*args): 

262 """ 

263 Computer-assisted language comparison with EDICTOR 3. 

264 """ 

265 parser = get_parser() 

266 args = parser.parse_args(args or None) 

267 if args.subcommand: 

268 return _cmd_by_name(args.subcommand)(args) 

269 else: 

270 parser.print_help()