4141"""
4242
4343
44+ from __future__ import annotations
45+
46+ from collections .abc import Hashable , Iterable , Callable
4447import copy
4548from functools import reduce
4649from itertools import product , cycle
4750from operator import mul , add
51+ from typing import TypeVar , Generic , Generator , Any , overload
4852
4953__version__ = '0.12.0.dev0'
5054
55+ K = TypeVar ("K" , bound = Hashable )
56+ V = TypeVar ("V" )
5157
52- def _process_keys (left , right ) :
58+ def _process_keys (left : Cycler [ K , V ] | Iterable [ dict [ K , V ]] | None , right : Cycler [ K , V ] | Iterable [ dict [ K , V ]] | None ) -> set [ K ] :
5359 """
5460 Helper function to compose cycler keys.
5561
@@ -63,16 +69,16 @@ def _process_keys(left, right):
6369 keys : set
6470 The keys in the composition of the two cyclers.
6571 """
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 ())
72+ l_peek : dict [ K , V ] = next (iter (left )) if left is not None else {}
73+ r_peek : dict [ K , V ] = next (iter (right )) if right is not None else {}
74+ l_key : set [ K ] = set (l_peek .keys ())
75+ r_key : set [ K ] = set (r_peek .keys ())
7076 if l_key & r_key :
7177 raise ValueError ("Can not compose overlapping cycles" )
7278 return l_key | r_key
7379
7480
75- def concat (left , right ) :
81+ def concat (left : Cycler [ K , V ], right : Cycler [ K , V ]) -> Cycler [ K , V ] :
7682 r"""
7783 Concatenate `Cycler`\s, as if chained using `itertools.chain`.
7884
@@ -100,8 +106,7 @@ def concat(left, right):
100106 _r = right .by_key ()
101107 return reduce (add , (_cycler (k , _l [k ] + _r [k ]) for k in left .keys ))
102108
103-
104- class Cycler :
109+ class Cycler (Generic [K , V ]):
105110 """
106111 Composable cycles.
107112
@@ -132,14 +137,14 @@ class Cycler:
132137 def __call__ (self ):
133138 return cycle (self )
134139
135- def __init__ (self , left , right = None , op = None ):
140+ def __init__ (self , left : Cycler [ K , V ] | Iterable [ dict [ K , V ]] | None , right : Cycler [ K , V ] | Iterable [ dict [ K , V ]] | None = None , op : Any = None ):
136141 """
137142 Semi-private init.
138143
139144 Do not use this directly, use `cycler` function instead.
140145 """
141146 if isinstance (left , Cycler ):
142- self ._left = Cycler (left ._left , left ._right , left ._op )
147+ self ._left : Cycler [ K , V ] | list [ dict [ K , V ]] | None = Cycler (left ._left , left ._right , left ._op )
143148 elif left is not None :
144149 # Need to copy the dictionary or else that will be a residual
145150 # mutable that could lead to strange errors
@@ -148,26 +153,26 @@ def __init__(self, left, right=None, op=None):
148153 self ._left = None
149154
150155 if isinstance (right , Cycler ):
151- self ._right = Cycler (right ._left , right ._right , right ._op )
156+ self ._right : Cycler [ K , V ] | list [ dict [ K , V ]] | None = Cycler (right ._left , right ._right , right ._op )
152157 elif right is not None :
153158 # Need to copy the dictionary or else that will be a residual
154159 # mutable that could lead to strange errors
155160 self ._right = [copy .copy (v ) for v in right ]
156161 else :
157162 self ._right = None
158163
159- self ._keys = _process_keys (self ._left , self ._right )
160- self ._op = op
164+ self ._keys : set [ K ] = _process_keys (self ._left , self ._right )
165+ self ._op : Any = op
161166
162167 def __contains__ (self , k ):
163168 return k in self ._keys
164169
165170 @property
166- def keys (self ):
171+ def keys (self ) -> set [ K ] :
167172 """The keys this Cycler knows about."""
168173 return set (self ._keys )
169174
170- def change_key (self , old , new ) :
175+ def change_key (self , old : K , new : K ) -> None :
171176 """
172177 Change a key in this cycler to a new name.
173178 Modification is performed in-place.
@@ -190,11 +195,12 @@ def change_key(self, old, new):
190195 self ._keys .remove (old )
191196 self ._keys .add (new )
192197
193- if self ._right is not None and old in self ._right .keys :
198+ if self ._right is not None and isinstance ( self . _right , Cycler ) and old in self ._right .keys :
194199 self ._right .change_key (old , new )
195200
196201 # self._left should always be non-None
197202 # if self._keys is non-empty.
203+ elif self ._left is None : pass
198204 elif isinstance (self ._left , Cycler ):
199205 self ._left .change_key (old , new )
200206 else :
@@ -204,15 +210,15 @@ def change_key(self, old, new):
204210 self ._left = [{new : entry [old ]} for entry in self ._left ]
205211
206212 @classmethod
207- def _from_iter (cls , label , itr ) :
213+ def _from_iter (cls , label : K , itr : Iterable [ V ]) -> Cycler [ K , V ] :
208214 """
209215 Class method to create 'base' Cycler objects
210216 that do not have a 'right' or 'op' and for which
211217 the 'left' object is not another Cycler.
212218
213219 Parameters
214220 ----------
215- label : str
221+ label : hashable
216222 The property key.
217223
218224 itr : iterable
@@ -223,31 +229,34 @@ def _from_iter(cls, label, itr):
223229 `Cycler`
224230 New 'base' cycler.
225231 """
226- ret = cls (None )
232+ ret : Cycler [ K , V ] = cls (None )
227233 ret ._left = list ({label : v } for v in itr )
228234 ret ._keys = {label }
229235 return ret
230236
231- def __getitem__ (self , key ) :
237+ def __getitem__ (self , key : slice ) -> Cycler [ K , V ] :
232238 # TODO : maybe add numpy style fancy slicing
233239 if isinstance (key , slice ):
234240 trans = self .by_key ()
235241 return reduce (add , (_cycler (k , v [key ]) for k , v in trans .items ()))
236242 else :
237243 raise ValueError ("Can only use slices with Cycler.__getitem__" )
238244
239- def __iter__ (self ):
240- if self ._right is None :
241- for left in self ._left :
242- yield dict (left )
245+ def __iter__ (self ) -> Generator [dict [K , V ], None , None ]:
246+ if self ._right is None or self ._left is None :
247+ if self ._left is not None :
248+ for left in self ._left :
249+ yield dict (left )
243250 else :
251+ if self ._op is None :
252+ raise TypeError ("Operation cannot be None when both left and right are defined" )
244253 for a , b in self ._op (self ._left , self ._right ):
245254 out = {}
246255 out .update (a )
247256 out .update (b )
248257 yield out
249258
250- def __add__ (self , other ) :
259+ def __add__ (self , other : Cycler [ K , V ]) -> Cycler [ K , V ] :
251260 """
252261 Pair-wise combine two equal length cyclers (zip).
253262
@@ -260,7 +269,7 @@ def __add__(self, other):
260269 f"not { len (self )} and { len (other )} " )
261270 return Cycler (self , other , zip )
262271
263- def __mul__ (self , other ) :
272+ def __mul__ (self , other : Cycler [ K , V ] | int ) -> Cycler [ K , V ] :
264273 """
265274 Outer product of two cyclers (`itertools.product`) or integer
266275 multiplication.
@@ -277,18 +286,22 @@ def __mul__(self, other):
277286 else :
278287 return NotImplemented
279288
280- def __rmul__ (self , other ) :
289+ def __rmul__ (self , other : Cycler [ K , V ]) -> Cycler [ K , V ] :
281290 return self * other
282291
283- def __len__ (self ):
292+ def __len__ (self ) -> int :
284293 op_dict = {zip : min , product : mul }
294+ if self ._left is None :
295+ if self ._left is None :
296+ return 0
297+ return 0
285298 if self ._right is None :
286299 return len (self ._left )
287300 l_len = len (self ._left )
288301 r_len = len (self ._right )
289- return op_dict [self ._op ](l_len , r_len )
302+ return op_dict [self ._op ](l_len , r_len ) # type: ignore
290303
291- def __iadd__ (self , other ) :
304+ def __iadd__ (self , other : Cycler [ K , V ]) -> Cycler [ K , V ] :
292305 """
293306 In-place pair-wise combine two equal length cyclers (zip).
294307
@@ -306,7 +319,7 @@ def __iadd__(self, other):
306319 self ._right = Cycler (other ._left , other ._right , other ._op )
307320 return self
308321
309- def __imul__ (self , other ) :
322+ def __imul__ (self , other : Cycler [ K , V ] | int ) -> Cycler [ K , V ] :
310323 """
311324 In-place outer product of two cyclers (`itertools.product`).
312325
@@ -324,16 +337,18 @@ def __imul__(self, other):
324337 self ._right = Cycler (other ._left , other ._right , other ._op )
325338 return self
326339
327- def __eq__ (self , other ):
340+ def __eq__ (self , other : object ) -> bool :
341+ if not isinstance (other , Cycler ):
342+ return False
328343 if len (self ) != len (other ):
329344 return False
330345 if self .keys ^ other .keys :
331346 return False
332347 return all (a == b for a , b in zip (self , other ))
333348
334- __hash__ = None
349+ __hash__ = None # type: ignore
335350
336- def __repr__ (self ):
351+ def __repr__ (self ) -> str :
337352 op_map = {zip : '+' , product : '*' }
338353 if self ._right is None :
339354 lab = self .keys .pop ()
@@ -344,7 +359,7 @@ def __repr__(self):
344359 msg = "({left!r} {op} {right!r})"
345360 return msg .format (left = self ._left , op = op , right = self ._right )
346361
347- def _repr_html_ (self ):
362+ def _repr_html_ (self ) -> str :
348363 # an table showing the value of each key through a full cycle
349364 output = "<table>"
350365 sorted_keys = sorted (self .keys , key = repr )
@@ -358,7 +373,7 @@ def _repr_html_(self):
358373 output += "</table>"
359374 return output
360375
361- def by_key (self ):
376+ def by_key (self ) -> dict [ K , list [ V ]] :
362377 """
363378 Values by key.
364379
@@ -380,7 +395,7 @@ def by_key(self):
380395 # and if we care.
381396
382397 keys = self .keys
383- out = {k : list () for k in keys }
398+ out : dict [ K , list [ V ]] = {k : list () for k in keys }
384399
385400 for d in self :
386401 for k in keys :
@@ -390,7 +405,7 @@ def by_key(self):
390405 # for back compatibility
391406 _transpose = by_key
392407
393- def simplify (self ):
408+ def simplify (self ) -> Cycler [ K , V ] :
394409 """
395410 Simplify the cycler into a sum (but no products) of cyclers.
396411
@@ -408,7 +423,12 @@ def simplify(self):
408423
409424 concat = concat
410425
411-
426+ @overload
427+ def cycler (args : Cycler [K , V ]) -> Cycler [K , V ]: ...
428+ @overload
429+ def cycler (** kwargs : Iterable [V ]) -> Cycler [str , V ]: ...
430+ @overload
431+ def cycler (label : K , itr : Iterable [V ]) -> Cycler [K , V ]: ...
412432def cycler (* args , ** kwargs ):
413433 """
414434 Create a new `Cycler` object from a single positional argument,
@@ -468,7 +488,7 @@ def cycler(*args, **kwargs):
468488 raise TypeError ("Must have at least a positional OR keyword arguments" )
469489
470490
471- def _cycler (label , itr ) :
491+ def _cycler (label : K , itr : Iterable [ V ]) -> Cycler [ K , V ] :
472492 """
473493 Create a new `Cycler` object from a property name and iterable of values.
474494
0 commit comments