Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/tables/flavor.py: 48%

154 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-10 06:15 +0000

1"""Utilities for handling different array flavors in PyTables. 

2 

3Variables 

4========= 

5 

6`__docformat`__ 

7 The format of documentation strings in this module. 

8`internal_flavor` 

9 The flavor used internally by PyTables. 

10`all_flavors` 

11 List of all flavors available to PyTables. 

12`alias_map` 

13 Maps old flavor names to the most similar current flavor. 

14`description_map` 

15 Maps flavors to short descriptions of their supported objects. 

16`identifier_map` 

17 Maps flavors to functions that can identify their objects. 

18 

19 The function associated with a given flavor will return a true 

20 value if the object passed to it can be identified as being of 

21 that flavor. 

22 

23 See the `flavor_of()` function for a friendlier interface to 

24 flavor identification. 

25 

26`converter_map` 

27 Maps (source, destination) flavor pairs to converter functions. 

28 

29 Converter functions get an array of the source flavor and return 

30 an array of the destination flavor. 

31 

32 See the `array_of_flavor()` and `flavor_to_flavor()` functions for 

33 friendlier interfaces to flavor conversion. 

34 

35""" 

36 

37import warnings 

38 

39import numpy as np 

40 

41from .exceptions import FlavorError, FlavorWarning 

42 

43 

44__docformat__ = 'reStructuredText' 

45"""The format of documentation strings in this module.""" 

46 

47internal_flavor = 'numpy' 

48"""The flavor used internally by PyTables.""" 

49 

50# This is very slightly slower than a set for a small number of values 

51# in terms of (infrequent) lookup time, but allows `flavor_of()` 

52# (which may be called much more frequently) to check for flavors in 

53# order, beginning with the most common one. 

54all_flavors = [] # filled as flavors are registered 

55"""List of all flavors available to PyTables.""" 

56 

57alias_map = {} # filled as flavors are registered 

58"""Maps old flavor names to the most similar current flavor.""" 

59 

60description_map = {} # filled as flavors are registered 

61"""Maps flavors to short descriptions of their supported objects.""" 

62 

63identifier_map = {} # filled as flavors are registered 

64"""Maps flavors to functions that can identify their objects. 

65 

66The function associated with a given flavor will return a true value 

67if the object passed to it can be identified as being of that flavor. 

68 

69See the `flavor_of()` function for a friendlier interface to flavor 

70identification. 

71""" 

72 

73converter_map = {} # filled as flavors are registered 

74"""Maps (source, destination) flavor pairs to converter functions. 

75 

76Converter functions get an array of the source flavor and return an 

77array of the destination flavor. 

78 

79See the `array_of_flavor()` and `flavor_to_flavor()` functions for 

80friendlier interfaces to flavor conversion. 

81""" 

82 

83 

84def check_flavor(flavor): 

85 """Raise a ``FlavorError`` if the `flavor` is not valid.""" 

86 

87 if flavor not in all_flavors: 

88 available_flavs = ", ".join(flav for flav in all_flavors) 

89 raise FlavorError( 

90 "flavor ``%s`` is unsupported or unavailable; " 

91 "available flavors in this system are: %s" 

92 % (flavor, available_flavs)) 

93 

94 

95def array_of_flavor2(array, src_flavor, dst_flavor): 

96 """Get a version of the given `array` in a different flavor. 

97 

98 The input `array` must be of the given `src_flavor`, and the 

99 returned array will be of the indicated `dst_flavor`. Both 

100 flavors may be the same, but it is not guaranteed that the 

101 returned array will be the same object as the input one in this 

102 case. 

103 

104 If the conversion is not supported, a ``FlavorError`` is raised. 

105 

106 """ 

107 

108 convkey = (src_flavor, dst_flavor) 

109 if convkey not in converter_map: 

110 raise FlavorError("conversion from flavor ``%s`` to flavor ``%s`` " 

111 "is unsupported or unavailable in this system" 

112 % (src_flavor, dst_flavor)) 

113 

114 convfunc = converter_map[convkey] 

115 return convfunc(array) 

116 

117 

118def flavor_to_flavor(array, src_flavor, dst_flavor): 

119 """Get a version of the given `array` in a different flavor. 

120 

121 The input `array` must be of the given `src_flavor`, and the 

122 returned array will be of the indicated `dst_flavor` (see below 

123 for an exception to this). Both flavors may be the same, but it 

124 is not guaranteed that the returned array will be the same object 

125 as the input one in this case. 

126 

127 If the conversion is not supported, a `FlavorWarning` is issued 

128 and the input `array` is returned as is. 

129 

130 """ 

131 

132 try: 

133 return array_of_flavor2(array, src_flavor, dst_flavor) 

134 except FlavorError as fe: 

135 warnings.warn("%s; returning an object of the ``%s`` flavor instead" 

136 % (fe.args[0], src_flavor), FlavorWarning) 

137 return array 

138 

139 

140def internal_to_flavor(array, dst_flavor): 

141 """Get a version of the given `array` in a different `dst_flavor`. 

142 

143 The input `array` must be of the internal flavor, and the returned 

144 array will be of the given `dst_flavor`. See `flavor_to_flavor()` 

145 for more information. 

146 

147 """ 

148 

149 return flavor_to_flavor(array, internal_flavor, dst_flavor) 

150 

151 

152def array_as_internal(array, src_flavor): 

153 """Get a version of the given `array` in the internal flavor. 

154 

155 The input `array` must be of the given `src_flavor`, and the 

156 returned array will be of the internal flavor. 

157 

158 If the conversion is not supported, a ``FlavorError`` is raised. 

159 

160 """ 

161 

162 return array_of_flavor2(array, src_flavor, internal_flavor) 

163 

164 

165def flavor_of(array): 

166 """Identify the flavor of a given `array`. 

167 

168 If the `array` can not be matched with any flavor, a ``TypeError`` 

169 is raised. 

170 

171 """ 

172 

173 for flavor in all_flavors: 

174 if identifier_map[flavor](array): 

175 return flavor 

176 type_name = type(array).__name__ 

177 supported_descs = "; ".join(description_map[fl] for fl in all_flavors) 

178 raise TypeError( 

179 "objects of type ``%s`` are not supported in this context, sorry; " 

180 "supported objects are: %s" % (type_name, supported_descs)) 

181 

182 

183def array_of_flavor(array, dst_flavor): 

184 """Get a version of the given `array` in a different `dst_flavor`. 

185 

186 The flavor of the input `array` is guessed, and the returned array 

187 will be of the given `dst_flavor`. 

188 

189 If the conversion is not supported, a ``FlavorError`` is raised. 

190 

191 """ 

192 

193 return array_of_flavor2(array, flavor_of(array), dst_flavor) 

194 

195 

196def restrict_flavors(keep=('python',)): 

197 """Disable all flavors except those in keep. 

198 

199 Providing an empty keep sequence implies disabling all flavors (but the 

200 internal one). If the sequence is not specified, only optional flavors are 

201 disabled. 

202 

203 .. important:: Once you disable a flavor, it can not be enabled again. 

204 

205 """ 

206 

207 remove = set(all_flavors) - set(keep) - {internal_flavor} 

208 for flavor in remove: 

209 _disable_flavor(flavor) 

210 

211 

212# Flavor registration 

213# 

214# The order in which flavors appear in `all_flavors` determines the 

215# order in which they will be tested for by `flavor_of()`, so place 

216# most frequent flavors first. 

217all_flavors.append('numpy') # this is the internal flavor 

218 

219all_flavors.append('python') # this is always supported 

220 

221 

222def _register_aliases(): 

223 """Register aliases of *available* flavors.""" 

224 

225 for flavor in all_flavors: 

226 aliases = eval('_%s_aliases' % flavor) 

227 for alias in aliases: 

228 alias_map[alias] = flavor 

229 

230 

231def _register_descriptions(): 

232 """Register descriptions of *available* flavors.""" 

233 for flavor in all_flavors: 

234 description_map[flavor] = eval('_%s_desc' % flavor) 

235 

236 

237def _register_identifiers(): 

238 """Register identifier functions of *available* flavors.""" 

239 

240 for flavor in all_flavors: 

241 identifier_map[flavor] = eval('_is_%s' % flavor) 

242 

243 

244def _register_converters(): 

245 """Register converter functions between *available* flavors.""" 

246 

247 def identity(array): 

248 return array 

249 for src_flavor in all_flavors: 

250 for dst_flavor in all_flavors: 

251 # Converters with the same source and destination flavor 

252 # are used when available, since they may perform some 

253 # optimizations on the resulting array (e.g. making it 

254 # contiguous). Otherwise, an identity function is used. 

255 convfunc = None 

256 try: 

257 convfunc = eval(f'_conv_{src_flavor}_to_{dst_flavor}') 

258 except NameError: 

259 if src_flavor == dst_flavor: 

260 convfunc = identity 

261 if convfunc: 

262 converter_map[(src_flavor, dst_flavor)] = convfunc 

263 

264 

265def _register_all(): 

266 """Register all *available* flavors.""" 

267 

268 _register_aliases() 

269 _register_descriptions() 

270 _register_identifiers() 

271 _register_converters() 

272 

273 

274def _deregister_aliases(flavor): 

275 """Deregister aliases of a given `flavor` (no checks).""" 

276 

277 rm_aliases = [] 

278 for (an_alias, a_flavor) in alias_map.items(): 

279 if a_flavor == flavor: 

280 rm_aliases.append(an_alias) 

281 for an_alias in rm_aliases: 

282 del alias_map[an_alias] 

283 

284 

285def _deregister_description(flavor): 

286 """Deregister description of a given `flavor` (no checks).""" 

287 

288 del description_map[flavor] 

289 

290 

291def _deregister_identifier(flavor): 

292 """Deregister identifier function of a given `flavor` (no checks).""" 

293 

294 del identifier_map[flavor] 

295 

296 

297def _deregister_converters(flavor): 

298 """Deregister converter functions of a given `flavor` (no checks).""" 

299 

300 rm_flavor_pairs = [] 

301 for flavor_pair in converter_map: 

302 if flavor in flavor_pair: 

303 rm_flavor_pairs.append(flavor_pair) 

304 for flavor_pair in rm_flavor_pairs: 

305 del converter_map[flavor_pair] 

306 

307 

308def _disable_flavor(flavor): 

309 """Completely disable the given `flavor` (no checks).""" 

310 

311 _deregister_aliases(flavor) 

312 _deregister_description(flavor) 

313 _deregister_identifier(flavor) 

314 _deregister_converters(flavor) 

315 all_flavors.remove(flavor) 

316 

317 

318# Implementation of flavors 

319_python_aliases = [ 

320 'List', 'Tuple', 

321 'Int', 'Float', 'String', 

322 'VLString', 'Object', 

323] 

324_python_desc = ("homogeneous list or tuple, " 

325 "integer, float, complex or bytes") 

326 

327 

328def _is_python(array): 

329 return isinstance(array, (tuple, list, int, float, complex, bytes)) 

330 

331 

332_numpy_aliases = [] 

333_numpy_desc = "NumPy array, record or scalar" 

334 

335 

336if np.lib.NumpyVersion(np.__version__) >= np.lib.NumpyVersion('1.19.0'): 

337 def toarray(array, *args, **kwargs): 

338 with warnings.catch_warnings(): 

339 warnings.simplefilter('error') 

340 try: 

341 array = np.array(array, *args, **kwargs) 

342 except np.VisibleDeprecationWarning: 

343 raise ValueError( 

344 'cannot guess the desired dtype from the input') 

345 

346 return array 

347else: 

348 toarray = np.array 

349 

350 

351def _is_numpy(array): 

352 return isinstance(array, (np.ndarray, np.generic)) 

353 

354 

355def _numpy_contiguous(convfunc): 

356 """Decorate `convfunc` to return a *contiguous* NumPy array. 

357 

358 Note: When arrays are 0-strided, the copy is avoided. This allows 

359 to use `array` to still carry info about the dtype and shape. 

360 """ 

361 

362 def conv_to_numpy(array): 

363 nparr = convfunc(array) 

364 if (hasattr(nparr, 'flags') and 

365 not nparr.flags.contiguous and 

366 sum(nparr.strides) != 0): 

367 nparr = nparr.copy() # copying the array makes it contiguous 

368 return nparr 

369 conv_to_numpy.__name__ = convfunc.__name__ 

370 conv_to_numpy.__doc__ = convfunc.__doc__ 

371 return conv_to_numpy 

372 

373 

374@_numpy_contiguous 

375def _conv_numpy_to_numpy(array): 

376 # Passes contiguous arrays through and converts scalars into 

377 # scalar arrays. 

378 nparr = np.asarray(array) 

379 if nparr.dtype.kind == 'U': 

380 # from Python 3 loads of common strings are disguised as Unicode 

381 try: 

382 # try to convert to basic 'S' type 

383 return nparr.astype('S') 

384 except UnicodeEncodeError: 

385 pass 

386 # pass on true Unicode arrays downstream in case it can be 

387 # handled in the future 

388 return nparr 

389 

390 

391@_numpy_contiguous 

392def _conv_python_to_numpy(array): 

393 nparr = toarray(array) 

394 if nparr.dtype.kind == 'U': 

395 # from Python 3 loads of common strings are disguised as Unicode 

396 try: 

397 # try to convert to basic 'S' type 

398 return nparr.astype('S') 

399 except UnicodeEncodeError: 

400 pass 

401 # pass on true Unicode arrays downstream in case it can be 

402 # handled in the future 

403 return nparr 

404 

405 

406def _conv_numpy_to_python(array): 

407 if array.shape != (): 

408 # Lists are the default for returning multidimensional objects 

409 array = array.tolist() 

410 else: 

411 # 0-dim or scalar case 

412 array = array.item() 

413 return array 

414 

415 

416# Now register everything related with *available* flavors. 

417_register_all() 

418 

419 

420def _test(): 

421 """Run ``doctest`` on this module.""" 

422 

423 import doctest 

424 doctest.testmod() 

425 

426 

427if __name__ == '__main__': 

428 _test()