1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

""" 

Cycler 

====== 

 

Cycling through combinations of values, producing dictionaries. 

 

You can add cyclers:: 

 

from cycler import cycler 

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

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

for d in cc: 

print(d) 

 

Results in:: 

 

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

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

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

 

 

You can multiply cyclers:: 

 

from cycler import cycler 

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

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

for d in cc: 

print(d) 

 

Results in:: 

 

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

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

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

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

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

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

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

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

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

""" 

 

from __future__ import (absolute_import, division, print_function, 

unicode_literals) 

 

import six 

from itertools import product, cycle 

from six.moves import zip, reduce 

from operator import mul, add 

import copy 

 

__version__ = '0.10.0' 

 

 

def _process_keys(left, right): 

""" 

Helper function to compose cycler keys 

 

Parameters 

---------- 

left, right : iterable of dictionaries or None 

The cyclers to be composed 

Returns 

------- 

keys : set 

The keys in the composition of the two cyclers 

""" 

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

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

l_key = set(l_peek.keys()) 

r_key = set(r_peek.keys()) 

if l_key & r_key: 

raise ValueError("Can not compose overlapping cycles") 

return l_key | r_key 

 

 

class Cycler(object): 

""" 

Composable cycles 

 

This class has compositions methods: 

 

``+`` 

for 'inner' products (zip) 

 

``+=`` 

in-place ``+`` 

 

``*`` 

for outer products (itertools.product) and integer multiplication 

 

``*=`` 

in-place ``*`` 

 

and supports basic slicing via ``[]`` 

 

Parameters 

---------- 

left : Cycler or None 

The 'left' cycler 

 

right : Cycler or None 

The 'right' cycler 

 

op : func or None 

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

 

""" 

def __call__(self): 

return cycle(self) 

 

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

"""Semi-private init 

 

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

""" 

if isinstance(left, Cycler): 

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

elif left is not None: 

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

# mutable that could lead to strange errors 

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

else: 

self._left = None 

 

if isinstance(right, Cycler): 

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

elif right is not None: 

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

# mutable that could lead to strange errors 

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

else: 

self._right = None 

 

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

self._op = op 

 

@property 

def keys(self): 

""" 

The keys this Cycler knows about 

""" 

return set(self._keys) 

 

def change_key(self, old, new): 

""" 

Change a key in this cycler to a new name. 

Modification is performed in-place. 

 

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

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

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

 

""" 

if old == new: 

return 

if new in self._keys: 

raise ValueError("Can't replace %s with %s, %s is already a key" % 

(old, new, new)) 

if old not in self._keys: 

raise KeyError("Can't replace %s with %s, %s is not a key" % 

(old, new, old)) 

 

self._keys.remove(old) 

self._keys.add(new) 

 

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

self._right.change_key(old, new) 

 

# self._left should always be non-None 

# if self._keys is non-empty. 

elif isinstance(self._left, Cycler): 

self._left.change_key(old, new) 

else: 

# It should be completely safe at this point to 

# assume that the old key can be found in each 

# iteration. 

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

 

def _compose(self): 

""" 

Compose the 'left' and 'right' components of this cycle 

with the proper operation (zip or product as of now) 

""" 

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

out = dict() 

out.update(a) 

out.update(b) 

yield out 

 

@classmethod 

def _from_iter(cls, label, itr): 

""" 

Class method to create 'base' Cycler objects 

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

the 'left' object is not another Cycler. 

 

Parameters 

---------- 

label : str 

The property key. 

 

itr : iterable 

Finite length iterable of the property values. 

 

Returns 

------- 

cycler : Cycler 

New 'base' `Cycler` 

""" 

ret = cls(None) 

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

ret._keys = set([label]) 

return ret 

 

def __getitem__(self, key): 

# TODO : maybe add numpy style fancy slicing 

if isinstance(key, slice): 

trans = self.by_key() 

return reduce(add, (_cycler(k, v[key]) 

for k, v in six.iteritems(trans))) 

else: 

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

 

def __iter__(self): 

if self._right is None: 

return iter(dict(l) for l in self._left) 

 

return self._compose() 

 

def __add__(self, other): 

""" 

Pair-wise combine two equal length cycles (zip) 

 

Parameters 

---------- 

other : Cycler 

The second Cycler 

""" 

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

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

"not {0} and {1}".format(len(self), len(other))) 

return Cycler(self, other, zip) 

 

def __mul__(self, other): 

""" 

Outer product of two cycles (`itertools.product`) or integer 

multiplication. 

 

Parameters 

---------- 

other : Cycler or int 

The second Cycler or integer 

""" 

if isinstance(other, Cycler): 

return Cycler(self, other, product) 

elif isinstance(other, int): 

trans = self.by_key() 

return reduce(add, (_cycler(k, v*other) 

for k, v in six.iteritems(trans))) 

else: 

return NotImplemented 

 

def __rmul__(self, other): 

return self * other 

 

def __len__(self): 

op_dict = {zip: min, product: mul} 

if self._right is None: 

return len(self._left) 

l_len = len(self._left) 

r_len = len(self._right) 

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

 

def __iadd__(self, other): 

""" 

In-place pair-wise combine two equal length cycles (zip) 

 

Parameters 

---------- 

other : Cycler 

The second Cycler 

""" 

if not isinstance(other, Cycler): 

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

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

old_self = copy.copy(self) 

self._keys = _process_keys(old_self, other) 

self._left = old_self 

self._op = zip 

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

return self 

 

def __imul__(self, other): 

""" 

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

 

Parameters 

---------- 

other : Cycler 

The second Cycler 

""" 

if not isinstance(other, Cycler): 

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

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

old_self = copy.copy(self) 

self._keys = _process_keys(old_self, other) 

self._left = old_self 

self._op = product 

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

return self 

 

def __eq__(self, other): 

""" 

Check equality 

""" 

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

return False 

if self.keys ^ other.keys: 

return False 

 

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

 

def __repr__(self): 

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

if self._right is None: 

lab = self.keys.pop() 

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

return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) 

else: 

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

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

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

 

def _repr_html_(self): 

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

output = "<table>" 

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

for key in sorted_keys: 

output += "<th>{key!r}</th>".format(key=key) 

for d in iter(self): 

output += "<tr>" 

for k in sorted_keys: 

output += "<td>{val!r}</td>".format(val=d[k]) 

output += "</tr>" 

output += "</table>" 

return output 

 

def by_key(self): 

"""Values by key 

 

This returns the transposed values of the cycler. Iterating 

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

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

for the given key. 

 

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

using only `+`. 

 

Returns 

------- 

transpose : dict 

dict of lists of the values for each key. 

""" 

 

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

# and if we care. 

 

keys = self.keys 

# change this to dict comprehension when drop 2.6 

out = dict((k, list()) for k in keys) 

 

for d in self: 

for k in keys: 

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

return out 

 

# for back compatibility 

_transpose = by_key 

 

def simplify(self): 

"""Simplify the Cycler 

 

Returned as a composition using only sums (no multiplications) 

 

Returns 

------- 

simple : Cycler 

An equivalent cycler using only summation""" 

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

# balanced. Currently it is is 

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

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

# I would believe that there is some performance implications 

 

trans = self.by_key() 

return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans))) 

 

def concat(self, other): 

"""Concatenate this cycler and an other. 

 

The keys must match exactly. 

 

This returns a single Cycler which is equivalent to 

`itertools.chain(self, other)` 

 

Examples 

-------- 

 

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

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

>>> num.concat(let) 

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

 

Parameters 

---------- 

other : `Cycler` 

The `Cycler` to concatenate to this one. 

 

Returns 

------- 

ret : `Cycler` 

The concatenated `Cycler` 

""" 

return concat(self, other) 

 

 

def concat(left, right): 

"""Concatenate two cyclers. 

 

The keys must match exactly. 

 

This returns a single Cycler which is equivalent to 

`itertools.chain(left, right)` 

 

Examples 

-------- 

 

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

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

>>> num.concat(let) 

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

 

Parameters 

---------- 

left, right : `Cycler` 

The two `Cycler` instances to concatenate 

 

Returns 

------- 

ret : `Cycler` 

The concatenated `Cycler` 

""" 

if left.keys != right.keys: 

msg = '\n\t'.join(["Keys do not match:", 

"Intersection: {both!r}", 

"Disjoint: {just_one!r}"]).format( 

both=left.keys & right.keys, 

just_one=left.keys ^ right.keys) 

 

raise ValueError(msg) 

 

_l = left.by_key() 

_r = right.by_key() 

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

 

 

def cycler(*args, **kwargs): 

""" 

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

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

 

cycler(arg) 

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

cycler(label, itr) 

 

Form 1 simply copies a given `Cycler` object. 

 

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

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

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

 

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

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

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

 

Parameters 

---------- 

arg : Cycler 

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

 

label : name 

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

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

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

 

itr : iterable 

Finite length iterable of the property values. 

Can be a single-property `Cycler` that would 

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

 

Returns 

------- 

cycler : Cycler 

New `Cycler` for the given property 

 

""" 

if args and kwargs: 

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

"arguments -- not both.") 

 

if len(args) == 1: 

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

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

" be a Cycler instance.") 

return Cycler(args[0]) 

elif len(args) == 2: 

return _cycler(*args) 

elif len(args) > 2: 

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

"positional argument. Use keyword arguments instead.") 

 

if kwargs: 

return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs))) 

 

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

 

 

def _cycler(label, itr): 

""" 

Create a new `Cycler` object from a property name and 

iterable of values. 

 

Parameters 

---------- 

label : hashable 

The property key. 

 

itr : iterable 

Finite length iterable of the property values. 

 

Returns 

------- 

cycler : Cycler 

New `Cycler` for the given property 

""" 

if isinstance(itr, Cycler): 

keys = itr.keys 

if len(keys) != 1: 

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

raise ValueError(msg) 

 

lab = keys.pop() 

# Doesn't need to be a new list because 

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

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

 

return Cycler._from_iter(label, itr)