altgraph-0.15/0000755000076500000240000000000013207760162013647 5ustar ronaldstaff00000000000000altgraph-0.15/altgraph/0000755000076500000240000000000013207760162015451 5ustar ronaldstaff00000000000000altgraph-0.15/altgraph/__init__.py0000644000076500000240000001162613207760071017567 0ustar ronaldstaff00000000000000''' altgraph - a python graph library ================================= altgraph is a fork of `graphlib `_ tailored to use newer Python 2.3+ features, including additional support used by the py2app suite (modulegraph and macholib, specifically). altgraph is a python based graph (network) representation and manipulation package. It has started out as an extension to the `graph_lib module `_ written by Nathan Denny it has been significantly optimized and expanded. The :class:`altgraph.Graph.Graph` class is loosely modeled after the `LEDA `_ (Library of Efficient Datatypes) representation. The library includes methods for constructing graphs, BFS and DFS traversals, topological sort, finding connected components, shortest paths as well as a number graph statistics functions. The library can also visualize graphs via `graphviz `_. The package contains the following modules: - the :py:mod:`altgraph.Graph` module contains the :class:`~altgraph.Graph.Graph` class that stores the graph data - the :py:mod:`altgraph.GraphAlgo` module implements graph algorithms operating on graphs (:py:class:`~altgraph.Graph.Graph`} instances) - the :py:mod:`altgraph.GraphStat` module contains functions for computing statistical measures on graphs - the :py:mod:`altgraph.GraphUtil` module contains functions for generating, reading and saving graphs - the :py:mod:`altgraph.Dot` module contains functions for displaying graphs via `graphviz `_ - the :py:mod:`altgraph.ObjectGraph` module implements a graph of objects with a unique identifier Installation ------------ Download and unpack the archive then type:: python setup.py install This will install the library in the default location. For instructions on how to customize the install procedure read the output of:: python setup.py --help install To verify that the code works run the test suite:: python setup.py test Example usage ------------- Lets assume that we want to analyze the graph below (links to the full picture) GRAPH_IMG. Our script then might look the following way:: from altgraph import Graph, GraphAlgo, Dot # these are the edges edges = [ (1,2), (2,4), (1,3), (2,4), (3,4), (4,5), (6,5), (6,14), (14,15), (6, 15), (5,7), (7, 8), (7,13), (12,8), (8,13), (11,12), (11,9), (13,11), (9,13), (13,10) ] # creates the graph graph = Graph.Graph() for head, tail in edges: graph.add_edge(head, tail) # do a forward bfs from 1 at most to 20 print(graph.forw_bfs(1)) This will print the nodes in some breadth first order:: [1, 2, 3, 4, 5, 7, 8, 13, 11, 10, 12, 9] If we wanted to get the hop-distance from node 1 to node 8 we coud write:: print(graph.get_hops(1, 8)) This will print the following:: [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] Node 1 is at 0 hops since it is the starting node, nodes 2,3 are 1 hop away ... node 8 is 5 hops away. To find the shortest distance between two nodes you can use:: print(GraphAlgo.shortest_path(graph, 1, 12)) It will print the nodes on one (if there are more) the shortest paths:: [1, 2, 4, 5, 7, 13, 11, 12] To display the graph we can use the GraphViz backend:: dot = Dot.Dot(graph) # display the graph on the monitor dot.display() # save it in an image file dot.save_img(file_name='graph', file_type='gif') .. @author: U{Istvan Albert} @license: MIT License Copyright (c) 2004 Istvan Albert unless otherwise noted. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @requires: Python 2.3 or higher @newfield contributor: Contributors: @contributor: U{Reka Albert } ''' import pkg_resources __version__ = pkg_resources.require('altgraph')[0].version class GraphError(ValueError): pass altgraph-0.15/altgraph/Dot.py0000644000076500000240000002321313207760071016551 0ustar ronaldstaff00000000000000''' altgraph.Dot - Interface to the dot language ============================================ The :py:mod:`~altgraph.Dot` module provides a simple interface to the file format used in the `graphviz `_ program. The module is intended to offload the most tedious part of the process (the **dot** file generation) while transparently exposing most of its features. To display the graphs or to generate image files the `graphviz `_ package needs to be installed on the system, moreover the :command:`dot` and :command:`dotty` programs must be accesible in the program path so that they can be ran from processes spawned within the module. Example usage ------------- Here is a typical usage:: from altgraph import Graph, Dot # create a graph edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ] graph = Graph.Graph(edges) # create a dot representation of the graph dot = Dot.Dot(graph) # display the graph dot.display() # save the dot representation into the mydot.dot file dot.save_dot(file_name='mydot.dot') # save dot file as gif image into the graph.gif file dot.save_img(file_name='graph', file_type='gif') Directed graph and non-directed graph ------------------------------------- Dot class can use for both directed graph and non-directed graph by passing ``graphtype`` parameter. Example:: # create directed graph(default) dot = Dot.Dot(graph, graphtype="digraph") # create non-directed graph dot = Dot.Dot(graph, graphtype="graph") Customizing the output ---------------------- The graph drawing process may be customized by passing valid :command:`dot` parameters for the nodes and edges. For a list of all parameters see the `graphviz `_ documentation. Example:: # customizing the way the overall graph is drawn dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75) # customizing node drawing dot.node_style(1, label='BASE_NODE',shape='box', color='blue' ) dot.node_style(2, style='filled', fillcolor='red') # customizing edge drawing dot.edge_style(1, 2, style='dotted') dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90') dot.edge_style(4, 5, arrowsize=2, style='bold') .. note:: dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to display all graphics styles. To verify the output save it to an image file and look at it that way. Valid attributes ---------------- - dot styles, passed via the :py:meth:`Dot.style` method:: rankdir = 'LR' (draws the graph horizontally, left to right) ranksep = number (rank separation in inches) - node attributes, passed via the :py:meth:`Dot.node_style` method:: style = 'filled' | 'invisible' | 'diagonals' | 'rounded' shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle' - edge attributes, passed via the :py:meth:`Dot.edge_style` method:: style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold' arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none' | 'tee' | 'vee' weight = number (the larger the number the closer the nodes will be) - valid `graphviz colors `_ - for more details on how to control the graph drawing process see the `graphviz reference `_. ''' import os import warnings from altgraph import GraphError class Dot(object): ''' A class providing a **graphviz** (dot language) representation allowing a fine grained control over how the graph is being displayed. If the :command:`dot` and :command:`dotty` programs are not in the current system path their location needs to be specified in the contructor. ''' def __init__( self, graph=None, nodes=None, edgefn=None, nodevisitor=None, edgevisitor=None, name="G", dot='dot', dotty='dotty', neato='neato', graphtype="digraph"): ''' Initialization. ''' self.name, self.attr = name, {} assert graphtype in ['graph', 'digraph'] self.type = graphtype self.temp_dot = "tmp_dot.dot" self.temp_neo = "tmp_neo.dot" self.dot, self.dotty, self.neato = dot, dotty, neato # self.nodes: node styles # self.edges: edge styles self.nodes, self.edges = {}, {} if graph is not None and nodes is None: nodes = graph if graph is not None and edgefn is None: def edgefn(node, graph=graph): return graph.out_nbrs(node) if nodes is None: nodes = () seen = set() for node in nodes: if nodevisitor is None: style = {} else: style = nodevisitor(node) if style is not None: self.nodes[node] = {} self.node_style(node, **style) seen.add(node) if edgefn is not None: for head in seen: for tail in (n for n in edgefn(head) if n in seen): if edgevisitor is None: edgestyle = {} else: edgestyle = edgevisitor(head, tail) if edgestyle is not None: if head not in self.edges: self.edges[head] = {} self.edges[head][tail] = {} self.edge_style(head, tail, **edgestyle) def style(self, **attr): ''' Changes the overall style ''' self.attr = attr def display(self, mode='dot'): ''' Displays the current graph via dotty ''' if mode == 'neato': self.save_dot(self.temp_neo) neato_cmd = "%s -o %s %s" % ( self.neato, self.temp_dot, self.temp_neo) os.system(neato_cmd) else: self.save_dot(self.temp_dot) plot_cmd = "%s %s" % (self.dotty, self.temp_dot) os.system(plot_cmd) def node_style(self, node, **kwargs): ''' Modifies a node style to the dot representation. ''' if node not in self.edges: self.edges[node] = {} self.nodes[node] = kwargs def all_node_style(self, **kwargs): ''' Modifies all node styles ''' for node in self.nodes: self.node_style(node, **kwargs) def edge_style(self, head, tail, **kwargs): ''' Modifies an edge style to the dot representation. ''' if tail not in self.nodes: raise GraphError("invalid node %s" % (tail,)) try: if tail not in self.edges[head]: self.edges[head][tail] = {} self.edges[head][tail] = kwargs except KeyError: raise GraphError("invalid edge %s -> %s " % (head, tail)) def iterdot(self): # write graph title if self.type == 'digraph': yield 'digraph %s {\n' % (self.name,) elif self.type == 'graph': yield 'graph %s {\n' % (self.name,) else: raise GraphError("unsupported graphtype %s" % (self.type,)) # write overall graph attributes for attr_name, attr_value in sorted(self.attr.items()): yield '%s="%s";' % (attr_name, attr_value) yield '\n' # some reusable patterns cpatt = '%s="%s",' # to separate attributes epatt = '];\n' # to end attributes # write node attributes for node_name, node_attr in sorted(self.nodes.items()): yield '\t"%s" [' % (node_name,) for attr_name, attr_value in sorted(node_attr.items()): yield cpatt % (attr_name, attr_value) yield epatt # write edge attributes for head in sorted(self.edges): for tail in sorted(self.edges[head]): if self.type == 'digraph': yield '\t"%s" -> "%s" [' % (head, tail) else: yield '\t"%s" -- "%s" [' % (head, tail) for attr_name, attr_value in \ sorted(self.edges[head][tail].items()): yield cpatt % (attr_name, attr_value) yield epatt # finish file yield '}\n' def __iter__(self): return self.iterdot() def save_dot(self, file_name=None): ''' Saves the current graph representation into a file ''' if not file_name: warnings.warn(DeprecationWarning, "always pass a file_name") file_name = self.temp_dot with open(file_name, "w") as fp: for chunk in self.iterdot(): fp.write(chunk) def save_img(self, file_name=None, file_type="gif", mode='dot'): ''' Saves the dot file as an image file ''' if not file_name: warnings.warn(DeprecationWarning, "always pass a file_name") file_name = "out" if mode == 'neato': self.save_dot(self.temp_neo) neato_cmd = "%s -o %s %s" % ( self.neato, self.temp_dot, self.temp_neo) os.system(neato_cmd) plot_cmd = self.dot else: self.save_dot(self.temp_dot) plot_cmd = self.dot file_name = "%s.%s" % (file_name, file_type) create_cmd = "%s -T%s %s -o %s" % ( plot_cmd, file_type, self.temp_dot, file_name) os.system(create_cmd) altgraph-0.15/altgraph/Graph.py0000644000076500000240000005047613207760071017077 0ustar ronaldstaff00000000000000""" altgraph.Graph - Base Graph class ================================= .. #--Version 2.1 #--Bob Ippolito October, 2004 #--Version 2.0 #--Istvan Albert June, 2004 #--Version 1.0 #--Nathan Denny, May 27, 1999 """ from altgraph import GraphError from collections import deque class Graph(object): """ The Graph class represents a directed graph with *N* nodes and *E* edges. Naming conventions: - the prefixes such as *out*, *inc* and *all* will refer to methods that operate on the outgoing, incoming or all edges of that node. For example: :py:meth:`inc_degree` will refer to the degree of the node computed over the incoming edges (the number of neighbours linking to the node). - the prefixes such as *forw* and *back* will refer to the orientation of the edges used in the method with respect to the node. For example: :py:meth:`forw_bfs` will start at the node then use the outgoing edges to traverse the graph (goes forward). """ def __init__(self, edges=None): """ Initialization """ self.next_edge = 0 self.nodes, self.edges = {}, {} self.hidden_edges, self.hidden_nodes = {}, {} if edges is not None: for item in edges: if len(item) == 2: head, tail = item self.add_edge(head, tail) elif len(item) == 3: head, tail, data = item self.add_edge(head, tail, data) else: raise GraphError("Cannot create edge from %s" % (item,)) def __repr__(self): return '' % ( self.number_of_nodes(), self.number_of_edges()) def add_node(self, node, node_data=None): """ Adds a new node to the graph. Arbitrary data can be attached to the node via the node_data parameter. Adding the same node twice will be silently ignored. The node must be a hashable value. """ # # the nodes will contain tuples that will store incoming edges, # outgoing edges and data # # index 0 -> incoming edges # index 1 -> outgoing edges if node in self.hidden_nodes: # Node is present, but hidden return if node not in self.nodes: self.nodes[node] = ([], [], node_data) def add_edge(self, head_id, tail_id, edge_data=1, create_nodes=True): """ Adds a directed edge going from head_id to tail_id. Arbitrary data can be attached to the edge via edge_data. It may create the nodes if adding edges between nonexisting ones. :param head_id: head node :param tail_id: tail node :param edge_data: (optional) data attached to the edge :param create_nodes: (optional) creates the head_id or tail_id node in case they did not exist """ # shorcut edge = self.next_edge # add nodes if on automatic node creation if create_nodes: self.add_node(head_id) self.add_node(tail_id) # update the corresponding incoming and outgoing lists in the nodes # index 0 -> incoming edges # index 1 -> outgoing edges try: self.nodes[tail_id][0].append(edge) self.nodes[head_id][1].append(edge) except KeyError: raise GraphError('Invalid nodes %s -> %s' % (head_id, tail_id)) # store edge information self.edges[edge] = (head_id, tail_id, edge_data) self.next_edge += 1 def hide_edge(self, edge): """ Hides an edge from the graph. The edge may be unhidden at some later time. """ try: head_id, tail_id, edge_data = \ self.hidden_edges[edge] = self.edges[edge] self.nodes[tail_id][0].remove(edge) self.nodes[head_id][1].remove(edge) del self.edges[edge] except KeyError: raise GraphError('Invalid edge %s' % edge) def hide_node(self, node): """ Hides a node from the graph. The incoming and outgoing edges of the node will also be hidden. The node may be unhidden at some later time. """ try: all_edges = self.all_edges(node) self.hidden_nodes[node] = (self.nodes[node], all_edges) for edge in all_edges: self.hide_edge(edge) del self.nodes[node] except KeyError: raise GraphError('Invalid node %s' % node) def restore_node(self, node): """ Restores a previously hidden node back into the graph and restores all of its incoming and outgoing edges. """ try: self.nodes[node], all_edges = self.hidden_nodes[node] for edge in all_edges: self.restore_edge(edge) del self.hidden_nodes[node] except KeyError: raise GraphError('Invalid node %s' % node) def restore_edge(self, edge): """ Restores a previously hidden edge back into the graph. """ try: head_id, tail_id, data = self.hidden_edges[edge] self.nodes[tail_id][0].append(edge) self.nodes[head_id][1].append(edge) self.edges[edge] = head_id, tail_id, data del self.hidden_edges[edge] except KeyError: raise GraphError('Invalid edge %s' % edge) def restore_all_edges(self): """ Restores all hidden edges. """ for edge in list(self.hidden_edges.keys()): try: self.restore_edge(edge) except GraphError: pass def restore_all_nodes(self): """ Restores all hidden nodes. """ for node in list(self.hidden_nodes.keys()): self.restore_node(node) def __contains__(self, node): """ Test whether a node is in the graph """ return node in self.nodes def edge_by_id(self, edge): """ Returns the edge that connects the head_id and tail_id nodes """ try: head, tail, data = self.edges[edge] except KeyError: head, tail = None, None raise GraphError('Invalid edge %s' % edge) return (head, tail) def edge_by_node(self, head, tail): """ Returns the edge that connects the head_id and tail_id nodes """ for edge in self.out_edges(head): if self.tail(edge) == tail: return edge return None def number_of_nodes(self): """ Returns the number of nodes """ return len(self.nodes) def number_of_edges(self): """ Returns the number of edges """ return len(self.edges) def __iter__(self): """ Iterates over all nodes in the graph """ return iter(self.nodes) def node_list(self): """ Return a list of the node ids for all visible nodes in the graph. """ return list(self.nodes.keys()) def edge_list(self): """ Returns an iterator for all visible nodes in the graph. """ return list(self.edges.keys()) def number_of_hidden_edges(self): """ Returns the number of hidden edges """ return len(self.hidden_edges) def number_of_hidden_nodes(self): """ Returns the number of hidden nodes """ return len(self.hidden_nodes) def hidden_node_list(self): """ Returns the list with the hidden nodes """ return list(self.hidden_nodes.keys()) def hidden_edge_list(self): """ Returns a list with the hidden edges """ return list(self.hidden_edges.keys()) def describe_node(self, node): """ return node, node data, outgoing edges, incoming edges for node """ incoming, outgoing, data = self.nodes[node] return node, data, outgoing, incoming def describe_edge(self, edge): """ return edge, edge data, head, tail for edge """ head, tail, data = self.edges[edge] return edge, data, head, tail def node_data(self, node): """ Returns the data associated with a node """ return self.nodes[node][2] def edge_data(self, edge): """ Returns the data associated with an edge """ return self.edges[edge][2] def update_edge_data(self, edge, edge_data): """ Replace the edge data for a specific edge """ self.edges[edge] = self.edges[edge][0:2] + (edge_data,) def head(self, edge): """ Returns the node of the head of the edge. """ return self.edges[edge][0] def tail(self, edge): """ Returns node of the tail of the edge. """ return self.edges[edge][1] def out_nbrs(self, node): """ List of nodes connected by outgoing edges """ return [self.tail(n) for n in self.out_edges(node)] def inc_nbrs(self, node): """ List of nodes connected by incoming edges """ return [self.head(n) for n in self.inc_edges(node)] def all_nbrs(self, node): """ List of nodes connected by incoming and outgoing edges """ return list(dict.fromkeys(self.inc_nbrs(node) + self.out_nbrs(node))) def out_edges(self, node): """ Returns a list of the outgoing edges """ try: return list(self.nodes[node][1]) except KeyError: raise GraphError('Invalid node %s' % node) def inc_edges(self, node): """ Returns a list of the incoming edges """ try: return list(self.nodes[node][0]) except KeyError: raise GraphError('Invalid node %s' % node) def all_edges(self, node): """ Returns a list of incoming and outging edges. """ return set(self.inc_edges(node) + self.out_edges(node)) def out_degree(self, node): """ Returns the number of outgoing edges """ return len(self.out_edges(node)) def inc_degree(self, node): """ Returns the number of incoming edges """ return len(self.inc_edges(node)) def all_degree(self, node): """ The total degree of a node """ return self.inc_degree(node) + self.out_degree(node) def _topo_sort(self, forward=True): """ Topological sort. Returns a list of nodes where the successors (based on outgoing and incoming edges selected by the forward parameter) of any given node appear in the sequence after that node. """ topo_list = [] queue = deque() indeg = {} # select the operation that will be performed if forward: get_edges = self.out_edges get_degree = self.inc_degree get_next = self.tail else: get_edges = self.inc_edges get_degree = self.out_degree get_next = self.head for node in self.node_list(): degree = get_degree(node) if degree: indeg[node] = degree else: queue.append(node) while queue: curr_node = queue.popleft() topo_list.append(curr_node) for edge in get_edges(curr_node): tail_id = get_next(edge) if tail_id in indeg: indeg[tail_id] -= 1 if indeg[tail_id] == 0: queue.append(tail_id) if len(topo_list) == len(self.node_list()): valid = True else: # the graph has cycles, invalid topological sort valid = False return (valid, topo_list) def forw_topo_sort(self): """ Topological sort. Returns a list of nodes where the successors (based on outgoing edges) of any given node appear in the sequence after that node. """ return self._topo_sort(forward=True) def back_topo_sort(self): """ Reverse topological sort. Returns a list of nodes where the successors (based on incoming edges) of any given node appear in the sequence after that node. """ return self._topo_sort(forward=False) def _bfs_subgraph(self, start_id, forward=True): """ Private method creates a subgraph in a bfs order. The forward parameter specifies whether it is a forward or backward traversal. """ if forward: get_bfs = self.forw_bfs get_nbrs = self.out_nbrs else: get_bfs = self.back_bfs get_nbrs = self.inc_nbrs g = Graph() bfs_list = get_bfs(start_id) for node in bfs_list: g.add_node(node) for node in bfs_list: for nbr_id in get_nbrs(node): if forward: g.add_edge(node, nbr_id) else: g.add_edge(nbr_id, node) return g def forw_bfs_subgraph(self, start_id): """ Creates and returns a subgraph consisting of the breadth first reachable nodes based on their outgoing edges. """ return self._bfs_subgraph(start_id, forward=True) def back_bfs_subgraph(self, start_id): """ Creates and returns a subgraph consisting of the breadth first reachable nodes based on the incoming edges. """ return self._bfs_subgraph(start_id, forward=False) def iterdfs(self, start, end=None, forward=True): """ Collecting nodes in some depth first traversal. The forward parameter specifies whether it is a forward or backward traversal. """ visited, stack = set([start]), deque([start]) if forward: get_edges = self.out_edges get_next = self.tail else: get_edges = self.inc_edges get_next = self.head while stack: curr_node = stack.pop() yield curr_node if curr_node == end: break for edge in sorted(get_edges(curr_node)): tail = get_next(edge) if tail not in visited: visited.add(tail) stack.append(tail) def iterdata(self, start, end=None, forward=True, condition=None): """ Perform a depth-first walk of the graph (as ``iterdfs``) and yield the item data of every node where condition matches. The condition callback is only called when node_data is not None. """ visited, stack = set([start]), deque([start]) if forward: get_edges = self.out_edges get_next = self.tail else: get_edges = self.inc_edges get_next = self.head get_data = self.node_data while stack: curr_node = stack.pop() curr_data = get_data(curr_node) if curr_data is not None: if condition is not None and not condition(curr_data): continue yield curr_data if curr_node == end: break for edge in get_edges(curr_node): tail = get_next(edge) if tail not in visited: visited.add(tail) stack.append(tail) def _iterbfs(self, start, end=None, forward=True): """ The forward parameter specifies whether it is a forward or backward traversal. Returns a list of tuples where the first value is the hop value the second value is the node id. """ queue, visited = deque([(start, 0)]), set([start]) # the direction of the bfs depends on the edges that are sampled if forward: get_edges = self.out_edges get_next = self.tail else: get_edges = self.inc_edges get_next = self.head while queue: curr_node, curr_step = queue.popleft() yield (curr_node, curr_step) if curr_node == end: break for edge in get_edges(curr_node): tail = get_next(edge) if tail not in visited: visited.add(tail) queue.append((tail, curr_step + 1)) def forw_bfs(self, start, end=None): """ Returns a list of nodes in some forward BFS order. Starting from the start node the breadth first search proceeds along outgoing edges. """ return [node for node, step in self._iterbfs(start, end, forward=True)] def back_bfs(self, start, end=None): """ Returns a list of nodes in some backward BFS order. Starting from the start node the breadth first search proceeds along incoming edges. """ return [node for node, _ in self._iterbfs(start, end, forward=False)] def forw_dfs(self, start, end=None): """ Returns a list of nodes in some forward DFS order. Starting with the start node the depth first search proceeds along outgoing edges. """ return list(self.iterdfs(start, end, forward=True)) def back_dfs(self, start, end=None): """ Returns a list of nodes in some backward DFS order. Starting from the start node the depth first search proceeds along incoming edges. """ return list(self.iterdfs(start, end, forward=False)) def connected(self): """ Returns :py:data:`True` if the graph's every node can be reached from every other node. """ node_list = self.node_list() for node in node_list: bfs_list = self.forw_bfs(node) if len(bfs_list) != len(node_list): return False return True def clust_coef(self, node): """ Computes and returns the local clustering coefficient of node. The local cluster coefficient is proportion of the actual number of edges between neighbours of node and the maximum number of edges between those neighbours. See "Local Clustering Coefficient" on for a formal definition. """ num = 0 nbr_set = set(self.out_nbrs(node)) if node in nbr_set: nbr_set.remove(node) # loop defense for nbr in nbr_set: sec_set = set(self.out_nbrs(nbr)) if nbr in sec_set: sec_set.remove(nbr) # loop defense num += len(nbr_set & sec_set) nbr_num = len(nbr_set) if nbr_num: clust_coef = float(num) / (nbr_num * (nbr_num - 1)) else: clust_coef = 0.0 return clust_coef def get_hops(self, start, end=None, forward=True): """ Computes the hop distance to all nodes centered around a node. First order neighbours are at hop 1, their neigbours are at hop 2 etc. Uses :py:meth:`forw_bfs` or :py:meth:`back_bfs` depending on the value of the forward parameter. If the distance between all neighbouring nodes is 1 the hop number corresponds to the shortest distance between the nodes. :param start: the starting node :param end: ending node (optional). When not specified will search the whole graph. :param forward: directionality parameter (optional). If C{True} (default) it uses L{forw_bfs} otherwise L{back_bfs}. :return: returns a list of tuples where each tuple contains the node and the hop. Typical usage:: >>> print (graph.get_hops(1, 8)) >>> [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] # node 1 is at 0 hops # node 2 is at 1 hop # ... # node 8 is at 5 hops """ if forward: return list(self._iterbfs(start=start, end=end, forward=True)) else: return list(self._iterbfs(start=start, end=end, forward=False)) altgraph-0.15/altgraph/GraphAlgo.py0000644000076500000240000001270613207760071017674 0ustar ronaldstaff00000000000000''' altgraph.GraphAlgo - Graph algorithms ===================================== ''' from altgraph import GraphError def dijkstra(graph, start, end=None): """ Dijkstra's algorithm for shortest paths `David Eppstein, UC Irvine, 4 April 2002 `_ `Python Cookbook Recipe `_ Find shortest paths from the start node to all nodes nearer than or equal to the end node. Dijkstra's algorithm is only guaranteed to work correctly when all edge lengths are positive. This code does not verify this property for all edges (only the edges examined until the end vertex is reached), but will correctly compute shortest paths even for some graphs with negative edges, and will raise an exception if it discovers that a negative edge has caused it to make a mistake. Adapted to altgraph by Istvan Albert, Pennsylvania State University - June, 9 2004 """ D = {} # dictionary of final distances P = {} # dictionary of predecessors Q = _priorityDictionary() # estimated distances of non-final vertices Q[start] = 0 for v in Q: D[v] = Q[v] if v == end: break for w in graph.out_nbrs(v): edge_id = graph.edge_by_node(v, w) vwLength = D[v] + graph.edge_data(edge_id) if w in D: if vwLength < D[w]: raise GraphError( "Dijkstra: found better path to already-final vertex") elif w not in Q or vwLength < Q[w]: Q[w] = vwLength P[w] = v return (D, P) def shortest_path(graph, start, end): """ Find a single shortest path from the *start* node to the *end* node. The input has the same conventions as dijkstra(). The output is a list of the nodes in order along the shortest path. **Note that the distances must be stored in the edge data as numeric data** """ D, P = dijkstra(graph, start, end) Path = [] while 1: Path.append(end) if end == start: break end = P[end] Path.reverse() return Path # # Utility classes and functions # class _priorityDictionary(dict): ''' Priority dictionary using binary heaps (internal use only) David Eppstein, UC Irvine, 8 Mar 2002 Implements a data structure that acts almost like a dictionary, with two modifications: 1. D.smallest() returns the value x minimizing D[x]. For this to work correctly, all values D[x] stored in the dictionary must be comparable. 2. iterating "for x in D" finds and removes the items from D in sorted order. Each item is not removed until the next item is requested, so D[x] will still return a useful value until the next iteration of the for-loop. Each operation takes logarithmic amortized time. ''' def __init__(self): ''' Initialize priorityDictionary by creating binary heap of pairs (value,key). Note that changing or removing a dict entry will not remove the old pair from the heap until it is found by smallest() or until the heap is rebuilt. ''' self.__heap = [] dict.__init__(self) def smallest(self): ''' Find smallest item after removing deleted items from front of heap. ''' if len(self) == 0: raise IndexError("smallest of empty priorityDictionary") heap = self.__heap while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]: lastItem = heap.pop() insertionPoint = 0 while 1: smallChild = 2*insertionPoint+1 if smallChild+1 < len(heap) and \ heap[smallChild] > heap[smallChild+1]: smallChild += 1 if smallChild >= len(heap) or lastItem <= heap[smallChild]: heap[insertionPoint] = lastItem break heap[insertionPoint] = heap[smallChild] insertionPoint = smallChild return heap[0][1] def __iter__(self): ''' Create destructive sorted iterator of priorityDictionary. ''' def iterfn(): while len(self) > 0: x = self.smallest() yield x del self[x] return iterfn() def __setitem__(self, key, val): ''' Change value stored in dictionary and add corresponding pair to heap. Rebuilds the heap if the number of deleted items gets large, to avoid memory leakage. ''' dict.__setitem__(self, key, val) heap = self.__heap if len(heap) > 2 * len(self): self.__heap = [(v, k) for k, v in self.items()] self.__heap.sort() else: newPair = (val, key) insertionPoint = len(heap) heap.append(None) while insertionPoint > 0 and newPair < heap[(insertionPoint-1)//2]: heap[insertionPoint] = heap[(insertionPoint-1)//2] insertionPoint = (insertionPoint-1)//2 heap[insertionPoint] = newPair def setdefault(self, key, val): ''' Reimplement setdefault to pass through our customized __setitem__. ''' if key not in self: self[key] = val return self[key] altgraph-0.15/altgraph/GraphStat.py0000644000076500000240000000353013207760071017720 0ustar ronaldstaff00000000000000''' altgraph.GraphStat - Functions providing various graph statistics ================================================================= ''' def degree_dist(graph, limits=(0, 0), bin_num=10, mode='out'): ''' Computes the degree distribution for a graph. Returns a list of tuples where the first element of the tuple is the center of the bin representing a range of degrees and the second element of the tuple are the number of nodes with the degree falling in the range. Example:: .... ''' deg = [] if mode == 'inc': get_deg = graph.inc_degree else: get_deg = graph.out_degree for node in graph: deg.append(get_deg(node)) if not deg: return [] results = _binning(values=deg, limits=limits, bin_num=bin_num) return results _EPS = 1.0/(2.0**32) def _binning(values, limits=(0, 0), bin_num=10): ''' Bins data that falls between certain limits, if the limits are (0, 0) the minimum and maximum values are used. Returns a list of tuples where the first element of the tuple is the center of the bin and the second element of the tuple are the counts. ''' if limits == (0, 0): min_val, max_val = min(values) - _EPS, max(values) + _EPS else: min_val, max_val = limits # get bin size bin_size = (max_val - min_val)/float(bin_num) bins = [0] * (bin_num) # will ignore these outliers for now for value in values: try: if (value - min_val) >= 0: index = int((value - min_val)/float(bin_size)) bins[index] += 1 except IndexError: pass # make it ready for an x,y plot result = [] center = (bin_size/2) + min_val for i, y in enumerate(bins): x = center + bin_size * i result.append((x, y)) return result altgraph-0.15/altgraph/GraphUtil.py0000644000076500000240000000774613207760071017737 0ustar ronaldstaff00000000000000''' altgraph.GraphUtil - Utility classes and functions ================================================== ''' import random from collections import deque from altgraph import Graph from altgraph import GraphError def generate_random_graph( node_num, edge_num, self_loops=False, multi_edges=False): ''' Generates and returns a :py:class:`~altgraph.Graph.Graph` instance with *node_num* nodes randomly connected by *edge_num* edges. ''' g = Graph.Graph() if not multi_edges: if self_loops: max_edges = node_num * node_num else: max_edges = node_num * (node_num-1) if edge_num > max_edges: raise GraphError( "inconsistent arguments to 'generate_random_graph'") nodes = range(node_num) for node in nodes: g.add_node(node) while 1: head = random.choice(nodes) tail = random.choice(nodes) # loop defense if head == tail and not self_loops: continue # multiple edge defense if g.edge_by_node(head, tail) is not None and not multi_edges: continue # add the edge g.add_edge(head, tail) if g.number_of_edges() >= edge_num: break return g def generate_scale_free_graph( steps, growth_num, self_loops=False, multi_edges=False): ''' Generates and returns a :py:class:`~altgraph.Graph.Graph` instance that will have *steps* \* *growth_num* nodes and a scale free (powerlaw) connectivity. Starting with a fully connected graph with *growth_num* nodes at every step *growth_num* nodes are added to the graph and are connected to existing nodes with a probability proportional to the degree of these existing nodes. ''' # FIXME: The code doesn't seem to do what the documentation claims. graph = Graph.Graph() # initialize the graph store = [] for i in range(growth_num): for j in range(i + 1, growth_num): store.append(i) store.append(j) graph.add_edge(i, j) # generate for node in range(growth_num, steps * growth_num): graph.add_node(node) while graph.out_degree(node) < growth_num: nbr = random.choice(store) # loop defense if node == nbr and not self_loops: continue # multi edge defense if graph.edge_by_node(node, nbr) and not multi_edges: continue graph.add_edge(node, nbr) for nbr in graph.out_nbrs(node): store.append(node) store.append(nbr) return graph def filter_stack(graph, head, filters): """ Perform a walk in a depth-first order starting at *head*. Returns (visited, removes, orphans). * visited: the set of visited nodes * removes: the list of nodes where the node data does not all *filters* * orphans: tuples of (last_good, node), where node is not in removes, is directly reachable from a node in *removes* and *last_good* is the closest upstream node that is not in *removes*. """ visited, removes, orphans = set([head]), set(), set() stack = deque([(head, head)]) get_data = graph.node_data get_edges = graph.out_edges get_tail = graph.tail while stack: last_good, node = stack.pop() data = get_data(node) if data is not None: for filtfunc in filters: if not filtfunc(data): removes.add(node) break else: last_good = node for edge in get_edges(node): tail = get_tail(edge) if last_good is not node: orphans.add((last_good, tail)) if tail not in visited: visited.add(tail) stack.append((last_good, tail)) orphans = [ (lg, tl) for (lg, tl) in orphans if tl not in removes] return visited, removes, orphans altgraph-0.15/altgraph/ObjectGraph.py0000644000076500000240000001455013207760071020217 0ustar ronaldstaff00000000000000""" altgraph.ObjectGraph - Graph of objects with an identifier ========================================================== A graph of objects that have a "graphident" attribute. graphident is the key for the object in the graph """ from altgraph import GraphError from altgraph.Graph import Graph from altgraph.GraphUtil import filter_stack class ObjectGraph(object): """ A graph of objects that have a "graphident" attribute. graphident is the key for the object in the graph """ def __init__(self, graph=None, debug=0): if graph is None: graph = Graph() self.graphident = self self.graph = graph self.debug = debug self.indent = 0 graph.add_node(self, None) def __repr__(self): return '<%s>' % (type(self).__name__,) def flatten(self, condition=None, start=None): """ Iterate over the subgraph that is entirely reachable by condition starting from the given start node or the ObjectGraph root """ if start is None: start = self start = self.getRawIdent(start) return self.graph.iterdata(start=start, condition=condition) def nodes(self): for ident in self.graph: node = self.graph.node_data(ident) if node is not None: yield self.graph.node_data(ident) def get_edges(self, node): if node is None: node = self start = self.getRawIdent(node) _, _, outraw, incraw = self.graph.describe_node(start) def iter_edges(lst, n): seen = set() for tpl in (self.graph.describe_edge(e) for e in lst): ident = tpl[n] if ident not in seen: yield self.findNode(ident) seen.add(ident) return iter_edges(outraw, 3), iter_edges(incraw, 2) def edgeData(self, fromNode, toNode): if fromNode is None: fromNode = self start = self.getRawIdent(fromNode) stop = self.getRawIdent(toNode) edge = self.graph.edge_by_node(start, stop) return self.graph.edge_data(edge) def updateEdgeData(self, fromNode, toNode, edgeData): if fromNode is None: fromNode = self start = self.getRawIdent(fromNode) stop = self.getRawIdent(toNode) edge = self.graph.edge_by_node(start, stop) self.graph.update_edge_data(edge, edgeData) def filterStack(self, filters): """ Filter the ObjectGraph in-place by removing all edges to nodes that do not match every filter in the given filter list Returns a tuple containing the number of: (nodes_visited, nodes_removed, nodes_orphaned) """ visited, removes, orphans = filter_stack(self.graph, self, filters) for last_good, tail in orphans: self.graph.add_edge(last_good, tail, edge_data='orphan') for node in removes: self.graph.hide_node(node) return len(visited)-1, len(removes), len(orphans) def removeNode(self, node): """ Remove the given node from the graph if it exists """ ident = self.getIdent(node) if ident is not None: self.graph.hide_node(ident) def removeReference(self, fromnode, tonode): """ Remove all edges from fromnode to tonode """ if fromnode is None: fromnode = self fromident = self.getIdent(fromnode) toident = self.getIdent(tonode) if fromident is not None and toident is not None: while True: edge = self.graph.edge_by_node(fromident, toident) if edge is None: break self.graph.hide_edge(edge) def getIdent(self, node): """ Get the graph identifier for a node """ ident = self.getRawIdent(node) if ident is not None: return ident node = self.findNode(node) if node is None: return None return node.graphident def getRawIdent(self, node): """ Get the identifier for a node object """ if node is self: return node ident = getattr(node, 'graphident', None) return ident def __contains__(self, node): return self.findNode(node) is not None def findNode(self, node): """ Find the node on the graph """ ident = self.getRawIdent(node) if ident is None: ident = node try: return self.graph.node_data(ident) except KeyError: return None def addNode(self, node): """ Add a node to the graph referenced by the root """ self.msg(4, "addNode", node) try: self.graph.restore_node(node.graphident) except GraphError: self.graph.add_node(node.graphident, node) def createReference(self, fromnode, tonode, edge_data=None): """ Create a reference from fromnode to tonode """ if fromnode is None: fromnode = self fromident, toident = self.getIdent(fromnode), self.getIdent(tonode) if fromident is None or toident is None: return self.msg(4, "createReference", fromnode, tonode, edge_data) self.graph.add_edge(fromident, toident, edge_data=edge_data) def createNode(self, cls, name, *args, **kw): """ Add a node of type cls to the graph if it does not already exist by the given name """ m = self.findNode(name) if m is None: m = cls(name, *args, **kw) self.addNode(m) return m def msg(self, level, s, *args): """ Print a debug message with the given level """ if s and level <= self.debug: print("%s%s %s" % ( " " * self.indent, s, ' '.join(map(repr, args)))) def msgin(self, level, s, *args): """ Print a debug message and indent """ if level <= self.debug: self.msg(level, s, *args) self.indent = self.indent + 1 def msgout(self, level, s, *args): """ Dedent and print a debug message """ if level <= self.debug: self.indent = self.indent - 1 self.msg(level, s, *args) altgraph-0.15/altgraph_tests/0000755000076500000240000000000013207760162016673 5ustar ronaldstaff00000000000000altgraph-0.15/altgraph_tests/__init__.py0000644000076500000240000000002713023764670021007 0ustar ronaldstaff00000000000000""" altgraph tests """ altgraph-0.15/altgraph_tests/test_altgraph.py0000644000076500000240000000255113023764670022115 0ustar ronaldstaff00000000000000#!/usr/bin/env py.test import os import sys from altgraph import Graph, GraphAlgo import unittest class BasicTests (unittest.TestCase): def setUp(self): self.edges = [ (1,2), (2,4), (1,3), (2,4), (3,4), (4,5), (6,5), (6,14), (14,15), (6, 15), (5,7), (7, 8), (7,13), (12,8), (8,13), (11,12), (11,9), (13,11), (9,13), (13,10) ] # these are the edges self.store = {} self.g = Graph.Graph() for head, tail in self.edges: self.store[head] = self.store[tail] = None self.g.add_edge(head, tail) def test_num_edges(self): # check the parameters self.assertEqual(self.g.number_of_nodes(), len(self.store)) self.assertEqual(self.g.number_of_edges(), len(self.edges)) def test_forw_bfs(self): # do a forward bfs self.assertEqual( self.g.forw_bfs(1), [1, 2, 3, 4, 5, 7, 8, 13, 11, 10, 12, 9]) def test_get_hops(self): # diplay the hops and hop numbers between nodes self.assertEqual(self.g.get_hops(1, 8), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]) def test_shortest_path(self): self.assertEqual(GraphAlgo.shortest_path(self.g, 1, 12), [1, 2, 4, 5, 7, 13, 11, 12]) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/altgraph_tests/test_dot.py0000644000076500000240000002422513023764670021103 0ustar ronaldstaff00000000000000import unittest import os from altgraph import Dot from altgraph import Graph from altgraph import GraphError class TestDot (unittest.TestCase): def test_constructor(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g) self.assertEqual(dot.name, 'G') self.assertEqual(dot.attr, {}) self.assertEqual(dot.temp_dot, 'tmp_dot.dot') self.assertEqual(dot.temp_neo, 'tmp_neo.dot') self.assertEqual(dot.dot, 'dot') self.assertEqual(dot.dotty, 'dotty') self.assertEqual(dot.neato, 'neato') self.assertEqual(dot.type, 'digraph') self.assertEqual(dot.nodes, dict([(x, {}) for x in g])) edges = {} for head in g: edges[head] = {} for tail in g.out_nbrs(head): edges[head][tail] = {} self.assertEqual(dot.edges[1], edges[1]) self.assertEqual(dot.edges, edges) dot = Dot.Dot(g, nodes=[1,2], edgefn=lambda node: list(sorted(g.out_nbrs(node)))[:-1], nodevisitor=lambda node: {'label': node}, edgevisitor=lambda head, tail: {'label': (head, tail) }, name="testgraph", dot='/usr/local/bin/dot', dotty='/usr/local/bin/dotty', neato='/usr/local/bin/neato', graphtype="graph") self.assertEqual(dot.name, 'testgraph') self.assertEqual(dot.attr, {}) self.assertEqual(dot.temp_dot, 'tmp_dot.dot') self.assertEqual(dot.temp_neo, 'tmp_neo.dot') self.assertEqual(dot.dot, '/usr/local/bin/dot') self.assertEqual(dot.dotty, '/usr/local/bin/dotty') self.assertEqual(dot.neato, '/usr/local/bin/neato') self.assertEqual(dot.type, 'graph') self.assertEqual(dot.nodes, dict([(x, {'label': x}) for x in [1,2]])) edges = {} for head in [1,2]: edges[head] = {} for tail in list(sorted(g.out_nbrs(head)))[:-1]: if tail not in [1,2]: continue edges[head][tail] = {'label': (head, tail) } self.assertEqual(dot.edges[1], edges[1]) self.assertEqual(dot.edges, edges) self.assertRaises(GraphError, Dot.Dot, g, nodes=[1,2, 9]) def test_style(self): g = Graph.Graph([]) dot = Dot.Dot(g) self.assertEqual(dot.attr, {}) dot.style(key='value') self.assertEqual(dot.attr, {'key': 'value'}) dot.style(key2='value2') self.assertEqual(dot.attr, {'key2': 'value2'}) def test_node_style(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g) self.assertEqual(dot.nodes[1], {}) dot.node_style(1, key='value') self.assertEqual(dot.nodes[1], {'key': 'value'}) dot.node_style(1, key2='value2') self.assertEqual(dot.nodes[1], {'key2': 'value2'}) self.assertEqual(dot.nodes[2], {}) dot.all_node_style(key3='value3') for n in g: self.assertEqual(dot.nodes[n], {'key3': 'value3'}) self.assertTrue(9 not in dot.nodes) dot.node_style(9, key='value') self.assertEqual(dot.nodes[9], {'key': 'value'}) def test_edge_style(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g) self.assertEqual(dot.edges[1][2], {}) dot.edge_style(1,2, foo='bar') self.assertEqual(dot.edges[1][2], {'foo': 'bar'}) dot.edge_style(1,2, foo2='2bar') self.assertEqual(dot.edges[1][2], {'foo2': '2bar'}) self.assertEqual(dot.edges[1][3], {}) self.assertFalse(6 in dot.edges[1]) dot.edge_style(1,6, foo2='2bar') self.assertEqual(dot.edges[1][6], {'foo2': '2bar'}) self.assertRaises(GraphError, dot.edge_style, 1, 9, a=1) self.assertRaises(GraphError, dot.edge_style, 9, 1, a=1) def test_iter(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g) dot.style(graph="foobar") dot.node_style(1, key='value') dot.node_style(2, key='another', key2='world') dot.edge_style(1,4, key1='value1', key2='value2') dot.edge_style(2,4, key1='valueA') self.assertEqual(list(iter(dot)), list(dot.iterdot())) for item in dot.iterdot(): self.assertTrue(isinstance(item, str)) first = list(dot.iterdot())[0] self.assertEqual(first, "digraph %s {\n"%(dot.name,)) dot.type = 'graph' first = list(dot.iterdot())[0] self.assertEqual(first, "graph %s {\n"%(dot.name,)) dot.type = 'foo' self.assertRaises(GraphError, list, dot.iterdot()) dot.type = 'digraph' self.assertEqual(list(dot), [ 'digraph G {\n', 'graph="foobar";', '\n', '\t"1" [', 'key="value",', '];\n', '\t"2" [', 'key="another",', 'key2="world",', '];\n', '\t"3" [', '];\n', '\t"4" [', '];\n', '\t"6" [', '];\n', '\t"7" [', '];\n', '\t"1" -> "2" [', '];\n', '\t"1" -> "3" [', '];\n', '\t"1" -> "4" [', 'key1="value1",', 'key2="value2",', '];\n', '\t"2" -> "4" [', 'key1="valueA",', '];\n', '\t"2" -> "6" [', '];\n', '\t"2" -> "7" [', '];\n', '\t"6" -> "1" [', '];\n', '\t"7" -> "4" [', '];\n', '}\n']) def test_save(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g) dot.style(graph="foobar") dot.node_style(1, key='value') dot.node_style(2, key='another', key2='world') dot.edge_style(1,4, key1='value1', key2='value2') dot.edge_style(2,4, key1='valueA') fn = 'test_dot.dot' self.assertTrue(not os.path.exists(fn)) try: dot.save_dot(fn) fp = open(fn, 'r') data = fp.read() fp.close() self.assertEqual(data, ''.join(dot)) finally: if os.path.exists(fn): os.unlink(fn) def test_img(self): g = Graph.Graph([ (1,2), (1,3), (1,4), (2,4), (2,6), (2,7), (7,4), (6,1), ] ) dot = Dot.Dot(g, dot='/usr/local/bin/!!dot', dotty='/usr/local/bin/!!dotty', neato='/usr/local/bin/!!neato') dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75) dot.node_style(1, label='BASE_NODE',shape='box', color='blue') dot.node_style(2, style='filled', fillcolor='red') dot.edge_style(1,4, style='dotted') dot.edge_style(2,4, arrowhead='dot', label='binds', labelangle='90') system_cmds = [] def fake_system(cmd): system_cmds.append(cmd) return None try: real_system = os.system os.system = fake_system system_cmds = [] dot.save_img('foo') self.assertEqual(system_cmds, ['/usr/local/bin/!!dot -Tgif tmp_dot.dot -o foo.gif']) system_cmds = [] dot.save_img('foo', file_type='jpg') self.assertEqual(system_cmds, ['/usr/local/bin/!!dot -Tjpg tmp_dot.dot -o foo.jpg']) system_cmds = [] dot.save_img('bar', file_type='jpg', mode='neato') self.assertEqual(system_cmds, [ '/usr/local/bin/!!neato -o tmp_dot.dot tmp_neo.dot', '/usr/local/bin/!!dot -Tjpg tmp_dot.dot -o bar.jpg', ]) system_cmds = [] dot.display() self.assertEqual(system_cmds, [ '/usr/local/bin/!!dotty tmp_dot.dot' ]) system_cmds = [] dot.display(mode='neato') self.assertEqual(system_cmds, [ '/usr/local/bin/!!neato -o tmp_dot.dot tmp_neo.dot', '/usr/local/bin/!!dotty tmp_dot.dot' ]) finally: if os.path.exists(dot.temp_dot): os.unlink(dot.temp_dot) if os.path.exists(dot.temp_neo): os.unlink(dot.temp_neo) os.system = real_system if os.path.exists('/usr/local/bin/dot') and os.path.exists('/usr/local/bin/neato'): try: dot.dot='/usr/local/bin/dot' dot.neato='/usr/local/bin/neato' self.assertFalse(os.path.exists('foo.gif')) dot.save_img('foo') self.assertTrue(os.path.exists('foo.gif')) os.unlink('foo.gif') self.assertFalse(os.path.exists('foo.gif')) dot.save_img('foo', mode='neato') self.assertTrue(os.path.exists('foo.gif')) os.unlink('foo.gif') finally: if os.path.exists(dot.temp_dot): os.unlink(dot.temp_dot) if os.path.exists(dot.temp_neo): os.unlink(dot.temp_neo) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/altgraph_tests/test_graph.py0000644000076500000240000005324213207760071021412 0ustar ronaldstaff00000000000000import unittest from altgraph import GraphError from altgraph.Graph import Graph class TestGraph (unittest.TestCase): def test_nodes(self): graph = Graph() self.assertEqual(graph.node_list(), []) o1 = object() o1b = object() o2 = object() graph.add_node(1, o1) graph.add_node(1, o1b) graph.add_node(2, o2) graph.add_node(3) self.assertRaises(TypeError, graph.add_node, []) self.assertTrue(graph.node_data(1) is o1) self.assertTrue(graph.node_data(2) is o2) self.assertTrue(graph.node_data(3) is None) self.assertTrue(1 in graph) self.assertTrue(2 in graph) self.assertTrue(3 in graph) self.assertEqual(graph.number_of_nodes(), 3) self.assertEqual(graph.number_of_hidden_nodes(), 0) self.assertEqual(graph.hidden_node_list(), []) self.assertEqual(list(sorted(graph)), [1, 2, 3]) graph.hide_node(1) graph.hide_node(2) graph.hide_node(3) self.assertEqual(graph.number_of_nodes(), 0) self.assertEqual(graph.number_of_hidden_nodes(), 3) self.assertEqual(list(sorted(graph.hidden_node_list())), [1, 2, 3]) self.assertFalse(1 in graph) self.assertFalse(2 in graph) self.assertFalse(3 in graph) graph.add_node(1) self.assertFalse(1 in graph) graph.restore_node(1) self.assertTrue(1 in graph) self.assertFalse(2 in graph) self.assertFalse(3 in graph) graph.restore_all_nodes() self.assertTrue(1 in graph) self.assertTrue(2 in graph) self.assertTrue(3 in graph) self.assertEqual(list(sorted(graph.node_list())), [1, 2, 3]) v = graph.describe_node(1) self.assertEqual(v, (1, o1, [], [])) def test_edges(self): graph = Graph() graph.add_node(1) graph.add_node(2) graph.add_node(3) graph.add_node(4) graph.add_node(5) self.assertTrue(isinstance(graph.edge_list(), list)) graph.add_edge(1, 2) graph.add_edge(4, 5, 'a') self.assertRaises(GraphError, graph.add_edge, 'a', 'b', create_nodes=False) self.assertEqual(graph.number_of_hidden_edges(), 0) self.assertEqual(graph.number_of_edges(), 2) self.assertEqual(graph.hidden_edge_list(), []) e = graph.edge_by_node(1, 2) self.assertTrue(isinstance(e, int)) graph.hide_edge(e) self.assertEqual(graph.number_of_hidden_edges(), 1) self.assertEqual(graph.number_of_edges(), 1) self.assertEqual(graph.hidden_edge_list(), [e]) e2 = graph.edge_by_node(1, 2) self.assertTrue(e2 is None) graph.restore_edge(e) e2 = graph.edge_by_node(1, 2) self.assertEqual(e, e2) self.assertEqual(graph.number_of_hidden_edges(), 0) self.assertEqual(graph.number_of_edges(), 2) e1 = graph.edge_by_node(1, 2) e2 = graph.edge_by_node(4, 5) graph.hide_edge(e1) graph.hide_edge(e2) self.assertEqual(graph.number_of_edges(), 0) graph.restore_all_edges() self.assertEqual(graph.number_of_edges(), 2) self.assertEqual(graph.edge_by_id(e1), (1,2)) self.assertRaises(GraphError, graph.edge_by_id, (e1+1)*(e2+1)+1) self.assertEqual(list(sorted(graph.edge_list())), [e1, e2]) self.assertEqual(graph.describe_edge(e1), (e1, 1, 1, 2)) self.assertEqual(graph.describe_edge(e2), (e2, 'a', 4, 5)) self.assertEqual(graph.edge_data(e1), 1) self.assertEqual(graph.edge_data(e2), 'a') self.assertEqual(graph.head(e2), 4) self.assertEqual(graph.tail(e2), 5) graph.add_edge(1, 3) graph.add_edge(1, 5) graph.add_edge(4, 1) self.assertEqual(list(sorted(graph.out_nbrs(1))), [2, 3, 5]) self.assertEqual(list(sorted(graph.inc_nbrs(1))), [4]) self.assertEqual(list(sorted(graph.inc_nbrs(5))), [1, 4]) self.assertEqual(list(sorted(graph.all_nbrs(1))), [2, 3, 4, 5]) graph.add_edge(5, 1) self.assertEqual(list(sorted(graph.all_nbrs(5))), [1, 4]) self.assertEqual(graph.out_degree(1), 3) self.assertEqual(graph.inc_degree(2), 1) self.assertEqual(graph.inc_degree(5), 2) self.assertEqual(graph.all_degree(5), 3) v = graph.out_edges(4) self.assertTrue(isinstance(v, list)) self.assertEqual(graph.edge_by_id(v[0]), (4, 5)) v = graph.out_edges(1) for e in v: self.assertEqual(graph.edge_by_id(e)[0], 1) v = graph.inc_edges(1) self.assertTrue(isinstance(v, list)) self.assertEqual(graph.edge_by_id(v[0]), (4, 1)) v = graph.inc_edges(5) for e in v: self.assertEqual(graph.edge_by_id(e)[1], 5) v = graph.all_edges(5) for e in v: self.assertTrue(graph.edge_by_id(e)[1] == 5 or graph.edge_by_id(e)[0] == 5) e1 = graph.edge_by_node(1, 2) self.assertTrue(isinstance(e1, int)) graph.hide_node(1) self.assertRaises(GraphError, graph.edge_by_node, 1, 2) graph.restore_node(1) e2 = graph.edge_by_node(1, 2) self.assertEqual(e1, e2) self.assertRaises(GraphError, graph.hide_edge, 'foo') self.assertRaises(GraphError, graph.hide_node, 'foo') self.assertRaises(GraphError, graph.inc_edges, 'foo') self.assertEqual(repr(graph), '') def test_toposort(self): graph = Graph() graph.add_node(1) graph.add_node(2) graph.add_node(3) graph.add_node(4) graph.add_node(5) graph.add_edge(1, 2) graph.add_edge(1, 3) graph.add_edge(2, 4) graph.add_edge(3, 5) ok, result = graph.forw_topo_sort() self.assertTrue(ok) for idx in range(1, 6): self.assertTrue(idx in result) self.assertTrue(result.index(1) < result.index(2)) self.assertTrue(result.index(1) < result.index(3)) self.assertTrue(result.index(2) < result.index(4)) self.assertTrue(result.index(3) < result.index(5)) ok, result = graph.back_topo_sort() self.assertTrue(ok) for idx in range(1, 6): self.assertTrue(idx in result) self.assertTrue(result.index(2) < result.index(1)) self.assertTrue(result.index(3) < result.index(1)) self.assertTrue(result.index(4) < result.index(2)) self.assertTrue(result.index(5) < result.index(3)) # Same graph as before, but with edges # reversed, which means we should get # the same results as before if using # back_topo_sort rather than forw_topo_sort # (and v.v.) graph = Graph() graph.add_node(1) graph.add_node(2) graph.add_node(3) graph.add_node(4) graph.add_node(5) graph.add_edge(2, 1) graph.add_edge(3, 1) graph.add_edge(4, 2) graph.add_edge(5, 3) ok, result = graph.back_topo_sort() self.assertTrue(ok) for idx in range(1, 6): self.assertTrue(idx in result) self.assertTrue(result.index(1) < result.index(2)) self.assertTrue(result.index(1) < result.index(3)) self.assertTrue(result.index(2) < result.index(4)) self.assertTrue(result.index(3) < result.index(5)) ok, result = graph.forw_topo_sort() self.assertTrue(ok) for idx in range(1, 6): self.assertTrue(idx in result) self.assertTrue(result.index(2) < result.index(1)) self.assertTrue(result.index(3) < result.index(1)) self.assertTrue(result.index(4) < result.index(2)) self.assertTrue(result.index(5) < result.index(3)) # Create a cycle graph.add_edge(1, 5) ok, result = graph.forw_topo_sort() self.assertFalse(ok) ok, result = graph.back_topo_sort() self.assertFalse(ok) def test_bfs_subgraph(self): graph = Graph() graph.add_edge(1, 2) graph.add_edge(1, 4) graph.add_edge(2, 4) graph.add_edge(4, 8) graph.add_edge(4, 9) graph.add_edge(4, 10) graph.add_edge(8, 10) subgraph = graph.forw_bfs_subgraph(10) self.assertTrue(isinstance(subgraph, Graph)) self.assertEqual(subgraph.number_of_nodes(), 1) self.assertTrue(10 in subgraph) self.assertEqual(subgraph.number_of_edges(), 0) subgraph = graph.forw_bfs_subgraph(4) self.assertTrue(isinstance(subgraph, Graph)) self.assertEqual(subgraph.number_of_nodes(), 4) self.assertTrue(4 in subgraph) self.assertTrue(8 in subgraph) self.assertTrue(9 in subgraph) self.assertTrue(10 in subgraph) self.assertEqual(subgraph.number_of_edges(), 4) e = subgraph.edge_by_node(4, 8) e = subgraph.edge_by_node(4, 9) e = subgraph.edge_by_node(4, 10) e = subgraph.edge_by_node(8, 10) # same graph as before, but switch around # edges. This results in the same test results # but now for back_bfs_subgraph rather than # forw_bfs_subgraph graph = Graph() graph.add_edge(2, 1) graph.add_edge(4, 1) graph.add_edge(4, 2) graph.add_edge(8, 4) graph.add_edge(9, 4) graph.add_edge(10, 4) graph.add_edge(10, 8) subgraph = graph.back_bfs_subgraph(10) self.assertTrue(isinstance(subgraph, Graph)) self.assertEqual(subgraph.number_of_nodes(), 1) self.assertTrue(10 in subgraph) self.assertEqual(subgraph.number_of_edges(), 0) subgraph = graph.back_bfs_subgraph(4) self.assertTrue(isinstance(subgraph, Graph)) self.assertEqual(subgraph.number_of_nodes(), 4) self.assertTrue(4 in subgraph) self.assertTrue(8 in subgraph) self.assertTrue(9 in subgraph) self.assertTrue(10 in subgraph) self.assertEqual(subgraph.number_of_edges(), 4) e = subgraph.edge_by_node(4, 8) e = subgraph.edge_by_node(4, 9) e = subgraph.edge_by_node(4, 10) e = subgraph.edge_by_node(8, 10) def test_bfs_subgraph_does_not_reverse_egde_direction(self): graph = Graph() graph.add_node('A') graph.add_node('B') graph.add_node('C') graph.add_edge('A', 'B') graph.add_edge('B', 'C') whole_graph = graph.forw_topo_sort() subgraph_backward = graph.back_bfs_subgraph('C') subgraph_backward = subgraph_backward.forw_topo_sort() self.assertEqual(whole_graph, subgraph_backward) subgraph_forward = graph.forw_bfs_subgraph('A') subgraph_forward = subgraph_forward.forw_topo_sort() self.assertEqual(whole_graph, subgraph_forward) def test_iterdfs(self): graph = Graph() graph.add_edge("1", "1.1") graph.add_edge("1", "1.2") graph.add_edge("1", "1.3") graph.add_edge("1.1", "1.1.1") graph.add_edge("1.1", "1.1.2") graph.add_edge("1.2", "1.2.1") graph.add_edge("1.2", "1.2.2") graph.add_edge("1.2.2", "1.2.2.1") graph.add_edge("1.2.2", "1.2.2.2") graph.add_edge("1.2.2", "1.2.2.3") result = list(graph.iterdfs("1")) self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) result = list(graph.iterdfs("1", "1.2.1")) self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1' ]) result = graph.forw_dfs("1") self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) result = graph.forw_dfs("1", "1.2.1") self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1' ]) graph = Graph() graph.add_edge("1.1", "1") graph.add_edge("1.2", "1") graph.add_edge("1.3", "1") graph.add_edge("1.1.1", "1.1") graph.add_edge("1.1.2", "1.1") graph.add_edge("1.2.1", "1.2") graph.add_edge("1.2.2", "1.2") graph.add_edge("1.2.2.1", "1.2.2") graph.add_edge("1.2.2.2", "1.2.2") graph.add_edge("1.2.2.3", "1.2.2") result = list(graph.iterdfs("1", forward=False)) self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) result = list(graph.iterdfs("1", "1.2.1", forward=False)) self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1' ]) result = graph.back_dfs("1") self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) result = graph.back_dfs("1", "1.2.1") self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1' ]) # Introduce cyle: graph.add_edge("1", "1.2") result = list(graph.iterdfs("1", forward=False)) self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) result = graph.back_dfs("1") self.assertEqual(result, [ '1', '1.3', '1.2', '1.2.2', '1.2.2.3', '1.2.2.2', '1.2.2.1', '1.2.1', '1.1', '1.1.2', '1.1.1' ]) def test_iterdata(self): graph = Graph() graph.add_node("1", "I") graph.add_node("1.1", "I.I") graph.add_node("1.2", "I.II") graph.add_node("1.3", "I.III") graph.add_node("1.1.1", "I.I.I") graph.add_node("1.1.2", "I.I.II") graph.add_node("1.2.1", "I.II.I") graph.add_node("1.2.2", "I.II.II") graph.add_node("1.2.2.1", "I.II.II.I") graph.add_node("1.2.2.2", "I.II.II.II") graph.add_node("1.2.2.3", "I.II.II.III") graph.add_edge("1", "1.1") graph.add_edge("1", "1.2") graph.add_edge("1", "1.3") graph.add_edge("1.1", "1.1.1") graph.add_edge("1.1", "1.1.2") graph.add_edge("1.2", "1.2.1") graph.add_edge("1.2", "1.2.2") graph.add_edge("1.2.2", "1.2.2.1") graph.add_edge("1.2.2", "1.2.2.2") graph.add_edge("1.2.2", "1.2.2.3") result = list(graph.iterdata("1", forward=True)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.II.II', 'I.II.II.III', 'I.II.II.II', 'I.II.II.I', 'I.II.I', 'I.I', 'I.I.II', 'I.I.I' ]) result = list(graph.iterdata("1", end="1.2.1", forward=True)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.II.II', 'I.II.II.III', 'I.II.II.II', 'I.II.II.I', 'I.II.I' ]) result = list(graph.iterdata("1", condition=lambda n: len(n) < 6, forward=True)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.I', 'I.I.I' ]) # And the revese option: graph = Graph() graph.add_node("1", "I") graph.add_node("1.1", "I.I") graph.add_node("1.2", "I.II") graph.add_node("1.3", "I.III") graph.add_node("1.1.1", "I.I.I") graph.add_node("1.1.2", "I.I.II") graph.add_node("1.2.1", "I.II.I") graph.add_node("1.2.2", "I.II.II") graph.add_node("1.2.2.1", "I.II.II.I") graph.add_node("1.2.2.2", "I.II.II.II") graph.add_node("1.2.2.3", "I.II.II.III") graph.add_edge("1.1", "1") graph.add_edge("1.2", "1") graph.add_edge("1.3", "1") graph.add_edge("1.1.1", "1.1") graph.add_edge("1.1.2", "1.1") graph.add_edge("1.2.1", "1.2") graph.add_edge("1.2.2", "1.2") graph.add_edge("1.2.2.1", "1.2.2") graph.add_edge("1.2.2.2", "1.2.2") graph.add_edge("1.2.2.3", "1.2.2") result = list(graph.iterdata("1", forward=False)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.II.II', 'I.II.II.III', 'I.II.II.II', 'I.II.II.I', 'I.II.I', 'I.I', 'I.I.II', 'I.I.I' ]) result = list(graph.iterdata("1", end="1.2.1", forward=False)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.II.II', 'I.II.II.III', 'I.II.II.II', 'I.II.II.I', 'I.II.I' ]) result = list(graph.iterdata("1", condition=lambda n: len(n) < 6, forward=False)) self.assertEqual(result, [ 'I', 'I.III', 'I.II', 'I.I', 'I.I.I' ]) def test_bfs(self): graph = Graph() graph.add_edge("1", "1.1") graph.add_edge("1.1", "1.1.1") graph.add_edge("1.1", "1.1.2") graph.add_edge("1.1.2", "1.1.2.1") graph.add_edge("1.1.2", "1.1.2.2") graph.add_edge("1", "1.2") graph.add_edge("1", "1.3") graph.add_edge("1.2", "1.2.1") self.assertEqual(graph.forw_bfs("1"), ['1', '1.1', '1.2', '1.3', '1.1.1', '1.1.2', '1.2.1', '1.1.2.1', '1.1.2.2']) self.assertEqual(graph.forw_bfs("1", "1.1.1"), ['1', '1.1', '1.2', '1.3', '1.1.1']) # And the "reverse" graph graph = Graph() graph.add_edge("1.1", "1") graph.add_edge("1.1.1", "1.1") graph.add_edge("1.1.2", "1.1") graph.add_edge("1.1.2.1", "1.1.2") graph.add_edge("1.1.2.2", "1.1.2") graph.add_edge("1.2", "1") graph.add_edge("1.3", "1") graph.add_edge("1.2.1", "1.2") self.assertEqual(graph.back_bfs("1"), ['1', '1.1', '1.2', '1.3', '1.1.1', '1.1.2', '1.2.1', '1.1.2.1', '1.1.2.2']) self.assertEqual(graph.back_bfs("1", "1.1.1"), ['1', '1.1', '1.2', '1.3', '1.1.1']) # check cycle handling graph.add_edge("1", "1.2.1") self.assertEqual(graph.back_bfs("1"), ['1', '1.1', '1.2', '1.3', '1.1.1', '1.1.2', '1.2.1', '1.1.2.1', '1.1.2.2']) def test_connected(self): graph = Graph() graph.add_node(1) graph.add_node(2) graph.add_node(3) graph.add_node(4) self.assertFalse(graph.connected()) graph.add_edge(1, 2) graph.add_edge(3, 4) self.assertFalse(graph.connected()) graph.add_edge(2, 3) graph.add_edge(4, 1) self.assertTrue(graph.connected()) def test_edges_complex(self): g = Graph() g.add_edge(1, 2) e = g.edge_by_node(1,2) g.hide_edge(e) g.hide_node(2) self.assertRaises(GraphError, g.restore_edge, e) g.restore_all_edges() self.assertRaises(GraphError, g.edge_by_id, e) def test_clust_coef(self): g = Graph() g.add_edge(1, 2) g.add_edge(1, 3) g.add_edge(1, 4) self.assertEqual(g.clust_coef(1), 0) g.add_edge(2, 5) g.add_edge(3, 5) g.add_edge(4, 5) self.assertEqual(g.clust_coef(1), 0) g.add_edge(2, 3) self.assertEqual(g.clust_coef(1), 1./6) g.add_edge(2, 4) self.assertEqual(g.clust_coef(1), 2./6) g.add_edge(4, 2) self.assertEqual(g.clust_coef(1), 3./6) g.add_edge(2, 3) g.add_edge(2, 4) g.add_edge(3, 4) g.add_edge(3, 2) g.add_edge(4, 2) g.add_edge(4, 3) self.assertEqual(g.clust_coef(1), 1) g.add_edge(1, 1) self.assertEqual(g.clust_coef(1), 1) g.add_edge(2, 2) self.assertEqual(g.clust_coef(1), 1) g.add_edge(99, 99) self.assertEqual(g.clust_coef(99), 0.0) def test_get_hops(self): graph = Graph() graph.add_edge(1, 2) graph.add_edge(1, 3) graph.add_edge(2, 4) graph.add_edge(4, 5) graph.add_edge(5, 7) graph.add_edge(7, 8) self.assertEqual(graph.get_hops(1), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]) self.assertEqual(graph.get_hops(1, 5), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3)]) graph.add_edge(5, 1) graph.add_edge(7, 1) graph.add_edge(7, 4) self.assertEqual(graph.get_hops(1), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]) # And the reverse graph graph = Graph() graph.add_edge(2, 1) graph.add_edge(3, 1) graph.add_edge(4, 2) graph.add_edge(5, 4) graph.add_edge(7, 5) graph.add_edge(8, 7) self.assertEqual(graph.get_hops(1, forward=False), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]) self.assertEqual(graph.get_hops(1, 5, forward=False), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3)]) graph.add_edge(1, 5) graph.add_edge(1, 7) graph.add_edge(4, 7) self.assertEqual(graph.get_hops(1, forward=False), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)]) def test_constructor(self): graph = Graph(iter([ (1, 2), (2, 3, 'a'), (1, 3), (3, 4), ])) self.assertEqual(graph.number_of_nodes(), 4) self.assertEqual(graph.number_of_edges(), 4) try: graph.edge_by_node(1,2) graph.edge_by_node(2,3) graph.edge_by_node(1,3) graph.edge_by_node(3,4) except GraphError: self.fail("Incorrect graph") self.assertEqual(graph.edge_data(graph.edge_by_node(2, 3)), 'a') self.assertRaises(GraphError, Graph, [(1,2,3,4)]) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/altgraph_tests/test_graphstat.py0000644000076500000240000000415113207760071022301 0ustar ronaldstaff00000000000000import unittest from altgraph import GraphStat from altgraph import Graph import sys class TestDegreesDist (unittest.TestCase): def test_simple(self): a = Graph.Graph() self.assertEqual(GraphStat.degree_dist(a), []) a.add_node(1) a.add_node(2) a.add_node(3) self.assertEqual(GraphStat.degree_dist(a), GraphStat._binning([0, 0, 0])) for x in range(100): a.add_node(x) for x in range(1, 100): for y in range(1, 50): if x % y == 0: a.add_edge(x, y) counts_inc = [] counts_out = [] for n in a: counts_inc.append(a.inc_degree(n)) counts_out.append(a.out_degree(n)) self.assertEqual(GraphStat.degree_dist(a), GraphStat._binning(counts_out)) self.assertEqual(GraphStat.degree_dist(a, mode='inc'), GraphStat._binning(counts_inc)) class TestBinning (unittest.TestCase): def test_simple(self): # Binning [0, 100) into 10 bins a = list(range(100)) out = GraphStat._binning(a, limits=(0, 100), bin_num=10) self.assertEqual(out, [ (x*1.0, 10) for x in range(5, 100, 10) ]) # Check that outliers are ignored. a = list(range(100)) out = GraphStat._binning(a, limits=(0, 90), bin_num=9) self.assertEqual(out, [ (x*1.0, 10) for x in range(5, 90, 10) ]) out = GraphStat._binning(a, limits=(0, 100), bin_num=15) binSize = 100 / 15.0 result = [0]*15 for i in range(100): bin = int(i/binSize) try: result[bin] += 1 except IndexError: pass result = [ (i * binSize + binSize/2, result[i]) for i in range(len(result))] self.assertEqual(result, out) # Binning (100, 0] into 10 bins a = list(range(99, -10, -1)) out = GraphStat._binning(a, limits=(0, 100), bin_num=10) self.assertEqual(out, [ (x*1.0, 10) for x in range(5, 100, 10) ]) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/altgraph_tests/test_graphutil.py0000644000076500000240000001052313207760071022303 0ustar ronaldstaff00000000000000import unittest from altgraph import GraphUtil from altgraph import Graph, GraphError class TestGraphUtil (unittest.TestCase): def test_generate_random(self): g = GraphUtil.generate_random_graph(10, 50) self.assertEqual(g.number_of_nodes(), 10) self.assertEqual(g.number_of_edges(), 50) seen = set() for e in g.edge_list(): h, t = g.edge_by_id(e) self.assertFalse(h == t) self.assertTrue((h, t) not in seen) seen.add((h, t)) g = GraphUtil.generate_random_graph(5, 30, multi_edges=True) self.assertEqual(g.number_of_nodes(), 5) self.assertEqual(g.number_of_edges(), 30) seen = set() for e in g.edge_list(): h, t = g.edge_by_id(e) self.assertFalse(h == t) if (h, t) in seen: break seen.add((h, t)) else: self.fail("no duplicates?") g = GraphUtil.generate_random_graph(5, 21, self_loops=True) self.assertEqual(g.number_of_nodes(), 5) self.assertEqual(g.number_of_edges(), 21) seen = set() for e in g.edge_list(): h, t = g.edge_by_id(e) self.assertFalse((h, t) in seen) if h == t: break seen.add((h, t)) else: self.fail("no self loops?") self.assertRaises(GraphError, GraphUtil.generate_random_graph, 5, 21) g = GraphUtil.generate_random_graph(5, 21, True) self.assertRaises(GraphError, GraphUtil.generate_random_graph, 5, 26, True) def test_generate_scale_free(self): graph = GraphUtil.generate_scale_free_graph(50, 10) self.assertEqual(graph.number_of_nodes(), 500) counts = {} for node in graph: degree = graph.inc_degree(node) try: counts[degree] += 1 except KeyError: counts[degree] = 1 total_counts = sum(counts.values()) P = {} for degree, count in counts.items(): P[degree] = count * 1.0 / total_counts # XXX: use algoritm # to check if P[degree] ~ degree ** G (for some G) #print sorted(P.items()) #print sorted([(count, degree) for degree, count in counts.items()]) #self.fail("missing tests for GraphUtil.generate_scale_free_graph") def test_filter_stack(self): g = Graph.Graph() g.add_node("1", "N.1") g.add_node("1.1", "N.1.1") g.add_node("1.1.1", "N.1.1.1") g.add_node("1.1.2", "N.1.1.2") g.add_node("1.1.3", "N.1.1.3") g.add_node("1.1.1.1", "N.1.1.1.1") g.add_node("1.1.1.2", "N.1.1.1.2") g.add_node("1.1.2.1", "N.1.1.2.1") g.add_node("1.1.2.2", "N.1.1.2.2") g.add_node("1.1.2.3", "N.1.1.2.3") g.add_node("2", "N.2") g.add_edge("1", "1.1") g.add_edge("1.1", "1.1.1") g.add_edge("1.1", "1.1.2") g.add_edge("1.1", "1.1.3") g.add_edge("1.1.1", "1.1.1.1") g.add_edge("1.1.1", "1.1.1.2") g.add_edge("1.1.2", "1.1.2.1") g.add_edge("1.1.2", "1.1.2.2") g.add_edge("1.1.2", "1.1.2.3") g.add_edge("1.1.2", "1.1.1.2") v, r, o = GraphUtil.filter_stack(g, "1", [ lambda n: n != "N.1.1.1", lambda n: n != "N.1.1.2.3" ]) self.assertEqual(v, set(["1", "1.1", "1.1.1", "1.1.2", "1.1.3", "1.1.1.1", "1.1.1.2", "1.1.2.1", "1.1.2.2", "1.1.2.3"])) self.assertEqual(r, set([ "1.1.1", "1.1.2.3"])) o.sort() self.assertEqual(o, [ ("1.1", "1.1.1.1"), ("1.1", "1.1.1.2") ]) v, r, o = GraphUtil.filter_stack(g, "1", [ lambda n: n != "N.1.1.1", lambda n: n != "N.1.1.1.2" ]) self.assertEqual(v, set(["1", "1.1", "1.1.1", "1.1.2", "1.1.3", "1.1.1.1", "1.1.1.2", "1.1.2.1", "1.1.2.2", "1.1.2.3"])) self.assertEqual(r, set([ "1.1.1", "1.1.1.2"])) self.assertEqual(o, [ ("1.1", "1.1.1.1"), ]) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/altgraph_tests/test_object_graph.py0000644000076500000240000003322013207760071022732 0ustar ronaldstaff00000000000000import unittest import sys from altgraph.ObjectGraph import ObjectGraph from altgraph.Graph import Graph try: from StringIO import StringIO except ImportError: from io import StringIO class Node (object): def __init__(self, graphident): self.graphident = graphident class SubNode (Node): pass class ArgNode (object): def __init__(self, graphident, *args, **kwds): self.graphident = graphident self.args = args self.kwds = kwds def __repr__(self): return ''%(self.graphident,) class TestObjectGraph (unittest.TestCase): def test_constructor(self): graph = ObjectGraph() self.assertTrue(isinstance(graph, ObjectGraph)) g = Graph() graph = ObjectGraph(g) self.assertTrue(graph.graph is g) self.assertEqual(graph.debug, 0) self.assertEqual(graph.indent, 0) graph = ObjectGraph(debug=5) self.assertEqual(graph.debug, 5) def test_repr(self): graph = ObjectGraph() self.assertEqual(repr(graph), '') def testNodes(self): graph = ObjectGraph() n1 = Node("n1") n2 = Node("n2") n3 = Node("n3") n4 = Node("n4") n1b = Node("n1") self.assertTrue(graph.getIdent(graph) is graph) self.assertTrue(graph.getRawIdent(graph) is graph) graph.addNode(n1) graph.addNode(n2) graph.addNode(n3) self.assertTrue(n1 in graph) self.assertFalse(n4 in graph) self.assertTrue("n1" in graph) self.assertFalse("n4" in graph) self.assertTrue(graph.findNode(n1) is n1) self.assertTrue(graph.findNode(n1b) is n1) self.assertTrue(graph.findNode(n2) is n2) self.assertTrue(graph.findNode(n4) is None) self.assertTrue(graph.findNode("n1") is n1) self.assertTrue(graph.findNode("n2") is n2) self.assertTrue(graph.findNode("n4") is None) self.assertEqual(graph.getRawIdent(n1), "n1") self.assertEqual(graph.getRawIdent(n1b), "n1") self.assertEqual(graph.getRawIdent(n4), "n4") self.assertEqual(graph.getRawIdent("n1"), None) self.assertEqual(graph.getIdent(n1), "n1") self.assertEqual(graph.getIdent(n1b), "n1") self.assertEqual(graph.getIdent(n4), "n4") self.assertEqual(graph.getIdent("n1"), "n1") self.assertTrue(n3 in graph) graph.removeNode(n3) self.assertTrue(n3 not in graph) graph.addNode(n3) self.assertTrue(n3 in graph) nx = Node("nx") self.assertFalse(nx in graph) graph.removeNode("nx") self.assertFalse(nx in graph) self.assertIs(graph.getIdent("nx"), None) n = graph.createNode(SubNode, "n1") self.assertTrue(n is n1) n = graph.createNode(SubNode, "n8") self.assertTrue(isinstance(n, SubNode)) self.assertTrue(n in graph) self.assertTrue(graph.findNode("n8") is n) n = graph.createNode(ArgNode, "args", 1, 2, 3, a='a', b='b') self.assertTrue(isinstance(n, ArgNode)) self.assertTrue(n in graph) self.assertTrue(graph.findNode("args") is n) self.assertEqual(n.args, (1, 2, 3)) self.assertEqual(n.kwds, {'a':'a', 'b':'b'}) def testEdges(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 1) n2 = graph.createNode(ArgNode, "n2", 1) n3 = graph.createNode(ArgNode, "n3", 1) n4 = graph.createNode(ArgNode, "n4", 1) graph.createReference(n1, n2, "n1-n2") graph.createReference("n1", "n3", "n1-n3") graph.createReference("n2", n3) g = graph.graph e = g.edge_by_node("n1", "n2") self.assertTrue(e is not None) self.assertEqual(g.edge_data(e), "n1-n2") e = g.edge_by_node("n1", "n3") self.assertTrue(e is not None) self.assertEqual(g.edge_data(e), "n1-n3") e = g.edge_by_node("n2", "n3") self.assertTrue(e is not None) self.assertEqual(g.edge_data(e), None) e = g.edge_by_node("n1", "n4") self.assertTrue(e is None) graph.removeReference(n1, n2) e = g.edge_by_node("n1", "n2") self.assertTrue(e is None) graph.removeReference("n1", "n3") e = g.edge_by_node("n1", "n3") self.assertTrue(e is None) graph.createReference(n1, n2, "foo") e = g.edge_by_node("n1", "n2") self.assertTrue(e is not None) self.assertEqual(g.edge_data(e), "foo") graph.removeReference("A", "B") # neither node exists, shouldn't fail def test_flatten(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 1) n2 = graph.createNode(ArgNode, "n2", 2) n3 = graph.createNode(ArgNode, "n3", 3) n4 = graph.createNode(ArgNode, "n4", 4) n5 = graph.createNode(ArgNode, "n5", 5) n6 = graph.createNode(ArgNode, "n6", 6) n7 = graph.createNode(ArgNode, "n7", 7) n8 = graph.createNode(ArgNode, "n8", 8) graph.createReference(graph, n1) graph.createReference(graph, n7) graph.createReference(n1, n2) graph.createReference(n1, n4) graph.createReference(n2, n3) graph.createReference(n2, n5) graph.createReference(n5, n6) graph.createReference(n4, n6) graph.createReference(n4, n2) self.assertFalse(isinstance(graph.flatten(), list)) fl = list(graph.flatten()) self.assertTrue(n1 in fl) self.assertTrue(n2 in fl) self.assertTrue(n3 in fl) self.assertTrue(n4 in fl) self.assertTrue(n5 in fl) self.assertTrue(n6 in fl) self.assertTrue(n7 in fl) self.assertFalse(n8 in fl) fl = list(graph.flatten(start=n2)) self.assertFalse(n1 in fl) self.assertTrue(n2 in fl) self.assertTrue(n3 in fl) self.assertFalse(n4 in fl) self.assertTrue(n5 in fl) self.assertTrue(n6 in fl) self.assertFalse(n7 in fl) self.assertFalse(n8 in fl) graph.createReference(n1, n5) fl = list(graph.flatten(lambda n: n.args[0] % 2 != 0)) self.assertTrue(n1 in fl) self.assertFalse(n2 in fl) self.assertFalse(n3 in fl) self.assertFalse(n4 in fl) self.assertTrue(n5 in fl) self.assertFalse(n6 in fl) self.assertTrue(n7 in fl) self.assertFalse(n8 in fl) def test_iter_nodes(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 1) n2 = graph.createNode(ArgNode, "n2", 2) n3 = graph.createNode(ArgNode, "n3", 3) n4 = graph.createNode(ArgNode, "n4", 4) n5 = graph.createNode(ArgNode, "n5", 5) n6 = graph.createNode(ArgNode, "n6", 5) nodes = graph.nodes() if sys.version[0] == '2': self.assertTrue(hasattr(nodes, 'next')) else: self.assertTrue(hasattr(nodes, '__next__')) self.assertTrue(hasattr(nodes, '__iter__')) nodes = list(nodes) self.assertEqual(len(nodes), 6) self.assertTrue(n1 in nodes) self.assertTrue(n2 in nodes) self.assertTrue(n3 in nodes) self.assertTrue(n4 in nodes) self.assertTrue(n5 in nodes) self.assertTrue(n6 in nodes) def test_get_edges(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 1) n2 = graph.createNode(ArgNode, "n2", 2) n3 = graph.createNode(ArgNode, "n3", 3) n4 = graph.createNode(ArgNode, "n4", 4) n5 = graph.createNode(ArgNode, "n5", 5) n6 = graph.createNode(ArgNode, "n6", 5) graph.createReference(None, n1) graph.createReference(n1, n2) graph.createReference(n1, n3) graph.createReference(n3, n1) graph.createReference(n5, n1) graph.createReference(n2, n4) graph.createReference(n2, n5) graph.createReference(n6, n2) graph.createReference(n6, n2) graph.createReference(n6, n6) outs, ins = graph.get_edges(n1) self.assertFalse(isinstance(outs, list)) self.assertFalse(isinstance(ins, list)) ins = list(ins) outs = list(outs) self.assertTrue(None not in outs) self.assertTrue(n1 not in outs) self.assertTrue(n2 in outs) self.assertTrue(n3 in outs) self.assertTrue(n4 not in outs) self.assertTrue(n5 not in outs) self.assertTrue(n6 not in outs) self.assertTrue(None in ins) self.assertTrue(n1 not in ins) self.assertTrue(n2 not in ins) self.assertTrue(n3 in ins) self.assertTrue(n4 not in ins) self.assertTrue(n5 in ins) self.assertTrue(n6 not in ins) outs, ins = graph.get_edges(n6) outs = list(outs) ints = list(ins) self.assertEqual(outs.count(n2), 1) self.assertTrue(None not in outs) self.assertTrue(None not in ins) def test_edge_data(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 1) n2 = graph.createNode(ArgNode, "n2", 2) n3 = graph.createNode(ArgNode, "n3", 3) graph.createReference(n1, n2) graph.createReference(n1, n3, 'foo') self.assertIs(graph.edgeData(n1, n2), None) self.assertIs(graph.edgeData(n1, n3), 'foo') graph.updateEdgeData(n1, n2, 'bar') self.assertIs(graph.edgeData(n1, n2), 'bar') def test_filterStack(self): graph = ObjectGraph() n1 = graph.createNode(ArgNode, "n1", 0) n11 = graph.createNode(ArgNode, "n1.1", 1) n12 = graph.createNode(ArgNode, "n1.2", 0) n111 = graph.createNode(ArgNode, "n1.1.1", 0) n112 = graph.createNode(ArgNode, "n1.1.2",2) n2 = graph.createNode(ArgNode, "n2", 0) n3 = graph.createNode(ArgNode, "n2", 0) graph.createReference(None, n1) graph.createReference(None, n2) graph.createReference(n1, n11) graph.createReference(n1, n12) graph.createReference(n11, n111) graph.createReference(n11, n112) self.assertTrue(n1 in graph) self.assertTrue(n2 in graph) self.assertTrue(n11 in graph) self.assertTrue(n12 in graph) self.assertTrue(n111 in graph) self.assertTrue(n112 in graph) self.assertTrue(n2 in graph) self.assertTrue(n3 in graph) visited, removes, orphans = graph.filterStack( [lambda n: n.args[0] != 1, lambda n: n.args[0] != 2]) self.assertEqual(visited, 6) self.assertEqual(removes, 2) self.assertEqual(orphans, 1) e = graph.graph.edge_by_node(n1.graphident, n111.graphident) self.assertEqual(graph.graph.edge_data(e), "orphan") self.assertTrue(n1 in graph) self.assertTrue(n2 in graph) self.assertTrue(n11 not in graph) self.assertTrue(n12 in graph) self.assertTrue(n111 in graph) self.assertTrue(n112 not in graph) self.assertTrue(n2 in graph) self.assertTrue(n3 in graph) class TestObjectGraphIO (unittest.TestCase): def setUp(self): self._stdout = sys.stdout def tearDown(self): sys.stdout = self._stdout def test_msg(self): graph = ObjectGraph() sys.stdout = fp = StringIO() graph.msg(0, "foo") self.assertEqual(fp.getvalue(), "foo \n") sys.stdout = fp = StringIO() graph.msg(5, "foo") self.assertEqual(fp.getvalue(), "") sys.stdout = fp = StringIO() graph.debug = 10 graph.msg(5, "foo") self.assertEqual(fp.getvalue(), "foo \n") sys.stdout = fp = StringIO() graph.msg(0, "foo", 1, "a") self.assertEqual(fp.getvalue(), "foo 1 'a'\n") sys.stdout = fp = StringIO() graph.msgin(0, "hello", "world") graph.msg(0, "test me") graph.msgout(0, "bye bye") self.assertEqual(fp.getvalue(), "hello 'world'\n test me \nbye bye \n") sys.stdout = fp = StringIO() graph.msgin(55, 'hello') graph.msgout(55, 'bye') self.assertEqual(fp.getvalue(), "") def test_graph_references(self): graph = ObjectGraph() n1 = graph.createNode(Node, "n1") n2 = graph.createNode(Node, "n2") graph.createReference(None, n1, None) graph.createReference(None, n2, "hello") self.assertEqual(graph.edgeData(None, n1), None) self.assertEqual(graph.edgeData(None, n2), "hello") graph.updateEdgeData(None, n1, "world") self.assertEqual(graph.edgeData(None, n1), "world") out, inc = graph.get_edges(None) out = list(out) inc = list(inc) self.assertEqual(out, [n1, n2]) self.assertEqual(len(out), 2) self.assertTrue(n1 in out) self.assertTrue(n2 in out) self.assertEqual(inc, []) graph.removeReference(None, n2) out, inc = graph.get_edges(None) out = list(out) inc = list(inc) self.assertTrue(n2 not in out) def test_reference_oddity(self): # Not sure if I actually like this, cannot change for now... graph = ObjectGraph() n1 = graph.createNode(Node, "n1") n2 = graph.createNode(Node, "n2") graph.createReference("n1", "n2") graph.createReference("n1", "n3") graph.createReference("n3", "n1") nodes = list(graph.nodes()) self.assertEqual(len(nodes), 2) self.assertTrue(n1 in nodes) self.assertTrue(n2 in nodes) out, inc = graph.get_edges(n1) out = list(out) inc = list(inc) self.assertEqual(out, [n2]) self.assertEqual(inc, []) if __name__ == "__main__": # pragma: no cover unittest.main() altgraph-0.15/doc/0000755000076500000240000000000013207760162014414 5ustar ronaldstaff00000000000000altgraph-0.15/doc/_templates/0000755000076500000240000000000013207760162016551 5ustar ronaldstaff00000000000000altgraph-0.15/doc/_templates/icons.html0000644000076500000240000000017213207760071020551 0ustar ronaldstaff00000000000000

Status icons

altgraph-0.15/doc/changelog.rst0000644000076500000240000001114413207760071017075 0ustar ronaldstaff00000000000000Release history =============== 0.15 ---- * ``ObjectGraph.get_edges``, ``ObjectGraph.getEdgeData`` and ``ObjectGraph.updateEdgeData`` accept *None* as the node to get and treat this as an alias for *self* (as other methods already did). 0.14 ---- - Issue #7: Remove use of ``iteritems`` in altgraph.GraphAlgo code 0.13 ---- - Issue #4: Graph._bfs_subgraph and back_bfs_subgraph return subgraphs with reversed edges Fix by "pombredanne" on bitbucket. 0.12 ---- - Added ``ObjectGraph.edgeData`` to retrieve the edge data from a specific edge. - Added ``AltGraph.update_edge_data`` and ``ObjectGraph.updateEdgeData`` to update the data associated with a graph edge. 0.11 ---- - Stabilize the order of elements in dot file exports, patch from bitbucket user 'pombredanne'. - Tweak setup.py file to remove dependency on distribute (but keep the dependency on setuptools) 0.10.2 ------ - There where no classifiers in the package metadata due to a bug in setup.py 0.10.1 ------ This is a bugfix release Bug fixes: - Issue #3: The source archive contains a README.txt while the setup file refers to ReadMe.txt. This is caused by a misfeature in distutils, as a workaround I've renamed ReadMe.txt to README.txt in the source tree and setup file. 0.10 ----- This is a minor feature release Features: - Do not use "2to3" to support Python 3. As a side effect of this altgraph now supports Python 2.6 and later, and no longer supports earlier releases of Python. - The order of attributes in the Dot output is now always alphabetical. With this change the output will be consistent between runs and Python versions. 0.9 --- This is a minor bugfix release Features: - Added ``altgraph.ObjectGraph.ObjectGraph.nodes``, a method yielding all nodes in an object graph. Bugfixes: - The 0.8 release didn't work with py2app when using python 3.x. 0.8 ----- This is a minor feature release. The major new feature is a extensive set of unittests, which explains almost all other changes in this release. Bugfixes: - Installing failed with Python 2.5 due to using a distutils class that isn't available in that version of Python (issue #1 on the issue tracker) - ``altgraph.GraphStat.degree_dist`` now actually works - ``altgraph.Graph.add_edge(a, b, create_nodes=False)`` will no longer create the edge when one of the nodes doesn't exist. - ``altgraph.Graph.forw_topo_sort`` failed for some sparse graphs. - ``altgraph.Graph.back_topo_sort`` was completely broken in previous releases. - ``altgraph.Graph.forw_bfs_subgraph`` now actually works. - ``altgraph.Graph.back_bfs_subgraph`` now actually works. - ``altgraph.Graph.iterdfs`` now returns the correct result when the ``forward`` argument is ``False``. - ``altgraph.Graph.iterdata`` now returns the correct result when the ``forward`` argument is ``False``. Features: - The ``altgraph.Graph`` constructor now accepts an argument that contains 2- and 3-tuples instead of requireing that all items have the same size. The (optional) argument can now also be any iterator. - ``altgraph.Graph.Graph.add_node`` has no effect when you add a hidden node. - The private method ``altgraph.Graph._bfs`` is no longer present. - The private method ``altgraph.Graph._dfs`` is no longer present. - ``altgraph.ObjectGraph`` now has a ``__contains__`` methods, which means you can use the ``in`` operator to check if a node is part of a graph. - ``altgraph.GraphUtil.generate_random_graph`` will raise ``GraphError`` instead of looping forever when it is impossible to create the requested graph. - ``altgraph.Dot.edge_style`` raises ``GraphError`` when one of the nodes is not present in the graph. The method silently added the tail in the past, but without ensuring a consistent graph state. - ``altgraph.Dot.save_img`` now works when the mode is ``"neato"``. 0.7.2 ----- This is a minor bugfix release Bugfixes: - distutils didn't include the documentation subtree 0.7.1 ----- This is a minor feature release Features: - Documentation is now generated using `sphinx `_ and can be viewed at . - The repository has moved to bitbucket - ``altgraph.GraphStat.avg_hops`` is no longer present, the function had no implementation and no specified behaviour. - the module ``altgraph.compat`` is gone, which means altgraph will no longer work with Python 2.3. 0.7.0 ----- This is a minor feature release. Features: - Support for Python 3 - It is now possible to run tests using 'python setup.py test' (The actual testsuite is still very minimal though) altgraph-0.15/doc/conf.py0000644000076500000240000001552113207760071015716 0ustar ronaldstaff00000000000000# -*- coding: utf-8 -*- # # altgraph documentation build configuration file, created by # sphinx-quickstart on Tue Aug 31 11:04:49 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os def get_version(): fn = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'setup.cfg') for ln in open(fn): if ln.startswith('version'): version = ln.split('=')[-1].strip() return version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.autodoc' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'altgraph' copyright = u'2010-2011, Ronald Oussoren, Bob Ippolito, 2004 Istvan Albert' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = get_version() # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ 'localtoc.html', 'searchbox.html', 'icons.html' ] } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'altgraphdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'altgraph.tex', u'altgraph Documentation', u'Ronald Oussoren', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'python': ('http://docs.python.org/', None) } altgraph-0.15/doc/core.rst0000644000076500000240000000234413023764670016105 0ustar ronaldstaff00000000000000:mod:`altgraph` --- A Python Graph Library ================================================== .. module:: altgraph :synopsis: A directional graph for python altgraph is a fork of `graphlib `_ tailored to use newer Python 2.3+ features, including additional support used by the py2app suite (modulegraph and macholib, specifically). altgraph is a python based graph (network) representation and manipulation package. It has started out as an extension to the `graph_lib module `_ written by Nathan Denny it has been significantly optimized and expanded. The :class:`altgraph.Graph.Graph` class is loosely modeled after the `LEDA `_ (Library of Efficient Datatypes) representation. The library includes methods for constructing graphs, BFS and DFS traversals, topological sort, finding connected components, shortest paths as well as a number graph statistics functions. The library can also visualize graphs via `graphviz `_. .. exception:: GraphError Exception raised when methods are called with bad values of an inconsistent state. altgraph-0.15/doc/dot.rst0000644000076500000240000001561313023764670015746 0ustar ronaldstaff00000000000000:mod:`altgraph.Dot` --- Interface to the dot language ===================================================== .. module:: altgraph.Dot :synopsis: Interface to the dot language as used by Graphviz.. The :py:mod:`~altgraph.Dot` module provides a simple interface to the file format used in the `graphviz`_ program. The module is intended to offload the most tedious part of the process (the **dot** file generation) while transparently exposing most of its features. .. _`graphviz`: `_ To display the graphs or to generate image files the `graphviz`_ package needs to be installed on the system, moreover the :command:`dot` and :command:`dotty` programs must be accesible in the program path so that they can be ran from processes spawned within the module. Example usage ------------- Here is a typical usage:: from altgraph import Graph, Dot # create a graph edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ] graph = Graph.Graph(edges) # create a dot representation of the graph dot = Dot.Dot(graph) # display the graph dot.display() # save the dot representation into the mydot.dot file dot.save_dot(file_name='mydot.dot') # save dot file as gif image into the graph.gif file dot.save_img(file_name='graph', file_type='gif') Directed graph and non-directed graph ------------------------------------- Dot class can use for both directed graph and non-directed graph by passing *graphtype* parameter. Example:: # create directed graph(default) dot = Dot.Dot(graph, graphtype="digraph") # create non-directed graph dot = Dot.Dot(graph, graphtype="graph") Customizing the output ---------------------- The graph drawing process may be customized by passing valid :command:`dot` parameters for the nodes and edges. For a list of all parameters see the `graphviz`_ documentation. Example:: # customizing the way the overall graph is drawn dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75) # customizing node drawing dot.node_style(1, label='BASE_NODE',shape='box', color='blue' ) dot.node_style(2, style='filled', fillcolor='red') # customizing edge drawing dot.edge_style(1, 2, style='dotted') dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90') dot.edge_style(4, 5, arrowsize=2, style='bold') .. note:: dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to display all graphics styles. To verify the output save it to an image file and look at it that way. Valid attributes ---------------- - dot styles, passed via the :py:meth:`Dot.style` method:: rankdir = 'LR' (draws the graph horizontally, left to right) ranksep = number (rank separation in inches) - node attributes, passed via the :py:meth:`Dot.node_style` method:: style = 'filled' | 'invisible' | 'diagonals' | 'rounded' shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle' - edge attributes, passed via the :py:meth:`Dot.edge_style` method:: style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold' arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none' | 'tee' | 'vee' weight = number (the larger the number the closer the nodes will be) - valid `graphviz colors `_ - for more details on how to control the graph drawing process see the `graphviz reference `_. Class interface --------------- .. class:: Dot(graph[, nodes[, edgefn[, nodevisitor[, edgevisitor[, name[, dot[, dotty[, neato[, graphtype]]]]]]]]]) Creates a new Dot generator based on the specified :class:`Graph `. The Dot generator won't reference the *graph* once it is constructed. If the *nodes* argument is present it is the list of nodes to include in the graph, otherwise all nodes in *graph* are included. If the *edgefn* argument is present it is a function that yields the nodes connected to another node, this defaults to :meth:`graph.out_nbr `. The constructor won't add edges to the dot file unless both the head and tail of the edge are in *nodes*. If the *name* is present it specifies the name of the graph in the resulting dot file. The default is ``"G"``. The functions *nodevisitor* and *edgevisitor* return the default style for a given edge or node (both default to functions that return an empty style). The arguments *dot*, *dotty* and *neato* are used to pass the path to the corresponding `graphviz`_ command. Updating graph attributes ......................... .. method:: Dot.style(\**attr) Sets the overall style (graph attributes) to the given attributes. See `Valid Attributes`_ for more information about the attributes. .. method:: Dot.node_style(node, \**attr) Sets the style for *node* to the given attributes. This method will add *node* to the graph when it isn't already present. See `Valid Attributes`_ for more information about the attributes. .. method:: Dot.all_node_style(\**attr) Replaces the current style for all nodes .. method:: edge_style(head, tail, \**attr) Sets the style of an edge to the given attributes. The edge will be added to the graph when it isn't already present, but *head* and *tail* must both be valid nodes. See `Valid Attributes`_ for more information about the attributes. Emitting output ............... .. method:: Dot.display([mode]) Displays the current graph via dotty. If the *mode* is ``"neato"`` the dot file is processed with the neato command before displaying. This method won't return until the dotty command exits. .. method:: save_dot(filename) Saves the current graph representation into the given file. .. note:: For backward compatibility reasons this method can also be called without an argument, it will then write the graph into a fixed filename (present in the attribute :data:`Graph.temp_dot`). This feature is deprecated and should not be used. .. method:: save_image(file_name[, file_type[, mode]]) Saves the current graph representation as an image file. The output is written into a file whose basename is *file_name* and whose suffix is *file_type*. The *file_type* specifies the type of file to write, the default is ``"gif"``. If the *mode* is ``"neato"`` the dot file is processed with the neato command before displaying. .. note:: For backward compatibility reasons this method can also be called without an argument, it will then write the graph with a fixed basename (``"out"``). This feature is deprecated and should not be used. .. method:: iterdot() Yields all lines of a `graphviz`_ input file (including line endings). .. method:: __iter__() Alias for the :meth:`iterdot` method. altgraph-0.15/doc/graph.rst0000644000076500000240000002030013023764670016246 0ustar ronaldstaff00000000000000:mod:`altgraph.Graph` --- Basic directional graphs ================================================== .. module:: altgraph.Graph :synopsis: Basic directional graphs. The module :mod:`altgraph.Graph` provides a class :class:`Graph` that represents a directed graph with *N* nodes and *E* edges. .. class:: Graph([edges]) Constructs a new empty :class:`Graph` object. If the optional *edges* parameter is supplied, updates the graph by adding the specified edges. All of the elements in *edges* should be tuples with two or three elements. The first two elements of the tuple are the source and destination node of the edge, the optional third element is the edge data. The source and destination nodes are added to the graph when the aren't already present. Node related methods -------------------- .. method:: Graph.add_node(node[, node_data]) Adds a new node to the graph if it is not already present. The new node must be a hashable object. Arbitrary data can be attached to the node via the optional *node_data* argument. .. note:: the node also won't be added to the graph when it is present but currently hidden. .. method:: Graph.hide_node(node) Hides a *node* from the graph. The incoming and outgoing edges of the node will also be hidden. Raises :class:`altgraph.GraphError` when the node is not (visible) node of the graph. .. method:: Graph.restore_node(node) Restores a previously hidden *node*. The incoming and outgoing edges of the node are also restored. Raises :class:`altgraph.GraphError` when the node is not a hidden node of the graph. .. method:: Graph.restore_all_nodes() Restores all hidden nodes. .. method:: Graph.number_of_nodes() Return the number of visible nodes in the graph. .. method:: Graph.number_of_hidden_nodes() Return the number of hidden nodes in the graph. .. method:: Graph.node_list() Return a list with all visible nodes in the graph. .. method:: Graph.hidden_node_list() Return a list with all hidden nodes in the graph. .. method:: node_data(node) Return the data associated with the *node* when it was added. .. method:: Graph.describe_node(node) Returns *node*, the node's data and the lists of outgoing and incoming edges for the node. .. note:: the edge lists should not be modified, doing so can result in unpredicatable behavior. .. method:: Graph.__contains__(node) Returns True iff *node* is a node in the graph. This method is accessed through the *in* operator. .. method:: Graph.__iter__() Yield all nodes in the graph. .. method:: Graph.out_edges(node) Return the list of outgoing edges for *node* .. method:: Graph.inc_edges(node) Return the list of incoming edges for *node* .. method:: Graph.all_edges(node) Return the list of incoming and outgoing edges for *node* .. method:: Graph.out_degree(node) Return the number of outgoing edges for *node*. .. method:: Graph.inc_degree(node) Return the number of incoming edges for *node*. .. method:: Graph.all_degree(node) Return the number of edges (incoming or outgoing) for *node*. Edge related methods -------------------- .. method:: Graph.add_edge(head_id, tail_id [, edge data [, create_nodes]]) Adds a directed edge from *head_id* to *tail_id*. Arbitrary data can be added via *edge_data*. When *create_nodes* is *True* (the default), *head_id* and *tail_id* will be added to the graph when the aren't already present. .. method:: Graph.hide_edge(edge) Hides an edge from the graph. The edge may be unhidden at some later time. .. method:: Graph.restore_edge(edge) Restores a previously hidden *edge*. .. method:: Graph.restore_all_edges() Restore all edges that were hidden before, except for edges referring to hidden nodes. .. method:: Graph.edge_by_node(head, tail) Return the edge ID for an edge from *head* to *tail*, or :data:`None` when no such edge exists. .. method:: Graph.edge_by_id(edge) Return the head and tail of the *edge* .. method:: Graph.edge_data(edge) Return the data associated with the *edge*. .. method:: Graph.update_edge_data(edge, data) Replace the edge data for *edge* by *data*. Raises :exc:`KeyError` when the edge does not exist. .. versionadded:: 0.12 .. method:: Graph.head(edge) Return the head of an *edge* .. method:: Graph.tail(edge) Return the tail of an *edge* .. method:: Graph.describe_edge(edge) Return the *edge*, the associated data, its head and tail. .. method:: Graph.number_of_edges() Return the number of visible edges. .. method:: Graph.number_of_hidden_edges() Return the number of hidden edges. .. method:: Graph.edge_list() Returns a list with all visible edges in the graph. .. method:: Graph.hidden_edge_list() Returns a list with all hidden edges in the graph. Graph traversal --------------- .. method:: Graph.out_nbrs(node) Return a list of all nodes connected by outgoing edges. .. method:: Graph.inc_nbrs(node) Return a list of all nodes connected by incoming edges. .. method:: Graph.all_nbrs(node) Returns a list of nodes connected by an incoming or outgoing edge. .. method:: Graph.forw_topo_sort() Return a list of nodes where the successors (based on outgoing edges) of any given node apear in the sequence after that node. .. method:: Graph.back_topo_sort() Return a list of nodes where the successors (based on incoming edges) of any given node apear in the sequence after that node. .. method:: Graph.forw_bfs_subgraph(start_id) Return a subgraph consisting of the breadth first reachable nodes from *start_id* based on their outgoing edges. .. method:: Graph.back_bfs_subgraph(start_id) Return a subgraph consisting of the breadth first reachable nodes from *start_id* based on their incoming edges. .. method:: Graph.iterdfs(start[, end[, forward]]) Yield nodes in a depth first traversal starting at the *start* node. If *end* is specified traversal stops when reaching that node. If forward is True (the default) edges are traversed in forward direction, otherwise they are traversed in reverse direction. .. method:: Graph.iterdata(start[, end[, forward[, condition]]]) Yield the associated data for nodes in a depth first traversal starting at the *start* node. This method will not yield values for nodes without associated data. If *end* is specified traversal stops when reaching that node. If *condition* is specified and the condition callable returns False for the associated data this method will not yield the associated data and will not follow the edges for the node. If forward is True (the default) edges are traversed in forward direction, otherwise they are traversed in reverse direction. .. method:: Graph.forw_bfs(start[, end]) Returns a list of nodes starting at *start* in some bread first search order (following outgoing edges). When *end* is specified iteration stops at that node. .. method:: Graph.back_bfs(start[, end]) Returns a list of nodes starting at *start* in some bread first search order (following incoming edges). When *end* is specified iteration stops at that node. .. method:: Graph.get_hops(start[, end[, forward]]) Computes the hop distance to all nodes centered around a specified node. First order neighbours are at hop 1, their neigbours are at hop 2 etc. Uses :py:meth:`forw_bfs` or :py:meth:`back_bfs` depending on the value of the forward parameter. If the distance between all neighbouring nodes is 1 the hop number corresponds to the shortest distance between the nodes. Typical usage:: >>> print graph.get_hops(1, 8) >>> [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] # node 1 is at 0 hops # node 2 is at 1 hop # ... # node 8 is at 5 hops Graph statistics ---------------- .. method:: Graph.connected() Returns True iff every node in the graph can be reached from every other node. .. method:: Graph.clust_coef(node) Returns the local clustering coefficient of node. The local cluster coefficient is the proportion of the actual number of edges between neighbours of node and the maximum number of edges between those nodes. altgraph-0.15/doc/graphalgo.rst0000644000076500000240000000211413023764670017114 0ustar ronaldstaff00000000000000:mod:`altgraph.GraphAlgo` --- Graph algorithms ================================================== .. module:: altgraph.GraphAlgo :synopsis: Basic graphs algoritms .. function:: dijkstra(graph, start[, end]) Dijkstra's algorithm for shortest paths. Find shortest paths from the start node to all nodes nearer than or equal to the *end* node. The edge data is assumed to be the edge length. .. note:: Dijkstra's algorithm is only guaranteed to work correctly when all edge lengths are positive. This code does not verify this property for all edges (only the edges examined until the end vertex is reached), but will correctly compute shortest paths even for some graphs with negative edges, and will raise an exception if it discovers that a negative edge has caused it to make a mistake. .. function:: shortest_path(graph, start, end) Find a single shortest path from the given start node to the given end node. The input has the same conventions as :func:`dijkstra`. The output is a list of the nodes in order along the shortest path. altgraph-0.15/doc/graphstat.rst0000644000076500000240000000200713023764670017146 0ustar ronaldstaff00000000000000:mod:`altgraph.GraphStat` --- Functions providing various graph statistics ========================================================================== .. module:: altgraph.GraphStat :synopsis: Functions providing various graph statistics The module :mod:`altgraph.GraphStat` provides function that calculate graph statistics. Currently there is only one such function, more may be added later. .. function:: degree_dist(graph[, limits[, bin_num[, mode]]]) Groups the number of edges per node into *bin_num* bins and returns the list of those bins. Every item in the result is a tuple with the center of the bin and the number of items in that bin. When the *limits* argument is present it must be a tuple with the mininum and maximum number of edges that get binned (that is, when *limits* is ``(4, 10)`` only nodes with between 4 and 10 edges get counted. The *mode* argument is used to count incoming (``'inc'``) or outgoing (``'out'``) edges. The default is to count the outgoing edges. altgraph-0.15/doc/graphutil.rst0000644000076500000240000000422013023764670017147 0ustar ronaldstaff00000000000000:mod:`altgraph.GraphUtil` --- Utility functions ================================================ .. module:: altgraph.GraphUtil :synopsis: Utility functions The module :mod:`altgraph.GraphUtil` performs a number of more or less useful utility functions. .. function:: generate_random_graph(node_num, edge_num[, self_loops[, multi_edges]) Generates and returns a :class:`Graph ` instance with *node_num* nodes randomly connected by *edge_num* edges. When *self_loops* is present and True there can be edges that point from a node to itself. When *multi_edge* is present and True there can be duplicate edges. This method raises :class:`GraphError `_ .. function:: filter_stack(graph, head, filters) Perform a depth-first oder walk of the graph starting at *head* and apply all filter functions in *filters* on the node data of the nodes found. Returns (*visited*, *removes*, *orphans*), where * *visited*: the set of visited nodes * *removes*: the list of nodes where the node data doesn't match all *filters*. * *orphans*: list of tuples (*last_good*, *node*), where node is not in *removes* and one of the nodes that is connected by an incoming edge is in *removes*. *Last_good* is the closest upstream node that is not in *removes*. altgraph-0.15/doc/index.rst0000644000076500000240000000201213023764670016254 0ustar ronaldstaff00000000000000.. altgraph documentation master file, created by sphinx-quickstart on Tue Aug 31 11:04:49 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Altgraph - A basic graph library ================================ altgraph is a fork of graphlib: a graph (network) package for constructing graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with graphviz output. The primary users of this package are `macholib `_ and `modulegraph `_. .. toctree:: :maxdepth: 1 changelog license core graph objectgraph graphalgo graphstat graphutil dot Online Resources ---------------- * `Sourcecode repository on bitbucket `_ * `The issue tracker `_ Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` altgraph-0.15/doc/license.rst0000644000076500000240000000203013023764670016567 0ustar ronaldstaff00000000000000License ======= Copyright (c) 2004 Istvan Albert unless otherwise noted. Parts are copyright (c) Bob Ippolito Parts are copyright (c) 2010-2014 Ronald Oussoren MIT License ........... Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. altgraph-0.15/doc/Makefile0000644000076500000240000000607213023764670016065 0ustar ronaldstaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/altgraph.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/altgraph.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." altgraph-0.15/doc/objectgraph.rst0000644000076500000240000001113613207760071017437 0ustar ronaldstaff00000000000000:mod:`altgraph.ObjectGraph` --- Graphs of objecs with an identifier =================================================================== .. module:: altgraph.ObjectGraph :synopsis: A graph of objects that have a "graphident" attribute. .. class:: ObjectGraph([graph[, debug]]) A graph of objects that have a "graphident" attribute. The value of this attribute is the key for the object in the graph. The optional *graph* is a previously constructed :class:`Graph `. The optional *debug* level controls the amount of debug output (see :meth:`msg`, :meth:`msgin` and :meth:`msgout`). .. note:: the altgraph library does not generate output, the debug attribute and message methods are present for use by subclasses. .. data:: ObjectGraph.graph An :class:`Graph ` object that contains the graph data. .. method:: ObjectGraph.addNode(node) Adds a *node* to the graph. .. note:: re-adding a node that was previously removed using :meth:`removeNode` will reinstate the previously removed node. .. method:: ObjectGraph.createNode(self, cls, name, \*args, \**kwds) Creates a new node using ``cls(*args, **kwds)`` and adds that node using :meth:`addNode`. Returns the newly created node. .. method:: ObjectGraph.removeNode(node) Removes a *node* from the graph when it exists. The *node* argument is either a node object, or the graphident of a node. .. method:: ObjectGraph.createReferences(fromnode, tonode[, edge_data]) Creates a reference from *fromnode* to *tonode*. The optional *edge_data* is associated with the edge. *Fromnode* and *tonode* can either be node objects or the graphident values for nodes. When *fromnode* is :data:`None` *tonode* is a root for the graph. .. method:: removeReference(fromnode, tonode) Removes the reference from *fromnode* to *tonode* if it exists. .. method:: ObjectGraph.getRawIdent(node) Returns the *graphident* attribute of *node*, or the graph itself when *node* is :data:`None`. .. method:: getIdent(node) Same as :meth:`getRawIdent`, but only if the node is part of the graph. *Node* can either be an actual node object or the graphident of a node. .. method:: ObjectGraph.findNode(node) Returns a given node in the graph, or :data:`Node` when it cannot be found. *Node* is either an object with a *graphident* attribute or the *graphident* attribute itself. .. method:: ObjectGraph.__contains__(node) Returns True if *node* is a member of the graph. *Node* is either an object with a *graphident* attribute or the *graphident* attribute itself. .. method:: ObjectGraph.flatten([condition[, start]]) Yield all nodes that are entirely reachable by *condition* starting fromt he given *start* node or the graph root. .. note:: objects are only reachable from the graph root when there is a reference from the root to the node (either directly or through another node) .. method:: ObjectGraph.nodes() Yield all nodes in the graph. .. method:: ObjectGraph.get_edges(node) Returns two iterators that yield the nodes reaching by outgoing and incoming edges for *node*. Note that the iterator for incoming edgets can yield :data:`None` when the *node* is a root of the graph. Use :data:`None` for *node* to fetch the roots of the graph. .. method:: ObjectGraph.filterStack(filters) Filter the ObjectGraph in-place by removing all edges to nodes that do not match every filter in the given filter list Returns a tuple containing the number of: (*nodes_visited*, *nodes_removed*, *nodes_orphaned*) .. method:: ObjectGraph.edgeData(fromNode, toNode): Return the edge data associated with the edge from *fromNode* to *toNode*. Raises :exc:`KeyError` when no such edge exists. .. versionadded: 0.12 .. method:: ObjectGraph.updateEdgeData(fromNode, toNode, edgeData) Replace the data associated with the edge from *fromNode* to *toNode* by *edgeData*. Raises :exc:`KeyError` when the edge does not exist. Debug output ------------ .. data:: ObjectGraph.debug The current debug level. .. method:: ObjectGraph.msg(level, text, \*args) Print a debug message at the current indentation level when the current debug level is *level* or less. .. method:: ObjectGraph.msgin(level, text, \*args) Print a debug message when the current debug level is *level* or less, and increase the indentation level. .. method:: ObjectGraph.msgout(level, text, \*args) Decrease the indentation level and print a debug message when the current debug level is *level* or less. altgraph-0.15/MANIFEST.in0000644000076500000240000000030213207760071015377 0ustar ronaldstaff00000000000000include ReadMe.txt tox.ini include *.txt MANIFEST.in *.py graft doc graft doc/_static graft doc/_templates graft altgraph_tests global-exclude .DS_Store global-exclude *.pyc global-exclude *.so altgraph-0.15/PKG-INFO0000644000076500000240000001743113207760162014752 0ustar ronaldstaff00000000000000Metadata-Version: 1.1 Name: altgraph Version: 0.15 Summary: Python graph (network) package Home-page: https://altgraph.readthedocs.io Author: Ronald Oussoren Author-email: ronaldoussoren@mac.com License: MIT Download-URL: http://pypi.python.org/pypi/altgraph Description: altgraph is a fork of graphlib: a graph (network) package for constructing graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with graphviz output. altgraph includes some additional usage of Python 2.6+ features and enhancements related to modulegraph and macholib. Project links ------------- * `Documentation `_ * `Issue Tracker `_ * `Repository `_ Release history =============== 0.15 ---- * ``ObjectGraph.get_edges``, ``ObjectGraph.getEdgeData`` and ``ObjectGraph.updateEdgeData`` accept *None* as the node to get and treat this as an alias for *self* (as other methods already did). 0.14 ---- - Issue #7: Remove use of ``iteritems`` in altgraph.GraphAlgo code 0.13 ---- - Issue #4: Graph._bfs_subgraph and back_bfs_subgraph return subgraphs with reversed edges Fix by "pombredanne" on bitbucket. 0.12 ---- - Added ``ObjectGraph.edgeData`` to retrieve the edge data from a specific edge. - Added ``AltGraph.update_edge_data`` and ``ObjectGraph.updateEdgeData`` to update the data associated with a graph edge. 0.11 ---- - Stabilize the order of elements in dot file exports, patch from bitbucket user 'pombredanne'. - Tweak setup.py file to remove dependency on distribute (but keep the dependency on setuptools) 0.10.2 ------ - There where no classifiers in the package metadata due to a bug in setup.py 0.10.1 ------ This is a bugfix release Bug fixes: - Issue #3: The source archive contains a README.txt while the setup file refers to ReadMe.txt. This is caused by a misfeature in distutils, as a workaround I've renamed ReadMe.txt to README.txt in the source tree and setup file. 0.10 ----- This is a minor feature release Features: - Do not use "2to3" to support Python 3. As a side effect of this altgraph now supports Python 2.6 and later, and no longer supports earlier releases of Python. - The order of attributes in the Dot output is now always alphabetical. With this change the output will be consistent between runs and Python versions. 0.9 --- This is a minor bugfix release Features: - Added ``altgraph.ObjectGraph.ObjectGraph.nodes``, a method yielding all nodes in an object graph. Bugfixes: - The 0.8 release didn't work with py2app when using python 3.x. 0.8 ----- This is a minor feature release. The major new feature is a extensive set of unittests, which explains almost all other changes in this release. Bugfixes: - Installing failed with Python 2.5 due to using a distutils class that isn't available in that version of Python (issue #1 on the issue tracker) - ``altgraph.GraphStat.degree_dist`` now actually works - ``altgraph.Graph.add_edge(a, b, create_nodes=False)`` will no longer create the edge when one of the nodes doesn't exist. - ``altgraph.Graph.forw_topo_sort`` failed for some sparse graphs. - ``altgraph.Graph.back_topo_sort`` was completely broken in previous releases. - ``altgraph.Graph.forw_bfs_subgraph`` now actually works. - ``altgraph.Graph.back_bfs_subgraph`` now actually works. - ``altgraph.Graph.iterdfs`` now returns the correct result when the ``forward`` argument is ``False``. - ``altgraph.Graph.iterdata`` now returns the correct result when the ``forward`` argument is ``False``. Features: - The ``altgraph.Graph`` constructor now accepts an argument that contains 2- and 3-tuples instead of requireing that all items have the same size. The (optional) argument can now also be any iterator. - ``altgraph.Graph.Graph.add_node`` has no effect when you add a hidden node. - The private method ``altgraph.Graph._bfs`` is no longer present. - The private method ``altgraph.Graph._dfs`` is no longer present. - ``altgraph.ObjectGraph`` now has a ``__contains__`` methods, which means you can use the ``in`` operator to check if a node is part of a graph. - ``altgraph.GraphUtil.generate_random_graph`` will raise ``GraphError`` instead of looping forever when it is impossible to create the requested graph. - ``altgraph.Dot.edge_style`` raises ``GraphError`` when one of the nodes is not present in the graph. The method silently added the tail in the past, but without ensuring a consistent graph state. - ``altgraph.Dot.save_img`` now works when the mode is ``"neato"``. 0.7.2 ----- This is a minor bugfix release Bugfixes: - distutils didn't include the documentation subtree 0.7.1 ----- This is a minor feature release Features: - Documentation is now generated using `sphinx `_ and can be viewed at . - The repository has moved to bitbucket - ``altgraph.GraphStat.avg_hops`` is no longer present, the function had no implementation and no specified behaviour. - the module ``altgraph.compat`` is gone, which means altgraph will no longer work with Python 2.3. 0.7.0 ----- This is a minor feature release. Features: - Support for Python 3 - It is now possible to run tests using 'python setup.py test' (The actual testsuite is still very minimal though) Keywords: graph Platform: any Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Scientific/Engineering :: Mathematics Classifier: Topic :: Scientific/Engineering :: Visualization altgraph-0.15/README.txt0000644000076500000240000000104113207760071015340 0ustar ronaldstaff00000000000000altgraph is a fork of graphlib: a graph (network) package for constructing graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with graphviz output. altgraph includes some additional usage of Python 2.6+ features and enhancements related to modulegraph and macholib. Project links ------------- * `Documentation `_ * `Issue Tracker `_ * `Repository `_ altgraph-0.15/setup.cfg0000644000076500000240000000200113207760162015461 0ustar ronaldstaff00000000000000[metadata] name = altgraph version = 0.15 description = Python graph (network) package long_description_file = README.txt doc/changelog.rst author = Ronald Oussoren author_email = ronaldoussoren@mac.com maintainer = Ronald Oussoren maintainer_email = ronaldoussoren@mac.com url = https://altgraph.readthedocs.io download_url = http://pypi.python.org/pypi/altgraph license = MIT classifiers = Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Topic :: Software Development :: Libraries :: Python Modules Topic :: Scientific/Engineering :: Mathematics Topic :: Scientific/Engineering :: Visualization keywords = graph platforms = any packages = altgraph zip-safe = 1 [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 altgraph-0.15/setup.py0000644000076500000240000006201413207760071015363 0ustar ronaldstaff00000000000000""" Shared setup file for simple python packages. Uses a setup.cfg that is the same as the distutils2 project, unless noted otherwise. It exists for two reasons: 1) This makes it easier to reuse setup.py code between my own projects 2) Easier migration to distutils2 when that catches on. Additional functionality: * Section metadata: requires-test: Same as 'tests_require' option for setuptools. """ import sys import os import re import platform from fnmatch import fnmatch import os import sys import time import tempfile import tarfile try: import urllib.request as urllib except ImportError: import urllib from distutils import log try: from hashlib import md5 except ImportError: from md5 import md5 if sys.version_info[0] == 2: from ConfigParser import RawConfigParser, NoOptionError, NoSectionError else: from configparser import RawConfigParser, NoOptionError, NoSectionError ROOTDIR = os.path.dirname(os.path.abspath(__file__)) # # # # Parsing the setup.cfg and converting it to something that can be # used by setuptools.setup() # # # def eval_marker(value): """ Evaluate an distutils2 environment marker. This code is unsafe when used with hostile setup.cfg files, but that's not a problem for our own files. """ value = value.strip() class M: def __init__(self, **kwds): for k, v in kwds.items(): setattr(self, k, v) variables = { 'python_version': '%d.%d'%(sys.version_info[0], sys.version_info[1]), 'python_full_version': sys.version.split()[0], 'os': M( name=os.name, ), 'sys': M( platform=sys.platform, ), 'platform': M( version=platform.version(), machine=platform.machine(), ), } return bool(eval(value, variables, variables)) return True def _opt_value(cfg, into, section, key, transform = None): try: v = cfg.get(section, key) if transform != _as_lines and ';' in v: v, marker = v.rsplit(';', 1) if not eval_marker(marker): return v = v.strip() if v: if transform: into[key] = transform(v.strip()) else: into[key] = v.strip() except (NoOptionError, NoSectionError): pass def _as_bool(value): if value.lower() in ('y', 'yes', 'on'): return True elif value.lower() in ('n', 'no', 'off'): return False elif value.isdigit(): return bool(int(value)) else: raise ValueError(value) def _as_list(value): return value.split() def _as_lines(value): result = [] for v in value.splitlines(): if ';' in v: v, marker = v.rsplit(';', 1) if not eval_marker(marker): continue v = v.strip() if v: result.append(v) else: result.append(v) return result def _map_requirement(value): m = re.search(r'(\S+)\s*(?:\((.*)\))?', value) name = m.group(1) version = m.group(2) if version is None: return name else: mapped = [] for v in version.split(','): v = v.strip() if v[0].isdigit(): # Checks for a specific version prefix m = v.rsplit('.', 1) mapped.append('>=%s,<%s.%s'%( v, m[0], int(m[1])+1)) else: mapped.append(v) return '%s %s'%(name, ','.join(mapped),) def _as_requires(value): requires = [] for req in value.splitlines(): if ';' in req: req, marker = v.rsplit(';', 1) if not eval_marker(marker): continue req = req.strip() if not req: continue requires.append(_map_requirement(req)) return requires def parse_setup_cfg(): cfg = RawConfigParser() r = cfg.read([os.path.join(ROOTDIR, 'setup.cfg')]) if len(r) != 1: print("Cannot read 'setup.cfg'") sys.exit(1) metadata = dict( name = cfg.get('metadata', 'name'), version = cfg.get('metadata', 'version'), description = cfg.get('metadata', 'description'), ) _opt_value(cfg, metadata, 'metadata', 'license') _opt_value(cfg, metadata, 'metadata', 'maintainer') _opt_value(cfg, metadata, 'metadata', 'maintainer_email') _opt_value(cfg, metadata, 'metadata', 'author') _opt_value(cfg, metadata, 'metadata', 'author_email') _opt_value(cfg, metadata, 'metadata', 'url') _opt_value(cfg, metadata, 'metadata', 'download_url') _opt_value(cfg, metadata, 'metadata', 'classifiers', _as_lines) _opt_value(cfg, metadata, 'metadata', 'platforms', _as_list) _opt_value(cfg, metadata, 'metadata', 'packages', _as_list) _opt_value(cfg, metadata, 'metadata', 'keywords', _as_list) try: v = cfg.get('metadata', 'requires-dist') except (NoOptionError, NoSectionError): pass else: requires = _as_requires(v) if requires: metadata['install_requires'] = requires try: v = cfg.get('metadata', 'requires-test') except (NoOptionError, NoSectionError): pass else: requires = _as_requires(v) if requires: metadata['tests_require'] = requires try: v = cfg.get('metadata', 'long_description_file') except (NoOptionError, NoSectionError): pass else: parts = [] for nm in v.split(): fp = open(nm, 'rU') parts.append(fp.read()) fp.close() metadata['long_description'] = '\n\n'.join(parts) metadata['long_description_content_type'] = 'text/x-rst; charset=UTF-8' try: v = cfg.get('metadata', 'zip-safe') except (NoOptionError, NoSectionError): pass else: metadata['zip_safe'] = _as_bool(v) try: v = cfg.get('metadata', 'console_scripts') except (NoOptionError, NoSectionError): pass else: if 'entry_points' not in metadata: metadata['entry_points'] = {} metadata['entry_points']['console_scripts'] = v.splitlines() if sys.version_info[:2] <= (2,6): try: metadata['tests_require'] += ", unittest2" except KeyError: metadata['tests_require'] = "unittest2" return metadata # # # # Bootstrapping setuptools/distribute, based on # a heavily modified version of distribute_setup.py # # # SETUPTOOLS_PACKAGE='setuptools' try: import subprocess def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 except ImportError: def _python_cmd(*args): args = (sys.executable,) + args new_args = [] for a in args: new_args.append(a.replace("'", "'\"'\"'")) os.system(' '.join(new_args)) == 0 try: import json def get_pypi_src_download(package): url = 'https://pypi.python.org/pypi/%s/json'%(package,) fp = urllib.urlopen(url) try: try: data = fp.read() finally: fp.close() except urllib.error: raise RuntimeError("Cannot determine download link for %s"%(package,)) pkgdata = json.loads(data.decode('utf-8')) if 'urls' not in pkgdata: raise RuntimeError("Cannot determine download link for %s"%(package,)) for info in pkgdata['urls']: if info['packagetype'] == 'sdist' and info['url'].endswith('tar.gz'): return (info.get('md5_digest'), info['url']) raise RuntimeError("Cannot determine downlink link for %s"%(package,)) except ImportError: # Python 2.5 compatibility, no JSON in stdlib but luckily JSON syntax is # simular enough to Python's syntax to be able to abuse the Python compiler import _ast as ast def get_pypi_src_download(package): url = 'https://pypi.python.org/pypi/%s/json'%(package,) fp = urllib.urlopen(url) try: try: data = fp.read() finally: fp.close() except urllib.error: raise RuntimeError("Cannot determine download link for %s"%(package,)) a = compile(data, '-', 'eval', ast.PyCF_ONLY_AST) if not isinstance(a, ast.Expression): raise RuntimeError("Cannot determine download link for %s"%(package,)) a = a.body if not isinstance(a, ast.Dict): raise RuntimeError("Cannot determine download link for %s"%(package,)) for k, v in zip(a.keys, a.values): if not isinstance(k, ast.Str): raise RuntimeError("Cannot determine download link for %s"%(package,)) k = k.s if k == 'urls': a = v break else: raise RuntimeError("PyPI JSON for %s doesn't contain URLs section"%(package,)) if not isinstance(a, ast.List): raise RuntimeError("Cannot determine download link for %s"%(package,)) for info in v.elts: if not isinstance(info, ast.Dict): raise RuntimeError("Cannot determine download link for %s"%(package,)) url = None packagetype = None chksum = None for k, v in zip(info.keys, info.values): if not isinstance(k, ast.Str): raise RuntimeError("Cannot determine download link for %s"%(package,)) if k.s == 'url': if not isinstance(v, ast.Str): raise RuntimeError("Cannot determine download link for %s"%(package,)) url = v.s elif k.s == 'packagetype': if not isinstance(v, ast.Str): raise RuntimeError("Cannot determine download link for %s"%(package,)) packagetype = v.s elif k.s == 'md5_digest': if not isinstance(v, ast.Str): raise RuntimeError("Cannot determine download link for %s"%(package,)) chksum = v.s if url is not None and packagetype == 'sdist' and url.endswith('.tar.gz'): return (chksum, url) raise RuntimeError("Cannot determine download link for %s"%(package,)) def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # building an egg log.warn('Building a %s egg in %s', egg, to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) finally: os.chdir(old_wd) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') def _do_download(to_dir, packagename=SETUPTOOLS_PACKAGE): tarball = download_setuptools(packagename, to_dir) version = tarball.split('-')[-1][:-7] egg = os.path.join(to_dir, '%s-%s-py%d.%d.egg' % (packagename, version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(): # making sure we use the absolute path return _do_download(os.path.abspath(os.curdir)) def download_setuptools(packagename, to_dir): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen chksum, url = get_pypi_src_download(packagename) tgz_name = os.path.basename(url) saveto = os.path.join(to_dir, tgz_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: log.warn("Downloading %s", url) src = urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = src.read() if chksum is not None: data_sum = md5(data).hexdigest() if data_sum != chksum: raise RuntimeError("Downloading %s failed: corrupt checksum"%(url,)) dst = open(saveto, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def _extractall(self, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ import copy import operator from tarfile import ExtractError directories = [] if members is None: members = self for tarinfo in members: if tarinfo.isdir(): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. if sys.version_info < (2, 4): def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) directories.sort(sorter) directories.reverse() else: directories.sort(key=operator.attrgetter('name'), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: dirpath = os.path.join(path, tarinfo.name) try: self.chown(tarinfo, dirpath) self.utime(tarinfo, dirpath) self.chmod(tarinfo, dirpath) except ExtractError: e = sys.exc_info()[1] if self.errorlevel > 1: raise else: self._dbg(1, "tarfile: %s" % e) # # # # Definitions of custom commands # # # try: import setuptools except ImportError: use_setuptools() from setuptools import setup from setuptools.command import egg_info try: from distutils.core import PyPIRCCommand except ImportError: PyPIRCCommand = None # Ancient python version from distutils.core import Command from distutils.errors import DistutilsError from distutils import log if PyPIRCCommand is None: class upload_docs (Command): description = "upload sphinx documentation" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): raise DistutilsError("not supported on this version of python") else: class upload_docs (PyPIRCCommand): description = "upload sphinx documentation" user_options = PyPIRCCommand.user_options def initialize_options(self): PyPIRCCommand.initialize_options(self) self.username = '' self.password = '' def finalize_options(self): PyPIRCCommand.finalize_options(self) config = self._read_pypirc() if config != {}: self.username = config['username'] self.password = config['password'] def run(self): import subprocess import shutil import zipfile import os import urllib import StringIO from base64 import standard_b64encode import httplib import urlparse # Extract the package name from distutils metadata meta = self.distribution.metadata name = meta.get_name() # Run sphinx if os.path.exists('doc/_build'): shutil.rmtree('doc/_build') os.mkdir('doc/_build') p = subprocess.Popen(['make', 'html'], cwd='doc') exit = p.wait() if exit != 0: raise DistutilsError("sphinx-build failed") # Collect sphinx output if not os.path.exists('dist'): os.mkdir('dist') zf = zipfile.ZipFile('dist/%s-docs.zip'%(name,), 'w', compression=zipfile.ZIP_DEFLATED) for toplevel, dirs, files in os.walk('doc/_build/html'): for fn in files: fullname = os.path.join(toplevel, fn) relname = os.path.relpath(fullname, 'doc/_build/html') print ("%s -> %s"%(fullname, relname)) zf.write(fullname, relname) zf.close() # Upload the results, this code is based on the distutils # 'upload' command. content = open('dist/%s-docs.zip'%(name,), 'rb').read() data = { ':action': 'doc_upload', 'name': name, 'content': ('%s-docs.zip'%(name,), content), } auth = "Basic " + standard_b64encode(self.username + ":" + self.password) boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' sep_boundary = '\n--' + boundary end_boundary = sep_boundary + '--' body = StringIO.StringIO() for key, value in data.items(): if not isinstance(value, list): value = [value] for value in value: if isinstance(value, tuple): fn = ';filename="%s"'%(value[0]) value = value[1] else: fn = '' body.write(sep_boundary) body.write('\nContent-Disposition: form-data; name="%s"'%key) body.write(fn) body.write("\n\n") body.write(value) body.write(end_boundary) body.write('\n') body = body.getvalue() self.announce("Uploading documentation to %s"%(self.repository,), log.INFO) schema, netloc, url, params, query, fragments = \ urlparse.urlparse(self.repository) if schema == 'http': http = httplib.HTTPConnection(netloc) elif schema == 'https': http = httplib.HTTPSConnection(netloc) else: raise AssertionError("unsupported schema "+schema) data = '' loglevel = log.INFO try: http.connect() http.putrequest("POST", url) http.putheader('Content-type', 'multipart/form-data; boundary=%s'%boundary) http.putheader('Content-length', str(len(body))) http.putheader('Authorization', auth) http.endheaders() http.send(body) except socket.error: e = socket.exc_info()[1] self.announce(str(e), log.ERROR) return r = http.getresponse() if r.status in (200, 301): self.announce('Upload succeeded (%s): %s' % (r.status, r.reason), log.INFO) else: self.announce('Upload failed (%s): %s' % (r.status, r.reason), log.ERROR) print ('-'*75) print (r.read()) print ('-'*75) def recursiveGlob(root, pathPattern): """ Recursively look for files matching 'pathPattern'. Return a list of matching files/directories. """ result = [] for rootpath, dirnames, filenames in os.walk(root): for fn in filenames: if fnmatch(fn, pathPattern): result.append(os.path.join(rootpath, fn)) return result def importExternalTestCases(unittest, pathPattern="test_*.py", root=".", package=None): """ Import all unittests in the PyObjC tree starting at 'root' """ testFiles = recursiveGlob(root, pathPattern) testModules = map(lambda x:x[len(root)+1:-3].replace('/', '.'), testFiles) if package is not None: testModules = [(package + '.' + m) for m in testModules] suites = [] for modName in testModules: try: module = __import__(modName) except ImportError: print("SKIP %s: %s"%(modName, sys.exc_info()[1])) continue if '.' in modName: for elem in modName.split('.')[1:]: module = getattr(module, elem) s = unittest.defaultTestLoader.loadTestsFromModule(module) suites.append(s) return unittest.TestSuite(suites) class my_egg_info (egg_info.egg_info): def run(self): egg_info.egg_info.run(self) path = os.path.join(self.egg_info, 'PKG-INFO') with open(path, 'a+') as fp: fp.write('Project-URL: Documentation, https://altgraph.readthedocs.io/en/latest/\n') fp.write('Project-URL: Issue tracker, https://bitbucket.org/ronaldoussoren/altgraph/issues?status=new&status=open\n') class my_test (Command): description = "run test suite" user_options = [ ('verbosity=', None, "print what tests are run"), ] def initialize_options(self): self.verbosity='1' def finalize_options(self): if isinstance(self.verbosity, str): self.verbosity = int(self.verbosity) def cleanup_environment(self): ei_cmd = self.get_finalized_command('egg_info') egg_name = ei_cmd.egg_name.replace('-', '_') to_remove = [] for dirname in sys.path: bn = os.path.basename(dirname) if bn.startswith(egg_name + "-"): to_remove.append(dirname) for dirname in to_remove: log.info("removing installed %r from sys.path before testing"%( dirname,)) sys.path.remove(dirname) def add_project_to_sys_path(self): from pkg_resources import normalize_path, add_activation_listener from pkg_resources import working_set, require self.reinitialize_command('egg_info') self.run_command('egg_info') self.reinitialize_command('build_ext', inplace=1) self.run_command('build_ext') # Check if this distribution is already on sys.path # and remove that version, this ensures that the right # copy of the package gets tested. self.__old_path = sys.path[:] self.__old_modules = sys.modules.copy() ei_cmd = self.get_finalized_command('egg_info') sys.path.insert(0, normalize_path(ei_cmd.egg_base)) sys.path.insert(1, os.path.dirname(__file__)) # Strip the namespace packages defined in this distribution # from sys.modules, needed to reset the search path for # those modules. nspkgs = getattr(self.distribution, 'namespace_packages') if nspkgs is not None: for nm in nspkgs: del sys.modules[nm] # Reset pkg_resources state: add_activation_listener(lambda dist: dist.activate()) working_set.__init__() require('%s==%s'%(ei_cmd.egg_name, ei_cmd.egg_version)) def remove_from_sys_path(self): from pkg_resources import working_set sys.path[:] = self.__old_path sys.modules.clear() sys.modules.update(self.__old_modules) working_set.__init__() def run(self): import unittest # Ensure that build directory is on sys.path (py3k) self.cleanup_environment() self.add_project_to_sys_path() try: meta = self.distribution.metadata name = meta.get_name() test_pkg = name + "_tests" suite = importExternalTestCases(unittest, "test_*.py", test_pkg, test_pkg) runner = unittest.TextTestRunner(verbosity=self.verbosity) result = runner.run(suite) # Print out summary. This is a structured format that # should make it easy to use this information in scripts. summary = dict( count=result.testsRun, fails=len(result.failures), errors=len(result.errors), xfails=len(getattr(result, 'expectedFailures', [])), xpass=len(getattr(result, 'expectedSuccesses', [])), skip=len(getattr(result, 'skipped', [])), ) print("SUMMARY: %s"%(summary,)) if summary['fails'] or summary['errors']: sys.exit(1) finally: self.remove_from_sys_path() # # # # And finally run the setuptools main entry point. # # # metadata = parse_setup_cfg() setup( cmdclass=dict( upload_docs=upload_docs, test=my_test, egg_info=my_egg_info, ), **metadata ) altgraph-0.15/tox.ini0000644000076500000240000000102413207760071015156 0ustar ronaldstaff00000000000000[tox] envlist = py27,py34,py35,py36,flake8,coverage-report [testenv] commands = {envbindir}/python -m coverage run --parallel setup.py test deps = coverage [testenv:flake8] deps = flake8 skip_install = true commands = flake8 altgraph [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine coverage html coverage report [coverage:run] branch = True source = altgraph [coverage:report] sort = Cover [coverage:paths] source = altgraph .tox/*/lib/python*/site-packages/altgraph