Debug Tools
dynamic_iteration.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 #
5 # Licensing
6 #
7 # Dynamic Iteration Data structures
8 # Copyright (C) 2018 Evandro Coan <https://github.com/evandrocoan>
9 #
10 # Redistributions of source code must retain the above
11 # copyright notice, this list of conditions and the
12 # following disclaimer.
13 #
14 # Redistributions in binary form must reproduce the above
15 # copyright notice, this list of conditions and the following
16 # disclaimer in the documentation and/or other materials
17 # provided with the distribution.
18 #
19 # Neither the name Evandro Coan nor the names of any
20 # contributors may be used to endorse or promote products
21 # derived from this software without specific prior written
22 # permission.
23 #
24 # This program is free software; you can redistribute it and/or modify it
25 # under the terms of the GNU General Public License as published by the
26 # Free Software Foundation; either version 3 of the License, or ( at
27 # your option ) any later version.
28 #
29 # This program is distributed in the hope that it will be useful, but
30 # WITHOUT ANY WARRANTY; without even the implied warranty of
31 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
32 # General Public License for more details.
33 #
34 # You should have received a copy of the GNU General Public License
35 # along with this program. If not, see <http://www.gnu.org/licenses/>.
36 #
37 
38 import copy
39 
40 from debug_tools import getLogger
41 
42 from debug_tools.utilities import emquote_string
43 from debug_tools.utilities import get_representation
44 
45 # level 4 - Abstract Syntax Tree Parsing
46 log = getLogger( 127-4, __name__ )
47 
48 
49 class DynamicIterable(object):
50  """
51  Dynamically creates creates a unique iterable which can be used one time.
52 
53  Why have an __iter__ method in Python?
54  https://stackoverflow.com/questions/36681312/why-have-an-iter-method-in-python
55  """
56 
57  def __init__(self, data_container, iterable_access, empty_slots, end_index=None, filled_slots=set()):
58  """
59  Receives a iterable an initialize the object to start an iteration.
60 
61  @param `iterable_access` a function pointer to function which returns the next element given its index
62  @param `end_index` it must be a list with one integer element
63  @param `empty_slots` it must be a set with indexes of free place to put new elements
64  """
65 
66  self.current_index = -1
67 
68 
69  self.empty_slots = empty_slots
70 
71 
72  self.filled_slots = filled_slots
73 
74 
75  self.data_container = data_container
76 
77 
78  if end_index is None:
79  self.iterable_access = iterable_access
80 
81  else:
82  self.iterable_access = lambda index: iterable_access( index ) if index < end_index[0] else self.stop_iteration( index )
83 
84  def __len__(self):
85  """
86  Return the total length of this container.
87  """
88  return len( self.data_container )
89 
90  def __iter__(self):
91  """
92  Resets the current index and return a copy if itself for iteration.
93  """
94  self.current_index = -1
95  return self
96 
97  def __next__(self):
98  """
99  Called by Python automatically when iterating over this set and python wants to know the
100  next element to iterate. Raises `StopIteration` when the iteration has been finished.
101 
102  How to make a custom object iterable?
103  https://stackoverflow.com/questions/21665485/how-to-make-a-custom-object-iterable
104  https://stackoverflow.com/questions/4019971/how-to-implement-iter-self-for-a-container-object-python
105  """
106  empty_slots = self.empty_slots
107  current_index = self.current_index + 1
108  filled_slots = self.filled_slots
109 
110  while current_index in empty_slots or current_index in filled_slots:
111  current_index += 1
112 
113  try:
114  self.current_index = current_index
115  # log( 1, "current_index: %s", current_index )
116  return self.iterable_access( current_index )
117 
118  except IndexError:
119  raise StopIteration
120 
121  def stop_iteration(self, index):
122  """
123  Raise the exception `StopIteration` to stop the current iteration.
124  """
125  raise StopIteration
126 
127  def __str__(self):
128  """
129  Return a nice string representation of this iterable.
130  """
131  representation = []
132 
133  for item in self:
134  representation.append( str( item ) )
135 
136  return "{%s}" % ( ", ".join( representation ) )
137 
138 
139 class DynamicIterationDict(object):
140  """
141  A `dict()` like object which allows to dynamically add and remove items while iterating over
142  its elements as in `for element in dynamic_set`
143 
144  https://wiki.python.org/moin/TimeComplexity
145  https://stackoverflow.com/questions/4014621/a-python-class-that-acts-like-dict
146  """
147 
148  def __init__(self, initial=None, is_set=False, emquote=False, index=False):
149  """
150  Fully initializes and create a new dictionary.
151 
152  @param `initial` is a dictionary used to initialize it with new values.
153  """
154 
155 
156  self._is_set = is_set
157 
158 
159  self.keys_list = list()
160 
161 
162  self.values_list = list()
163 
164 
165  self._emquote_keys = emquote
166 
167 
168  self._add_index = index
169 
170 
171  self.empty_slots = set()
172 
173 
174  self.filled_slots = set()
175 
176 
177  self.items_dictionary = dict()
178 
179 
181 
182 
184 
185 
187 
188  if initial:
189 
190  if isinstance( initial, dict ):
191 
192  for key, value in initial.items():
193  self[key] = value
194 
195  else:
196  self.fromkeys( initial )
197 
198  def __repr__(self):
199  """
200  Return a full representation of all public attributes of this object set state for
201  debugging purposes.
202  """
203  return get_representation( self )
204 
205  def __str__(self):
206  """
207  Return a nice string representation of this collection.
208  """
209  keys_list = self.keys_list
210  empty_slots = self.empty_slots
211  representation = []
212 
213  if self._emquote_keys:
214  get_key = lambda: emquote_string( key )
215 
216  else:
217  get_key = lambda: key
218 
219  if self._add_index:
220  get_index = lambda: "{}, {}".format( get_key(), index )
221 
222  else:
223  get_index = get_key
224 
225  if self._is_set:
226  return str( self.keys() )
227 
228  else:
229  values_list = self.values_list
230 
231  for index in range( 0, len( keys_list ) ):
232 
233  if index not in empty_slots:
234  key = keys_list[index]
235  representation.append( "%s: %s" % ( get_index(), values_list[index] ) )
236 
237  return "{%s}" % ", ".join( representation )
238 
239  def __contains__(self, key):
240  """
241  Determines whether this dictionary contains or not a given `key`.
242  """
243  return key in self.items_dictionary
244 
245  def __len__(self):
246  """
247  Return the total length of this set.
248  """
249  return len( self.items_dictionary )
250 
251  def __call__(self, how_many_times=-1):
252  """
253  Return a iterable for the keys elements of this collection when calling its object as a
254  function call.
255 
256  `how_many_times` is for how many iterations it should keep ignoring the new items.
257  """
258  return self.get_iterator( self.get_key, how_many_times )
259 
260  def __iter__(self):
261  """
262  Called by Python automatically when iterating over this set and python wants to start
263  the iteration process.
264 
265  Why have an __iter__ method in Python?
266  https://stackoverflow.com/questions/36681312/why-have-an-iter-method-in-python
267  """
268  return self.get_iterator( self.get_key )
269 
270  def __setitem__(self, key, value):
271  """
272  Given a `key` and `item` add it to this dictionary as a non yet iterated item, replacing
273  the existent value. It a iteration is running, and the item was already iterated, then
274  it will be updated on the `niterated_items` dict.
275  """
276  items_dictionary = self.items_dictionary
277 
278  if key in items_dictionary:
279  item_index = items_dictionary[key]
280  self.values_list[item_index] = value
281 
282  else:
283  empty_slots = self.empty_slots
284  values_list = self.values_list
285 
286  if empty_slots and self.new_items_skip_count > 0:
287  free_slot = empty_slots.pop()
288  self.filled_slots.add( free_slot )
289 
290  values_list[free_slot] = value
291  self.keys_list[free_slot] = key
292 
293  else:
294  free_slot = len( values_list )
295  values_list.append( value )
296  self.keys_list.append( key )
297 
298  self.items_dictionary[key] = free_slot
299 
300  def __getitem__(self, key):
301  """
302  Given a `key` returns its existent value.
303  """
304  # log( 1, "index: %s, key: %s", self.items_dictionary[key], key )
305  return self.values_list[self.items_dictionary[key]]
306 
307  def __delitem__(self, key):
308  """
309  Given a `key` deletes if from this dict.
310  """
311  # log( 1, "key: %s, self: %s", key, self )
312  items_dictionary = self.items_dictionary
313  item_index = items_dictionary[key]
314 
315  self.empty_slots.add( item_index )
316  del items_dictionary[key]
317 
319  """
320  Fix the the outdated indexes on the internal lists after their dictionary removal,
321  keeping the items original ordering O(n).
322  """
323  new_index = -1
324  clean_keys = []
325  clean_values = []
326 
327  values_list = self.values_list
328  items_dictionary = self.items_dictionary
329 
330  for key, value_index in items_dictionary.items():
331  new_index += 1
332  items_dictionary[key] = new_index
333  clean_keys.append( key )
334  clean_values.append( values_list[value_index] )
335 
336  self.empty_slots.clear()
337  self.keys_list = clean_keys
338  self.values_list = clean_values
339 
341  """
342  Fix the the outdated indexes on the internal lists after their dictionary removal.
343  keeping the items original ordering O(k), where `k` is length of `self.empty_slots`.
344  """
345  keys_list = self.keys_list
346  values_list = self.values_list
347  empty_slots = self.empty_slots
348  last_slot = -1
349 
350  while empty_slots:
351  empty_slot = empty_slots.pop()
352  list_length = len( keys_list )
353  key = keys_list.pop()
354 
355  keys_list[empty_slot] = key
356  values_list[empty_slot] = values_list.pop()
357  self.items_dictionary[key] = empty_slot
358 
359  if last_slot >= list_length:
360  last_slot = last_slot
361 
362  if last_slot > -1:
363  del keys_list[last_slot:]
364  del values_list[last_slot:]
365 
366  def keys(self, how_many_times=-1):
367  """
368  Return a DynamicIterable over the keys stored in this collection.
369 
370  `how_many_times` is for how many iterations it should keep ignoring the new items.
371  """
372  return self.get_iterator( self.get_key, how_many_times )
373 
374  def values(self, how_many_times=-1):
375  """
376  Return a DynamicIterable over the values stored in this collection.
377 
378  `how_many_times` is for how many iterations it should keep ignoring the new items.
379  """
380  return self.get_iterator( self.get_value, how_many_times )
381 
382  def items(self, how_many_times=-1):
383  """
384  Return a DynamicIterable over the (key, value) stored in this collection.
385 
386  `how_many_times` is for how many iterations it should keep ignoring the new items.
387  """
388  return self.get_iterator( self.get_key_value, how_many_times )
389 
390  def get_key(self, index):
391  """
392  Given a `index` returns its corresponding key.
393  """
394  return self.keys_list[index]
395 
396  def get_value(self, index):
397  """
398  Given a `index` returns its corresponding value.
399  """
400  return self.values_list[index]
401 
402  def get_key_value(self, index):
403  """
404  Given a `index` returns its corresponding (key, value) pair.
405  """
406  return ( self.keys_list[index], self.values_list[index] )
407 
408  def get_iterator(self, target_generation, how_many_times=-1):
409  """
410  Get fully configured iterable given the getter `target_generation(index)` function.
411 
412  `how_many_times` is for how many iterations it should keep ignoring the new items.
413  """
414  self.new_items_skip_count -= 1
415 
416  # When the current sequence is exhausted, search for an old one
417  if not self.new_items_skip_count > 0:
418 
419  # Unwind the stack until a valid item is found
420  if self.new_items_skip_stack:
422 
423  self.not_iterate_over_new_items( how_many_times )
424 
425  if self.new_items_skip_count > 0:
426  return DynamicIterable( self.items_dictionary, target_generation, self.empty_slots, self.maximum_iterable_index, self.filled_slots )
427 
428  return DynamicIterable( self.items_dictionary, target_generation, self.empty_slots )
429 
430  def not_iterate_over_new_items(self, how_many_times=1):
431  """
432  If called before start iterating over this dictionary, it will not iterate over the
433  new keys added until the current iteration is over.
434 
435  `how_many_times` is for how many iterations it should keep ignoring the new items.
436  """
437 
438  if how_many_times > 0:
439 
440  if self.new_items_skip_count > 0:
442 
443  self.filled_slots = set()
444  self.new_items_skip_count = how_many_times
445  self.maximum_iterable_index = [0]
446  self.maximum_iterable_index[0] = len( self.keys_list )
447 
448  def fromkeys(self, iterable):
449  """
450  Initialize a dictionary with None default value, acting like a indexed set.
451  """
452 
453  for key in iterable:
454  self[key] = None
455 
456  def append(self, element):
457  """
458  Add a new element to the end of the list.
459  """
460  self[element] = None
461 
462  def remove(self, element):
463  """
464  Remove new `element` anywhere in the container.
465  """
466  del self[element]
467 
468  def add(self, element):
469  """
470  Add a new element to the end of the list.
471  """
472  self[element] = None
473 
474  def discard(self, element):
475  """
476  Remove new `element` anywhere in the container.
477  """
478  try:
479  del self[element]
480  except KeyError:
481  pass
482 
483  def copy(self):
484  """
485  Return a deep copy of this collection.
486  """
487  return copy.deepcopy( self )
488 
489  def clear(self):
490  """
491  Remove all items from this dict.
492  """
493  self.empty_slots.clear()
494  self.filled_slots.clear()
495 
496  self.keys_list.clear()
497  self.values_list.clear()
498  self.items_dictionary.clear()
499 
new_items_skip_count
Whether the iteration process is allowed to use new items added on the current iteration.
empty_slots
List the empty free spots for old items, used globally before the iteration starts.
_emquote_keys
Whether the keys of this dictionary should be emquoted when creating a string representation.
empty_slots
List the empty free spots for old items, which should be skipped when iterating over.
iterable_access
The iterable access method to get the next item given a index.
filled_slots
List the empty free spots for old items, used globally after the iteration starts with new_items_skip...
_add_index
Whether the string representation of this collection will have item index attached to it...
data_container
The underlying container used to calculate this iterable length.
items_dictionary
A dictionary with the indexes of the elements in this collection.
maximum_iterable_index
Holds the global maximum iterable index used when new_items_skip_count is set.
_is_set
Whether this collection elements are going to be used as a list, instead of dictionary.
current_index
The current index used when iterating over this collection items.
values_list
The list with the elements of this collection.
def __init__(self, initial=None, is_set=False, emquote=False, index=False)
new_items_skip_stack
A stack of new_items_skip_count which is required when the current iterator is called recursively...
def get_iterator(self, target_generation, how_many_times=-1)
def __init__(self, data_container, iterable_access, empty_slots, end_index=None, filled_slots=set())
filled_slots
List the empty free spots for new items, which should be skipped when iterating over.
keys_list
The list with the keys of this collection elements.