""" Infocalypse Freenet hg repo update graph. Copyright (C) 2009 Darrell Karbott This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks """ # REDFLAG: Document how dealing with missing indices works... # REDFLAG: Document how pruning works. # REDFLAG: Remove unused crap from this file # REDFLAG: push MAX_PATH_LEN into graph class -> max_cannonical_len import copy import mercurial import os import random from binascii import hexlify from mercurial import commands # Index for an empty repo. FIRST_INDEX = -1 NULL_REV = '0000000000000000000000000000000000000000' PENDING_INSERT = 'pending' PENDING_INSERT1 = 'pending1' MAX_PATH_LEN = 4 INSERT_NORMAL = 1 # Don't transform inserted data. INSERT_PADDED = 2 # Add one trailing byte. INSERT_SALTED_METADATA = 3 # Salt Freenet splitfile metadata. # The size of Freenet data blocks. FREENET_BLOCK_LEN = 32 * 1024 # Hmmm... arbitrary. MAX_REDUNDANT_LENGTH = 128 * 1024 # HACK: Approximate multi-level splitfile boundry MAX_METADATA_HACK_LEN = 7 * 1024 * 1024 ############################################################ # Mercurial helper functions. def hex_version(repo, version = 'tip', offset=0): """ Returns the 40 digit hex changeset id for an changeset in repo. """ #print "hex_version -- ", version ctx = repo.changectx(version) assert not ctx is None if offset != 0: ctx = repo.changectx(ctx.rev() + offset) assert not ctx is None return hexlify(ctx.node()) def has_version(repo, version): """ Returns True if repo already contains the changeset version, False otherwise. """ try: # Is there a faster way? repo.changectx(version) except mercurial.repo.RepoError: return False except mercurial.revlog.LookupError: return False return True def pull_bundle(repo, ui_, bundle_file): """ Pull an hg bundle file. bundle_file must be an absolute path. """ # REDFLAG: djk20090319, is this still an issue? # IMPORTANT: # You must be in the repository root directory in order to pull # from the bundle. This is not obvious from the Hg doc. # # See: http://marc.info/?l=mercurial&m=118362491521186&w=2 # # MUST use --cwd # MUST use an absolute path for the bundle field prev_cwd = os.getcwd() os.chdir(repo.root) try: commands.pull(ui_, repo, bundle_file, rev=[], force=None, update=None) finally: os.chdir(prev_cwd) ############################################################ def cmp_age_weight(path_a, path_b): """ Comparison function used to sort paths in ascending order of 'canonicalness'. """ # Only works for equivalent paths! assert path_a[0][0] == path_b[0][0] assert path_b[-1][1] == path_b[-1][1] # NO! path step tuples contain a third entry which keeps this # from working. # if path_a == path_b: # return 0 index = 0 while index < len(path_a) and index < len(path_b): if path_a[index][1] == path_b[index][1]: if path_a[index][2] == path_b[index][2]: index += 1 continue # If the edges are the same age prefer the one # the the lower (i.e. older) CHK ordinal. return path_b[index][2] - path_a[index][2] return path_a[index][1] - path_b[index][1] #print "CMP == ", path_a, path_b return 0 def block_cost(length): """ Return the number of Freenet blocks required to store data of length, length. """ blocks = length/FREENET_BLOCK_LEN if (length % FREENET_BLOCK_LEN) != 0: blocks += 1 return blocks ############################################################ # Doesn't dump FIRST_INDEX entry. def graph_to_string(graph): """ Returns a human readable representation of the graph. """ lines = [] # Indices indices = graph.index_table.keys() indices.sort() for index in indices: if index == FIRST_INDEX: continue entry = graph.index_table[index] #print entry lines.append("I:%i:%s:%s" % (index, entry[0], entry[1])) # Edges index_pairs = graph.edge_table.keys() for index_pair in index_pairs: edge_info = graph.edge_table[index_pair] as_str = ':'.join(edge_info[1:]) if as_str != '': as_str = ':' + as_str lines.append("E:%i:%i:%i%s" % (index_pair[0], index_pair[1], edge_info[0], as_str)) return '\n'.join(lines) + '\n' def parse_graph(text): """ Returns a graph parsed from text. text must be in the format used by graph_to_string(). Lines starting with '#' are ignored. """ graph = UpdateGraph() lines = text.split('\n') for line in lines: fields = line.split(':') if fields[0] == 'I': if len(fields) != 4: raise ValueError("Exception parsing index values.") index = int(fields[1]) if index in graph.index_table: print "OVERWRITING INDEX: " , index if len(tuple(fields[2:])) != 2: raise ValueError("Error parsing index value: %i" % index) graph.index_table[index] = tuple(fields[2:]) elif fields[0] == 'E': #print fields if len(fields) < 5: raise ValueError("Exception parsing edge values.") index_pair = (int(fields[1]), int(fields[2])) length = int(fields[3]) chk_list = [] for chk in fields[4:]: chk_list.append(chk) graph.edge_table[index_pair] = tuple([length, ] + chk_list) #else: # print "SKIPPED LINE:" # print line indices = graph.index_table.keys() if len(indices) == 0: raise ValueError("No indices?") indices.sort() graph.latest_index = indices[-1] graph.rep_invariant() return graph ############################################################ class UpdateGraphException(Exception): """ Base class for UpdateGraph exceptions. """ def __init__(self, msg): Exception.__init__(self, msg) class UpToDate(UpdateGraphException): """ Exception thrown to indicate that an update failed because the graph already contains the specified local changes. """ def __init__(self, msg): UpdateGraphException.__init__(self, msg) class UpdateGraph: """ A digraph representing an Infocalypse Freenet hg repository. """ def __init__(self): # Vertices in the update digraph. # index_ordinal -> (start_rev, end_rev) self.index_table = {FIRST_INDEX:(NULL_REV, NULL_REV)} # These are edges in the update digraph. # There can be multiple redundant edges. # # This is what is actually stored in Freenet. # Edges contain changesets for the indices from # start_index + 1 to end_index, but not for start_index. # (start_index, end_index) -> (length, chk@, chk@, ...) self.edge_table = {} # Bound path search length. self.max_search_path = 10 self.latest_index = -1 def clone(self): """ Return a deep copy of the graph. """ return copy.deepcopy(self) # REDFLAG: correct # Contains the end_index changesets but not the start index changesets. def add_edge(self, index_pair, length_chk_pair): """ Add a new edge to the graph. """ assert len(index_pair) == 2 assert len(length_chk_pair) == 2 assert index_pair[0] <= index_pair[1] edge_info = self.edge_table.get(index_pair) if edge_info is None: edge_info = tuple(length_chk_pair) else: if length_chk_pair[0] != edge_info[0]: raise ValueError("Redundant edge doesn't have same length.") edge_info = list(edge_info) edge_info.append(length_chk_pair[1]) edge_info = tuple(edge_info) self.edge_table[index_pair] = edge_info return (index_pair[0], index_pair[1], len(self.edge_table[index_pair]) - 2) def subgraph(self, containing_paths): """ Return a subgraph which contains the vertices and edges in containing_paths. """ self.rep_invariant() graph = UpdateGraph() max_index = -1 for path in containing_paths: for step in path: pair = step[:2] # REDFLAG: copies ALL redundant paths graph.edge_table[pair] = self.edge_table[pair][:] for index in pair: if index not in graph.index_table: graph.index_table[index] = self.index_table[index][:] max_index = max(max_index, index) graph.latest_index = max_index graph.rep_invariant() return graph ############################################################ # Helper functions used when inserting / requesting # the edge CHKs. ############################################################ def has_chk(self, edge_triple): """ Return True if the graph has a CHK for the edge, false otherwise. """ chk = self.edge_table.get(edge_triple[:2])[1:][edge_triple[2]] return chk.startswith('CHK@') # Hmmm... False for pending??? def get_chk(self, edge_triple): """ Return the CHK for an edge. """ return self.edge_table[edge_triple[:2]][1:][edge_triple[2]] def get_length(self, edge_triple): """ Return the length of the hg bundle file for an edge. """ return self.edge_table.get(edge_triple[:2])[0] def is_redundant(self, edge_triple): """ Return True if there is more than one CHK for the edge, False otherwise. """ return len(self.edge_table[edge_triple[:2]]) > 2 # REDFLAG: fix signature to take an edge triplet? # Hmmm... too much paranoia. just assert? def set_chk(self, index_pair, ordinal, length, chk): """ Set the CHK for an edge. """ edge_info = self.edge_table.get(index_pair) if edge_info is None: raise UpdateGraphException("No such edge: %s" % str(index_pair)) edge_list = list(edge_info) if len(edge_list) < ordinal + 2: raise UpdateGraphException("No such chk ordinal: [%s]:%i" % (str(index_pair), ordinal)) if edge_list[0] != length: raise UpdateGraphException("Length mismatch: [%s]:%i" % (str(index_pair), ordinal)) if not edge_list[ordinal + 1].startswith(PENDING_INSERT): print "set_chk -- replacing a non pending chk (%i, %i, %i)?" % \ (index_pair[0], index_pair[1], ordinal) if edge_list[ordinal + 1] == chk: print "Values are same." else: print "Values are different:" print "old:", edge_list[ordinal + 1] print "new:", chk edge_list[ordinal + 1] = chk self.edge_table[index_pair] = tuple(edge_list) def insert_type_(self, edge_triple): """ Return the kind of insert required to insert the CHK for the edge. INSERT_NORMAL -> No modification to the bundle file. INSERT_PADDED -> Add one trailing pad byte. INSERT_SALTED_METADATA -> Copy and salt the Freenet split file metadata for the normal insert. """ edge_info = self.edge_table[edge_triple[:2]] #print "insert_type -- ", edge_triple, entry if edge_info[edge_triple[2] + 1] == PENDING_INSERT: return INSERT_NORMAL if edge_info[edge_triple[2] + 1] != PENDING_INSERT1: raise ValueError("CHK already set?") if edge_info[0] <= FREENET_BLOCK_LEN: return INSERT_PADDED return INSERT_SALTED_METADATA def insert_type(self, edge_triple): """ Return the kind of insert required to insert the CHK for the edge. INSERT_NORMAL -> No modification to the bundle file. INSERT_PADDED -> Add one trailing pad byte. INSERT_SALTED_METADATA -> Copy and salt the Freenet split file metadata for the normal insert. """ if edge_triple[2] == 0: return INSERT_NORMAL assert edge_triple[2] == 1 length = self.edge_table[edge_triple[:2]][0] # REDFLAG: DCI. MUST DEAL WITH ==32k case if length <= FREENET_BLOCK_LEN: # Made redundant path by padding. return INSERT_PADDED if length <= MAX_METADATA_HACK_LEN: return INSERT_SALTED_METADATA print "insert_type called for edge that's too big to salt???" print edge_triple assert False def insert_length(self, step): """ Returns the actual length of the data inserted into Freenet for the edge. """ length = self.edge_table.get(step[:2])[0] if step[2] == 0: # No hacks on primary insert. return length if length < FREENET_BLOCK_LEN: # Made redundant path by padding. return length + 1 # Salted the metadata. Data length unaffected. return length ############################################################ # REDFLAG: really no need for ui? if so, remove arg # Index and edges to insert # Returns index triples with new edges that need to be inserted. def update(self, repo, dummy, version, cache): """ Update the graph to include versions up to version in repo. This may add multiple edges for redundancy. Returns the new edges. The client code is responsible for setting their CHKs!""" if self.latest_index > FIRST_INDEX: if (repo.changectx(version).rev() <= repo.changectx(self.index_table[self.latest_index][1]).rev()): raise UpToDate("Version: %s is already in the repo." % hex_version(repo, version)[:12]) new_edges = [] # Add changes to graph. prev_changes = self.index_table[self.latest_index] parent_rev = prev_changes[1] # REDFLAG: Think. What are the implicit assumptions here? first_rev = hex_version(repo, prev_changes[1], 1) latest_rev = hex_version(repo, version) index = self._add_changes(parent_rev, first_rev, latest_rev) #print "ADDED INDEX: ", index #print self.index_table # Insert index w/ rollup if possible. first_bundle = cache.make_redundant_bundle(self, index) new_edges.append(self.add_edge(first_bundle[2], (first_bundle[0], PENDING_INSERT))) #print "ADDED EDGE: ", new_edges[-1] canonical_path = self.canonical_path(index, MAX_PATH_LEN + 1) assert len(canonical_path) <= MAX_PATH_LEN + 1 bundle = None if len(canonical_path) > MAX_PATH_LEN: print "CANNONICAL LEN: ", len(canonical_path) short_cut = self._compress_canonical_path(index, MAX_PATH_LEN + 1) bundle = cache.make_bundle(self, short_cut) new_edges.append(self.add_edge(bundle[2], (bundle[0], PENDING_INSERT))) canonical_path = self.canonical_path(index, MAX_PATH_LEN + 1) assert len(canonical_path) <= MAX_PATH_LEN if bundle == None: if (first_bundle[0] <= FREENET_BLOCK_LEN and first_bundle[2][0] < index - 1): # This gives us redundancy at the cost of one 32K block. bundle = cache.make_bundle(self, (first_bundle[2][0] + 1, index)) new_edges.append(self.add_edge(bundle[2], (bundle[0], PENDING_INSERT))) elif first_bundle[0] <= MAX_METADATA_HACK_LEN: # Request insert of a redundant copy of exactly the same # bundle. bundle = first_bundle[:] new_edges.append(self.add_edge(bundle[2], (bundle[0], PENDING_INSERT1))) else: print "update -- Bundle too big to add redundant CHK: %i" \ % first_bundle[0] new_edges = new_edges + self._add_canonical_path_redundancy() return new_edges def get_top_key_edges(self): """ Returns the ordered list of edges that should be included in the top key. """ self.rep_invariant() edges = [] #print "LATEST_INDEX: ", self.latest_index paths = self.enumerate_update_paths(self.latest_index, self.latest_index, 1) #print paths paths.sort(self._cmp_block_cost) #dump_paths(self, paths, "Paths sorted by block cost") if len(paths) > 0: # Path with the most changes in the least blocks. edges.append(paths[0][0]) del paths[0] if len(paths) > 0: # REDFLAG: == 32k case for padding crosses block boundry... if (block_cost(self.path_cost([edges[0], ])) == block_cost(self.path_cost(paths[0]))): # One more at the same cost if there is one. edges.append(paths[0][0]) del paths[0] # The canonical path path = list(self.canonical_path(self.latest_index, MAX_PATH_LEN)) path.reverse() # most recent first. for step in path: if not step in edges: edges.append(step) # 2 possibly redundant immediate update keys, and MAX_PATH_LEN # canonical path keys. Actually one of the canonical keys # should already be in the immediate updates. assert len(edges) < 4 + MAX_PATH_LEN return edges def enumerate_update_paths(self, containing_start, to_end, max_len, partial_path=()): """ INTERNAL: Returns a list of paths from the start index to the end index. """ if max_len <= 0: return [] ret = [] candidates = self.contain(containing_start) #print "CANDIDATES: ", candidates for candidate in candidates: if candidate[1] >= to_end: ret.append(partial_path + (candidate,)) else: ret += self.enumerate_update_paths(candidate[1] + 1, to_end, max_len - 1, partial_path + (candidate,)) return ret # REQUIRES: Using the same index mappings! def copy_path(self, from_graph, path): """ Copy a path from one graph to another. """ copied = False for step in path: pair = step[:2] if not pair in self.edge_table: copied = True self.edge_table[pair] = ( from_graph.edge_table[pair][:]) # Deep copy for index in pair: if index not in self.index_table: self.index_table[index] = ( from_graph.index_table[index][:]) # Deep copy return copied def canonical_path(self, to_index, max_search_len): """ Returns shortest preferred path from no updates to latest_index. This is what you would use to bootstrap from hg rev -1. """ return self.canonical_paths(to_index, max_search_len)[-1] def canonical_paths(self, to_index, max_search_len): """ Returns a list of paths from no updates to to_index in ascending order of 'canonicalness'. i.e. so you can pop() the candidates off the list. """ paths = self.enumerate_update_paths(0, to_index, max_search_len) if len(paths) == 0: raise UpdateGraphException("No such path: %s" % str((0, to_index))) paths.sort(cmp_age_weight) return paths def path_cost(self, path, blocks=False): """ The sum of the lengths of the hg bundles required to update using the path. """ value = 0 for step in path: if blocks: value += block_cost(self.edge_table[step[:2]][0]) else: value += self.edge_table[step[:2]][0] return value # Returns ((start_index, end_index, chk_list_ordinal), ...) def contain(self, contains_index): """ Returns a list of edge triples which contain contains_index. """ ret = [] for pair in self.edge_table: if pair[0] >= contains_index: continue if pair[1] < contains_index: continue for index in range(0, len(self.edge_table[pair]) - 1): ret.append(pair + (index,)) return ret def cmp_recency(self, path_a, path_b): """ INTERNAL: A comparison function for sorting single edge paths by recency. """ # Only for steps assert len(path_a) == 1 assert len(path_b) == 1 # Only steps in the paths. step_a = path_a[0] step_b = path_b[0] if step_a[1] == step_b[1]: if step_a[2] == step_b[2]: # Ascending Length. TRICKY: because of padding hacks. return (self.insert_length(step_a) - self.insert_length(step_b)) # Ascending redundancy. i.e. "most canonical" first return step_a[2] - step_b[2] # descending initial update. i.e. Most recent first. return step_b[1] - step_a[1] # REDFLAG: add_index instead ??? # REDFLAG: rethink parent_rev def _add_changes(self, parent_rev, first_rev, last_rev): """ Add changes to the graph. """ assert parent_rev == self.index_table[self.latest_index][1] self.latest_index += 1 self.index_table[self.latest_index] = (first_rev, last_rev) return self.latest_index def _cmp_block_cost(self, path_a, path_b): """ INTERNAL: A comparison function for sorting single edge paths in order of ascending order of block count. """ assert len(path_a) == 1 assert len(path_b) == 1 cost_a = self.insert_length(path_a[0]) cost_b = self.insert_length(path_b[0]) # Actually block cost - 1, but that's ok. block_cost_a = cost_a / FREENET_BLOCK_LEN block_cost_b = cost_b / FREENET_BLOCK_LEN if block_cost_a == block_cost_b: mod_a = cost_a % FREENET_BLOCK_LEN mod_b = cost_b % FREENET_BLOCK_LEN if mod_a == mod_b: # Ascending order of redundancy ordinal. return int(path_a[0][2] - path_b[0][2]) # Descending order of length (for same block size) return int(mod_b - mod_a) # Ascending order of length in blocks return int(block_cost_a - block_cost_b) # REDFLAG: Can the edge already exists? # Only makes sense for latest index. get rid of latest_index argument? # enforce constraint that sum of costs head must be less # than the cost for the prev step. power law??? # REQURIES: len(canonical_path(latest_index)) > 1 def _compress_canonical_path(self, to_index, max_search_len=10): """ Return an index tuple for a new shortcut path that would reduces the canonical path length by at least one, favoring accumulation of hg bundle size at the start of the path. """ shortest_known = self.canonical_path(to_index, max_search_len) #print "SHORTEST_KNOWN: ", shortest_known assert len(shortest_known) > 1 if len(shortest_known) == 2: # We only have one move. return (shortest_known[0][0], shortest_known[-1][1]) # REDFLAG: Shouldn't this be using block cost? for index in range(1, len(shortest_known)): prev_cost = self.path_cost((shortest_known[index - 1],)) if self.path_cost(shortest_known[index:]) > prev_cost: return (shortest_known[index - 1][0], shortest_known[-1][1]) return (shortest_known[-2][0], shortest_known[-1][1]) def _add_canonical_path_redundancy(self): """ Adds redundant edges for steps on the canonical path. Returns the new edges. """ ret = [] path = self.canonical_path(self.latest_index, MAX_PATH_LEN) for index, step in enumerate(path): if index == MAX_PATH_LEN - 1: # Don't try to add redundancy to the last (latest) step break entries = self.edge_table[step[:2]] if len(entries) > 2: # Already redundant continue assert step[2] == 0 assert entries[1] != PENDING_INSERT1 if entries[0] <= FREENET_BLOCK_LEN: #print "_add_canonical_path_redundancy -- too small: ", \ # str(step) continue if entries[0] > MAX_METADATA_HACK_LEN: #print "_add_canonical_path_redundancy -- too big: ", str(step) continue edge = self.add_edge(step[:2], (entries[0], PENDING_INSERT1)) #print "_add_canonical_path_redundancy -- added edge: ", str(edge) ret.append(edge) return ret def rep_invariant(self): """ Debugging function to check invariants. """ max_index = -1 for index in self.index_table.keys(): max_index = max(index, max_index) assert self.latest_index == max_index for edge in self.edge_table.keys(): assert edge[0] in self.index_table assert edge[1] in self.index_table # REDFLAG: O(n), has_index(). def latest_index(graph, repo): """ Returns the index of the latest hg version in the graph that exists in repo. """ graph.rep_invariant() for index in range(graph.latest_index, FIRST_INDEX - 1, -1): if not index in graph.index_table: continue if has_version(repo, graph.index_table[index][1]): return index return FIRST_INDEX # REDFLAG: fix this so that it always includes pending edges. def minimal_update_graph(graph, max_size=32*1024, formatter_func=graph_to_string): """ Returns a subgraph that can be formatted to <= max_size bytes with formatter_func. """ index = graph.latest_index assert index > FIRST_INDEX # All the edges that would be included in the top key. # This includes the canonical bootstrap path and the # two cheapest updates from the previous index. paths = [[edge, ] for edge in graph.get_top_key_edges()] minimal = graph.subgraph(paths) if len(formatter_func(minimal)) > max_size: raise UpdateGraphException("Too big with only required paths.") # REDFLAG: read up on clone() prev_minimal = minimal.clone() # Then add all other full bootstrap paths. canonical_paths = graph.canonical_paths(index, MAX_PATH_LEN) while len(canonical_paths): if minimal.copy_path(graph, canonical_paths.pop()): size = len(formatter_func(minimal)) #print "minimal_update_graph -- size: %i " % size if size > max_size: return prev_minimal else: prev_minimal = minimal.clone() if index == 0: return prev_minimal # Favors older edges # Then add bootstrap paths back to previous indices for upper_index in range(index - 1, FIRST_INDEX, - 1): canonical_paths = graph.canonical_paths(upper_index, MAX_PATH_LEN) while len(canonical_paths): if minimal.copy_path(graph, canonical_paths.pop()): size = len(formatter_func(minimal)) #print "minimal_update_graph -- size(1): %i" % size if size > max_size: return prev_minimal else: prev_minimal = minimal.clone() return prev_minimal def chk_to_edge_triple_map(graph): """ Returns a CHK -> edge triple map. """ ret = {} for edge in graph.edge_table: #print "EDGE: ", edge chks = graph.edge_table[edge][1:] #print "ENTRIES: ", entries for index, chk in enumerate(chks): assert ret.get(chk) is None ret[chk] = (edge[0], edge[1], index) return ret def break_edges(graph, kill_probability, skip_chks): """ Testing function breaks edges by replacing the CHKs with a known bad one. """ bad_chk = ('CHK@badroutingkeyB55JblbGup0yNSpoDJgVPnL8E5WXoc,' +'KZ6azHOwEm4ga6dLy6UfbdSzVhJEz3OvIbSS4o5BMKU,AAIC--8') for edge in graph.edge_table: edge_info = graph.edge_table[edge[:2]] length = edge_info[0] chks = edge_info[1:] for index in range(0, len(chks)): if graph.get_chk((edge[0], edge[1], index)) in skip_chks: # Hack to skip pending requests. print "break_edges -- skipped: ", (edge[0], edge[1], index) continue if random.random() < kill_probability: graph.set_chk(edge, index, length, bad_chk) def pretty_index(index): """ Format an index value for output. """ if index == FIRST_INDEX: return "." else: return str(index) def dump_path(graph, path): """ Debugging function to print a path. """ if len(path) == 0: print "EMPTY PATH!" return print "(%s)-->[%s] cost=%0.2f" % (pretty_index(path[0][0]), pretty_index(path[-1][1]), graph.path_cost(path, True)) for step in path: cost = graph.get_length(step) print " (%s) -- (%0.2f, %i) --> [%s]" % (pretty_index(step[0]), cost, step[2], pretty_index(step[1])) def dump_paths(graph, paths, msg): """ Debugging function to dump a list of paths. """ print "--- %s ---" % msg for path in paths: dump_path(graph, path) print "---" def print_list(msg, values): """ INTERNAL: Helper function. """ if msg: print msg for value in values: print " ", value if len(values) == 0: print