Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/util.py: 100%

96 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-08-26 14:27 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2024 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24WuttJamaican - utilities 

25""" 

26 

27import importlib 

28import logging 

29import os 

30import shlex 

31from uuid import uuid1 

32 

33 

34log = logging.getLogger(__name__) 

35 

36 

37# nb. this is used as default kwarg value in some places, to 

38# distinguish passing a ``None`` value, vs. *no* value at all 

39UNSPECIFIED = object() 

40 

41 

42def get_class_hierarchy(klass, topfirst=True): 

43 """ 

44 Returns a list of all classes in the inheritance chain for the 

45 given class. 

46 

47 For instance:: 

48 

49 class A: 

50 pass 

51 

52 class B(A): 

53 pass 

54 

55 class C(B): 

56 pass 

57 

58 get_class_hierarchy(C) 

59 # -> [A, B, C] 

60 

61 :param klass: The reference class. The list of classes returned 

62 will include this class and all its parents. 

63 

64 :param topfirst: Whether the returned list should be sorted in a 

65 "top first" way, e.g. A) grandparent, B) parent, C) child. 

66 This is the default but pass ``False`` to get the reverse. 

67 """ 

68 hierarchy = [] 

69 

70 def traverse(cls): 

71 if cls is not object: 

72 hierarchy.append(cls) 

73 for parent in cls.__bases__: 

74 traverse(parent) 

75 

76 traverse(klass) 

77 if topfirst: 

78 hierarchy.reverse() 

79 return hierarchy 

80 

81 

82def load_entry_points(group, ignore_errors=False): 

83 """ 

84 Load a set of ``setuptools``-style entry points. 

85 

86 This is used to locate "plugins" and similar things, e.g. the set 

87 of subcommands which belong to a main command. 

88 

89 :param group: The group (string name) of entry points to be 

90 loaded, e.g. ``'wutta.commands'``. 

91 

92 :param ignore_errors: If false (the default), any errors will be 

93 raised normally. If true, errors will be logged but not 

94 raised. 

95 

96 :returns: A dictionary whose keys are the entry point names, and 

97 values are the loaded entry points. 

98 """ 

99 entry_points = {} 

100 

101 try: 

102 # nb. this package was added in python 3.8 

103 import importlib.metadata as importlib_metadata 

104 except ImportError: 

105 import importlib_metadata 

106 

107 eps = importlib_metadata.entry_points() 

108 if not hasattr(eps, 'select'): 

109 # python < 3.10 

110 eps = eps.get(group, []) 

111 else: 

112 # python >= 3.10 

113 eps = eps.select(group=group) 

114 for entry_point in eps: 

115 try: 

116 ep = entry_point.load() 

117 except: 

118 if not ignore_errors: 

119 raise 

120 log.warning("failed to load entry point: %s", entry_point, 

121 exc_info=True) 

122 else: 

123 entry_points[entry_point.name] = ep 

124 

125 return entry_points 

126 

127 

128def load_object(spec): 

129 """ 

130 Load an arbitrary object from a module, according to the spec. 

131 

132 The spec string should contain a dotted path to an importable module, 

133 followed by a colon (``':'``), followed by the name of the object to be 

134 loaded. For example: 

135 

136 .. code-block:: none 

137 

138 wuttjamaican.util:parse_bool 

139 

140 You'll notice from this example that "object" in this context refers to any 

141 valid Python object, i.e. not necessarily a class instance. The name may 

142 refer to a class, function, variable etc. Once the module is imported, the 

143 ``getattr()`` function is used to obtain a reference to the named object; 

144 therefore anything supported by that approach should work. 

145 

146 :param spec: Spec string. 

147 

148 :returns: The specified object. 

149 """ 

150 if not spec: 

151 raise ValueError("no object spec provided") 

152 

153 module_path, name = spec.split(':') 

154 module = importlib.import_module(module_path) 

155 return getattr(module, name) 

156 

157 

158def make_title(text): 

159 """ 

160 Return a human-friendly "title" for the given text. 

161 

162 This is mostly useful for converting a Python variable name (or 

163 similar) to a human-friendly string, e.g.:: 

164 

165 make_title('foo_bar') # => 'Foo Bar' 

166 """ 

167 text = text.replace('_', ' ') 

168 text = text.replace('-', ' ') 

169 words = text.split() 

170 return ' '.join([x.capitalize() for x in words]) 

171 

172 

173def make_uuid(): 

174 """ 

175 Generate a universally-unique identifier. 

176 

177 :returns: A 32-character hex string. 

178 """ 

179 return uuid1().hex 

180 

181 

182def parse_bool(value): 

183 """ 

184 Derive a boolean from the given string value. 

185 """ 

186 if value is None: 

187 return None 

188 if isinstance(value, bool): 

189 return value 

190 if str(value).lower() in ('true', 'yes', 'y', 'on', '1'): 

191 return True 

192 return False 

193 

194 

195def parse_list(value): 

196 """ 

197 Parse a configuration value, splitting by whitespace and/or commas 

198 and taking quoting into account etc., yielding a list of strings. 

199 """ 

200 if value is None: 

201 return [] 

202 if isinstance(value, list): 

203 return value 

204 parser = shlex.shlex(value) 

205 parser.whitespace += ',' 

206 parser.whitespace_split = True 

207 values = list(parser) 

208 for i, value in enumerate(values): 

209 if value.startswith('"') and value.endswith('"'): 

210 values[i] = value[1:-1] 

211 elif value.startswith("'") and value.endswith("'"): 

212 values[i] = value[1:-1] 

213 return values 

214 

215 

216def progress_loop(func, items, factory, message=None): 

217 """ 

218 Convenience function to iterate over a set of items, invoking 

219 logic for each, and updating a progress indicator along the way. 

220 

221 This function may also be called via the :term:`app handler`; see 

222 :meth:`~wuttjamaican.app.AppHandler.progress_loop()`. 

223 

224 The ``factory`` will be called to create the progress indicator, 

225 which should be an instance of 

226 :class:`~wuttjamaican.progress.ProgressBase`. 

227 

228 The ``factory`` may also be ``None`` in which case there is no 

229 progress, and this is really just a simple "for loop". 

230 

231 :param func: Callable to be invoked for each item in the sequence. 

232 See below for more details. 

233 

234 :param items: Sequence of items over which to iterate. 

235 

236 :param factory: Callable which creates/returns a progress 

237 indicator, or can be ``None`` for no progress. 

238 

239 :param message: Message to display along with the progress 

240 indicator. If no message is specified, whether a default is 

241 shown will be up to the progress indicator. 

242 

243 The ``func`` param should be a callable which accepts 2 positional 

244 args ``(obj, i)`` - meaning for which is as follows: 

245 

246 :param obj: This will be an item within the sequence. 

247 

248 :param i: This will be the *one-based* sequence number for the 

249 item. 

250 

251 See also :class:`~wuttjamaican.progress.ConsoleProgress` for a 

252 usage example. 

253 """ 

254 progress = None 

255 if factory: 

256 count = len(items) 

257 progress = factory(message, count) 

258 

259 for i, item in enumerate(items, 1): 

260 

261 func(item, i) 

262 

263 if progress: 

264 progress.update(i) 

265 

266 if progress: 

267 progress.finish() 

268 

269 

270def resource_path(path): 

271 """ 

272 Returns the absolute file path for the given resource path. 

273 

274 A "resource path" is one which designates a python package name, 

275 plus some path under that. For instance: 

276 

277 .. code-block:: none 

278 

279 wuttjamaican.email:templates 

280 

281 Assuming such a path should exist, the question is "where?" 

282 

283 So this function uses :mod:`python:importlib.resources` to locate 

284 the path, possibly extracting the file(s) from a zipped package, 

285 and returning the final path on disk. 

286 

287 It only does this if it detects it is needed, based on the given 

288 ``path`` argument. If that is already an absolute path then it 

289 will be returned as-is. 

290 

291 :param path: Either a package resource specifier as shown above, 

292 or regular file path. 

293 

294 :returns: Absolute file path to the resource. 

295 """ 

296 if not os.path.isabs(path) and ':' in path: 

297 

298 try: 

299 # nb. these were added in python 3.9 

300 from importlib.resources import files, as_file 

301 except ImportError: # python < 3.9 

302 from importlib_resources import files, as_file 

303 

304 package, filename = path.split(':') 

305 ref = files(package) / filename 

306 with as_file(ref) as path: 

307 return str(path) 

308 

309 return path