Coverage for /usr/lib/python3/dist-packages/cycler.py: 40%

177 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2Cycler 

3====== 

4 

5Cycling through combinations of values, producing dictionaries. 

6 

7You can add cyclers:: 

8 

9 from cycler import cycler 

10 cc = (cycler(color=list('rgb')) + 

11 cycler(linestyle=['-', '--', '-.'])) 

12 for d in cc: 

13 print(d) 

14 

15Results in:: 

16 

17 {'color': 'r', 'linestyle': '-'} 

18 {'color': 'g', 'linestyle': '--'} 

19 {'color': 'b', 'linestyle': '-.'} 

20 

21 

22You can multiply cyclers:: 

23 

24 from cycler import cycler 

25 cc = (cycler(color=list('rgb')) * 

26 cycler(linestyle=['-', '--', '-.'])) 

27 for d in cc: 

28 print(d) 

29 

30Results in:: 

31 

32 {'color': 'r', 'linestyle': '-'} 

33 {'color': 'r', 'linestyle': '--'} 

34 {'color': 'r', 'linestyle': '-.'} 

35 {'color': 'g', 'linestyle': '-'} 

36 {'color': 'g', 'linestyle': '--'} 

37 {'color': 'g', 'linestyle': '-.'} 

38 {'color': 'b', 'linestyle': '-'} 

39 {'color': 'b', 'linestyle': '--'} 

40 {'color': 'b', 'linestyle': '-.'} 

41""" 

42 

43 

44import copy 

45from functools import reduce 

46from itertools import product, cycle 

47from operator import mul, add 

48 

49__version__ = '0.10.0' 

50 

51 

52def _process_keys(left, right): 

53 """ 

54 Helper function to compose cycler keys. 

55 

56 Parameters 

57 ---------- 

58 left, right : iterable of dictionaries or None 

59 The cyclers to be composed. 

60 

61 Returns 

62 ------- 

63 keys : set 

64 The keys in the composition of the two cyclers. 

65 """ 

66 l_peek = next(iter(left)) if left is not None else {} 

67 r_peek = next(iter(right)) if right is not None else {} 

68 l_key = set(l_peek.keys()) 

69 r_key = set(r_peek.keys()) 

70 if l_key & r_key: 

71 raise ValueError("Can not compose overlapping cycles") 

72 return l_key | r_key 

73 

74 

75def concat(left, right): 

76 r""" 

77 Concatenate `Cycler`\s, as if chained using `itertools.chain`. 

78 

79 The keys must match exactly. 

80 

81 Examples 

82 -------- 

83 >>> num = cycler('a', range(3)) 

84 >>> let = cycler('a', 'abc') 

85 >>> num.concat(let) 

86 cycler('a', [0, 1, 2, 'a', 'b', 'c']) 

87 

88 Returns 

89 ------- 

90 `Cycler` 

91 The concatenated cycler. 

92 """ 

93 if left.keys != right.keys: 

94 raise ValueError("Keys do not match:\n" 

95 "\tIntersection: {both!r}\n" 

96 "\tDisjoint: {just_one!r}".format( 

97 both=left.keys & right.keys, 

98 just_one=left.keys ^ right.keys)) 

99 _l = left.by_key() 

100 _r = right.by_key() 

101 return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) 

102 

103 

104class Cycler: 

105 """ 

106 Composable cycles. 

107 

108 This class has compositions methods: 

109 

110 ``+`` 

111 for 'inner' products (zip) 

112 

113 ``+=`` 

114 in-place ``+`` 

115 

116 ``*`` 

117 for outer products (`itertools.product`) and integer multiplication 

118 

119 ``*=`` 

120 in-place ``*`` 

121 

122 and supports basic slicing via ``[]``. 

123 

124 Parameters 

125 ---------- 

126 left, right : Cycler or None 

127 The 'left' and 'right' cyclers. 

128 op : func or None 

129 Function which composes the 'left' and 'right' cyclers. 

130 """ 

131 

132 def __call__(self): 

133 return cycle(self) 

134 

135 def __init__(self, left, right=None, op=None): 

136 """ 

137 Semi-private init. 

138 

139 Do not use this directly, use `cycler` function instead. 

140 """ 

141 if isinstance(left, Cycler): 

142 self._left = Cycler(left._left, left._right, left._op) 

143 elif left is not None: 

144 # Need to copy the dictionary or else that will be a residual 

145 # mutable that could lead to strange errors 

146 self._left = [copy.copy(v) for v in left] 

147 else: 

148 self._left = None 

149 

150 if isinstance(right, Cycler): 

151 self._right = Cycler(right._left, right._right, right._op) 

152 elif right is not None: 

153 # Need to copy the dictionary or else that will be a residual 

154 # mutable that could lead to strange errors 

155 self._right = [copy.copy(v) for v in right] 

156 else: 

157 self._right = None 

158 

159 self._keys = _process_keys(self._left, self._right) 

160 self._op = op 

161 

162 def __contains__(self, k): 

163 return k in self._keys 

164 

165 @property 

166 def keys(self): 

167 """The keys this Cycler knows about.""" 

168 return set(self._keys) 

169 

170 def change_key(self, old, new): 

171 """ 

172 Change a key in this cycler to a new name. 

173 Modification is performed in-place. 

174 

175 Does nothing if the old key is the same as the new key. 

176 Raises a ValueError if the new key is already a key. 

177 Raises a KeyError if the old key isn't a key. 

178 """ 

179 if old == new: 

180 return 

181 if new in self._keys: 

182 raise ValueError( 

183 "Can't replace {old} with {new}, {new} is already a key" 

184 .format(old=old, new=new) 

185 ) 

186 if old not in self._keys: 

187 raise KeyError("Can't replace {old} with {new}, {old} is not a key" 

188 .format(old=old, new=new)) 

189 

190 self._keys.remove(old) 

191 self._keys.add(new) 

192 

193 if self._right is not None and old in self._right.keys: 

194 self._right.change_key(old, new) 

195 

196 # self._left should always be non-None 

197 # if self._keys is non-empty. 

198 elif isinstance(self._left, Cycler): 

199 self._left.change_key(old, new) 

200 else: 

201 # It should be completely safe at this point to 

202 # assume that the old key can be found in each 

203 # iteration. 

204 self._left = [{new: entry[old]} for entry in self._left] 

205 

206 @classmethod 

207 def _from_iter(cls, label, itr): 

208 """ 

209 Class method to create 'base' Cycler objects 

210 that do not have a 'right' or 'op' and for which 

211 the 'left' object is not another Cycler. 

212 

213 Parameters 

214 ---------- 

215 label : str 

216 The property key. 

217 

218 itr : iterable 

219 Finite length iterable of the property values. 

220 

221 Returns 

222 ------- 

223 `Cycler` 

224 New 'base' cycler. 

225 """ 

226 ret = cls(None) 

227 ret._left = list({label: v} for v in itr) 

228 ret._keys = {label} 

229 return ret 

230 

231 def __getitem__(self, key): 

232 # TODO : maybe add numpy style fancy slicing 

233 if isinstance(key, slice): 

234 trans = self.by_key() 

235 return reduce(add, (_cycler(k, v[key]) for k, v in trans.items())) 

236 else: 

237 raise ValueError("Can only use slices with Cycler.__getitem__") 

238 

239 def __iter__(self): 

240 if self._right is None: 

241 for left in self._left: 

242 yield dict(left) 

243 else: 

244 for a, b in self._op(self._left, self._right): 

245 out = {} 

246 out.update(a) 

247 out.update(b) 

248 yield out 

249 

250 def __add__(self, other): 

251 """ 

252 Pair-wise combine two equal length cyclers (zip). 

253 

254 Parameters 

255 ---------- 

256 other : Cycler 

257 """ 

258 if len(self) != len(other): 

259 raise ValueError("Can only add equal length cycles, " 

260 f"not {len(self)} and {len(other)}") 

261 return Cycler(self, other, zip) 

262 

263 def __mul__(self, other): 

264 """ 

265 Outer product of two cyclers (`itertools.product`) or integer 

266 multiplication. 

267 

268 Parameters 

269 ---------- 

270 other : Cycler or int 

271 """ 

272 if isinstance(other, Cycler): 

273 return Cycler(self, other, product) 

274 elif isinstance(other, int): 

275 trans = self.by_key() 

276 return reduce(add, (_cycler(k, v*other) for k, v in trans.items())) 

277 else: 

278 return NotImplemented 

279 

280 def __rmul__(self, other): 

281 return self * other 

282 

283 def __len__(self): 

284 op_dict = {zip: min, product: mul} 

285 if self._right is None: 

286 return len(self._left) 

287 l_len = len(self._left) 

288 r_len = len(self._right) 

289 return op_dict[self._op](l_len, r_len) 

290 

291 def __iadd__(self, other): 

292 """ 

293 In-place pair-wise combine two equal length cyclers (zip). 

294 

295 Parameters 

296 ---------- 

297 other : Cycler 

298 """ 

299 if not isinstance(other, Cycler): 

300 raise TypeError("Cannot += with a non-Cycler object") 

301 # True shallow copy of self is fine since this is in-place 

302 old_self = copy.copy(self) 

303 self._keys = _process_keys(old_self, other) 

304 self._left = old_self 

305 self._op = zip 

306 self._right = Cycler(other._left, other._right, other._op) 

307 return self 

308 

309 def __imul__(self, other): 

310 """ 

311 In-place outer product of two cyclers (`itertools.product`). 

312 

313 Parameters 

314 ---------- 

315 other : Cycler 

316 """ 

317 if not isinstance(other, Cycler): 

318 raise TypeError("Cannot *= with a non-Cycler object") 

319 # True shallow copy of self is fine since this is in-place 

320 old_self = copy.copy(self) 

321 self._keys = _process_keys(old_self, other) 

322 self._left = old_self 

323 self._op = product 

324 self._right = Cycler(other._left, other._right, other._op) 

325 return self 

326 

327 def __eq__(self, other): 

328 if len(self) != len(other): 

329 return False 

330 if self.keys ^ other.keys: 

331 return False 

332 return all(a == b for a, b in zip(self, other)) 

333 

334 def __ne__(self, other): 

335 return not (self == other) 

336 

337 __hash__ = None 

338 

339 def __repr__(self): 

340 op_map = {zip: '+', product: '*'} 

341 if self._right is None: 

342 lab = self.keys.pop() 

343 itr = list(v[lab] for v in self) 

344 return f"cycler({lab!r}, {itr!r})" 

345 else: 

346 op = op_map.get(self._op, '?') 

347 msg = "({left!r} {op} {right!r})" 

348 return msg.format(left=self._left, op=op, right=self._right) 

349 

350 def _repr_html_(self): 

351 # an table showing the value of each key through a full cycle 

352 output = "<table>" 

353 sorted_keys = sorted(self.keys, key=repr) 

354 for key in sorted_keys: 

355 output += f"<th>{key!r}</th>" 

356 for d in iter(self): 

357 output += "<tr>" 

358 for k in sorted_keys: 

359 output += f"<td>{d[k]!r}</td>" 

360 output += "</tr>" 

361 output += "</table>" 

362 return output 

363 

364 def by_key(self): 

365 """ 

366 Values by key. 

367 

368 This returns the transposed values of the cycler. Iterating 

369 over a `Cycler` yields dicts with a single value for each key, 

370 this method returns a `dict` of `list` which are the values 

371 for the given key. 

372 

373 The returned value can be used to create an equivalent `Cycler` 

374 using only `+`. 

375 

376 Returns 

377 ------- 

378 transpose : dict 

379 dict of lists of the values for each key. 

380 """ 

381 

382 # TODO : sort out if this is a bottle neck, if there is a better way 

383 # and if we care. 

384 

385 keys = self.keys 

386 out = {k: list() for k in keys} 

387 

388 for d in self: 

389 for k in keys: 

390 out[k].append(d[k]) 

391 return out 

392 

393 # for back compatibility 

394 _transpose = by_key 

395 

396 def simplify(self): 

397 """ 

398 Simplify the cycler into a sum (but no products) of cyclers. 

399 

400 Returns 

401 ------- 

402 simple : Cycler 

403 """ 

404 # TODO: sort out if it is worth the effort to make sure this is 

405 # balanced. Currently it is is 

406 # (((a + b) + c) + d) vs 

407 # ((a + b) + (c + d)) 

408 # I would believe that there is some performance implications 

409 trans = self.by_key() 

410 return reduce(add, (_cycler(k, v) for k, v in trans.items())) 

411 

412 concat = concat 

413 

414 

415def cycler(*args, **kwargs): 

416 """ 

417 Create a new `Cycler` object from a single positional argument, 

418 a pair of positional arguments, or the combination of keyword arguments. 

419 

420 cycler(arg) 

421 cycler(label1=itr1[, label2=iter2[, ...]]) 

422 cycler(label, itr) 

423 

424 Form 1 simply copies a given `Cycler` object. 

425 

426 Form 2 composes a `Cycler` as an inner product of the 

427 pairs of keyword arguments. In other words, all of the 

428 iterables are cycled simultaneously, as if through zip(). 

429 

430 Form 3 creates a `Cycler` from a label and an iterable. 

431 This is useful for when the label cannot be a keyword argument 

432 (e.g., an integer or a name that has a space in it). 

433 

434 Parameters 

435 ---------- 

436 arg : Cycler 

437 Copy constructor for Cycler (does a shallow copy of iterables). 

438 label : name 

439 The property key. In the 2-arg form of the function, 

440 the label can be any hashable object. In the keyword argument 

441 form of the function, it must be a valid python identifier. 

442 itr : iterable 

443 Finite length iterable of the property values. 

444 Can be a single-property `Cycler` that would 

445 be like a key change, but as a shallow copy. 

446 

447 Returns 

448 ------- 

449 cycler : Cycler 

450 New `Cycler` for the given property 

451 

452 """ 

453 if args and kwargs: 

454 raise TypeError("cyl() can only accept positional OR keyword " 

455 "arguments -- not both.") 

456 

457 if len(args) == 1: 

458 if not isinstance(args[0], Cycler): 

459 raise TypeError("If only one positional argument given, it must " 

460 "be a Cycler instance.") 

461 return Cycler(args[0]) 

462 elif len(args) == 2: 

463 return _cycler(*args) 

464 elif len(args) > 2: 

465 raise TypeError("Only a single Cycler can be accepted as the lone " 

466 "positional argument. Use keyword arguments instead.") 

467 

468 if kwargs: 

469 return reduce(add, (_cycler(k, v) for k, v in kwargs.items())) 

470 

471 raise TypeError("Must have at least a positional OR keyword arguments") 

472 

473 

474def _cycler(label, itr): 

475 """ 

476 Create a new `Cycler` object from a property name and iterable of values. 

477 

478 Parameters 

479 ---------- 

480 label : hashable 

481 The property key. 

482 itr : iterable 

483 Finite length iterable of the property values. 

484 

485 Returns 

486 ------- 

487 cycler : Cycler 

488 New `Cycler` for the given property 

489 """ 

490 if isinstance(itr, Cycler): 

491 keys = itr.keys 

492 if len(keys) != 1: 

493 msg = "Can not create Cycler from a multi-property Cycler" 

494 raise ValueError(msg) 

495 

496 lab = keys.pop() 

497 # Doesn't need to be a new list because 

498 # _from_iter() will be creating that new list anyway. 

499 itr = (v[lab] for v in itr) 

500 

501 return Cycler._from_iter(label, itr)