├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── altgraph ├── Dot.py ├── Graph.py ├── GraphAlgo.py ├── GraphStat.py ├── GraphUtil.py ├── ObjectGraph.py └── __init__.py ├── altgraph_tests ├── __init__.py ├── test_altgraph.py ├── test_dot.py ├── test_graph.py ├── test_graphstat.py ├── test_graphutil.py └── test_object_graph.py ├── doc ├── Makefile ├── _templates │ └── icons.html ├── changelog.rst ├── conf.py ├── core.rst ├── dot.rst ├── graph.rst ├── graphalgo.rst ├── graphstat.rst ├── graphutil.rst ├── index.rst ├── license.rst └── objectgraph.rst ├── setup.cfg ├── setup.py └── tox.ini /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.7] 11 | toxenv: [isort, black, flake8] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: pip cache 17 | uses: actions/cache@v1 18 | with: 19 | path: ~/.cache/pip 20 | key: ${{ matrix.toxenv }}-pip-${{ hashFiles('**/setup.py') }} 21 | restore-keys: | 22 | ${{ matrix.toxenv }}-pip- 23 | 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install --upgrade tox 33 | 34 | - name: Lint 35 | run: tox -e ${{ matrix.toxenv }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: [3.5, 3.6, 3.7, 3.8] 12 | os: [ubuntu-18.04, macos-latest, windows-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Ubuntu cache 18 | uses: actions/cache@v1 19 | if: startsWith(matrix.os, 'ubuntu') 20 | with: 21 | path: ~/.cache/pip 22 | key: 23 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') 24 | }} 25 | restore-keys: | 26 | ${{ matrix.os }}-${{ matrix.python-version }}- 27 | 28 | - name: macOS cache 29 | uses: actions/cache@v1 30 | if: startsWith(matrix.os, 'macOS') 31 | with: 32 | path: ~/Library/Caches/pip 33 | key: 34 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') 35 | }} 36 | restore-keys: | 37 | ${{ matrix.os }}-${{ matrix.python-version }}- 38 | 39 | - name: Windows cache 40 | uses: actions/cache@v1 41 | if: startsWith(matrix.os, 'windows') 42 | with: 43 | path: ~\AppData\Local\pip\Cache 44 | key: 45 | ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') 46 | }} 47 | restore-keys: | 48 | ${{ matrix.os }}-${{ matrix.python-version }}- 49 | 50 | - name: Set up Python ${{ matrix.python-version }} 51 | uses: actions/setup-python@v1 52 | with: 53 | python-version: ${{ matrix.python-version }} 54 | 55 | - name: Install dependencies 56 | run: | 57 | python -m pip install --upgrade pip 58 | python -m pip install --upgrade tox 59 | 60 | - name: Tox tests 61 | shell: bash 62 | run: | 63 | tox -e py 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/_build 2 | build 3 | dist 4 | altgraph.egg-info 5 | .DS_Store 6 | __pycache__ 7 | .coverage 8 | htmlcov 9 | .tox 10 | 11 | syntax: glob 12 | *.dSYM 13 | *.pyc 14 | *.pyo 15 | *.swp 16 | .coverage* 17 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | known_third_party = distutils,pkg_resources,setuptools 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.9 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 23.9.1 6 | hooks: 7 | - id: black 8 | # override until resolved: https://github.com/ambv/black/issues/402 9 | files: \.pyi?$ 10 | types: [] 11 | 12 | - repo: https://github.com/PyCQA/flake8 13 | rev: 6.1.0 14 | hooks: 15 | - id: flake8 16 | exclude: ^altgraph_tests/ 17 | additional_dependencies: 18 | - flake8-bugbear 19 | - flake8-deprecated 20 | - flake8-comprehensions 21 | - flake8-isort 22 | - flake8-quotes 23 | - flake8-mutable 24 | - flake8-todo 25 | 26 | # - repo: https://github.com/asottile/seed-isort-config 27 | # rev: v2.2.0 28 | # hooks: 29 | # - id: seed-isort-config 30 | 31 | # - repo: https://github.com/pre-commit/mirrors-isort 32 | # rev: v5.10.1 33 | # hooks: 34 | # - id: isort 35 | # additional_dependencies: [toml] 36 | 37 | - repo: https://github.com/pre-commit/pre-commit-hooks 38 | rev: v4.4.0 39 | hooks: 40 | - id: trailing-whitespace 41 | - id: end-of-file-fixer 42 | - id: debug-statements 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004 Istvan Albert unless otherwise noted. 2 | Copyright (c) 2006-2010 Bob Ippolito 3 | Copyright (2) 2010-2020 Ronald Oussoren, et. al. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ReadMe.txt tox.ini LICENSE 2 | include *.txt MANIFEST.in *.py 3 | graft doc 4 | graft doc/_static 5 | graft doc/_templates 6 | graft altgraph_tests 7 | global-exclude .DS_Store 8 | global-exclude *.pyc 9 | global-exclude *.so 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | altgraph is a fork of graphlib: a graph (network) package for constructing 2 | graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with 3 | graphviz output. 4 | 5 | altgraph includes some additional usage of Python 2.6+ features and 6 | enhancements related to modulegraph and macholib. 7 | 8 | Project links 9 | ------------- 10 | 11 | * `Documentation `_ 12 | 13 | * `Issue Tracker `_ 14 | 15 | * `Repository `_ 16 | -------------------------------------------------------------------------------- /altgraph/Dot.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.Dot - Interface to the dot language 3 | ============================================ 4 | 5 | The :py:mod:`~altgraph.Dot` module provides a simple interface to the 6 | file format used in the 7 | `graphviz `_ 8 | program. The module is intended to offload the most tedious part of the process 9 | (the **dot** file generation) while transparently exposing most of its 10 | features. 11 | 12 | To display the graphs or to generate image files the 13 | `graphviz `_ 14 | package needs to be installed on the system, moreover the :command:`dot` and 15 | :command:`dotty` programs must be accesible in the program path so that they 16 | can be ran from processes spawned within the module. 17 | 18 | Example usage 19 | ------------- 20 | 21 | Here is a typical usage:: 22 | 23 | from altgraph import Graph, Dot 24 | 25 | # create a graph 26 | edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ] 27 | graph = Graph.Graph(edges) 28 | 29 | # create a dot representation of the graph 30 | dot = Dot.Dot(graph) 31 | 32 | # display the graph 33 | dot.display() 34 | 35 | # save the dot representation into the mydot.dot file 36 | dot.save_dot(file_name='mydot.dot') 37 | 38 | # save dot file as gif image into the graph.gif file 39 | dot.save_img(file_name='graph', file_type='gif') 40 | 41 | Directed graph and non-directed graph 42 | ------------------------------------- 43 | 44 | Dot class can use for both directed graph and non-directed graph 45 | by passing ``graphtype`` parameter. 46 | 47 | Example:: 48 | 49 | # create directed graph(default) 50 | dot = Dot.Dot(graph, graphtype="digraph") 51 | 52 | # create non-directed graph 53 | dot = Dot.Dot(graph, graphtype="graph") 54 | 55 | Customizing the output 56 | ---------------------- 57 | 58 | The graph drawing process may be customized by passing 59 | valid :command:`dot` parameters for the nodes and edges. For a list of all 60 | parameters see the `graphviz `_ 61 | documentation. 62 | 63 | Example:: 64 | 65 | # customizing the way the overall graph is drawn 66 | dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75) 67 | 68 | # customizing node drawing 69 | dot.node_style(1, label='BASE_NODE',shape='box', color='blue' ) 70 | dot.node_style(2, style='filled', fillcolor='red') 71 | 72 | # customizing edge drawing 73 | dot.edge_style(1, 2, style='dotted') 74 | dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90') 75 | dot.edge_style(4, 5, arrowsize=2, style='bold') 76 | 77 | 78 | .. note:: 79 | 80 | dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to 81 | display all graphics styles. To verify the output save it to an image file 82 | and look at it that way. 83 | 84 | Valid attributes 85 | ---------------- 86 | 87 | - dot styles, passed via the :py:meth:`Dot.style` method:: 88 | 89 | rankdir = 'LR' (draws the graph horizontally, left to right) 90 | ranksep = number (rank separation in inches) 91 | 92 | - node attributes, passed via the :py:meth:`Dot.node_style` method:: 93 | 94 | style = 'filled' | 'invisible' | 'diagonals' | 'rounded' 95 | shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle' 96 | 97 | - edge attributes, passed via the :py:meth:`Dot.edge_style` method:: 98 | 99 | style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold' 100 | arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none' 101 | | 'tee' | 'vee' 102 | weight = number (the larger the number the closer the nodes will be) 103 | 104 | - valid `graphviz colors 105 | `_ 106 | 107 | - for more details on how to control the graph drawing process see the 108 | `graphviz reference 109 | `_. 110 | """ 111 | import os 112 | import warnings 113 | 114 | from altgraph import GraphError 115 | 116 | 117 | class Dot(object): 118 | """ 119 | A class providing a **graphviz** (dot language) representation 120 | allowing a fine grained control over how the graph is being 121 | displayed. 122 | 123 | If the :command:`dot` and :command:`dotty` programs are not in the current 124 | system path their location needs to be specified in the contructor. 125 | """ 126 | 127 | def __init__( 128 | self, 129 | graph=None, 130 | nodes=None, 131 | edgefn=None, 132 | nodevisitor=None, 133 | edgevisitor=None, 134 | name="G", 135 | dot="dot", 136 | dotty="dotty", 137 | neato="neato", 138 | graphtype="digraph", 139 | ): 140 | """ 141 | Initialization. 142 | """ 143 | self.name, self.attr = name, {} 144 | 145 | assert graphtype in ["graph", "digraph"] 146 | self.type = graphtype 147 | 148 | self.temp_dot = "tmp_dot.dot" 149 | self.temp_neo = "tmp_neo.dot" 150 | 151 | self.dot, self.dotty, self.neato = dot, dotty, neato 152 | 153 | # self.nodes: node styles 154 | # self.edges: edge styles 155 | self.nodes, self.edges = {}, {} 156 | 157 | if graph is not None and nodes is None: 158 | nodes = graph 159 | if graph is not None and edgefn is None: 160 | 161 | def edgefn(node, graph=graph): 162 | return graph.out_nbrs(node) 163 | 164 | if nodes is None: 165 | nodes = () 166 | 167 | seen = set() 168 | for node in nodes: 169 | if nodevisitor is None: 170 | style = {} 171 | else: 172 | style = nodevisitor(node) 173 | if style is not None: 174 | self.nodes[node] = {} 175 | self.node_style(node, **style) 176 | seen.add(node) 177 | if edgefn is not None: 178 | for head in seen: 179 | for tail in (n for n in edgefn(head) if n in seen): 180 | if edgevisitor is None: 181 | edgestyle = {} 182 | else: 183 | edgestyle = edgevisitor(head, tail) 184 | if edgestyle is not None: 185 | if head not in self.edges: 186 | self.edges[head] = {} 187 | self.edges[head][tail] = {} 188 | self.edge_style(head, tail, **edgestyle) 189 | 190 | def style(self, **attr): 191 | """ 192 | Changes the overall style 193 | """ 194 | self.attr = attr 195 | 196 | def display(self, mode="dot"): 197 | """ 198 | Displays the current graph via dotty 199 | """ 200 | 201 | if mode == "neato": 202 | self.save_dot(self.temp_neo) 203 | neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo) 204 | os.system(neato_cmd) 205 | else: 206 | self.save_dot(self.temp_dot) 207 | 208 | plot_cmd = "%s %s" % (self.dotty, self.temp_dot) 209 | os.system(plot_cmd) 210 | 211 | def node_style(self, node, **kwargs): 212 | """ 213 | Modifies a node style to the dot representation. 214 | """ 215 | if node not in self.edges: 216 | self.edges[node] = {} 217 | self.nodes[node] = kwargs 218 | 219 | def all_node_style(self, **kwargs): 220 | """ 221 | Modifies all node styles 222 | """ 223 | for node in self.nodes: 224 | self.node_style(node, **kwargs) 225 | 226 | def edge_style(self, head, tail, **kwargs): 227 | """ 228 | Modifies an edge style to the dot representation. 229 | """ 230 | if tail not in self.nodes: 231 | raise GraphError("invalid node %s" % (tail,)) 232 | 233 | try: 234 | if tail not in self.edges[head]: 235 | self.edges[head][tail] = {} 236 | self.edges[head][tail] = kwargs 237 | except KeyError: 238 | raise GraphError("invalid edge %s -> %s " % (head, tail)) 239 | 240 | def iterdot(self): 241 | # write graph title 242 | if self.type == "digraph": 243 | yield "digraph %s {\n" % (self.name,) 244 | elif self.type == "graph": 245 | yield "graph %s {\n" % (self.name,) 246 | 247 | else: 248 | raise GraphError("unsupported graphtype %s" % (self.type,)) 249 | 250 | # write overall graph attributes 251 | for attr_name, attr_value in sorted(self.attr.items()): 252 | yield '%s="%s";' % (attr_name, attr_value) 253 | yield "\n" 254 | 255 | # some reusable patterns 256 | cpatt = '%s="%s",' # to separate attributes 257 | epatt = "];\n" # to end attributes 258 | 259 | # write node attributes 260 | for node_name, node_attr in sorted(self.nodes.items()): 261 | yield '\t"%s" [' % (node_name,) 262 | for attr_name, attr_value in sorted(node_attr.items()): 263 | yield cpatt % (attr_name, attr_value) 264 | yield epatt 265 | 266 | # write edge attributes 267 | for head in sorted(self.edges): 268 | for tail in sorted(self.edges[head]): 269 | if self.type == "digraph": 270 | yield '\t"%s" -> "%s" [' % (head, tail) 271 | else: 272 | yield '\t"%s" -- "%s" [' % (head, tail) 273 | for attr_name, attr_value in sorted(self.edges[head][tail].items()): 274 | yield cpatt % (attr_name, attr_value) 275 | yield epatt 276 | 277 | # finish file 278 | yield "}\n" 279 | 280 | def __iter__(self): 281 | return self.iterdot() 282 | 283 | def save_dot(self, file_name=None): 284 | """ 285 | Saves the current graph representation into a file 286 | """ 287 | 288 | if not file_name: 289 | warnings.warn(DeprecationWarning, "always pass a file_name", stacklevel=2) 290 | file_name = self.temp_dot 291 | 292 | with open(file_name, "w") as fp: 293 | for chunk in self.iterdot(): 294 | fp.write(chunk) 295 | 296 | def save_img(self, file_name=None, file_type="gif", mode="dot"): 297 | """ 298 | Saves the dot file as an image file 299 | """ 300 | 301 | if not file_name: 302 | warnings.warn(DeprecationWarning, "always pass a file_name", stacklevel=2) 303 | file_name = "out" 304 | 305 | if mode == "neato": 306 | self.save_dot(self.temp_neo) 307 | neato_cmd = "%s -o %s %s" % (self.neato, self.temp_dot, self.temp_neo) 308 | os.system(neato_cmd) 309 | plot_cmd = self.dot 310 | else: 311 | self.save_dot(self.temp_dot) 312 | plot_cmd = self.dot 313 | 314 | file_name = "%s.%s" % (file_name, file_type) 315 | create_cmd = "%s -T%s %s -o %s" % ( 316 | plot_cmd, 317 | file_type, 318 | self.temp_dot, 319 | file_name, 320 | ) 321 | os.system(create_cmd) 322 | -------------------------------------------------------------------------------- /altgraph/Graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.Graph - Base Graph class 3 | ================================= 4 | 5 | .. 6 | #--Version 2.1 7 | #--Bob Ippolito October, 2004 8 | 9 | #--Version 2.0 10 | #--Istvan Albert June, 2004 11 | 12 | #--Version 1.0 13 | #--Nathan Denny, May 27, 1999 14 | """ 15 | 16 | from collections import deque 17 | 18 | from altgraph import GraphError 19 | 20 | 21 | class Graph(object): 22 | """ 23 | The Graph class represents a directed graph with *N* nodes and *E* edges. 24 | 25 | Naming conventions: 26 | 27 | - the prefixes such as *out*, *inc* and *all* will refer to methods 28 | that operate on the outgoing, incoming or all edges of that node. 29 | 30 | For example: :py:meth:`inc_degree` will refer to the degree of the node 31 | computed over the incoming edges (the number of neighbours linking to 32 | the node). 33 | 34 | - the prefixes such as *forw* and *back* will refer to the 35 | orientation of the edges used in the method with respect to the node. 36 | 37 | For example: :py:meth:`forw_bfs` will start at the node then use the 38 | outgoing edges to traverse the graph (goes forward). 39 | """ 40 | 41 | def __init__(self, edges=None): 42 | """ 43 | Initialization 44 | """ 45 | 46 | self.next_edge = 0 47 | self.nodes, self.edges = {}, {} 48 | self.hidden_edges, self.hidden_nodes = {}, {} 49 | 50 | if edges is not None: 51 | for item in edges: 52 | if len(item) == 2: 53 | head, tail = item 54 | self.add_edge(head, tail) 55 | elif len(item) == 3: 56 | head, tail, data = item 57 | self.add_edge(head, tail, data) 58 | else: 59 | raise GraphError("Cannot create edge from %s" % (item,)) 60 | 61 | def __repr__(self): 62 | return "" % ( 63 | self.number_of_nodes(), 64 | self.number_of_edges(), 65 | ) 66 | 67 | def add_node(self, node, node_data=None): 68 | """ 69 | Adds a new node to the graph. Arbitrary data can be attached to the 70 | node via the node_data parameter. Adding the same node twice will be 71 | silently ignored. 72 | 73 | The node must be a hashable value. 74 | """ 75 | # 76 | # the nodes will contain tuples that will store incoming edges, 77 | # outgoing edges and data 78 | # 79 | # index 0 -> incoming edges 80 | # index 1 -> outgoing edges 81 | 82 | if node in self.hidden_nodes: 83 | # Node is present, but hidden 84 | return 85 | 86 | if node not in self.nodes: 87 | self.nodes[node] = ([], [], node_data) 88 | 89 | def add_edge(self, head_id, tail_id, edge_data=1, create_nodes=True): 90 | """ 91 | Adds a directed edge going from head_id to tail_id. 92 | Arbitrary data can be attached to the edge via edge_data. 93 | It may create the nodes if adding edges between nonexisting ones. 94 | 95 | :param head_id: head node 96 | :param tail_id: tail node 97 | :param edge_data: (optional) data attached to the edge 98 | :param create_nodes: (optional) creates the head_id or tail_id 99 | node in case they did not exist 100 | """ 101 | # shorcut 102 | edge = self.next_edge 103 | 104 | # add nodes if on automatic node creation 105 | if create_nodes: 106 | self.add_node(head_id) 107 | self.add_node(tail_id) 108 | 109 | # update the corresponding incoming and outgoing lists in the nodes 110 | # index 0 -> incoming edges 111 | # index 1 -> outgoing edges 112 | 113 | try: 114 | self.nodes[tail_id][0].append(edge) 115 | self.nodes[head_id][1].append(edge) 116 | except KeyError: 117 | raise GraphError("Invalid nodes %s -> %s" % (head_id, tail_id)) 118 | 119 | # store edge information 120 | self.edges[edge] = (head_id, tail_id, edge_data) 121 | 122 | self.next_edge += 1 123 | 124 | def hide_edge(self, edge): 125 | """ 126 | Hides an edge from the graph. The edge may be unhidden at some later 127 | time. 128 | """ 129 | try: 130 | head_id, tail_id, edge_data = self.hidden_edges[edge] = self.edges[edge] 131 | self.nodes[tail_id][0].remove(edge) 132 | self.nodes[head_id][1].remove(edge) 133 | del self.edges[edge] 134 | except KeyError: 135 | raise GraphError("Invalid edge %s" % edge) 136 | 137 | def hide_node(self, node): 138 | """ 139 | Hides a node from the graph. The incoming and outgoing edges of the 140 | node will also be hidden. The node may be unhidden at some later time. 141 | """ 142 | try: 143 | all_edges = self.all_edges(node) 144 | self.hidden_nodes[node] = (self.nodes[node], all_edges) 145 | for edge in all_edges: 146 | self.hide_edge(edge) 147 | del self.nodes[node] 148 | except KeyError: 149 | raise GraphError("Invalid node %s" % node) 150 | 151 | def restore_node(self, node): 152 | """ 153 | Restores a previously hidden node back into the graph and restores 154 | all of its incoming and outgoing edges. 155 | """ 156 | try: 157 | self.nodes[node], all_edges = self.hidden_nodes[node] 158 | for edge in all_edges: 159 | self.restore_edge(edge) 160 | del self.hidden_nodes[node] 161 | except KeyError: 162 | raise GraphError("Invalid node %s" % node) 163 | 164 | def restore_edge(self, edge): 165 | """ 166 | Restores a previously hidden edge back into the graph. 167 | """ 168 | try: 169 | head_id, tail_id, data = self.hidden_edges[edge] 170 | self.nodes[tail_id][0].append(edge) 171 | self.nodes[head_id][1].append(edge) 172 | self.edges[edge] = head_id, tail_id, data 173 | del self.hidden_edges[edge] 174 | except KeyError: 175 | raise GraphError("Invalid edge %s" % edge) 176 | 177 | def restore_all_edges(self): 178 | """ 179 | Restores all hidden edges. 180 | """ 181 | for edge in list(self.hidden_edges.keys()): 182 | try: 183 | self.restore_edge(edge) 184 | except GraphError: 185 | pass 186 | 187 | def restore_all_nodes(self): 188 | """ 189 | Restores all hidden nodes. 190 | """ 191 | for node in list(self.hidden_nodes.keys()): 192 | self.restore_node(node) 193 | 194 | def __contains__(self, node): 195 | """ 196 | Test whether a node is in the graph 197 | """ 198 | return node in self.nodes 199 | 200 | def edge_by_id(self, edge): 201 | """ 202 | Returns the edge that connects the head_id and tail_id nodes 203 | """ 204 | try: 205 | head, tail, data = self.edges[edge] 206 | except KeyError: 207 | head, tail = None, None 208 | raise GraphError("Invalid edge %s" % edge) 209 | 210 | return (head, tail) 211 | 212 | def edge_by_node(self, head, tail): 213 | """ 214 | Returns the edge that connects the head_id and tail_id nodes 215 | """ 216 | for edge in self.out_edges(head): 217 | if self.tail(edge) == tail: 218 | return edge 219 | return None 220 | 221 | def number_of_nodes(self): 222 | """ 223 | Returns the number of nodes 224 | """ 225 | return len(self.nodes) 226 | 227 | def number_of_edges(self): 228 | """ 229 | Returns the number of edges 230 | """ 231 | return len(self.edges) 232 | 233 | def __iter__(self): 234 | """ 235 | Iterates over all nodes in the graph 236 | """ 237 | return iter(self.nodes) 238 | 239 | def node_list(self): 240 | """ 241 | Return a list of the node ids for all visible nodes in the graph. 242 | """ 243 | return list(self.nodes.keys()) 244 | 245 | def edge_list(self): 246 | """ 247 | Returns an iterator for all visible nodes in the graph. 248 | """ 249 | return list(self.edges.keys()) 250 | 251 | def number_of_hidden_edges(self): 252 | """ 253 | Returns the number of hidden edges 254 | """ 255 | return len(self.hidden_edges) 256 | 257 | def number_of_hidden_nodes(self): 258 | """ 259 | Returns the number of hidden nodes 260 | """ 261 | return len(self.hidden_nodes) 262 | 263 | def hidden_node_list(self): 264 | """ 265 | Returns the list with the hidden nodes 266 | """ 267 | return list(self.hidden_nodes.keys()) 268 | 269 | def hidden_edge_list(self): 270 | """ 271 | Returns a list with the hidden edges 272 | """ 273 | return list(self.hidden_edges.keys()) 274 | 275 | def describe_node(self, node): 276 | """ 277 | return node, node data, outgoing edges, incoming edges for node 278 | """ 279 | incoming, outgoing, data = self.nodes[node] 280 | return node, data, outgoing, incoming 281 | 282 | def describe_edge(self, edge): 283 | """ 284 | return edge, edge data, head, tail for edge 285 | """ 286 | head, tail, data = self.edges[edge] 287 | return edge, data, head, tail 288 | 289 | def node_data(self, node): 290 | """ 291 | Returns the data associated with a node 292 | """ 293 | return self.nodes[node][2] 294 | 295 | def edge_data(self, edge): 296 | """ 297 | Returns the data associated with an edge 298 | """ 299 | return self.edges[edge][2] 300 | 301 | def update_edge_data(self, edge, edge_data): 302 | """ 303 | Replace the edge data for a specific edge 304 | """ 305 | self.edges[edge] = self.edges[edge][0:2] + (edge_data,) 306 | 307 | def head(self, edge): 308 | """ 309 | Returns the node of the head of the edge. 310 | """ 311 | return self.edges[edge][0] 312 | 313 | def tail(self, edge): 314 | """ 315 | Returns node of the tail of the edge. 316 | """ 317 | return self.edges[edge][1] 318 | 319 | def out_nbrs(self, node): 320 | """ 321 | List of nodes connected by outgoing edges 322 | """ 323 | return [self.tail(n) for n in self.out_edges(node)] 324 | 325 | def inc_nbrs(self, node): 326 | """ 327 | List of nodes connected by incoming edges 328 | """ 329 | return [self.head(n) for n in self.inc_edges(node)] 330 | 331 | def all_nbrs(self, node): 332 | """ 333 | List of nodes connected by incoming and outgoing edges 334 | """ 335 | return list(dict.fromkeys(self.inc_nbrs(node) + self.out_nbrs(node))) 336 | 337 | def out_edges(self, node): 338 | """ 339 | Returns a list of the outgoing edges 340 | """ 341 | try: 342 | return list(self.nodes[node][1]) 343 | except KeyError: 344 | raise GraphError("Invalid node %s" % node) 345 | 346 | def inc_edges(self, node): 347 | """ 348 | Returns a list of the incoming edges 349 | """ 350 | try: 351 | return list(self.nodes[node][0]) 352 | except KeyError: 353 | raise GraphError("Invalid node %s" % node) 354 | 355 | def all_edges(self, node): 356 | """ 357 | Returns a list of incoming and outging edges. 358 | """ 359 | return set(self.inc_edges(node) + self.out_edges(node)) 360 | 361 | def out_degree(self, node): 362 | """ 363 | Returns the number of outgoing edges 364 | """ 365 | return len(self.out_edges(node)) 366 | 367 | def inc_degree(self, node): 368 | """ 369 | Returns the number of incoming edges 370 | """ 371 | return len(self.inc_edges(node)) 372 | 373 | def all_degree(self, node): 374 | """ 375 | The total degree of a node 376 | """ 377 | return self.inc_degree(node) + self.out_degree(node) 378 | 379 | def _topo_sort(self, forward=True): 380 | """ 381 | Topological sort. 382 | 383 | Returns a list of nodes where the successors (based on outgoing and 384 | incoming edges selected by the forward parameter) of any given node 385 | appear in the sequence after that node. 386 | """ 387 | topo_list = [] 388 | queue = deque() 389 | indeg = {} 390 | 391 | # select the operation that will be performed 392 | if forward: 393 | get_edges = self.out_edges 394 | get_degree = self.inc_degree 395 | get_next = self.tail 396 | else: 397 | get_edges = self.inc_edges 398 | get_degree = self.out_degree 399 | get_next = self.head 400 | 401 | for node in self.node_list(): 402 | degree = get_degree(node) 403 | if degree: 404 | indeg[node] = degree 405 | else: 406 | queue.append(node) 407 | 408 | while queue: 409 | curr_node = queue.popleft() 410 | topo_list.append(curr_node) 411 | for edge in get_edges(curr_node): 412 | tail_id = get_next(edge) 413 | if tail_id in indeg: 414 | indeg[tail_id] -= 1 415 | if indeg[tail_id] == 0: 416 | queue.append(tail_id) 417 | 418 | if len(topo_list) == len(self.node_list()): 419 | valid = True 420 | else: 421 | # the graph has cycles, invalid topological sort 422 | valid = False 423 | 424 | return (valid, topo_list) 425 | 426 | def forw_topo_sort(self): 427 | """ 428 | Topological sort. 429 | 430 | Returns a list of nodes where the successors (based on outgoing edges) 431 | of any given node appear in the sequence after that node. 432 | """ 433 | return self._topo_sort(forward=True) 434 | 435 | def back_topo_sort(self): 436 | """ 437 | Reverse topological sort. 438 | 439 | Returns a list of nodes where the successors (based on incoming edges) 440 | of any given node appear in the sequence after that node. 441 | """ 442 | return self._topo_sort(forward=False) 443 | 444 | def _bfs_subgraph(self, start_id, forward=True): 445 | """ 446 | Private method creates a subgraph in a bfs order. 447 | 448 | The forward parameter specifies whether it is a forward or backward 449 | traversal. 450 | """ 451 | if forward: 452 | get_bfs = self.forw_bfs 453 | get_nbrs = self.out_nbrs 454 | else: 455 | get_bfs = self.back_bfs 456 | get_nbrs = self.inc_nbrs 457 | 458 | g = Graph() 459 | bfs_list = get_bfs(start_id) 460 | for node in bfs_list: 461 | g.add_node(node) 462 | 463 | for node in bfs_list: 464 | for nbr_id in get_nbrs(node): 465 | if forward: 466 | g.add_edge(node, nbr_id) 467 | else: 468 | g.add_edge(nbr_id, node) 469 | 470 | return g 471 | 472 | def forw_bfs_subgraph(self, start_id): 473 | """ 474 | Creates and returns a subgraph consisting of the breadth first 475 | reachable nodes based on their outgoing edges. 476 | """ 477 | return self._bfs_subgraph(start_id, forward=True) 478 | 479 | def back_bfs_subgraph(self, start_id): 480 | """ 481 | Creates and returns a subgraph consisting of the breadth first 482 | reachable nodes based on the incoming edges. 483 | """ 484 | return self._bfs_subgraph(start_id, forward=False) 485 | 486 | def iterdfs(self, start, end=None, forward=True): 487 | """ 488 | Collecting nodes in some depth first traversal. 489 | 490 | The forward parameter specifies whether it is a forward or backward 491 | traversal. 492 | """ 493 | visited, stack = {start}, deque([start]) 494 | 495 | if forward: 496 | get_edges = self.out_edges 497 | get_next = self.tail 498 | else: 499 | get_edges = self.inc_edges 500 | get_next = self.head 501 | 502 | while stack: 503 | curr_node = stack.pop() 504 | yield curr_node 505 | if curr_node == end: 506 | break 507 | for edge in sorted(get_edges(curr_node)): 508 | tail = get_next(edge) 509 | if tail not in visited: 510 | visited.add(tail) 511 | stack.append(tail) 512 | 513 | def iterdata(self, start, end=None, forward=True, condition=None): 514 | """ 515 | Perform a depth-first walk of the graph (as ``iterdfs``) 516 | and yield the item data of every node where condition matches. The 517 | condition callback is only called when node_data is not None. 518 | """ 519 | 520 | visited, stack = {start}, deque([start]) 521 | 522 | if forward: 523 | get_edges = self.out_edges 524 | get_next = self.tail 525 | else: 526 | get_edges = self.inc_edges 527 | get_next = self.head 528 | 529 | get_data = self.node_data 530 | 531 | while stack: 532 | curr_node = stack.pop() 533 | curr_data = get_data(curr_node) 534 | if curr_data is not None: 535 | if condition is not None and not condition(curr_data): 536 | continue 537 | yield curr_data 538 | if curr_node == end: 539 | break 540 | for edge in get_edges(curr_node): 541 | tail = get_next(edge) 542 | if tail not in visited: 543 | visited.add(tail) 544 | stack.append(tail) 545 | 546 | def _iterbfs(self, start, end=None, forward=True): 547 | """ 548 | The forward parameter specifies whether it is a forward or backward 549 | traversal. Returns a list of tuples where the first value is the hop 550 | value the second value is the node id. 551 | """ 552 | queue, visited = deque([(start, 0)]), {start} 553 | 554 | # the direction of the bfs depends on the edges that are sampled 555 | if forward: 556 | get_edges = self.out_edges 557 | get_next = self.tail 558 | else: 559 | get_edges = self.inc_edges 560 | get_next = self.head 561 | 562 | while queue: 563 | curr_node, curr_step = queue.popleft() 564 | yield (curr_node, curr_step) 565 | if curr_node == end: 566 | break 567 | for edge in get_edges(curr_node): 568 | tail = get_next(edge) 569 | if tail not in visited: 570 | visited.add(tail) 571 | queue.append((tail, curr_step + 1)) 572 | 573 | def forw_bfs(self, start, end=None): 574 | """ 575 | Returns a list of nodes in some forward BFS order. 576 | 577 | Starting from the start node the breadth first search proceeds along 578 | outgoing edges. 579 | """ 580 | return [node for node, step in self._iterbfs(start, end, forward=True)] 581 | 582 | def back_bfs(self, start, end=None): 583 | """ 584 | Returns a list of nodes in some backward BFS order. 585 | 586 | Starting from the start node the breadth first search proceeds along 587 | incoming edges. 588 | """ 589 | return [node for node, _ in self._iterbfs(start, end, forward=False)] 590 | 591 | def forw_dfs(self, start, end=None): 592 | """ 593 | Returns a list of nodes in some forward DFS order. 594 | 595 | Starting with the start node the depth first search proceeds along 596 | outgoing edges. 597 | """ 598 | return list(self.iterdfs(start, end, forward=True)) 599 | 600 | def back_dfs(self, start, end=None): 601 | """ 602 | Returns a list of nodes in some backward DFS order. 603 | 604 | Starting from the start node the depth first search proceeds along 605 | incoming edges. 606 | """ 607 | return list(self.iterdfs(start, end, forward=False)) 608 | 609 | def connected(self): 610 | """ 611 | Returns :py:data:`True` if the graph's every node can be reached from 612 | every other node. 613 | """ 614 | node_list = self.node_list() 615 | for node in node_list: 616 | bfs_list = self.forw_bfs(node) 617 | if len(bfs_list) != len(node_list): 618 | return False 619 | return True 620 | 621 | def clust_coef(self, node): 622 | """ 623 | Computes and returns the local clustering coefficient of node. 624 | 625 | The local cluster coefficient is proportion of the actual number of 626 | edges between neighbours of node and the maximum number of edges 627 | between those neighbours. 628 | 629 | See "Local Clustering Coefficient" on 630 | 631 | for a formal definition. 632 | """ 633 | num = 0 634 | nbr_set = set(self.out_nbrs(node)) 635 | 636 | if node in nbr_set: 637 | nbr_set.remove(node) # loop defense 638 | 639 | for nbr in nbr_set: 640 | sec_set = set(self.out_nbrs(nbr)) 641 | if nbr in sec_set: 642 | sec_set.remove(nbr) # loop defense 643 | num += len(nbr_set & sec_set) 644 | 645 | nbr_num = len(nbr_set) 646 | if nbr_num: 647 | clust_coef = float(num) / (nbr_num * (nbr_num - 1)) 648 | else: 649 | clust_coef = 0.0 650 | return clust_coef 651 | 652 | def get_hops(self, start, end=None, forward=True): 653 | """ 654 | Computes the hop distance to all nodes centered around a node. 655 | 656 | First order neighbours are at hop 1, their neigbours are at hop 2 etc. 657 | Uses :py:meth:`forw_bfs` or :py:meth:`back_bfs` depending on the value 658 | of the forward parameter. If the distance between all neighbouring 659 | nodes is 1 the hop number corresponds to the shortest distance between 660 | the nodes. 661 | 662 | :param start: the starting node 663 | :param end: ending node (optional). When not specified will search the 664 | whole graph. 665 | :param forward: directionality parameter (optional). 666 | If C{True} (default) it uses L{forw_bfs} otherwise L{back_bfs}. 667 | :return: returns a list of tuples where each tuple contains the 668 | node and the hop. 669 | 670 | Typical usage:: 671 | 672 | >>> print (graph.get_hops(1, 8)) 673 | >>> [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] 674 | # node 1 is at 0 hops 675 | # node 2 is at 1 hop 676 | # ... 677 | # node 8 is at 5 hops 678 | """ 679 | if forward: 680 | return list(self._iterbfs(start=start, end=end, forward=True)) 681 | else: 682 | return list(self._iterbfs(start=start, end=end, forward=False)) 683 | -------------------------------------------------------------------------------- /altgraph/GraphAlgo.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.GraphAlgo - Graph algorithms 3 | ===================================== 4 | """ 5 | from altgraph import GraphError 6 | 7 | 8 | def dijkstra(graph, start, end=None): 9 | """ 10 | Dijkstra's algorithm for shortest paths 11 | 12 | `David Eppstein, UC Irvine, 4 April 2002 13 | `_ 14 | 15 | `Python Cookbook Recipe 16 | `_ 17 | 18 | Find shortest paths from the start node to all nodes nearer than or 19 | equal to the end node. 20 | 21 | Dijkstra's algorithm is only guaranteed to work correctly when all edge 22 | lengths are positive. This code does not verify this property for all 23 | edges (only the edges examined until the end vertex is reached), but will 24 | correctly compute shortest paths even for some graphs with negative edges, 25 | and will raise an exception if it discovers that a negative edge has 26 | caused it to make a mistake. 27 | 28 | Adapted to altgraph by Istvan Albert, Pennsylvania State University - 29 | June, 9 2004 30 | """ 31 | D = {} # dictionary of final distances 32 | P = {} # dictionary of predecessors 33 | Q = _priorityDictionary() # estimated distances of non-final vertices 34 | Q[start] = 0 35 | 36 | for v in Q: 37 | D[v] = Q[v] 38 | if v == end: 39 | break 40 | 41 | for w in graph.out_nbrs(v): 42 | edge_id = graph.edge_by_node(v, w) 43 | vwLength = D[v] + graph.edge_data(edge_id) 44 | if w in D: 45 | if vwLength < D[w]: 46 | raise GraphError( 47 | "Dijkstra: found better path to already-final vertex" 48 | ) 49 | elif w not in Q or vwLength < Q[w]: 50 | Q[w] = vwLength 51 | P[w] = v 52 | 53 | return (D, P) 54 | 55 | 56 | def shortest_path(graph, start, end): 57 | """ 58 | Find a single shortest path from the *start* node to the *end* node. 59 | The input has the same conventions as dijkstra(). The output is a list of 60 | the nodes in order along the shortest path. 61 | 62 | **Note that the distances must be stored in the edge data as numeric data** 63 | """ 64 | 65 | D, P = dijkstra(graph, start, end) 66 | Path = [] 67 | while 1: 68 | Path.append(end) 69 | if end == start: 70 | break 71 | end = P[end] 72 | Path.reverse() 73 | return Path 74 | 75 | 76 | # 77 | # Utility classes and functions 78 | # 79 | class _priorityDictionary(dict): 80 | """ 81 | Priority dictionary using binary heaps (internal use only) 82 | 83 | David Eppstein, UC Irvine, 8 Mar 2002 84 | 85 | Implements a data structure that acts almost like a dictionary, with 86 | two modifications: 87 | 88 | 1. D.smallest() returns the value x minimizing D[x]. For this to 89 | work correctly, all values D[x] stored in the dictionary must be 90 | comparable. 91 | 92 | 2. iterating "for x in D" finds and removes the items from D in sorted 93 | order. Each item is not removed until the next item is requested, 94 | so D[x] will still return a useful value until the next iteration 95 | of the for-loop. Each operation takes logarithmic amortized time. 96 | """ 97 | 98 | def __init__(self): 99 | """ 100 | Initialize priorityDictionary by creating binary heap of pairs 101 | (value,key). Note that changing or removing a dict entry will not 102 | remove the old pair from the heap until it is found by smallest() 103 | or until the heap is rebuilt. 104 | """ 105 | self.__heap = [] 106 | dict.__init__(self) 107 | 108 | def smallest(self): 109 | """ 110 | Find smallest item after removing deleted items from front of heap. 111 | """ 112 | if len(self) == 0: 113 | raise IndexError("smallest of empty priorityDictionary") 114 | heap = self.__heap 115 | while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]: 116 | lastItem = heap.pop() 117 | insertionPoint = 0 118 | while 1: 119 | smallChild = 2 * insertionPoint + 1 120 | if ( 121 | smallChild + 1 < len(heap) 122 | and heap[smallChild] > heap[smallChild + 1] 123 | ): 124 | smallChild += 1 125 | if smallChild >= len(heap) or lastItem <= heap[smallChild]: 126 | heap[insertionPoint] = lastItem 127 | break 128 | heap[insertionPoint] = heap[smallChild] 129 | insertionPoint = smallChild 130 | return heap[0][1] 131 | 132 | def __iter__(self): 133 | """ 134 | Create destructive sorted iterator of priorityDictionary. 135 | """ 136 | 137 | def iterfn(): 138 | while len(self) > 0: 139 | x = self.smallest() 140 | yield x 141 | del self[x] 142 | 143 | return iterfn() 144 | 145 | def __setitem__(self, key, val): 146 | """ 147 | Change value stored in dictionary and add corresponding pair to heap. 148 | Rebuilds the heap if the number of deleted items gets large, to avoid 149 | memory leakage. 150 | """ 151 | dict.__setitem__(self, key, val) 152 | heap = self.__heap 153 | if len(heap) > 2 * len(self): 154 | self.__heap = [(v, k) for k, v in self.items()] 155 | self.__heap.sort() 156 | else: 157 | newPair = (val, key) 158 | insertionPoint = len(heap) 159 | heap.append(None) 160 | while insertionPoint > 0 and newPair < heap[(insertionPoint - 1) // 2]: 161 | heap[insertionPoint] = heap[(insertionPoint - 1) // 2] 162 | insertionPoint = (insertionPoint - 1) // 2 163 | heap[insertionPoint] = newPair 164 | 165 | def setdefault(self, key, val): 166 | """ 167 | Reimplement setdefault to pass through our customized __setitem__. 168 | """ 169 | if key not in self: 170 | self[key] = val 171 | return self[key] 172 | -------------------------------------------------------------------------------- /altgraph/GraphStat.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.GraphStat - Functions providing various graph statistics 3 | ================================================================= 4 | """ 5 | 6 | 7 | def degree_dist(graph, limits=(0, 0), bin_num=10, mode="out"): 8 | """ 9 | Computes the degree distribution for a graph. 10 | 11 | Returns a list of tuples where the first element of the tuple is the 12 | center of the bin representing a range of degrees and the second element 13 | of the tuple are the number of nodes with the degree falling in the range. 14 | 15 | Example:: 16 | 17 | .... 18 | """ 19 | 20 | deg = [] 21 | if mode == "inc": 22 | get_deg = graph.inc_degree 23 | else: 24 | get_deg = graph.out_degree 25 | 26 | for node in graph: 27 | deg.append(get_deg(node)) 28 | 29 | if not deg: 30 | return [] 31 | 32 | results = _binning(values=deg, limits=limits, bin_num=bin_num) 33 | 34 | return results 35 | 36 | 37 | _EPS = 1.0 / (2.0**32) 38 | 39 | 40 | def _binning(values, limits=(0, 0), bin_num=10): 41 | """ 42 | Bins data that falls between certain limits, if the limits are (0, 0) the 43 | minimum and maximum values are used. 44 | 45 | Returns a list of tuples where the first element of the tuple is the 46 | center of the bin and the second element of the tuple are the counts. 47 | """ 48 | if limits == (0, 0): 49 | min_val, max_val = min(values) - _EPS, max(values) + _EPS 50 | else: 51 | min_val, max_val = limits 52 | 53 | # get bin size 54 | bin_size = (max_val - min_val) / float(bin_num) 55 | bins = [0] * (bin_num) 56 | 57 | # will ignore these outliers for now 58 | for value in values: 59 | try: 60 | if (value - min_val) >= 0: 61 | index = int((value - min_val) / float(bin_size)) 62 | bins[index] += 1 63 | except IndexError: 64 | pass 65 | 66 | # make it ready for an x,y plot 67 | result = [] 68 | center = (bin_size / 2) + min_val 69 | for i, y in enumerate(bins): 70 | x = center + bin_size * i 71 | result.append((x, y)) 72 | 73 | return result 74 | -------------------------------------------------------------------------------- /altgraph/GraphUtil.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.GraphUtil - Utility classes and functions 3 | ================================================== 4 | """ 5 | 6 | import random 7 | from collections import deque 8 | 9 | from altgraph import Graph, GraphError 10 | 11 | 12 | def generate_random_graph(node_num, edge_num, self_loops=False, multi_edges=False): 13 | """ 14 | Generates and returns a :py:class:`~altgraph.Graph.Graph` instance with 15 | *node_num* nodes randomly connected by *edge_num* edges. 16 | """ 17 | g = Graph.Graph() 18 | 19 | if not multi_edges: 20 | if self_loops: 21 | max_edges = node_num * node_num 22 | else: 23 | max_edges = node_num * (node_num - 1) 24 | 25 | if edge_num > max_edges: 26 | raise GraphError("inconsistent arguments to 'generate_random_graph'") 27 | 28 | nodes = range(node_num) 29 | 30 | for node in nodes: 31 | g.add_node(node) 32 | 33 | while 1: 34 | head = random.choice(nodes) 35 | tail = random.choice(nodes) 36 | 37 | # loop defense 38 | if head == tail and not self_loops: 39 | continue 40 | 41 | # multiple edge defense 42 | if g.edge_by_node(head, tail) is not None and not multi_edges: 43 | continue 44 | 45 | # add the edge 46 | g.add_edge(head, tail) 47 | if g.number_of_edges() >= edge_num: 48 | break 49 | 50 | return g 51 | 52 | 53 | def generate_scale_free_graph(steps, growth_num, self_loops=False, multi_edges=False): 54 | """ 55 | Generates and returns a :py:class:`~altgraph.Graph.Graph` instance that 56 | will have *steps* \\* *growth_num* nodes and a scale free (powerlaw) 57 | connectivity. Starting with a fully connected graph with *growth_num* 58 | nodes at every step *growth_num* nodes are added to the graph and are 59 | connected to existing nodes with a probability proportional to the degree 60 | of these existing nodes. 61 | """ 62 | # The code doesn't seem to do what the documentation claims. 63 | graph = Graph.Graph() 64 | 65 | # initialize the graph 66 | store = [] 67 | for i in range(growth_num): 68 | for j in range(i + 1, growth_num): 69 | store.append(i) 70 | store.append(j) 71 | graph.add_edge(i, j) 72 | 73 | # generate 74 | for node in range(growth_num, steps * growth_num): 75 | graph.add_node(node) 76 | while graph.out_degree(node) < growth_num: 77 | nbr = random.choice(store) 78 | 79 | # loop defense 80 | if node == nbr and not self_loops: 81 | continue 82 | 83 | # multi edge defense 84 | if graph.edge_by_node(node, nbr) and not multi_edges: 85 | continue 86 | 87 | graph.add_edge(node, nbr) 88 | 89 | for nbr in graph.out_nbrs(node): 90 | store.append(node) 91 | store.append(nbr) 92 | 93 | return graph 94 | 95 | 96 | def filter_stack(graph, head, filters): 97 | """ 98 | Perform a walk in a depth-first order starting 99 | at *head*. 100 | 101 | Returns (visited, removes, orphans). 102 | 103 | * visited: the set of visited nodes 104 | * removes: the list of nodes where the node 105 | data does not all *filters* 106 | * orphans: tuples of (last_good, node), 107 | where node is not in removes, is directly 108 | reachable from a node in *removes* and 109 | *last_good* is the closest upstream node that is not 110 | in *removes*. 111 | """ 112 | 113 | visited, removes, orphans = {head}, set(), set() 114 | stack = deque([(head, head)]) 115 | get_data = graph.node_data 116 | get_edges = graph.out_edges 117 | get_tail = graph.tail 118 | 119 | while stack: 120 | last_good, node = stack.pop() 121 | data = get_data(node) 122 | if data is not None: 123 | for filtfunc in filters: 124 | if not filtfunc(data): 125 | removes.add(node) 126 | break 127 | else: 128 | last_good = node 129 | for edge in get_edges(node): 130 | tail = get_tail(edge) 131 | if last_good is not node: 132 | orphans.add((last_good, tail)) 133 | if tail not in visited: 134 | visited.add(tail) 135 | stack.append((last_good, tail)) 136 | 137 | orphans = [(lg, tl) for (lg, tl) in orphans if tl not in removes] 138 | 139 | return visited, removes, orphans 140 | -------------------------------------------------------------------------------- /altgraph/ObjectGraph.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph.ObjectGraph - Graph of objects with an identifier 3 | ========================================================== 4 | 5 | A graph of objects that have a "graphident" attribute. 6 | graphident is the key for the object in the graph 7 | """ 8 | 9 | from altgraph import GraphError 10 | from altgraph.Graph import Graph 11 | from altgraph.GraphUtil import filter_stack 12 | 13 | 14 | class ObjectGraph(object): 15 | """ 16 | A graph of objects that have a "graphident" attribute. 17 | graphident is the key for the object in the graph 18 | """ 19 | 20 | def __init__(self, graph=None, debug=0): 21 | if graph is None: 22 | graph = Graph() 23 | self.graphident = self 24 | self.graph = graph 25 | self.debug = debug 26 | self.indent = 0 27 | graph.add_node(self, None) 28 | 29 | def __repr__(self): 30 | return "<%s>" % (type(self).__name__,) 31 | 32 | def flatten(self, condition=None, start=None): 33 | """ 34 | Iterate over the subgraph that is entirely reachable by condition 35 | starting from the given start node or the ObjectGraph root 36 | """ 37 | if start is None: 38 | start = self 39 | start = self.getRawIdent(start) 40 | return self.graph.iterdata(start=start, condition=condition) 41 | 42 | def nodes(self): 43 | for ident in self.graph: 44 | node = self.graph.node_data(ident) 45 | if node is not None: 46 | yield self.graph.node_data(ident) 47 | 48 | def get_edges(self, node): 49 | if node is None: 50 | node = self 51 | start = self.getRawIdent(node) 52 | _, _, outraw, incraw = self.graph.describe_node(start) 53 | 54 | def iter_edges(lst, n): 55 | seen = set() 56 | for tpl in (self.graph.describe_edge(e) for e in lst): 57 | ident = tpl[n] 58 | if ident not in seen: 59 | yield self.findNode(ident) 60 | seen.add(ident) 61 | 62 | return iter_edges(outraw, 3), iter_edges(incraw, 2) 63 | 64 | def edgeData(self, fromNode, toNode): 65 | if fromNode is None: 66 | fromNode = self 67 | start = self.getRawIdent(fromNode) 68 | stop = self.getRawIdent(toNode) 69 | edge = self.graph.edge_by_node(start, stop) 70 | return self.graph.edge_data(edge) 71 | 72 | def updateEdgeData(self, fromNode, toNode, edgeData): 73 | if fromNode is None: 74 | fromNode = self 75 | start = self.getRawIdent(fromNode) 76 | stop = self.getRawIdent(toNode) 77 | edge = self.graph.edge_by_node(start, stop) 78 | self.graph.update_edge_data(edge, edgeData) 79 | 80 | def filterStack(self, filters): 81 | """ 82 | Filter the ObjectGraph in-place by removing all edges to nodes that 83 | do not match every filter in the given filter list 84 | 85 | Returns a tuple containing the number of: 86 | (nodes_visited, nodes_removed, nodes_orphaned) 87 | """ 88 | visited, removes, orphans = filter_stack(self.graph, self, filters) 89 | 90 | for last_good, tail in orphans: 91 | self.graph.add_edge(last_good, tail, edge_data="orphan") 92 | 93 | for node in removes: 94 | self.graph.hide_node(node) 95 | 96 | return len(visited) - 1, len(removes), len(orphans) 97 | 98 | def removeNode(self, node): 99 | """ 100 | Remove the given node from the graph if it exists 101 | """ 102 | ident = self.getIdent(node) 103 | if ident is not None: 104 | self.graph.hide_node(ident) 105 | 106 | def removeReference(self, fromnode, tonode): 107 | """ 108 | Remove all edges from fromnode to tonode 109 | """ 110 | if fromnode is None: 111 | fromnode = self 112 | fromident = self.getIdent(fromnode) 113 | toident = self.getIdent(tonode) 114 | if fromident is not None and toident is not None: 115 | while True: 116 | edge = self.graph.edge_by_node(fromident, toident) 117 | if edge is None: 118 | break 119 | self.graph.hide_edge(edge) 120 | 121 | def getIdent(self, node): 122 | """ 123 | Get the graph identifier for a node 124 | """ 125 | ident = self.getRawIdent(node) 126 | if ident is not None: 127 | return ident 128 | node = self.findNode(node) 129 | if node is None: 130 | return None 131 | return node.graphident 132 | 133 | def getRawIdent(self, node): 134 | """ 135 | Get the identifier for a node object 136 | """ 137 | if node is self: 138 | return node 139 | ident = getattr(node, "graphident", None) 140 | return ident 141 | 142 | def __contains__(self, node): 143 | return self.findNode(node) is not None 144 | 145 | def findNode(self, node): 146 | """ 147 | Find the node on the graph 148 | """ 149 | ident = self.getRawIdent(node) 150 | if ident is None: 151 | ident = node 152 | try: 153 | return self.graph.node_data(ident) 154 | except KeyError: 155 | return None 156 | 157 | def addNode(self, node): 158 | """ 159 | Add a node to the graph referenced by the root 160 | """ 161 | self.msg(4, "addNode", node) 162 | 163 | try: 164 | self.graph.restore_node(node.graphident) 165 | except GraphError: 166 | self.graph.add_node(node.graphident, node) 167 | 168 | def createReference(self, fromnode, tonode, edge_data=None): 169 | """ 170 | Create a reference from fromnode to tonode 171 | """ 172 | if fromnode is None: 173 | fromnode = self 174 | fromident, toident = self.getIdent(fromnode), self.getIdent(tonode) 175 | if fromident is None or toident is None: 176 | return 177 | self.msg(4, "createReference", fromnode, tonode, edge_data) 178 | self.graph.add_edge(fromident, toident, edge_data=edge_data) 179 | 180 | def createNode(self, cls, name, *args, **kw): 181 | """ 182 | Add a node of type cls to the graph if it does not already exist 183 | by the given name 184 | """ 185 | m = self.findNode(name) 186 | if m is None: 187 | m = cls(name, *args, **kw) 188 | self.addNode(m) 189 | return m 190 | 191 | def msg(self, level, s, *args): 192 | """ 193 | Print a debug message with the given level 194 | """ 195 | if s and level <= self.debug: 196 | print("%s%s %s" % (" " * self.indent, s, " ".join(map(repr, args)))) 197 | 198 | def msgin(self, level, s, *args): 199 | """ 200 | Print a debug message and indent 201 | """ 202 | if level <= self.debug: 203 | self.msg(level, s, *args) 204 | self.indent = self.indent + 1 205 | 206 | def msgout(self, level, s, *args): 207 | """ 208 | Dedent and print a debug message 209 | """ 210 | if level <= self.debug: 211 | self.indent = self.indent - 1 212 | self.msg(level, s, *args) 213 | -------------------------------------------------------------------------------- /altgraph/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | altgraph - a python graph library 3 | ================================= 4 | 5 | altgraph is a fork of `graphlib `_ tailored 6 | to use newer Python 2.3+ features, including additional support used by the 7 | py2app suite (modulegraph and macholib, specifically). 8 | 9 | altgraph is a python based graph (network) representation and manipulation 10 | package. It has started out as an extension to the 11 | `graph_lib module 12 | `_ 13 | written by Nathan Denny it has been significantly optimized and expanded. 14 | 15 | The :class:`altgraph.Graph.Graph` class is loosely modeled after the 16 | `LEDA `_ 17 | (Library of Efficient Datatypes) representation. The library 18 | includes methods for constructing graphs, BFS and DFS traversals, 19 | topological sort, finding connected components, shortest paths as well as a 20 | number graph statistics functions. The library can also visualize graphs 21 | via `graphviz `_. 22 | 23 | The package contains the following modules: 24 | 25 | - the :py:mod:`altgraph.Graph` module contains the 26 | :class:`~altgraph.Graph.Graph` class that stores the graph data 27 | 28 | - the :py:mod:`altgraph.GraphAlgo` module implements graph algorithms 29 | operating on graphs (:py:class:`~altgraph.Graph.Graph`} instances) 30 | 31 | - the :py:mod:`altgraph.GraphStat` module contains functions for 32 | computing statistical measures on graphs 33 | 34 | - the :py:mod:`altgraph.GraphUtil` module contains functions for 35 | generating, reading and saving graphs 36 | 37 | - the :py:mod:`altgraph.Dot` module contains functions for displaying 38 | graphs via `graphviz `_ 39 | 40 | - the :py:mod:`altgraph.ObjectGraph` module implements a graph of 41 | objects with a unique identifier 42 | 43 | Installation 44 | ------------ 45 | 46 | Download and unpack the archive then type:: 47 | 48 | python setup.py install 49 | 50 | This will install the library in the default location. For instructions on 51 | how to customize the install procedure read the output of:: 52 | 53 | python setup.py --help install 54 | 55 | To verify that the code works run the test suite:: 56 | 57 | python setup.py test 58 | 59 | Example usage 60 | ------------- 61 | 62 | Lets assume that we want to analyze the graph below (links to the full picture) 63 | GRAPH_IMG. Our script then might look the following way:: 64 | 65 | from altgraph import Graph, GraphAlgo, Dot 66 | 67 | # these are the edges 68 | edges = [ (1,2), (2,4), (1,3), (2,4), (3,4), (4,5), (6,5), 69 | (6,14), (14,15), (6, 15), (5,7), (7, 8), (7,13), (12,8), 70 | (8,13), (11,12), (11,9), (13,11), (9,13), (13,10) ] 71 | 72 | # creates the graph 73 | graph = Graph.Graph() 74 | for head, tail in edges: 75 | graph.add_edge(head, tail) 76 | 77 | # do a forward bfs from 1 at most to 20 78 | print(graph.forw_bfs(1)) 79 | 80 | This will print the nodes in some breadth first order:: 81 | 82 | [1, 2, 3, 4, 5, 7, 8, 13, 11, 10, 12, 9] 83 | 84 | If we wanted to get the hop-distance from node 1 to node 8 85 | we coud write:: 86 | 87 | print(graph.get_hops(1, 8)) 88 | 89 | This will print the following:: 90 | 91 | [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] 92 | 93 | Node 1 is at 0 hops since it is the starting node, nodes 2,3 are 1 hop away ... 94 | node 8 is 5 hops away. To find the shortest distance between two nodes you 95 | can use:: 96 | 97 | print(GraphAlgo.shortest_path(graph, 1, 12)) 98 | 99 | It will print the nodes on one (if there are more) the shortest paths:: 100 | 101 | [1, 2, 4, 5, 7, 13, 11, 12] 102 | 103 | To display the graph we can use the GraphViz backend:: 104 | 105 | dot = Dot.Dot(graph) 106 | 107 | # display the graph on the monitor 108 | dot.display() 109 | 110 | # save it in an image file 111 | dot.save_img(file_name='graph', file_type='gif') 112 | 113 | 114 | 115 | .. 116 | @author: U{Istvan Albert} 117 | 118 | @license: MIT License 119 | 120 | Copyright (c) 2004 Istvan Albert unless otherwise noted. 121 | 122 | Permission is hereby granted, free of charge, to any person obtaining a copy 123 | of this software and associated documentation files (the "Software"), to 124 | deal in the Software without restriction, including without limitation the 125 | rights to use, copy, modify, merge, publish, distribute, sublicense, 126 | and/or sell copies of the Software, and to permit persons to whom the 127 | Software is furnished to do so. 128 | 129 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 130 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 131 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 132 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 133 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 134 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 135 | IN THE SOFTWARE. 136 | @requires: Python 2.3 or higher 137 | 138 | @newfield contributor: Contributors: 139 | @contributor: U{Reka Albert } 140 | 141 | """ 142 | import pkg_resources 143 | 144 | __version__ = pkg_resources.require("altgraph")[0].version 145 | 146 | 147 | class GraphError(ValueError): 148 | pass 149 | -------------------------------------------------------------------------------- /altgraph_tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ altgraph tests """ 2 | -------------------------------------------------------------------------------- /altgraph_tests/test_altgraph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env py.test 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from altgraph import Graph, GraphAlgo 7 | 8 | 9 | class BasicTests(unittest.TestCase): 10 | def setUp(self): 11 | self.edges = [ 12 | (1, 2), 13 | (2, 4), 14 | (1, 3), 15 | (2, 4), 16 | (3, 4), 17 | (4, 5), 18 | (6, 5), 19 | (6, 14), 20 | (14, 15), 21 | (6, 15), 22 | (5, 7), 23 | (7, 8), 24 | (7, 13), 25 | (12, 8), 26 | (8, 13), 27 | (11, 12), 28 | (11, 9), 29 | (13, 11), 30 | (9, 13), 31 | (13, 10), 32 | ] 33 | 34 | # these are the edges 35 | self.store = {} 36 | self.g = Graph.Graph() 37 | for head, tail in self.edges: 38 | self.store[head] = self.store[tail] = None 39 | self.g.add_edge(head, tail) 40 | 41 | def test_num_edges(self): 42 | # check the parameters 43 | self.assertEqual(self.g.number_of_nodes(), len(self.store)) 44 | self.assertEqual(self.g.number_of_edges(), len(self.edges)) 45 | 46 | def test_forw_bfs(self): 47 | # do a forward bfs 48 | self.assertEqual(self.g.forw_bfs(1), [1, 2, 3, 4, 5, 7, 8, 13, 11, 10, 12, 9]) 49 | 50 | def test_get_hops(self): 51 | # diplay the hops and hop numbers between nodes 52 | self.assertEqual( 53 | self.g.get_hops(1, 8), 54 | [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)], 55 | ) 56 | 57 | def test_shortest_path(self): 58 | self.assertEqual( 59 | GraphAlgo.shortest_path(self.g, 1, 12), [1, 2, 4, 5, 7, 13, 11, 12] 60 | ) 61 | 62 | 63 | if __name__ == "__main__": # pragma: no cover 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /altgraph_tests/test_dot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from altgraph import Dot, Graph, GraphError 5 | 6 | 7 | class TestDot(unittest.TestCase): 8 | def test_constructor(self): 9 | g = Graph.Graph( 10 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 11 | ) 12 | 13 | dot = Dot.Dot(g) 14 | 15 | self.assertEqual(dot.name, "G") 16 | self.assertEqual(dot.attr, {}) 17 | self.assertEqual(dot.temp_dot, "tmp_dot.dot") 18 | self.assertEqual(dot.temp_neo, "tmp_neo.dot") 19 | self.assertEqual(dot.dot, "dot") 20 | self.assertEqual(dot.dotty, "dotty") 21 | self.assertEqual(dot.neato, "neato") 22 | self.assertEqual(dot.type, "digraph") 23 | 24 | self.assertEqual(dot.nodes, dict([(x, {}) for x in g])) 25 | 26 | edges = {} 27 | for head in g: 28 | edges[head] = {} 29 | for tail in g.out_nbrs(head): 30 | edges[head][tail] = {} 31 | 32 | self.assertEqual(dot.edges[1], edges[1]) 33 | self.assertEqual(dot.edges, edges) 34 | 35 | dot = Dot.Dot( 36 | g, 37 | nodes=[1, 2], 38 | edgefn=lambda node: list(sorted(g.out_nbrs(node)))[:-1], 39 | nodevisitor=lambda node: {"label": node}, 40 | edgevisitor=lambda head, tail: {"label": (head, tail)}, 41 | name="testgraph", 42 | dot="/usr/local/bin/dot", 43 | dotty="/usr/local/bin/dotty", 44 | neato="/usr/local/bin/neato", 45 | graphtype="graph", 46 | ) 47 | 48 | self.assertEqual(dot.name, "testgraph") 49 | self.assertEqual(dot.attr, {}) 50 | self.assertEqual(dot.temp_dot, "tmp_dot.dot") 51 | self.assertEqual(dot.temp_neo, "tmp_neo.dot") 52 | self.assertEqual(dot.dot, "/usr/local/bin/dot") 53 | self.assertEqual(dot.dotty, "/usr/local/bin/dotty") 54 | self.assertEqual(dot.neato, "/usr/local/bin/neato") 55 | self.assertEqual(dot.type, "graph") 56 | 57 | self.assertEqual(dot.nodes, dict([(x, {"label": x}) for x in [1, 2]])) 58 | 59 | edges = {} 60 | for head in [1, 2]: 61 | edges[head] = {} 62 | for tail in list(sorted(g.out_nbrs(head)))[:-1]: 63 | if tail not in [1, 2]: 64 | continue 65 | edges[head][tail] = {"label": (head, tail)} 66 | 67 | self.assertEqual(dot.edges[1], edges[1]) 68 | self.assertEqual(dot.edges, edges) 69 | 70 | self.assertRaises(GraphError, Dot.Dot, g, nodes=[1, 2, 9]) 71 | 72 | def test_style(self): 73 | g = Graph.Graph([]) 74 | 75 | dot = Dot.Dot(g) 76 | 77 | self.assertEqual(dot.attr, {}) 78 | 79 | dot.style(key="value") 80 | self.assertEqual(dot.attr, {"key": "value"}) 81 | 82 | dot.style(key2="value2") 83 | self.assertEqual(dot.attr, {"key2": "value2"}) 84 | 85 | def test_node_style(self): 86 | g = Graph.Graph( 87 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 88 | ) 89 | 90 | dot = Dot.Dot(g) 91 | 92 | self.assertEqual(dot.nodes[1], {}) 93 | 94 | dot.node_style(1, key="value") 95 | self.assertEqual(dot.nodes[1], {"key": "value"}) 96 | 97 | dot.node_style(1, key2="value2") 98 | self.assertEqual(dot.nodes[1], {"key2": "value2"}) 99 | self.assertEqual(dot.nodes[2], {}) 100 | 101 | dot.all_node_style(key3="value3") 102 | for n in g: 103 | self.assertEqual(dot.nodes[n], {"key3": "value3"}) 104 | 105 | self.assertTrue(9 not in dot.nodes) 106 | dot.node_style(9, key="value") 107 | self.assertEqual(dot.nodes[9], {"key": "value"}) 108 | 109 | def test_edge_style(self): 110 | g = Graph.Graph( 111 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 112 | ) 113 | 114 | dot = Dot.Dot(g) 115 | 116 | self.assertEqual(dot.edges[1][2], {}) 117 | dot.edge_style(1, 2, foo="bar") 118 | self.assertEqual(dot.edges[1][2], {"foo": "bar"}) 119 | 120 | dot.edge_style(1, 2, foo2="2bar") 121 | self.assertEqual(dot.edges[1][2], {"foo2": "2bar"}) 122 | 123 | self.assertEqual(dot.edges[1][3], {}) 124 | 125 | self.assertFalse(6 in dot.edges[1]) 126 | dot.edge_style(1, 6, foo2="2bar") 127 | self.assertEqual(dot.edges[1][6], {"foo2": "2bar"}) 128 | 129 | self.assertRaises(GraphError, dot.edge_style, 1, 9, a=1) 130 | self.assertRaises(GraphError, dot.edge_style, 9, 1, a=1) 131 | 132 | def test_iter(self): 133 | g = Graph.Graph( 134 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 135 | ) 136 | 137 | dot = Dot.Dot(g) 138 | dot.style(graph="foobar") 139 | dot.node_style(1, key="value") 140 | dot.node_style(2, key="another", key2="world") 141 | dot.edge_style(1, 4, key1="value1", key2="value2") 142 | dot.edge_style(2, 4, key1="valueA") 143 | 144 | self.assertEqual(list(iter(dot)), list(dot.iterdot())) 145 | 146 | for item in dot.iterdot(): 147 | self.assertTrue(isinstance(item, str)) 148 | 149 | first = list(dot.iterdot())[0] 150 | self.assertEqual(first, "digraph %s {\n" % (dot.name,)) 151 | 152 | dot.type = "graph" 153 | first = list(dot.iterdot())[0] 154 | self.assertEqual(first, "graph %s {\n" % (dot.name,)) 155 | 156 | dot.type = "foo" 157 | self.assertRaises(GraphError, list, dot.iterdot()) 158 | dot.type = "digraph" 159 | 160 | self.assertEqual( 161 | list(dot), 162 | [ 163 | "digraph G {\n", 164 | 'graph="foobar";', 165 | "\n", 166 | '\t"1" [', 167 | 'key="value",', 168 | "];\n", 169 | '\t"2" [', 170 | 'key="another",', 171 | 'key2="world",', 172 | "];\n", 173 | '\t"3" [', 174 | "];\n", 175 | '\t"4" [', 176 | "];\n", 177 | '\t"6" [', 178 | "];\n", 179 | '\t"7" [', 180 | "];\n", 181 | '\t"1" -> "2" [', 182 | "];\n", 183 | '\t"1" -> "3" [', 184 | "];\n", 185 | '\t"1" -> "4" [', 186 | 'key1="value1",', 187 | 'key2="value2",', 188 | "];\n", 189 | '\t"2" -> "4" [', 190 | 'key1="valueA",', 191 | "];\n", 192 | '\t"2" -> "6" [', 193 | "];\n", 194 | '\t"2" -> "7" [', 195 | "];\n", 196 | '\t"6" -> "1" [', 197 | "];\n", 198 | '\t"7" -> "4" [', 199 | "];\n", 200 | "}\n", 201 | ], 202 | ) 203 | 204 | def test_save(self): 205 | g = Graph.Graph( 206 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 207 | ) 208 | 209 | dot = Dot.Dot(g) 210 | dot.style(graph="foobar") 211 | dot.node_style(1, key="value") 212 | dot.node_style(2, key="another", key2="world") 213 | dot.edge_style(1, 4, key1="value1", key2="value2") 214 | dot.edge_style(2, 4, key1="valueA") 215 | 216 | fn = "test_dot.dot" 217 | self.assertTrue(not os.path.exists(fn)) 218 | 219 | try: 220 | dot.save_dot(fn) 221 | 222 | fp = open(fn, "r") 223 | data = fp.read() 224 | fp.close() 225 | self.assertEqual(data, "".join(dot)) 226 | 227 | finally: 228 | if os.path.exists(fn): 229 | os.unlink(fn) 230 | 231 | def test_img(self): 232 | g = Graph.Graph( 233 | [(1, 2), (1, 3), (1, 4), (2, 4), (2, 6), (2, 7), (7, 4), (6, 1)] 234 | ) 235 | 236 | dot = Dot.Dot( 237 | g, 238 | dot="/usr/local/bin/!!dot", 239 | dotty="/usr/local/bin/!!dotty", 240 | neato="/usr/local/bin/!!neato", 241 | ) 242 | dot.style(size="10,10", rankdir="RL", page="5, 5", ranksep=0.75) 243 | dot.node_style(1, label="BASE_NODE", shape="box", color="blue") 244 | dot.node_style(2, style="filled", fillcolor="red") 245 | dot.edge_style(1, 4, style="dotted") 246 | dot.edge_style(2, 4, arrowhead="dot", label="binds", labelangle="90") 247 | 248 | system_cmds = [] 249 | 250 | def fake_system(cmd): 251 | system_cmds.append(cmd) 252 | return None 253 | 254 | try: 255 | real_system = os.system 256 | os.system = fake_system 257 | 258 | system_cmds = [] 259 | dot.save_img("foo") 260 | self.assertEqual( 261 | system_cmds, ["/usr/local/bin/!!dot -Tgif tmp_dot.dot -o foo.gif"] 262 | ) 263 | 264 | system_cmds = [] 265 | dot.save_img("foo", file_type="jpg") 266 | self.assertEqual( 267 | system_cmds, ["/usr/local/bin/!!dot -Tjpg tmp_dot.dot -o foo.jpg"] 268 | ) 269 | 270 | system_cmds = [] 271 | dot.save_img("bar", file_type="jpg", mode="neato") 272 | self.assertEqual( 273 | system_cmds, 274 | [ 275 | "/usr/local/bin/!!neato -o tmp_dot.dot tmp_neo.dot", 276 | "/usr/local/bin/!!dot -Tjpg tmp_dot.dot -o bar.jpg", 277 | ], 278 | ) 279 | 280 | system_cmds = [] 281 | dot.display() 282 | self.assertEqual(system_cmds, ["/usr/local/bin/!!dotty tmp_dot.dot"]) 283 | 284 | system_cmds = [] 285 | dot.display(mode="neato") 286 | self.assertEqual( 287 | system_cmds, 288 | [ 289 | "/usr/local/bin/!!neato -o tmp_dot.dot tmp_neo.dot", 290 | "/usr/local/bin/!!dotty tmp_dot.dot", 291 | ], 292 | ) 293 | 294 | finally: 295 | if os.path.exists(dot.temp_dot): 296 | os.unlink(dot.temp_dot) 297 | if os.path.exists(dot.temp_neo): 298 | os.unlink(dot.temp_neo) 299 | os.system = real_system 300 | 301 | if os.path.exists("/usr/local/bin/dot") and os.path.exists( 302 | "/usr/local/bin/neato" 303 | ): 304 | try: 305 | dot.dot = "/usr/local/bin/dot" 306 | dot.neato = "/usr/local/bin/neato" 307 | self.assertFalse(os.path.exists("foo.gif")) 308 | dot.save_img("foo") 309 | self.assertTrue(os.path.exists("foo.gif")) 310 | os.unlink("foo.gif") 311 | 312 | self.assertFalse(os.path.exists("foo.gif")) 313 | dot.save_img("foo", mode="neato") 314 | self.assertTrue(os.path.exists("foo.gif")) 315 | os.unlink("foo.gif") 316 | 317 | finally: 318 | if os.path.exists(dot.temp_dot): 319 | os.unlink(dot.temp_dot) 320 | if os.path.exists(dot.temp_neo): 321 | os.unlink(dot.temp_neo) 322 | 323 | 324 | if __name__ == "__main__": # pragma: no cover 325 | unittest.main() 326 | -------------------------------------------------------------------------------- /altgraph_tests/test_graph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from altgraph import GraphError 4 | from altgraph.Graph import Graph 5 | 6 | 7 | class TestGraph(unittest.TestCase): 8 | def test_nodes(self): 9 | graph = Graph() 10 | 11 | self.assertEqual(graph.node_list(), []) 12 | 13 | o1 = object() 14 | o1b = object() 15 | o2 = object() 16 | graph.add_node(1, o1) 17 | graph.add_node(1, o1b) 18 | graph.add_node(2, o2) 19 | graph.add_node(3) 20 | 21 | self.assertRaises(TypeError, graph.add_node, []) 22 | 23 | self.assertTrue(graph.node_data(1) is o1) 24 | self.assertTrue(graph.node_data(2) is o2) 25 | self.assertTrue(graph.node_data(3) is None) 26 | 27 | self.assertTrue(1 in graph) 28 | self.assertTrue(2 in graph) 29 | self.assertTrue(3 in graph) 30 | 31 | self.assertEqual(graph.number_of_nodes(), 3) 32 | self.assertEqual(graph.number_of_hidden_nodes(), 0) 33 | self.assertEqual(graph.hidden_node_list(), []) 34 | self.assertEqual(list(sorted(graph)), [1, 2, 3]) 35 | 36 | graph.hide_node(1) 37 | graph.hide_node(2) 38 | graph.hide_node(3) 39 | 40 | self.assertEqual(graph.number_of_nodes(), 0) 41 | self.assertEqual(graph.number_of_hidden_nodes(), 3) 42 | self.assertEqual(list(sorted(graph.hidden_node_list())), [1, 2, 3]) 43 | 44 | self.assertFalse(1 in graph) 45 | self.assertFalse(2 in graph) 46 | self.assertFalse(3 in graph) 47 | 48 | graph.add_node(1) 49 | self.assertFalse(1 in graph) 50 | 51 | graph.restore_node(1) 52 | self.assertTrue(1 in graph) 53 | self.assertFalse(2 in graph) 54 | self.assertFalse(3 in graph) 55 | 56 | graph.restore_all_nodes() 57 | self.assertTrue(1 in graph) 58 | self.assertTrue(2 in graph) 59 | self.assertTrue(3 in graph) 60 | 61 | self.assertEqual(list(sorted(graph.node_list())), [1, 2, 3]) 62 | 63 | v = graph.describe_node(1) 64 | self.assertEqual(v, (1, o1, [], [])) 65 | 66 | def test_edges(self): 67 | graph = Graph() 68 | graph.add_node(1) 69 | graph.add_node(2) 70 | graph.add_node(3) 71 | graph.add_node(4) 72 | graph.add_node(5) 73 | 74 | self.assertTrue(isinstance(graph.edge_list(), list)) 75 | 76 | graph.add_edge(1, 2) 77 | graph.add_edge(4, 5, "a") 78 | 79 | self.assertRaises(GraphError, graph.add_edge, "a", "b", create_nodes=False) 80 | 81 | self.assertEqual(graph.number_of_hidden_edges(), 0) 82 | self.assertEqual(graph.number_of_edges(), 2) 83 | self.assertEqual(graph.hidden_edge_list(), []) 84 | e = graph.edge_by_node(1, 2) 85 | self.assertTrue(isinstance(e, int)) 86 | graph.hide_edge(e) 87 | self.assertEqual(graph.number_of_hidden_edges(), 1) 88 | self.assertEqual(graph.number_of_edges(), 1) 89 | self.assertEqual(graph.hidden_edge_list(), [e]) 90 | e2 = graph.edge_by_node(1, 2) 91 | self.assertTrue(e2 is None) 92 | 93 | graph.restore_edge(e) 94 | e2 = graph.edge_by_node(1, 2) 95 | self.assertEqual(e, e2) 96 | self.assertEqual(graph.number_of_hidden_edges(), 0) 97 | 98 | self.assertEqual(graph.number_of_edges(), 2) 99 | 100 | e1 = graph.edge_by_node(1, 2) 101 | e2 = graph.edge_by_node(4, 5) 102 | graph.hide_edge(e1) 103 | graph.hide_edge(e2) 104 | 105 | self.assertEqual(graph.number_of_edges(), 0) 106 | graph.restore_all_edges() 107 | self.assertEqual(graph.number_of_edges(), 2) 108 | 109 | self.assertEqual(graph.edge_by_id(e1), (1, 2)) 110 | self.assertRaises(GraphError, graph.edge_by_id, (e1 + 1) * (e2 + 1) + 1) 111 | 112 | self.assertEqual(list(sorted(graph.edge_list())), [e1, e2]) 113 | 114 | self.assertEqual(graph.describe_edge(e1), (e1, 1, 1, 2)) 115 | self.assertEqual(graph.describe_edge(e2), (e2, "a", 4, 5)) 116 | 117 | self.assertEqual(graph.edge_data(e1), 1) 118 | self.assertEqual(graph.edge_data(e2), "a") 119 | 120 | self.assertEqual(graph.head(e2), 4) 121 | self.assertEqual(graph.tail(e2), 5) 122 | 123 | graph.add_edge(1, 3) 124 | graph.add_edge(1, 5) 125 | graph.add_edge(4, 1) 126 | 127 | self.assertEqual(list(sorted(graph.out_nbrs(1))), [2, 3, 5]) 128 | self.assertEqual(list(sorted(graph.inc_nbrs(1))), [4]) 129 | self.assertEqual(list(sorted(graph.inc_nbrs(5))), [1, 4]) 130 | self.assertEqual(list(sorted(graph.all_nbrs(1))), [2, 3, 4, 5]) 131 | 132 | graph.add_edge(5, 1) 133 | self.assertEqual(list(sorted(graph.all_nbrs(5))), [1, 4]) 134 | 135 | self.assertEqual(graph.out_degree(1), 3) 136 | self.assertEqual(graph.inc_degree(2), 1) 137 | self.assertEqual(graph.inc_degree(5), 2) 138 | self.assertEqual(graph.all_degree(5), 3) 139 | 140 | v = graph.out_edges(4) 141 | self.assertTrue(isinstance(v, list)) 142 | self.assertEqual(graph.edge_by_id(v[0]), (4, 5)) 143 | 144 | v = graph.out_edges(1) 145 | for e in v: 146 | self.assertEqual(graph.edge_by_id(e)[0], 1) 147 | 148 | v = graph.inc_edges(1) 149 | self.assertTrue(isinstance(v, list)) 150 | self.assertEqual(graph.edge_by_id(v[0]), (4, 1)) 151 | 152 | v = graph.inc_edges(5) 153 | for e in v: 154 | self.assertEqual(graph.edge_by_id(e)[1], 5) 155 | 156 | v = graph.all_edges(5) 157 | for e in v: 158 | self.assertTrue(graph.edge_by_id(e)[1] == 5 or graph.edge_by_id(e)[0] == 5) 159 | 160 | e1 = graph.edge_by_node(1, 2) 161 | self.assertTrue(isinstance(e1, int)) 162 | graph.hide_node(1) 163 | self.assertRaises(GraphError, graph.edge_by_node, 1, 2) 164 | graph.restore_node(1) 165 | e2 = graph.edge_by_node(1, 2) 166 | self.assertEqual(e1, e2) 167 | 168 | self.assertRaises(GraphError, graph.hide_edge, "foo") 169 | self.assertRaises(GraphError, graph.hide_node, "foo") 170 | self.assertRaises(GraphError, graph.inc_edges, "foo") 171 | 172 | self.assertEqual(repr(graph), "") 173 | 174 | def test_toposort(self): 175 | graph = Graph() 176 | graph.add_node(1) 177 | graph.add_node(2) 178 | graph.add_node(3) 179 | graph.add_node(4) 180 | graph.add_node(5) 181 | 182 | graph.add_edge(1, 2) 183 | graph.add_edge(1, 3) 184 | graph.add_edge(2, 4) 185 | graph.add_edge(3, 5) 186 | 187 | ok, result = graph.forw_topo_sort() 188 | self.assertTrue(ok) 189 | for idx in range(1, 6): 190 | self.assertTrue(idx in result) 191 | 192 | self.assertTrue(result.index(1) < result.index(2)) 193 | self.assertTrue(result.index(1) < result.index(3)) 194 | self.assertTrue(result.index(2) < result.index(4)) 195 | self.assertTrue(result.index(3) < result.index(5)) 196 | 197 | ok, result = graph.back_topo_sort() 198 | self.assertTrue(ok) 199 | for idx in range(1, 6): 200 | self.assertTrue(idx in result) 201 | self.assertTrue(result.index(2) < result.index(1)) 202 | self.assertTrue(result.index(3) < result.index(1)) 203 | self.assertTrue(result.index(4) < result.index(2)) 204 | self.assertTrue(result.index(5) < result.index(3)) 205 | 206 | # Same graph as before, but with edges 207 | # reversed, which means we should get 208 | # the same results as before if using 209 | # back_topo_sort rather than forw_topo_sort 210 | # (and v.v.) 211 | 212 | graph = Graph() 213 | graph.add_node(1) 214 | graph.add_node(2) 215 | graph.add_node(3) 216 | graph.add_node(4) 217 | graph.add_node(5) 218 | 219 | graph.add_edge(2, 1) 220 | graph.add_edge(3, 1) 221 | graph.add_edge(4, 2) 222 | graph.add_edge(5, 3) 223 | 224 | ok, result = graph.back_topo_sort() 225 | self.assertTrue(ok) 226 | for idx in range(1, 6): 227 | self.assertTrue(idx in result) 228 | 229 | self.assertTrue(result.index(1) < result.index(2)) 230 | self.assertTrue(result.index(1) < result.index(3)) 231 | self.assertTrue(result.index(2) < result.index(4)) 232 | self.assertTrue(result.index(3) < result.index(5)) 233 | 234 | ok, result = graph.forw_topo_sort() 235 | self.assertTrue(ok) 236 | for idx in range(1, 6): 237 | self.assertTrue(idx in result) 238 | self.assertTrue(result.index(2) < result.index(1)) 239 | self.assertTrue(result.index(3) < result.index(1)) 240 | self.assertTrue(result.index(4) < result.index(2)) 241 | self.assertTrue(result.index(5) < result.index(3)) 242 | 243 | # Create a cycle 244 | graph.add_edge(1, 5) 245 | ok, result = graph.forw_topo_sort() 246 | self.assertFalse(ok) 247 | ok, result = graph.back_topo_sort() 248 | self.assertFalse(ok) 249 | 250 | def test_bfs_subgraph(self): 251 | graph = Graph() 252 | graph.add_edge(1, 2) 253 | graph.add_edge(1, 4) 254 | graph.add_edge(2, 4) 255 | graph.add_edge(4, 8) 256 | graph.add_edge(4, 9) 257 | graph.add_edge(4, 10) 258 | graph.add_edge(8, 10) 259 | 260 | subgraph = graph.forw_bfs_subgraph(10) 261 | self.assertTrue(isinstance(subgraph, Graph)) 262 | self.assertEqual(subgraph.number_of_nodes(), 1) 263 | self.assertTrue(10 in subgraph) 264 | self.assertEqual(subgraph.number_of_edges(), 0) 265 | 266 | subgraph = graph.forw_bfs_subgraph(4) 267 | self.assertTrue(isinstance(subgraph, Graph)) 268 | self.assertEqual(subgraph.number_of_nodes(), 4) 269 | self.assertTrue(4 in subgraph) 270 | self.assertTrue(8 in subgraph) 271 | self.assertTrue(9 in subgraph) 272 | self.assertTrue(10 in subgraph) 273 | self.assertEqual(subgraph.number_of_edges(), 4) 274 | e = subgraph.edge_by_node(4, 8) 275 | e = subgraph.edge_by_node(4, 9) 276 | e = subgraph.edge_by_node(4, 10) 277 | e = subgraph.edge_by_node(8, 10) 278 | 279 | # same graph as before, but switch around 280 | # edges. This results in the same test results 281 | # but now for back_bfs_subgraph rather than 282 | # forw_bfs_subgraph 283 | 284 | graph = Graph() 285 | graph.add_edge(2, 1) 286 | graph.add_edge(4, 1) 287 | graph.add_edge(4, 2) 288 | graph.add_edge(8, 4) 289 | graph.add_edge(9, 4) 290 | graph.add_edge(10, 4) 291 | graph.add_edge(10, 8) 292 | 293 | subgraph = graph.back_bfs_subgraph(10) 294 | self.assertTrue(isinstance(subgraph, Graph)) 295 | self.assertEqual(subgraph.number_of_nodes(), 1) 296 | self.assertTrue(10 in subgraph) 297 | self.assertEqual(subgraph.number_of_edges(), 0) 298 | 299 | subgraph = graph.back_bfs_subgraph(4) 300 | self.assertTrue(isinstance(subgraph, Graph)) 301 | self.assertEqual(subgraph.number_of_nodes(), 4) 302 | self.assertTrue(4 in subgraph) 303 | self.assertTrue(8 in subgraph) 304 | self.assertTrue(9 in subgraph) 305 | self.assertTrue(10 in subgraph) 306 | self.assertEqual(subgraph.number_of_edges(), 4) 307 | e = subgraph.edge_by_node(4, 8) 308 | e = subgraph.edge_by_node(4, 9) 309 | e = subgraph.edge_by_node(4, 10) 310 | e = subgraph.edge_by_node(8, 10) 311 | 312 | def test_bfs_subgraph_does_not_reverse_egde_direction(self): 313 | graph = Graph() 314 | graph.add_node("A") 315 | graph.add_node("B") 316 | graph.add_node("C") 317 | graph.add_edge("A", "B") 318 | graph.add_edge("B", "C") 319 | 320 | whole_graph = graph.forw_topo_sort() 321 | subgraph_backward = graph.back_bfs_subgraph("C") 322 | subgraph_backward = subgraph_backward.forw_topo_sort() 323 | self.assertEqual(whole_graph, subgraph_backward) 324 | 325 | subgraph_forward = graph.forw_bfs_subgraph("A") 326 | subgraph_forward = subgraph_forward.forw_topo_sort() 327 | self.assertEqual(whole_graph, subgraph_forward) 328 | 329 | def test_iterdfs(self): 330 | graph = Graph() 331 | graph.add_edge("1", "1.1") 332 | graph.add_edge("1", "1.2") 333 | graph.add_edge("1", "1.3") 334 | graph.add_edge("1.1", "1.1.1") 335 | graph.add_edge("1.1", "1.1.2") 336 | graph.add_edge("1.2", "1.2.1") 337 | graph.add_edge("1.2", "1.2.2") 338 | graph.add_edge("1.2.2", "1.2.2.1") 339 | graph.add_edge("1.2.2", "1.2.2.2") 340 | graph.add_edge("1.2.2", "1.2.2.3") 341 | 342 | result = list(graph.iterdfs("1")) 343 | self.assertEqual( 344 | result, 345 | [ 346 | "1", 347 | "1.3", 348 | "1.2", 349 | "1.2.2", 350 | "1.2.2.3", 351 | "1.2.2.2", 352 | "1.2.2.1", 353 | "1.2.1", 354 | "1.1", 355 | "1.1.2", 356 | "1.1.1", 357 | ], 358 | ) 359 | result = list(graph.iterdfs("1", "1.2.1")) 360 | self.assertEqual( 361 | result, 362 | ["1", "1.3", "1.2", "1.2.2", "1.2.2.3", "1.2.2.2", "1.2.2.1", "1.2.1"], 363 | ) 364 | 365 | result = graph.forw_dfs("1") 366 | self.assertEqual( 367 | result, 368 | [ 369 | "1", 370 | "1.3", 371 | "1.2", 372 | "1.2.2", 373 | "1.2.2.3", 374 | "1.2.2.2", 375 | "1.2.2.1", 376 | "1.2.1", 377 | "1.1", 378 | "1.1.2", 379 | "1.1.1", 380 | ], 381 | ) 382 | result = graph.forw_dfs("1", "1.2.1") 383 | self.assertEqual( 384 | result, 385 | ["1", "1.3", "1.2", "1.2.2", "1.2.2.3", "1.2.2.2", "1.2.2.1", "1.2.1"], 386 | ) 387 | 388 | graph = Graph() 389 | graph.add_edge("1.1", "1") 390 | graph.add_edge("1.2", "1") 391 | graph.add_edge("1.3", "1") 392 | graph.add_edge("1.1.1", "1.1") 393 | graph.add_edge("1.1.2", "1.1") 394 | graph.add_edge("1.2.1", "1.2") 395 | graph.add_edge("1.2.2", "1.2") 396 | graph.add_edge("1.2.2.1", "1.2.2") 397 | graph.add_edge("1.2.2.2", "1.2.2") 398 | graph.add_edge("1.2.2.3", "1.2.2") 399 | 400 | result = list(graph.iterdfs("1", forward=False)) 401 | self.assertEqual( 402 | result, 403 | [ 404 | "1", 405 | "1.3", 406 | "1.2", 407 | "1.2.2", 408 | "1.2.2.3", 409 | "1.2.2.2", 410 | "1.2.2.1", 411 | "1.2.1", 412 | "1.1", 413 | "1.1.2", 414 | "1.1.1", 415 | ], 416 | ) 417 | result = list(graph.iterdfs("1", "1.2.1", forward=False)) 418 | self.assertEqual( 419 | result, 420 | ["1", "1.3", "1.2", "1.2.2", "1.2.2.3", "1.2.2.2", "1.2.2.1", "1.2.1"], 421 | ) 422 | result = graph.back_dfs("1") 423 | self.assertEqual( 424 | result, 425 | [ 426 | "1", 427 | "1.3", 428 | "1.2", 429 | "1.2.2", 430 | "1.2.2.3", 431 | "1.2.2.2", 432 | "1.2.2.1", 433 | "1.2.1", 434 | "1.1", 435 | "1.1.2", 436 | "1.1.1", 437 | ], 438 | ) 439 | result = graph.back_dfs("1", "1.2.1") 440 | self.assertEqual( 441 | result, 442 | ["1", "1.3", "1.2", "1.2.2", "1.2.2.3", "1.2.2.2", "1.2.2.1", "1.2.1"], 443 | ) 444 | 445 | # Introduce cyle: 446 | graph.add_edge("1", "1.2") 447 | result = list(graph.iterdfs("1", forward=False)) 448 | self.assertEqual( 449 | result, 450 | [ 451 | "1", 452 | "1.3", 453 | "1.2", 454 | "1.2.2", 455 | "1.2.2.3", 456 | "1.2.2.2", 457 | "1.2.2.1", 458 | "1.2.1", 459 | "1.1", 460 | "1.1.2", 461 | "1.1.1", 462 | ], 463 | ) 464 | 465 | result = graph.back_dfs("1") 466 | self.assertEqual( 467 | result, 468 | [ 469 | "1", 470 | "1.3", 471 | "1.2", 472 | "1.2.2", 473 | "1.2.2.3", 474 | "1.2.2.2", 475 | "1.2.2.1", 476 | "1.2.1", 477 | "1.1", 478 | "1.1.2", 479 | "1.1.1", 480 | ], 481 | ) 482 | 483 | def test_iterdata(self): 484 | graph = Graph() 485 | graph.add_node("1", "I") 486 | graph.add_node("1.1", "I.I") 487 | graph.add_node("1.2", "I.II") 488 | graph.add_node("1.3", "I.III") 489 | graph.add_node("1.1.1", "I.I.I") 490 | graph.add_node("1.1.2", "I.I.II") 491 | graph.add_node("1.2.1", "I.II.I") 492 | graph.add_node("1.2.2", "I.II.II") 493 | graph.add_node("1.2.2.1", "I.II.II.I") 494 | graph.add_node("1.2.2.2", "I.II.II.II") 495 | graph.add_node("1.2.2.3", "I.II.II.III") 496 | 497 | graph.add_edge("1", "1.1") 498 | graph.add_edge("1", "1.2") 499 | graph.add_edge("1", "1.3") 500 | graph.add_edge("1.1", "1.1.1") 501 | graph.add_edge("1.1", "1.1.2") 502 | graph.add_edge("1.2", "1.2.1") 503 | graph.add_edge("1.2", "1.2.2") 504 | graph.add_edge("1.2.2", "1.2.2.1") 505 | graph.add_edge("1.2.2", "1.2.2.2") 506 | graph.add_edge("1.2.2", "1.2.2.3") 507 | 508 | result = list(graph.iterdata("1", forward=True)) 509 | self.assertEqual( 510 | result, 511 | [ 512 | "I", 513 | "I.III", 514 | "I.II", 515 | "I.II.II", 516 | "I.II.II.III", 517 | "I.II.II.II", 518 | "I.II.II.I", 519 | "I.II.I", 520 | "I.I", 521 | "I.I.II", 522 | "I.I.I", 523 | ], 524 | ) 525 | 526 | result = list(graph.iterdata("1", end="1.2.1", forward=True)) 527 | self.assertEqual( 528 | result, 529 | [ 530 | "I", 531 | "I.III", 532 | "I.II", 533 | "I.II.II", 534 | "I.II.II.III", 535 | "I.II.II.II", 536 | "I.II.II.I", 537 | "I.II.I", 538 | ], 539 | ) 540 | 541 | result = list(graph.iterdata("1", condition=lambda n: len(n) < 6, forward=True)) 542 | self.assertEqual(result, ["I", "I.III", "I.II", "I.I", "I.I.I"]) 543 | 544 | # And the revese option: 545 | graph = Graph() 546 | graph.add_node("1", "I") 547 | graph.add_node("1.1", "I.I") 548 | graph.add_node("1.2", "I.II") 549 | graph.add_node("1.3", "I.III") 550 | graph.add_node("1.1.1", "I.I.I") 551 | graph.add_node("1.1.2", "I.I.II") 552 | graph.add_node("1.2.1", "I.II.I") 553 | graph.add_node("1.2.2", "I.II.II") 554 | graph.add_node("1.2.2.1", "I.II.II.I") 555 | graph.add_node("1.2.2.2", "I.II.II.II") 556 | graph.add_node("1.2.2.3", "I.II.II.III") 557 | 558 | graph.add_edge("1.1", "1") 559 | graph.add_edge("1.2", "1") 560 | graph.add_edge("1.3", "1") 561 | graph.add_edge("1.1.1", "1.1") 562 | graph.add_edge("1.1.2", "1.1") 563 | graph.add_edge("1.2.1", "1.2") 564 | graph.add_edge("1.2.2", "1.2") 565 | graph.add_edge("1.2.2.1", "1.2.2") 566 | graph.add_edge("1.2.2.2", "1.2.2") 567 | graph.add_edge("1.2.2.3", "1.2.2") 568 | 569 | result = list(graph.iterdata("1", forward=False)) 570 | self.assertEqual( 571 | result, 572 | [ 573 | "I", 574 | "I.III", 575 | "I.II", 576 | "I.II.II", 577 | "I.II.II.III", 578 | "I.II.II.II", 579 | "I.II.II.I", 580 | "I.II.I", 581 | "I.I", 582 | "I.I.II", 583 | "I.I.I", 584 | ], 585 | ) 586 | 587 | result = list(graph.iterdata("1", end="1.2.1", forward=False)) 588 | self.assertEqual( 589 | result, 590 | [ 591 | "I", 592 | "I.III", 593 | "I.II", 594 | "I.II.II", 595 | "I.II.II.III", 596 | "I.II.II.II", 597 | "I.II.II.I", 598 | "I.II.I", 599 | ], 600 | ) 601 | 602 | result = list( 603 | graph.iterdata("1", condition=lambda n: len(n) < 6, forward=False) 604 | ) 605 | self.assertEqual(result, ["I", "I.III", "I.II", "I.I", "I.I.I"]) 606 | 607 | def test_bfs(self): 608 | graph = Graph() 609 | graph.add_edge("1", "1.1") 610 | graph.add_edge("1.1", "1.1.1") 611 | graph.add_edge("1.1", "1.1.2") 612 | graph.add_edge("1.1.2", "1.1.2.1") 613 | graph.add_edge("1.1.2", "1.1.2.2") 614 | graph.add_edge("1", "1.2") 615 | graph.add_edge("1", "1.3") 616 | graph.add_edge("1.2", "1.2.1") 617 | 618 | self.assertEqual( 619 | graph.forw_bfs("1"), 620 | ["1", "1.1", "1.2", "1.3", "1.1.1", "1.1.2", "1.2.1", "1.1.2.1", "1.1.2.2"], 621 | ) 622 | self.assertEqual( 623 | graph.forw_bfs("1", "1.1.1"), ["1", "1.1", "1.2", "1.3", "1.1.1"] 624 | ) 625 | 626 | # And the "reverse" graph 627 | graph = Graph() 628 | graph.add_edge("1.1", "1") 629 | graph.add_edge("1.1.1", "1.1") 630 | graph.add_edge("1.1.2", "1.1") 631 | graph.add_edge("1.1.2.1", "1.1.2") 632 | graph.add_edge("1.1.2.2", "1.1.2") 633 | graph.add_edge("1.2", "1") 634 | graph.add_edge("1.3", "1") 635 | graph.add_edge("1.2.1", "1.2") 636 | 637 | self.assertEqual( 638 | graph.back_bfs("1"), 639 | ["1", "1.1", "1.2", "1.3", "1.1.1", "1.1.2", "1.2.1", "1.1.2.1", "1.1.2.2"], 640 | ) 641 | self.assertEqual( 642 | graph.back_bfs("1", "1.1.1"), ["1", "1.1", "1.2", "1.3", "1.1.1"] 643 | ) 644 | 645 | # check cycle handling 646 | graph.add_edge("1", "1.2.1") 647 | self.assertEqual( 648 | graph.back_bfs("1"), 649 | ["1", "1.1", "1.2", "1.3", "1.1.1", "1.1.2", "1.2.1", "1.1.2.1", "1.1.2.2"], 650 | ) 651 | 652 | def test_connected(self): 653 | graph = Graph() 654 | graph.add_node(1) 655 | graph.add_node(2) 656 | graph.add_node(3) 657 | graph.add_node(4) 658 | 659 | self.assertFalse(graph.connected()) 660 | 661 | graph.add_edge(1, 2) 662 | graph.add_edge(3, 4) 663 | self.assertFalse(graph.connected()) 664 | 665 | graph.add_edge(2, 3) 666 | graph.add_edge(4, 1) 667 | self.assertTrue(graph.connected()) 668 | 669 | def test_edges_complex(self): 670 | g = Graph() 671 | g.add_edge(1, 2) 672 | e = g.edge_by_node(1, 2) 673 | g.hide_edge(e) 674 | g.hide_node(2) 675 | self.assertRaises(GraphError, g.restore_edge, e) 676 | 677 | g.restore_all_edges() 678 | self.assertRaises(GraphError, g.edge_by_id, e) 679 | 680 | def test_clust_coef(self): 681 | g = Graph() 682 | g.add_edge(1, 2) 683 | g.add_edge(1, 3) 684 | g.add_edge(1, 4) 685 | self.assertEqual(g.clust_coef(1), 0) 686 | 687 | g.add_edge(2, 5) 688 | g.add_edge(3, 5) 689 | g.add_edge(4, 5) 690 | self.assertEqual(g.clust_coef(1), 0) 691 | 692 | g.add_edge(2, 3) 693 | self.assertEqual(g.clust_coef(1), 1.0 / 6) 694 | g.add_edge(2, 4) 695 | self.assertEqual(g.clust_coef(1), 2.0 / 6) 696 | g.add_edge(4, 2) 697 | self.assertEqual(g.clust_coef(1), 3.0 / 6) 698 | 699 | g.add_edge(2, 3) 700 | g.add_edge(2, 4) 701 | g.add_edge(3, 4) 702 | g.add_edge(3, 2) 703 | g.add_edge(4, 2) 704 | g.add_edge(4, 3) 705 | self.assertEqual(g.clust_coef(1), 1) 706 | 707 | g.add_edge(1, 1) 708 | self.assertEqual(g.clust_coef(1), 1) 709 | 710 | g.add_edge(2, 2) 711 | self.assertEqual(g.clust_coef(1), 1) 712 | 713 | g.add_edge(99, 99) 714 | self.assertEqual(g.clust_coef(99), 0.0) 715 | 716 | def test_get_hops(self): 717 | graph = Graph() 718 | graph.add_edge(1, 2) 719 | graph.add_edge(1, 3) 720 | graph.add_edge(2, 4) 721 | graph.add_edge(4, 5) 722 | graph.add_edge(5, 7) 723 | graph.add_edge(7, 8) 724 | 725 | self.assertEqual( 726 | graph.get_hops(1), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] 727 | ) 728 | 729 | self.assertEqual(graph.get_hops(1, 5), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3)]) 730 | 731 | graph.add_edge(5, 1) 732 | graph.add_edge(7, 1) 733 | graph.add_edge(7, 4) 734 | 735 | self.assertEqual( 736 | graph.get_hops(1), [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] 737 | ) 738 | 739 | # And the reverse graph 740 | graph = Graph() 741 | graph.add_edge(2, 1) 742 | graph.add_edge(3, 1) 743 | graph.add_edge(4, 2) 744 | graph.add_edge(5, 4) 745 | graph.add_edge(7, 5) 746 | graph.add_edge(8, 7) 747 | 748 | self.assertEqual( 749 | graph.get_hops(1, forward=False), 750 | [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)], 751 | ) 752 | 753 | self.assertEqual( 754 | graph.get_hops(1, 5, forward=False), 755 | [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3)], 756 | ) 757 | 758 | graph.add_edge(1, 5) 759 | graph.add_edge(1, 7) 760 | graph.add_edge(4, 7) 761 | 762 | self.assertEqual( 763 | graph.get_hops(1, forward=False), 764 | [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)], 765 | ) 766 | 767 | def test_constructor(self): 768 | graph = Graph(iter([(1, 2), (2, 3, "a"), (1, 3), (3, 4)])) 769 | self.assertEqual(graph.number_of_nodes(), 4) 770 | self.assertEqual(graph.number_of_edges(), 4) 771 | try: 772 | graph.edge_by_node(1, 2) 773 | graph.edge_by_node(2, 3) 774 | graph.edge_by_node(1, 3) 775 | graph.edge_by_node(3, 4) 776 | except GraphError: 777 | self.fail("Incorrect graph") 778 | 779 | self.assertEqual(graph.edge_data(graph.edge_by_node(2, 3)), "a") 780 | 781 | self.assertRaises(GraphError, Graph, [(1, 2, 3, 4)]) 782 | 783 | 784 | if __name__ == "__main__": # pragma: no cover 785 | unittest.main() 786 | -------------------------------------------------------------------------------- /altgraph_tests/test_graphstat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from altgraph import Graph, GraphStat 5 | 6 | 7 | class TestDegreesDist(unittest.TestCase): 8 | def test_simple(self): 9 | a = Graph.Graph() 10 | self.assertEqual(GraphStat.degree_dist(a), []) 11 | 12 | a.add_node(1) 13 | a.add_node(2) 14 | a.add_node(3) 15 | 16 | self.assertEqual(GraphStat.degree_dist(a), GraphStat._binning([0, 0, 0])) 17 | 18 | for x in range(100): 19 | a.add_node(x) 20 | 21 | for x in range(1, 100): 22 | for y in range(1, 50): 23 | if x % y == 0: 24 | a.add_edge(x, y) 25 | 26 | counts_inc = [] 27 | counts_out = [] 28 | for n in a: 29 | counts_inc.append(a.inc_degree(n)) 30 | counts_out.append(a.out_degree(n)) 31 | 32 | self.assertEqual(GraphStat.degree_dist(a), GraphStat._binning(counts_out)) 33 | self.assertEqual( 34 | GraphStat.degree_dist(a, mode="inc"), GraphStat._binning(counts_inc) 35 | ) 36 | 37 | 38 | class TestBinning(unittest.TestCase): 39 | def test_simple(self): 40 | # Binning [0, 100) into 10 bins 41 | a = list(range(100)) 42 | out = GraphStat._binning(a, limits=(0, 100), bin_num=10) 43 | 44 | self.assertEqual(out, [(x * 1.0, 10) for x in range(5, 100, 10)]) 45 | 46 | # Check that outliers are ignored. 47 | a = list(range(100)) 48 | out = GraphStat._binning(a, limits=(0, 90), bin_num=9) 49 | 50 | self.assertEqual(out, [(x * 1.0, 10) for x in range(5, 90, 10)]) 51 | 52 | out = GraphStat._binning(a, limits=(0, 100), bin_num=15) 53 | binSize = 100 / 15.0 54 | result = [0] * 15 55 | for i in range(100): 56 | bin = int(i / binSize) 57 | try: 58 | result[bin] += 1 59 | except IndexError: 60 | pass 61 | 62 | result = [(i * binSize + binSize / 2, result[i]) for i in range(len(result))] 63 | 64 | self.assertEqual(result, out) 65 | 66 | # Binning (100, 0] into 10 bins 67 | a = list(range(99, -10, -1)) 68 | out = GraphStat._binning(a, limits=(0, 100), bin_num=10) 69 | 70 | self.assertEqual(out, [(x * 1.0, 10) for x in range(5, 100, 10)]) 71 | 72 | 73 | if __name__ == "__main__": # pragma: no cover 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /altgraph_tests/test_graphutil.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from altgraph import Graph, GraphError, GraphUtil 4 | 5 | 6 | class TestGraphUtil(unittest.TestCase): 7 | def test_generate_random(self): 8 | g = GraphUtil.generate_random_graph(10, 50) 9 | self.assertEqual(g.number_of_nodes(), 10) 10 | self.assertEqual(g.number_of_edges(), 50) 11 | 12 | seen = set() 13 | 14 | for e in g.edge_list(): 15 | h, t = g.edge_by_id(e) 16 | self.assertFalse(h == t) 17 | self.assertTrue((h, t) not in seen) 18 | seen.add((h, t)) 19 | 20 | g = GraphUtil.generate_random_graph(5, 30, multi_edges=True) 21 | self.assertEqual(g.number_of_nodes(), 5) 22 | self.assertEqual(g.number_of_edges(), 30) 23 | 24 | seen = set() 25 | 26 | for e in g.edge_list(): 27 | h, t = g.edge_by_id(e) 28 | self.assertFalse(h == t) 29 | if (h, t) in seen: 30 | break 31 | seen.add((h, t)) 32 | 33 | else: 34 | self.fail("no duplicates?") 35 | 36 | g = GraphUtil.generate_random_graph(5, 21, self_loops=True) 37 | self.assertEqual(g.number_of_nodes(), 5) 38 | self.assertEqual(g.number_of_edges(), 21) 39 | 40 | seen = set() 41 | 42 | for e in g.edge_list(): 43 | h, t = g.edge_by_id(e) 44 | self.assertFalse((h, t) in seen) 45 | if h == t: 46 | break 47 | seen.add((h, t)) 48 | 49 | else: 50 | self.fail("no self loops?") 51 | 52 | self.assertRaises(GraphError, GraphUtil.generate_random_graph, 5, 21) 53 | g = GraphUtil.generate_random_graph(5, 21, True) 54 | self.assertRaises(GraphError, GraphUtil.generate_random_graph, 5, 26, True) 55 | 56 | def test_generate_scale_free(self): 57 | graph = GraphUtil.generate_scale_free_graph(50, 10) 58 | self.assertEqual(graph.number_of_nodes(), 500) 59 | 60 | counts = {} 61 | for node in graph: 62 | degree = graph.inc_degree(node) 63 | try: 64 | counts[degree] += 1 65 | except KeyError: 66 | counts[degree] = 1 67 | 68 | total_counts = sum(counts.values()) 69 | P = {} 70 | for degree, count in counts.items(): 71 | P[degree] = count * 1.0 / total_counts 72 | 73 | # Use algoritm 74 | # to check if P[degree] ~ degree ** G (for some G) 75 | 76 | # print sorted(P.items()) 77 | 78 | # print sorted([(count, degree) for degree, count in counts.items()]) 79 | 80 | # self.fail("missing tests for GraphUtil.generate_scale_free_graph") 81 | 82 | def test_filter_stack(self): 83 | g = Graph.Graph() 84 | g.add_node("1", "N.1") 85 | g.add_node("1.1", "N.1.1") 86 | g.add_node("1.1.1", "N.1.1.1") 87 | g.add_node("1.1.2", "N.1.1.2") 88 | g.add_node("1.1.3", "N.1.1.3") 89 | g.add_node("1.1.1.1", "N.1.1.1.1") 90 | g.add_node("1.1.1.2", "N.1.1.1.2") 91 | g.add_node("1.1.2.1", "N.1.1.2.1") 92 | g.add_node("1.1.2.2", "N.1.1.2.2") 93 | g.add_node("1.1.2.3", "N.1.1.2.3") 94 | g.add_node("2", "N.2") 95 | 96 | g.add_edge("1", "1.1") 97 | g.add_edge("1.1", "1.1.1") 98 | g.add_edge("1.1", "1.1.2") 99 | g.add_edge("1.1", "1.1.3") 100 | g.add_edge("1.1.1", "1.1.1.1") 101 | g.add_edge("1.1.1", "1.1.1.2") 102 | g.add_edge("1.1.2", "1.1.2.1") 103 | g.add_edge("1.1.2", "1.1.2.2") 104 | g.add_edge("1.1.2", "1.1.2.3") 105 | g.add_edge("1.1.2", "1.1.1.2") 106 | 107 | v, r, o = GraphUtil.filter_stack( 108 | g, "1", [lambda n: n != "N.1.1.1", lambda n: n != "N.1.1.2.3"] 109 | ) 110 | 111 | self.assertEqual( 112 | v, 113 | set( 114 | [ 115 | "1", 116 | "1.1", 117 | "1.1.1", 118 | "1.1.2", 119 | "1.1.3", 120 | "1.1.1.1", 121 | "1.1.1.2", 122 | "1.1.2.1", 123 | "1.1.2.2", 124 | "1.1.2.3", 125 | ] 126 | ), 127 | ) 128 | self.assertEqual(r, set(["1.1.1", "1.1.2.3"])) 129 | 130 | o.sort() 131 | self.assertEqual(o, [("1.1", "1.1.1.1"), ("1.1", "1.1.1.2")]) 132 | 133 | v, r, o = GraphUtil.filter_stack( 134 | g, "1", [lambda n: n != "N.1.1.1", lambda n: n != "N.1.1.1.2"] 135 | ) 136 | 137 | self.assertEqual( 138 | v, 139 | set( 140 | [ 141 | "1", 142 | "1.1", 143 | "1.1.1", 144 | "1.1.2", 145 | "1.1.3", 146 | "1.1.1.1", 147 | "1.1.1.2", 148 | "1.1.2.1", 149 | "1.1.2.2", 150 | "1.1.2.3", 151 | ] 152 | ), 153 | ) 154 | self.assertEqual(r, set(["1.1.1", "1.1.1.2"])) 155 | 156 | self.assertEqual(o, [("1.1", "1.1.1.1")]) 157 | 158 | 159 | if __name__ == "__main__": # pragma: no cover 160 | unittest.main() 161 | -------------------------------------------------------------------------------- /altgraph_tests/test_object_graph.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from altgraph.Graph import Graph 5 | from altgraph.ObjectGraph import ObjectGraph 6 | 7 | try: 8 | from StringIO import StringIO 9 | except ImportError: 10 | from io import StringIO 11 | 12 | 13 | class Node(object): 14 | def __init__(self, graphident): 15 | self.graphident = graphident 16 | 17 | 18 | class SubNode(Node): 19 | pass 20 | 21 | 22 | class ArgNode(object): 23 | def __init__(self, graphident, *args, **kwds): 24 | self.graphident = graphident 25 | self.args = args 26 | self.kwds = kwds 27 | 28 | def __repr__(self): 29 | return "" % (self.graphident,) 30 | 31 | 32 | class TestObjectGraph(unittest.TestCase): 33 | def test_constructor(self): 34 | graph = ObjectGraph() 35 | self.assertTrue(isinstance(graph, ObjectGraph)) 36 | 37 | g = Graph() 38 | graph = ObjectGraph(g) 39 | self.assertTrue(graph.graph is g) 40 | self.assertEqual(graph.debug, 0) 41 | self.assertEqual(graph.indent, 0) 42 | 43 | graph = ObjectGraph(debug=5) 44 | self.assertEqual(graph.debug, 5) 45 | 46 | def test_repr(self): 47 | graph = ObjectGraph() 48 | self.assertEqual(repr(graph), "") 49 | 50 | def testNodes(self): 51 | graph = ObjectGraph() 52 | n1 = Node("n1") 53 | n2 = Node("n2") 54 | n3 = Node("n3") 55 | n4 = Node("n4") 56 | 57 | n1b = Node("n1") 58 | 59 | self.assertTrue(graph.getIdent(graph) is graph) 60 | self.assertTrue(graph.getRawIdent(graph) is graph) 61 | 62 | graph.addNode(n1) 63 | graph.addNode(n2) 64 | graph.addNode(n3) 65 | 66 | self.assertTrue(n1 in graph) 67 | self.assertFalse(n4 in graph) 68 | self.assertTrue("n1" in graph) 69 | self.assertFalse("n4" in graph) 70 | 71 | self.assertTrue(graph.findNode(n1) is n1) 72 | self.assertTrue(graph.findNode(n1b) is n1) 73 | self.assertTrue(graph.findNode(n2) is n2) 74 | self.assertTrue(graph.findNode(n4) is None) 75 | self.assertTrue(graph.findNode("n1") is n1) 76 | self.assertTrue(graph.findNode("n2") is n2) 77 | self.assertTrue(graph.findNode("n4") is None) 78 | 79 | self.assertEqual(graph.getRawIdent(n1), "n1") 80 | self.assertEqual(graph.getRawIdent(n1b), "n1") 81 | self.assertEqual(graph.getRawIdent(n4), "n4") 82 | self.assertEqual(graph.getRawIdent("n1"), None) 83 | 84 | self.assertEqual(graph.getIdent(n1), "n1") 85 | self.assertEqual(graph.getIdent(n1b), "n1") 86 | self.assertEqual(graph.getIdent(n4), "n4") 87 | self.assertEqual(graph.getIdent("n1"), "n1") 88 | 89 | self.assertTrue(n3 in graph) 90 | graph.removeNode(n3) 91 | self.assertTrue(n3 not in graph) 92 | graph.addNode(n3) 93 | self.assertTrue(n3 in graph) 94 | 95 | nx = Node("nx") 96 | self.assertFalse(nx in graph) 97 | graph.removeNode("nx") 98 | self.assertFalse(nx in graph) 99 | 100 | self.assertIs(graph.getIdent("nx"), None) 101 | 102 | n = graph.createNode(SubNode, "n1") 103 | self.assertTrue(n is n1) 104 | 105 | n = graph.createNode(SubNode, "n8") 106 | self.assertTrue(isinstance(n, SubNode)) 107 | self.assertTrue(n in graph) 108 | self.assertTrue(graph.findNode("n8") is n) 109 | 110 | n = graph.createNode(ArgNode, "args", 1, 2, 3, a="a", b="b") 111 | self.assertTrue(isinstance(n, ArgNode)) 112 | self.assertTrue(n in graph) 113 | self.assertTrue(graph.findNode("args") is n) 114 | self.assertEqual(n.args, (1, 2, 3)) 115 | self.assertEqual(n.kwds, {"a": "a", "b": "b"}) 116 | 117 | def testEdges(self): 118 | graph = ObjectGraph() 119 | n1 = graph.createNode(ArgNode, "n1", 1) 120 | n2 = graph.createNode(ArgNode, "n2", 1) 121 | n3 = graph.createNode(ArgNode, "n3", 1) 122 | n4 = graph.createNode(ArgNode, "n4", 1) 123 | 124 | graph.createReference(n1, n2, "n1-n2") 125 | graph.createReference("n1", "n3", "n1-n3") 126 | graph.createReference("n2", n3) 127 | 128 | g = graph.graph 129 | e = g.edge_by_node("n1", "n2") 130 | self.assertTrue(e is not None) 131 | self.assertEqual(g.edge_data(e), "n1-n2") 132 | 133 | e = g.edge_by_node("n1", "n3") 134 | self.assertTrue(e is not None) 135 | self.assertEqual(g.edge_data(e), "n1-n3") 136 | 137 | e = g.edge_by_node("n2", "n3") 138 | self.assertTrue(e is not None) 139 | self.assertEqual(g.edge_data(e), None) 140 | 141 | e = g.edge_by_node("n1", "n4") 142 | self.assertTrue(e is None) 143 | 144 | graph.removeReference(n1, n2) 145 | e = g.edge_by_node("n1", "n2") 146 | self.assertTrue(e is None) 147 | 148 | graph.removeReference("n1", "n3") 149 | e = g.edge_by_node("n1", "n3") 150 | self.assertTrue(e is None) 151 | 152 | graph.createReference(n1, n2, "foo") 153 | e = g.edge_by_node("n1", "n2") 154 | self.assertTrue(e is not None) 155 | self.assertEqual(g.edge_data(e), "foo") 156 | 157 | graph.removeReference("A", "B") # neither node exists, shouldn't fail 158 | 159 | def test_flatten(self): 160 | graph = ObjectGraph() 161 | n1 = graph.createNode(ArgNode, "n1", 1) 162 | n2 = graph.createNode(ArgNode, "n2", 2) 163 | n3 = graph.createNode(ArgNode, "n3", 3) 164 | n4 = graph.createNode(ArgNode, "n4", 4) 165 | n5 = graph.createNode(ArgNode, "n5", 5) 166 | n6 = graph.createNode(ArgNode, "n6", 6) 167 | n7 = graph.createNode(ArgNode, "n7", 7) 168 | n8 = graph.createNode(ArgNode, "n8", 8) 169 | 170 | graph.createReference(graph, n1) 171 | graph.createReference(graph, n7) 172 | graph.createReference(n1, n2) 173 | graph.createReference(n1, n4) 174 | graph.createReference(n2, n3) 175 | graph.createReference(n2, n5) 176 | graph.createReference(n5, n6) 177 | graph.createReference(n4, n6) 178 | graph.createReference(n4, n2) 179 | 180 | self.assertFalse(isinstance(graph.flatten(), list)) 181 | 182 | fl = list(graph.flatten()) 183 | self.assertTrue(n1 in fl) 184 | self.assertTrue(n2 in fl) 185 | self.assertTrue(n3 in fl) 186 | self.assertTrue(n4 in fl) 187 | self.assertTrue(n5 in fl) 188 | self.assertTrue(n6 in fl) 189 | self.assertTrue(n7 in fl) 190 | self.assertFalse(n8 in fl) 191 | 192 | fl = list(graph.flatten(start=n2)) 193 | self.assertFalse(n1 in fl) 194 | self.assertTrue(n2 in fl) 195 | self.assertTrue(n3 in fl) 196 | self.assertFalse(n4 in fl) 197 | self.assertTrue(n5 in fl) 198 | self.assertTrue(n6 in fl) 199 | self.assertFalse(n7 in fl) 200 | self.assertFalse(n8 in fl) 201 | 202 | graph.createReference(n1, n5) 203 | fl = list(graph.flatten(lambda n: n.args[0] % 2 != 0)) 204 | self.assertTrue(n1 in fl) 205 | self.assertFalse(n2 in fl) 206 | self.assertFalse(n3 in fl) 207 | self.assertFalse(n4 in fl) 208 | self.assertTrue(n5 in fl) 209 | self.assertFalse(n6 in fl) 210 | self.assertTrue(n7 in fl) 211 | self.assertFalse(n8 in fl) 212 | 213 | def test_iter_nodes(self): 214 | graph = ObjectGraph() 215 | n1 = graph.createNode(ArgNode, "n1", 1) 216 | n2 = graph.createNode(ArgNode, "n2", 2) 217 | n3 = graph.createNode(ArgNode, "n3", 3) 218 | n4 = graph.createNode(ArgNode, "n4", 4) 219 | n5 = graph.createNode(ArgNode, "n5", 5) 220 | n6 = graph.createNode(ArgNode, "n6", 5) 221 | 222 | nodes = graph.nodes() 223 | if sys.version[0] == "2": 224 | self.assertTrue(hasattr(nodes, "next")) 225 | else: 226 | self.assertTrue(hasattr(nodes, "__next__")) 227 | self.assertTrue(hasattr(nodes, "__iter__")) 228 | 229 | nodes = list(nodes) 230 | self.assertEqual(len(nodes), 6) 231 | self.assertTrue(n1 in nodes) 232 | self.assertTrue(n2 in nodes) 233 | self.assertTrue(n3 in nodes) 234 | self.assertTrue(n4 in nodes) 235 | self.assertTrue(n5 in nodes) 236 | self.assertTrue(n6 in nodes) 237 | 238 | def test_get_edges(self): 239 | graph = ObjectGraph() 240 | n1 = graph.createNode(ArgNode, "n1", 1) 241 | n2 = graph.createNode(ArgNode, "n2", 2) 242 | n3 = graph.createNode(ArgNode, "n3", 3) 243 | n4 = graph.createNode(ArgNode, "n4", 4) 244 | n5 = graph.createNode(ArgNode, "n5", 5) 245 | n6 = graph.createNode(ArgNode, "n6", 5) 246 | 247 | graph.createReference(None, n1) 248 | graph.createReference(n1, n2) 249 | graph.createReference(n1, n3) 250 | graph.createReference(n3, n1) 251 | graph.createReference(n5, n1) 252 | graph.createReference(n2, n4) 253 | graph.createReference(n2, n5) 254 | graph.createReference(n6, n2) 255 | graph.createReference(n6, n2) 256 | graph.createReference(n6, n6) 257 | 258 | outs, ins = graph.get_edges(n1) 259 | 260 | self.assertFalse(isinstance(outs, list)) 261 | self.assertFalse(isinstance(ins, list)) 262 | 263 | ins = list(ins) 264 | outs = list(outs) 265 | 266 | self.assertTrue(None not in outs) 267 | self.assertTrue(n1 not in outs) 268 | self.assertTrue(n2 in outs) 269 | self.assertTrue(n3 in outs) 270 | self.assertTrue(n4 not in outs) 271 | self.assertTrue(n5 not in outs) 272 | self.assertTrue(n6 not in outs) 273 | 274 | self.assertTrue(None in ins) 275 | self.assertTrue(n1 not in ins) 276 | self.assertTrue(n2 not in ins) 277 | self.assertTrue(n3 in ins) 278 | self.assertTrue(n4 not in ins) 279 | self.assertTrue(n5 in ins) 280 | self.assertTrue(n6 not in ins) 281 | 282 | outs, ins = graph.get_edges(n6) 283 | outs = list(outs) 284 | ints = list(ins) 285 | 286 | self.assertEqual(outs.count(n2), 1) 287 | 288 | self.assertTrue(None not in outs) 289 | self.assertTrue(None not in ins) 290 | 291 | def test_edge_data(self): 292 | graph = ObjectGraph() 293 | n1 = graph.createNode(ArgNode, "n1", 1) 294 | n2 = graph.createNode(ArgNode, "n2", 2) 295 | n3 = graph.createNode(ArgNode, "n3", 3) 296 | 297 | graph.createReference(n1, n2) 298 | graph.createReference(n1, n3, "foo") 299 | 300 | self.assertIs(graph.edgeData(n1, n2), None) 301 | self.assertIs(graph.edgeData(n1, n3), "foo") 302 | 303 | graph.updateEdgeData(n1, n2, "bar") 304 | self.assertIs(graph.edgeData(n1, n2), "bar") 305 | 306 | def test_filterStack(self): 307 | graph = ObjectGraph() 308 | n1 = graph.createNode(ArgNode, "n1", 0) 309 | n11 = graph.createNode(ArgNode, "n1.1", 1) 310 | n12 = graph.createNode(ArgNode, "n1.2", 0) 311 | n111 = graph.createNode(ArgNode, "n1.1.1", 0) 312 | n112 = graph.createNode(ArgNode, "n1.1.2", 2) 313 | n2 = graph.createNode(ArgNode, "n2", 0) 314 | n3 = graph.createNode(ArgNode, "n2", 0) 315 | 316 | graph.createReference(None, n1) 317 | graph.createReference(None, n2) 318 | graph.createReference(n1, n11) 319 | graph.createReference(n1, n12) 320 | graph.createReference(n11, n111) 321 | graph.createReference(n11, n112) 322 | 323 | self.assertTrue(n1 in graph) 324 | self.assertTrue(n2 in graph) 325 | self.assertTrue(n11 in graph) 326 | self.assertTrue(n12 in graph) 327 | self.assertTrue(n111 in graph) 328 | self.assertTrue(n112 in graph) 329 | self.assertTrue(n2 in graph) 330 | self.assertTrue(n3 in graph) 331 | 332 | visited, removes, orphans = graph.filterStack( 333 | [lambda n: n.args[0] != 1, lambda n: n.args[0] != 2] 334 | ) 335 | 336 | self.assertEqual(visited, 6) 337 | self.assertEqual(removes, 2) 338 | self.assertEqual(orphans, 1) 339 | 340 | e = graph.graph.edge_by_node(n1.graphident, n111.graphident) 341 | self.assertEqual(graph.graph.edge_data(e), "orphan") 342 | 343 | self.assertTrue(n1 in graph) 344 | self.assertTrue(n2 in graph) 345 | self.assertTrue(n11 not in graph) 346 | self.assertTrue(n12 in graph) 347 | self.assertTrue(n111 in graph) 348 | self.assertTrue(n112 not in graph) 349 | self.assertTrue(n2 in graph) 350 | self.assertTrue(n3 in graph) 351 | 352 | 353 | class TestObjectGraphIO(unittest.TestCase): 354 | def setUp(self): 355 | self._stdout = sys.stdout 356 | 357 | def tearDown(self): 358 | sys.stdout = self._stdout 359 | 360 | def test_msg(self): 361 | graph = ObjectGraph() 362 | 363 | sys.stdout = fp = StringIO() 364 | graph.msg(0, "foo") 365 | self.assertEqual(fp.getvalue(), "foo \n") 366 | 367 | sys.stdout = fp = StringIO() 368 | graph.msg(5, "foo") 369 | self.assertEqual(fp.getvalue(), "") 370 | 371 | sys.stdout = fp = StringIO() 372 | graph.debug = 10 373 | graph.msg(5, "foo") 374 | self.assertEqual(fp.getvalue(), "foo \n") 375 | 376 | sys.stdout = fp = StringIO() 377 | graph.msg(0, "foo", 1, "a") 378 | self.assertEqual(fp.getvalue(), "foo 1 'a'\n") 379 | 380 | sys.stdout = fp = StringIO() 381 | graph.msgin(0, "hello", "world") 382 | graph.msg(0, "test me") 383 | graph.msgout(0, "bye bye") 384 | self.assertEqual(fp.getvalue(), "hello 'world'\n test me \nbye bye \n") 385 | 386 | sys.stdout = fp = StringIO() 387 | graph.msgin(55, "hello") 388 | graph.msgout(55, "bye") 389 | self.assertEqual(fp.getvalue(), "") 390 | 391 | def test_graph_references(self): 392 | graph = ObjectGraph() 393 | n1 = graph.createNode(Node, "n1") 394 | n2 = graph.createNode(Node, "n2") 395 | 396 | graph.createReference(None, n1, None) 397 | graph.createReference(None, n2, "hello") 398 | 399 | self.assertEqual(graph.edgeData(None, n1), None) 400 | self.assertEqual(graph.edgeData(None, n2), "hello") 401 | 402 | graph.updateEdgeData(None, n1, "world") 403 | self.assertEqual(graph.edgeData(None, n1), "world") 404 | 405 | out, inc = graph.get_edges(None) 406 | out = list(out) 407 | inc = list(inc) 408 | 409 | self.assertEqual(out, [n1, n2]) 410 | self.assertEqual(len(out), 2) 411 | self.assertTrue(n1 in out) 412 | self.assertTrue(n2 in out) 413 | self.assertEqual(inc, []) 414 | 415 | graph.removeReference(None, n2) 416 | out, inc = graph.get_edges(None) 417 | out = list(out) 418 | inc = list(inc) 419 | 420 | self.assertTrue(n2 not in out) 421 | 422 | def test_reference_oddity(self): 423 | # Not sure if I actually like this, cannot change for now... 424 | graph = ObjectGraph() 425 | n1 = graph.createNode(Node, "n1") 426 | n2 = graph.createNode(Node, "n2") 427 | 428 | graph.createReference("n1", "n2") 429 | graph.createReference("n1", "n3") 430 | graph.createReference("n3", "n1") 431 | 432 | nodes = list(graph.nodes()) 433 | self.assertEqual(len(nodes), 2) 434 | self.assertTrue(n1 in nodes) 435 | self.assertTrue(n2 in nodes) 436 | 437 | out, inc = graph.get_edges(n1) 438 | out = list(out) 439 | inc = list(inc) 440 | 441 | self.assertEqual(out, [n2]) 442 | self.assertEqual(inc, []) 443 | 444 | 445 | if __name__ == "__main__": # pragma: no cover 446 | unittest.main() 447 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/altgraph.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/altgraph.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /doc/_templates/icons.html: -------------------------------------------------------------------------------- 1 |

Status icons

2 |
    3 |
  • 4 |
5 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | 0.17.3 5 | ------ 6 | 7 | * Update classifiers for Python 3.11 8 | 9 | 0.17.2 10 | ------ 11 | 12 | * Change in setup.py to fix the sidebar links on PyPI 13 | 14 | 0.17.1 15 | ------ 16 | 17 | * Explicitly mark Python 3.10 as supported in wheel metadata. 18 | 19 | 0.17 20 | ---- 21 | 22 | * Explicitly mark Python 3.8 as supported in wheel metadata. 23 | 24 | * Migrate from Bitbucket to GitHub 25 | 26 | * Run black on the entire repository 27 | 28 | 0.16.1 29 | ------ 30 | 31 | * Explicitly mark Python 3.7 as supported in wheel metadata. 32 | 33 | 0.16 34 | ---- 35 | 36 | * Add LICENSE file 37 | 38 | 0.15 39 | ---- 40 | 41 | * ``ObjectGraph.get_edges``, ``ObjectGraph.getEdgeData`` and ``ObjectGraph.updateEdgeData`` 42 | accept *None* as the node to get and treat this as an alias for *self* (as other 43 | methods already did). 44 | 45 | 0.14 46 | ---- 47 | 48 | - Issue #7: Remove use of ``iteritems`` in altgraph.GraphAlgo code 49 | 50 | 0.13 51 | ---- 52 | 53 | - Issue #4: Graph._bfs_subgraph and back_bfs_subgraph return subgraphs with reversed edges 54 | 55 | Fix by "pombredanne" on bitbucket. 56 | 57 | 58 | 0.12 59 | ---- 60 | 61 | - Added ``ObjectGraph.edgeData`` to retrieve the edge data 62 | from a specific edge. 63 | 64 | - Added ``AltGraph.update_edge_data`` and ``ObjectGraph.updateEdgeData`` 65 | to update the data associated with a graph edge. 66 | 67 | 0.11 68 | ---- 69 | 70 | - Stabilize the order of elements in dot file exports, 71 | patch from bitbucket user 'pombredanne'. 72 | 73 | - Tweak setup.py file to remove dependency on distribute (but 74 | keep the dependency on setuptools) 75 | 76 | 77 | 0.10.2 78 | ------ 79 | 80 | - There where no classifiers in the package metadata due to a bug 81 | in setup.py 82 | 83 | 0.10.1 84 | ------ 85 | 86 | This is a bugfix release 87 | 88 | Bug fixes: 89 | 90 | - Issue #3: The source archive contains a README.txt 91 | while the setup file refers to ReadMe.txt. 92 | 93 | This is caused by a misfeature in distutils, as a 94 | workaround I've renamed ReadMe.txt to README.txt 95 | in the source tree and setup file. 96 | 97 | 98 | 0.10 99 | ----- 100 | 101 | This is a minor feature release 102 | 103 | Features: 104 | 105 | - Do not use "2to3" to support Python 3. 106 | 107 | As a side effect of this altgraph now supports 108 | Python 2.6 and later, and no longer supports 109 | earlier releases of Python. 110 | 111 | - The order of attributes in the Dot output 112 | is now always alphabetical. 113 | 114 | With this change the output will be consistent 115 | between runs and Python versions. 116 | 117 | 0.9 118 | --- 119 | 120 | This is a minor bugfix release 121 | 122 | Features: 123 | 124 | - Added ``altgraph.ObjectGraph.ObjectGraph.nodes``, a method 125 | yielding all nodes in an object graph. 126 | 127 | Bugfixes: 128 | 129 | - The 0.8 release didn't work with py2app when using 130 | python 3.x. 131 | 132 | 133 | 0.8 134 | ----- 135 | 136 | This is a minor feature release. The major new feature 137 | is a extensive set of unittests, which explains almost 138 | all other changes in this release. 139 | 140 | Bugfixes: 141 | 142 | - Installing failed with Python 2.5 due to using a distutils 143 | class that isn't available in that version of Python 144 | (issue #1 on the issue tracker) 145 | 146 | - ``altgraph.GraphStat.degree_dist`` now actually works 147 | 148 | - ``altgraph.Graph.add_edge(a, b, create_nodes=False)`` will 149 | no longer create the edge when one of the nodes doesn't 150 | exist. 151 | 152 | - ``altgraph.Graph.forw_topo_sort`` failed for some sparse graphs. 153 | 154 | - ``altgraph.Graph.back_topo_sort`` was completely broken in 155 | previous releases. 156 | 157 | - ``altgraph.Graph.forw_bfs_subgraph`` now actually works. 158 | 159 | - ``altgraph.Graph.back_bfs_subgraph`` now actually works. 160 | 161 | - ``altgraph.Graph.iterdfs`` now returns the correct result 162 | when the ``forward`` argument is ``False``. 163 | 164 | - ``altgraph.Graph.iterdata`` now returns the correct result 165 | when the ``forward`` argument is ``False``. 166 | 167 | 168 | Features: 169 | 170 | - The ``altgraph.Graph`` constructor now accepts an argument 171 | that contains 2- and 3-tuples instead of requireing that 172 | all items have the same size. The (optional) argument can now 173 | also be any iterator. 174 | 175 | - ``altgraph.Graph.Graph.add_node`` has no effect when you 176 | add a hidden node. 177 | 178 | - The private method ``altgraph.Graph._bfs`` is no longer 179 | present. 180 | 181 | - The private method ``altgraph.Graph._dfs`` is no longer 182 | present. 183 | 184 | - ``altgraph.ObjectGraph`` now has a ``__contains__`` methods, 185 | which means you can use the ``in`` operator to check if a 186 | node is part of a graph. 187 | 188 | - ``altgraph.GraphUtil.generate_random_graph`` will raise 189 | ``GraphError`` instead of looping forever when it is 190 | impossible to create the requested graph. 191 | 192 | - ``altgraph.Dot.edge_style`` raises ``GraphError`` when 193 | one of the nodes is not present in the graph. The method 194 | silently added the tail in the past, but without ensuring 195 | a consistent graph state. 196 | 197 | - ``altgraph.Dot.save_img`` now works when the mode is 198 | ``"neato"``. 199 | 200 | 0.7.2 201 | ----- 202 | 203 | This is a minor bugfix release 204 | 205 | Bugfixes: 206 | 207 | - distutils didn't include the documentation subtree 208 | 209 | 0.7.1 210 | ----- 211 | 212 | This is a minor feature release 213 | 214 | Features: 215 | 216 | - Documentation is now generated using `sphinx `_ 217 | and can be viewed at . 218 | 219 | - The repository has moved to bitbucket 220 | 221 | - ``altgraph.GraphStat.avg_hops`` is no longer present, the function had no 222 | implementation and no specified behaviour. 223 | 224 | - the module ``altgraph.compat`` is gone, which means altgraph will no 225 | longer work with Python 2.3. 226 | 227 | 228 | 0.7.0 229 | ----- 230 | 231 | This is a minor feature release. 232 | 233 | Features: 234 | 235 | - Support for Python 3 236 | 237 | - It is now possible to run tests using 'python setup.py test' 238 | 239 | (The actual testsuite is still very minimal though) 240 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # altgraph documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Aug 31 11:04:49 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | 18 | def get_version(): 19 | fn = os.path.join( 20 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "setup.cfg" 21 | ) 22 | for ln in open(fn): 23 | if ln.startswith("version"): 24 | version = ln.split("=")[-1].strip() 25 | return version 26 | 27 | 28 | # If extensions (or modules to document with autodoc) are in another directory, 29 | # add these directories to sys.path here. If the directory is relative to the 30 | # documentation root, use os.path.abspath to make it absolute, like shown here. 31 | # sys.path.append(os.path.abspath('.')) 32 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 33 | 34 | # -- General configuration ----------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be extensions 37 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 38 | extensions = ["sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.autodoc"] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = ".rst" 45 | 46 | # The encoding of source files. 47 | # source_encoding = 'utf-8' 48 | 49 | # The master toctree document. 50 | master_doc = "index" 51 | 52 | # General information about the project. 53 | project = "altgraph" 54 | copyright = "2010-2011, Ronald Oussoren, Bob Ippolito, 2004 Istvan Albert" 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = get_version() 62 | # The full version, including alpha/beta/rc tags. 63 | release = version 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | # today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | # today_fmt = '%B %d, %Y' 74 | 75 | # List of documents that shouldn't be included in the build. 76 | # unused_docs = [] 77 | 78 | # List of directories, relative to source directory, that shouldn't be searched 79 | # for source files. 80 | exclude_trees = ["_build"] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all documents. 83 | # default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | # add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | # add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | # show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = "sphinx" 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | # modindex_common_prefix = [] 101 | 102 | 103 | # -- Options for HTML output --------------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. Major themes that come with 106 | # Sphinx are currently 'default' and 'sphinxdoc'. 107 | html_theme = "nature" 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | # html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | # html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | # html_title = None 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | # html_short_title = None 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | # html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | # html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | # html_static_path = ['_static'] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | # html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | # html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | html_sidebars = {"**": ["localtoc.html", "searchbox.html", "icons.html"]} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | # html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | # html_use_modindex = True 155 | 156 | # If false, no index is generated. 157 | # html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | # html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | html_show_sourcelink = False 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | # html_use_opensearch = '' 169 | 170 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 171 | # html_file_suffix = '' 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = "altgraphdoc" 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | # The paper size ('letter' or 'a4'). 180 | # latex_paper_size = 'letter' 181 | 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | # latex_font_size = '10pt' 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ("index", "altgraph.tex", "altgraph Documentation", "Ronald Oussoren", "manual") 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | # latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | # latex_use_parts = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | # latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | # latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | # latex_use_modindex = True 207 | 208 | 209 | # Example configuration for intersphinx: refer to the Python standard library. 210 | intersphinx_mapping = {"python": ("http://docs.python.org/", None)} 211 | -------------------------------------------------------------------------------- /doc/core.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph` --- A Python Graph Library 2 | ================================================== 3 | 4 | .. module:: altgraph 5 | :synopsis: A directional graph for python 6 | 7 | altgraph is a fork of `graphlib `_ tailored 8 | to use newer Python 2.3+ features, including additional support used by the 9 | py2app suite (modulegraph and macholib, specifically). 10 | 11 | altgraph is a python based graph (network) representation and manipulation package. 12 | It has started out as an extension to the `graph_lib module `_ 13 | written by Nathan Denny it has been significantly optimized and expanded. 14 | 15 | The :class:`altgraph.Graph.Graph` class is loosely modeled after the `LEDA `_ 16 | (Library of Efficient Datatypes) representation. The library 17 | includes methods for constructing graphs, BFS and DFS traversals, 18 | topological sort, finding connected components, shortest paths as well as a number 19 | graph statistics functions. The library can also visualize graphs 20 | via `graphviz `_. 21 | 22 | 23 | .. exception:: GraphError 24 | 25 | Exception raised when methods are called with bad values of 26 | an inconsistent state. 27 | -------------------------------------------------------------------------------- /doc/dot.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.Dot` --- Interface to the dot language 2 | ===================================================== 3 | 4 | .. module:: altgraph.Dot 5 | :synopsis: Interface to the dot language as used by Graphviz.. 6 | 7 | The :py:mod:`~altgraph.Dot` module provides a simple interface to the 8 | file format used in the `graphviz`_ program. The module is intended to 9 | offload the most tedious part of the process (the **dot** file generation) 10 | while transparently exposing most of its features. 11 | 12 | .. _`graphviz`: `_ 13 | 14 | To display the graphs or to generate image files the `graphviz`_ 15 | package needs to be installed on the system, moreover the :command:`dot` and :command:`dotty` programs must 16 | be accesible in the program path so that they can be ran from processes spawned 17 | within the module. 18 | 19 | Example usage 20 | ------------- 21 | 22 | Here is a typical usage:: 23 | 24 | from altgraph import Graph, Dot 25 | 26 | # create a graph 27 | edges = [ (1,2), (1,3), (3,4), (3,5), (4,5), (5,4) ] 28 | graph = Graph.Graph(edges) 29 | 30 | # create a dot representation of the graph 31 | dot = Dot.Dot(graph) 32 | 33 | # display the graph 34 | dot.display() 35 | 36 | # save the dot representation into the mydot.dot file 37 | dot.save_dot(file_name='mydot.dot') 38 | 39 | # save dot file as gif image into the graph.gif file 40 | dot.save_img(file_name='graph', file_type='gif') 41 | 42 | 43 | Directed graph and non-directed graph 44 | ------------------------------------- 45 | 46 | Dot class can use for both directed graph and non-directed graph 47 | by passing *graphtype* parameter. 48 | 49 | Example:: 50 | 51 | # create directed graph(default) 52 | dot = Dot.Dot(graph, graphtype="digraph") 53 | 54 | # create non-directed graph 55 | dot = Dot.Dot(graph, graphtype="graph") 56 | 57 | 58 | Customizing the output 59 | ---------------------- 60 | 61 | The graph drawing process may be customized by passing 62 | valid :command:`dot` parameters for the nodes and edges. For a list of all 63 | parameters see the `graphviz`_ documentation. 64 | 65 | Example:: 66 | 67 | # customizing the way the overall graph is drawn 68 | dot.style(size='10,10', rankdir='RL', page='5, 5' , ranksep=0.75) 69 | 70 | # customizing node drawing 71 | dot.node_style(1, label='BASE_NODE',shape='box', color='blue' ) 72 | dot.node_style(2, style='filled', fillcolor='red') 73 | 74 | # customizing edge drawing 75 | dot.edge_style(1, 2, style='dotted') 76 | dot.edge_style(3, 5, arrowhead='dot', label='binds', labelangle='90') 77 | dot.edge_style(4, 5, arrowsize=2, style='bold') 78 | 79 | 80 | .. note:: 81 | 82 | dotty (invoked via :py:func:`~altgraph.Dot.display`) may not be able to 83 | display all graphics styles. To verify the output save it to an image 84 | file and look at it that way. 85 | 86 | Valid attributes 87 | ---------------- 88 | 89 | - dot styles, passed via the :py:meth:`Dot.style` method:: 90 | 91 | rankdir = 'LR' (draws the graph horizontally, left to right) 92 | ranksep = number (rank separation in inches) 93 | 94 | - node attributes, passed via the :py:meth:`Dot.node_style` method:: 95 | 96 | style = 'filled' | 'invisible' | 'diagonals' | 'rounded' 97 | shape = 'box' | 'ellipse' | 'circle' | 'point' | 'triangle' 98 | 99 | - edge attributes, passed via the :py:meth:`Dot.edge_style` method:: 100 | 101 | style = 'dashed' | 'dotted' | 'solid' | 'invis' | 'bold' 102 | arrowhead = 'box' | 'crow' | 'diamond' | 'dot' | 'inv' | 'none' | 'tee' | 'vee' 103 | weight = number (the larger the number the closer the nodes will be) 104 | 105 | - valid `graphviz colors `_ 106 | 107 | - for more details on how to control the graph drawing process see the 108 | `graphviz reference `_. 109 | 110 | 111 | Class interface 112 | --------------- 113 | 114 | .. class:: Dot(graph[, nodes[, edgefn[, nodevisitor[, edgevisitor[, name[, dot[, dotty[, neato[, graphtype]]]]]]]]]) 115 | 116 | Creates a new Dot generator based on the specified 117 | :class:`Graph `. The Dot generator won't reference 118 | the *graph* once it is constructed. 119 | 120 | If the *nodes* argument is present it is the list of nodes to include 121 | in the graph, otherwise all nodes in *graph* are included. 122 | 123 | If the *edgefn* argument is present it is a function that yields the 124 | nodes connected to another node, this defaults to 125 | :meth:`graph.out_nbr `. The constructor won't 126 | add edges to the dot file unless both the head and tail of the edge 127 | are in *nodes*. 128 | 129 | If the *name* is present it specifies the name of the graph in the resulting 130 | dot file. The default is ``"G"``. 131 | 132 | The functions *nodevisitor* and *edgevisitor* return the default style 133 | for a given edge or node (both default to functions that return an empty 134 | style). 135 | 136 | The arguments *dot*, *dotty* and *neato* are used to pass the path to 137 | the corresponding `graphviz`_ command. 138 | 139 | 140 | Updating graph attributes 141 | ......................... 142 | 143 | .. method:: Dot.style(\**attr) 144 | 145 | Sets the overall style (graph attributes) to the given attributes. 146 | 147 | See `Valid Attributes`_ for more information about the attributes. 148 | 149 | .. method:: Dot.node_style(node, \**attr) 150 | 151 | Sets the style for *node* to the given attributes. 152 | 153 | This method will add *node* to the graph when it isn't already 154 | present. 155 | 156 | See `Valid Attributes`_ for more information about the attributes. 157 | 158 | .. method:: Dot.all_node_style(\**attr) 159 | 160 | Replaces the current style for all nodes 161 | 162 | 163 | .. method:: edge_style(head, tail, \**attr) 164 | 165 | Sets the style of an edge to the given attributes. The edge will 166 | be added to the graph when it isn't already present, but *head* 167 | and *tail* must both be valid nodes. 168 | 169 | See `Valid Attributes`_ for more information about the attributes. 170 | 171 | 172 | 173 | Emitting output 174 | ............... 175 | 176 | .. method:: Dot.display([mode]) 177 | 178 | Displays the current graph via dotty. 179 | 180 | If the *mode* is ``"neato"`` the dot file is processed with 181 | the neato command before displaying. 182 | 183 | This method won't return until the dotty command exits. 184 | 185 | .. method:: save_dot(filename) 186 | 187 | Saves the current graph representation into the given file. 188 | 189 | .. note:: 190 | 191 | For backward compatibility reasons this method can also 192 | be called without an argument, it will then write the graph 193 | into a fixed filename (present in the attribute :data:`Graph.temp_dot`). 194 | 195 | This feature is deprecated and should not be used. 196 | 197 | 198 | .. method:: save_image(file_name[, file_type[, mode]]) 199 | 200 | Saves the current graph representation as an image file. The output 201 | is written into a file whose basename is *file_name* and whose suffix 202 | is *file_type*. 203 | 204 | The *file_type* specifies the type of file to write, the default 205 | is ``"gif"``. 206 | 207 | If the *mode* is ``"neato"`` the dot file is processed with 208 | the neato command before displaying. 209 | 210 | .. note:: 211 | 212 | For backward compatibility reasons this method can also 213 | be called without an argument, it will then write the graph 214 | with a fixed basename (``"out"``). 215 | 216 | This feature is deprecated and should not be used. 217 | 218 | .. method:: iterdot() 219 | 220 | Yields all lines of a `graphviz`_ input file (including line endings). 221 | 222 | .. method:: __iter__() 223 | 224 | Alias for the :meth:`iterdot` method. 225 | -------------------------------------------------------------------------------- /doc/graph.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.Graph` --- Basic directional graphs 2 | ================================================== 3 | 4 | .. module:: altgraph.Graph 5 | :synopsis: Basic directional graphs. 6 | 7 | The module :mod:`altgraph.Graph` provides a class :class:`Graph` that 8 | represents a directed graph with *N* nodes and *E* edges. 9 | 10 | .. class:: Graph([edges]) 11 | 12 | Constructs a new empty :class:`Graph` object. If the optional 13 | *edges* parameter is supplied, updates the graph by adding the 14 | specified edges. 15 | 16 | All of the elements in *edges* should be tuples with two or three 17 | elements. The first two elements of the tuple are the source and 18 | destination node of the edge, the optional third element is the 19 | edge data. The source and destination nodes are added to the graph 20 | when the aren't already present. 21 | 22 | 23 | Node related methods 24 | -------------------- 25 | 26 | .. method:: Graph.add_node(node[, node_data]) 27 | 28 | Adds a new node to the graph if it is not already present. The new 29 | node must be a hashable object. 30 | 31 | Arbitrary data can be attached to the node via the optional *node_data* 32 | argument. 33 | 34 | .. note:: the node also won't be added to the graph when it is 35 | present but currently hidden. 36 | 37 | 38 | .. method:: Graph.hide_node(node) 39 | 40 | Hides a *node* from the graph. The incoming and outgoing edges of 41 | the node will also be hidden. 42 | 43 | Raises :class:`altgraph.GraphError` when the node is not (visible) 44 | node of the graph. 45 | 46 | 47 | .. method:: Graph.restore_node(node) 48 | 49 | Restores a previously hidden *node*. The incoming and outgoing 50 | edges of the node are also restored. 51 | 52 | Raises :class:`altgraph.GraphError` when the node is not a hidden 53 | node of the graph. 54 | 55 | .. method:: Graph.restore_all_nodes() 56 | 57 | Restores all hidden nodes. 58 | 59 | .. method:: Graph.number_of_nodes() 60 | 61 | Return the number of visible nodes in the graph. 62 | 63 | .. method:: Graph.number_of_hidden_nodes() 64 | 65 | Return the number of hidden nodes in the graph. 66 | 67 | .. method:: Graph.node_list() 68 | 69 | Return a list with all visible nodes in the graph. 70 | 71 | .. method:: Graph.hidden_node_list() 72 | 73 | Return a list with all hidden nodes in the graph. 74 | 75 | .. method:: node_data(node) 76 | 77 | Return the data associated with the *node* when it was 78 | added. 79 | 80 | .. method:: Graph.describe_node(node) 81 | 82 | Returns *node*, the node's data and the lists of outgoing 83 | and incoming edges for the node. 84 | 85 | .. note:: 86 | 87 | the edge lists should not be modified, doing so 88 | can result in unpredicatable behavior. 89 | 90 | .. method:: Graph.__contains__(node) 91 | 92 | Returns True iff *node* is a node in the graph. This 93 | method is accessed through the *in* operator. 94 | 95 | .. method:: Graph.__iter__() 96 | 97 | Yield all nodes in the graph. 98 | 99 | .. method:: Graph.out_edges(node) 100 | 101 | Return the list of outgoing edges for *node* 102 | 103 | .. method:: Graph.inc_edges(node) 104 | 105 | Return the list of incoming edges for *node* 106 | 107 | .. method:: Graph.all_edges(node) 108 | 109 | Return the list of incoming and outgoing edges for *node* 110 | 111 | .. method:: Graph.out_degree(node) 112 | 113 | Return the number of outgoing edges for *node*. 114 | 115 | .. method:: Graph.inc_degree(node) 116 | 117 | Return the number of incoming edges for *node*. 118 | 119 | .. method:: Graph.all_degree(node) 120 | 121 | Return the number of edges (incoming or outgoing) for *node*. 122 | 123 | Edge related methods 124 | -------------------- 125 | 126 | .. method:: Graph.add_edge(head_id, tail_id [, edge data [, create_nodes]]) 127 | 128 | Adds a directed edge from *head_id* to *tail_id*. Arbitrary data can 129 | be added via *edge_data*. When *create_nodes* is *True* (the default), 130 | *head_id* and *tail_id* will be added to the graph when the aren't 131 | already present. 132 | 133 | .. method:: Graph.hide_edge(edge) 134 | 135 | Hides an edge from the graph. The edge may be unhidden at some later 136 | time. 137 | 138 | .. method:: Graph.restore_edge(edge) 139 | 140 | Restores a previously hidden *edge*. 141 | 142 | .. method:: Graph.restore_all_edges() 143 | 144 | Restore all edges that were hidden before, except for edges 145 | referring to hidden nodes. 146 | 147 | .. method:: Graph.edge_by_node(head, tail) 148 | 149 | Return the edge ID for an edge from *head* to *tail*, 150 | or :data:`None` when no such edge exists. 151 | 152 | .. method:: Graph.edge_by_id(edge) 153 | 154 | Return the head and tail of the *edge* 155 | 156 | .. method:: Graph.edge_data(edge) 157 | 158 | Return the data associated with the *edge*. 159 | 160 | .. method:: Graph.update_edge_data(edge, data) 161 | 162 | Replace the edge data for *edge* by *data*. Raises 163 | :exc:`KeyError` when the edge does not exist. 164 | 165 | .. versionadded:: 0.12 166 | 167 | .. method:: Graph.head(edge) 168 | 169 | Return the head of an *edge* 170 | 171 | .. method:: Graph.tail(edge) 172 | 173 | Return the tail of an *edge* 174 | 175 | .. method:: Graph.describe_edge(edge) 176 | 177 | Return the *edge*, the associated data, its head and tail. 178 | 179 | .. method:: Graph.number_of_edges() 180 | 181 | Return the number of visible edges. 182 | 183 | .. method:: Graph.number_of_hidden_edges() 184 | 185 | Return the number of hidden edges. 186 | 187 | .. method:: Graph.edge_list() 188 | 189 | Returns a list with all visible edges in the graph. 190 | 191 | .. method:: Graph.hidden_edge_list() 192 | 193 | Returns a list with all hidden edges in the graph. 194 | 195 | Graph traversal 196 | --------------- 197 | 198 | .. method:: Graph.out_nbrs(node) 199 | 200 | Return a list of all nodes connected by outgoing edges. 201 | 202 | .. method:: Graph.inc_nbrs(node) 203 | 204 | Return a list of all nodes connected by incoming edges. 205 | 206 | .. method:: Graph.all_nbrs(node) 207 | 208 | Returns a list of nodes connected by an incoming or outgoing edge. 209 | 210 | .. method:: Graph.forw_topo_sort() 211 | 212 | Return a list of nodes where the successors (based on outgoing 213 | edges) of any given node apear in the sequence after that node. 214 | 215 | .. method:: Graph.back_topo_sort() 216 | 217 | Return a list of nodes where the successors (based on incoming 218 | edges) of any given node apear in the sequence after that node. 219 | 220 | .. method:: Graph.forw_bfs_subgraph(start_id) 221 | 222 | Return a subgraph consisting of the breadth first 223 | reachable nodes from *start_id* based on their outgoing edges. 224 | 225 | 226 | .. method:: Graph.back_bfs_subgraph(start_id) 227 | 228 | Return a subgraph consisting of the breadth first 229 | reachable nodes from *start_id* based on their incoming edges. 230 | 231 | .. method:: Graph.iterdfs(start[, end[, forward]]) 232 | 233 | Yield nodes in a depth first traversal starting at the *start* 234 | node. 235 | 236 | If *end* is specified traversal stops when reaching that node. 237 | 238 | If forward is True (the default) edges are traversed in forward 239 | direction, otherwise they are traversed in reverse direction. 240 | 241 | .. method:: Graph.iterdata(start[, end[, forward[, condition]]]) 242 | 243 | Yield the associated data for nodes in a depth first traversal 244 | starting at the *start* node. This method will not yield values for nodes 245 | without associated data. 246 | 247 | If *end* is specified traversal stops when reaching that node. 248 | 249 | If *condition* is specified and the condition callable returns 250 | False for the associated data this method will not yield the 251 | associated data and will not follow the edges for the node. 252 | 253 | If forward is True (the default) edges are traversed in forward 254 | direction, otherwise they are traversed in reverse direction. 255 | 256 | .. method:: Graph.forw_bfs(start[, end]) 257 | 258 | Returns a list of nodes starting at *start* in some bread first 259 | search order (following outgoing edges). 260 | 261 | When *end* is specified iteration stops at that node. 262 | 263 | .. method:: Graph.back_bfs(start[, end]) 264 | 265 | Returns a list of nodes starting at *start* in some bread first 266 | search order (following incoming edges). 267 | 268 | When *end* is specified iteration stops at that node. 269 | 270 | .. method:: Graph.get_hops(start[, end[, forward]]) 271 | 272 | Computes the hop distance to all nodes centered around a specified node. 273 | 274 | First order neighbours are at hop 1, their neigbours are at hop 2 etc. 275 | Uses :py:meth:`forw_bfs` or :py:meth:`back_bfs` depending on the value of 276 | the forward parameter. 277 | 278 | If the distance between all neighbouring nodes is 1 the hop number 279 | corresponds to the shortest distance between the nodes. 280 | 281 | Typical usage:: 282 | 283 | >>> print graph.get_hops(1, 8) 284 | >>> [(1, 0), (2, 1), (3, 1), (4, 2), (5, 3), (7, 4), (8, 5)] 285 | # node 1 is at 0 hops 286 | # node 2 is at 1 hop 287 | # ... 288 | # node 8 is at 5 hops 289 | 290 | 291 | Graph statistics 292 | ---------------- 293 | 294 | .. method:: Graph.connected() 295 | 296 | Returns True iff every node in the graph can be reached from 297 | every other node. 298 | 299 | .. method:: Graph.clust_coef(node) 300 | 301 | Returns the local clustering coefficient of node. 302 | 303 | The local cluster coefficient is the proportion of the actual number 304 | of edges between neighbours of node and the maximum number of 305 | edges between those nodes. 306 | -------------------------------------------------------------------------------- /doc/graphalgo.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.GraphAlgo` --- Graph algorithms 2 | ================================================== 3 | 4 | .. module:: altgraph.GraphAlgo 5 | :synopsis: Basic graphs algoritms 6 | 7 | .. function:: dijkstra(graph, start[, end]) 8 | 9 | Dijkstra's algorithm for shortest paths. 10 | 11 | Find shortest paths from the start node to all nodes nearer 12 | than or equal to the *end* node. The edge data is assumed to be the edge length. 13 | 14 | .. note:: 15 | 16 | Dijkstra's algorithm is only guaranteed to work correctly when all edge lengths are positive. 17 | This code does not verify this property for all edges (only the edges examined until the end 18 | vertex is reached), but will correctly compute shortest paths even for some graphs with negative 19 | edges, and will raise an exception if it discovers that a negative edge has caused it to make a mistake. 20 | 21 | 22 | .. function:: shortest_path(graph, start, end) 23 | 24 | Find a single shortest path from the given start node to the given end node. 25 | The input has the same conventions as :func:`dijkstra`. The output is a list 26 | of the nodes in order along the shortest path. 27 | -------------------------------------------------------------------------------- /doc/graphstat.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.GraphStat` --- Functions providing various graph statistics 2 | ========================================================================== 3 | 4 | .. module:: altgraph.GraphStat 5 | :synopsis: Functions providing various graph statistics 6 | 7 | The module :mod:`altgraph.GraphStat` provides function that calculate 8 | graph statistics. Currently there is only one such function, more may 9 | be added later. 10 | 11 | .. function:: degree_dist(graph[, limits[, bin_num[, mode]]]) 12 | 13 | Groups the number of edges per node into *bin_num* bins 14 | and returns the list of those bins. Every item in the result 15 | is a tuple with the center of the bin and the number of items 16 | in that bin. 17 | 18 | When the *limits* argument is present it must be a tuple with 19 | the mininum and maximum number of edges that get binned (that 20 | is, when *limits* is ``(4, 10)`` only nodes with between 4 21 | and 10 edges get counted. 22 | 23 | The *mode* argument is used to count incoming (``'inc'``) or 24 | outgoing (``'out'``) edges. The default is to count the outgoing 25 | edges. 26 | -------------------------------------------------------------------------------- /doc/graphutil.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.GraphUtil` --- Utility functions 2 | ================================================ 3 | 4 | .. module:: altgraph.GraphUtil 5 | :synopsis: Utility functions 6 | 7 | The module :mod:`altgraph.GraphUtil` performs a number of more 8 | or less useful utility functions. 9 | 10 | .. function:: generate_random_graph(node_num, edge_num[, self_loops[, multi_edges]) 11 | 12 | Generates and returns a :class:`Graph ` instance 13 | with *node_num* nodes randomly connected by *edge_num* edges. 14 | 15 | When *self_loops* is present and True there can be edges that point from 16 | a node to itself. 17 | 18 | When *multi_edge* is present and True there can be duplicate edges. 19 | 20 | This method raises :class:`GraphError `_ 38 | 39 | .. function:: filter_stack(graph, head, filters) 40 | 41 | Perform a depth-first oder walk of the graph starting at *head* and 42 | apply all filter functions in *filters* on the node data of the nodes 43 | found. 44 | 45 | Returns (*visited*, *removes*, *orphans*), where 46 | 47 | * *visited*: the set of visited nodes 48 | 49 | * *removes*: the list of nodes where the node data doesn't match 50 | all *filters*. 51 | 52 | * *orphans*: list of tuples (*last_good*, *node*), where 53 | node is not in *removes* and one of the nodes that is connected 54 | by an incoming edge is in *removes*. *Last_good* is the 55 | closest upstream node that is not in *removes*. 56 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. altgraph documentation master file, created by 2 | sphinx-quickstart on Tue Aug 31 11:04:49 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Altgraph - A basic graph library 7 | ================================ 8 | 9 | altgraph is a fork of graphlib: a graph (network) package for constructing 10 | graphs, BFS and DFS traversals, topological sort, shortest paths, etc. with 11 | graphviz output. 12 | 13 | The primary users of this package are `macholib `_ and `modulegraph `_. 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | 18 | changelog 19 | license 20 | core 21 | graph 22 | objectgraph 23 | graphalgo 24 | graphstat 25 | graphutil 26 | dot 27 | 28 | Online Resources 29 | ---------------- 30 | 31 | * `Sourcecode repository on GitHub `_ 32 | 33 | * `The issue tracker `_ 34 | 35 | Indices and tables 36 | ------------------ 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2004 Istvan Albert unless otherwise noted. 5 | 6 | Parts are copyright (c) Bob Ippolito 7 | 8 | Parts are copyright (c) 2010-2014 Ronald Oussoren 9 | 10 | MIT License 11 | ........... 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 14 | and associated documentation files (the "Software"), to deal in the Software without restriction, 15 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 16 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 17 | so. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 20 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 21 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 22 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 23 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /doc/objectgraph.rst: -------------------------------------------------------------------------------- 1 | :mod:`altgraph.ObjectGraph` --- Graphs of objecs with an identifier 2 | =================================================================== 3 | 4 | .. module:: altgraph.ObjectGraph 5 | :synopsis: A graph of objects that have a "graphident" attribute. 6 | 7 | .. class:: ObjectGraph([graph[, debug]]) 8 | 9 | A graph of objects that have a "graphident" attribute. The 10 | value of this attribute is the key for the object in the 11 | graph. 12 | 13 | The optional *graph* is a previously constructed 14 | :class:`Graph `. 15 | 16 | The optional *debug* level controls the amount of debug output 17 | (see :meth:`msg`, :meth:`msgin` and :meth:`msgout`). 18 | 19 | .. note:: the altgraph library does not generate output, the 20 | debug attribute and message methods are present for use 21 | by subclasses. 22 | 23 | .. data:: ObjectGraph.graph 24 | 25 | An :class:`Graph ` object that contains 26 | the graph data. 27 | 28 | 29 | .. method:: ObjectGraph.addNode(node) 30 | 31 | Adds a *node* to the graph. 32 | 33 | .. note:: re-adding a node that was previously removed 34 | using :meth:`removeNode` will reinstate the previously 35 | removed node. 36 | 37 | .. method:: ObjectGraph.createNode(self, cls, name, \*args, \**kwds) 38 | 39 | Creates a new node using ``cls(*args, **kwds)`` and adds that 40 | node using :meth:`addNode`. 41 | 42 | Returns the newly created node. 43 | 44 | .. method:: ObjectGraph.removeNode(node) 45 | 46 | Removes a *node* from the graph when it exists. The *node* argument 47 | is either a node object, or the graphident of a node. 48 | 49 | .. method:: ObjectGraph.createReferences(fromnode, tonode[, edge_data]) 50 | 51 | Creates a reference from *fromnode* to *tonode*. The optional 52 | *edge_data* is associated with the edge. 53 | 54 | *Fromnode* and *tonode* can either be node objects or the graphident 55 | values for nodes. When *fromnode* is :data:`None` *tonode* is a root 56 | for the graph. 57 | 58 | .. method:: removeReference(fromnode, tonode) 59 | 60 | Removes the reference from *fromnode* to *tonode* if it exists. 61 | 62 | .. method:: ObjectGraph.getRawIdent(node) 63 | 64 | Returns the *graphident* attribute of *node*, or the graph itself 65 | when *node* is :data:`None`. 66 | 67 | .. method:: getIdent(node) 68 | 69 | Same as :meth:`getRawIdent`, but only if the node is part 70 | of the graph. 71 | 72 | *Node* can either be an actual node object or the graphident of 73 | a node. 74 | 75 | .. method:: ObjectGraph.findNode(node) 76 | 77 | Returns a given node in the graph, or :data:`Node` when it cannot 78 | be found. 79 | 80 | *Node* is either an object with a *graphident* attribute or 81 | the *graphident* attribute itself. 82 | 83 | .. method:: ObjectGraph.__contains__(node) 84 | 85 | Returns True if *node* is a member of the graph. *Node* is either an 86 | object with a *graphident* attribute or the *graphident* attribute itself. 87 | 88 | .. method:: ObjectGraph.flatten([condition[, start]]) 89 | 90 | Yield all nodes that are entirely reachable by *condition* 91 | starting fromt he given *start* node or the graph root. 92 | 93 | .. note:: objects are only reachable from the graph root 94 | when there is a reference from the root to the node 95 | (either directly or through another node) 96 | 97 | .. method:: ObjectGraph.nodes() 98 | 99 | Yield all nodes in the graph. 100 | 101 | .. method:: ObjectGraph.get_edges(node) 102 | 103 | Returns two iterators that yield the nodes reaching by 104 | outgoing and incoming edges for *node*. Note that the 105 | iterator for incoming edgets can yield :data:`None` when the 106 | *node* is a root of the graph. 107 | 108 | Use :data:`None` for *node* to fetch the roots of the graph. 109 | 110 | .. method:: ObjectGraph.filterStack(filters) 111 | 112 | Filter the ObjectGraph in-place by removing all edges to nodes that 113 | do not match every filter in the given filter list 114 | 115 | Returns a tuple containing the number of: 116 | (*nodes_visited*, *nodes_removed*, *nodes_orphaned*) 117 | 118 | .. method:: ObjectGraph.edgeData(fromNode, toNode): 119 | Return the edge data associated with the edge from *fromNode* 120 | to *toNode*. Raises :exc:`KeyError` when no such edge exists. 121 | 122 | .. versionadded: 0.12 123 | 124 | .. method:: ObjectGraph.updateEdgeData(fromNode, toNode, edgeData) 125 | 126 | Replace the data associated with the edge from *fromNode* to 127 | *toNode* by *edgeData*. 128 | 129 | Raises :exc:`KeyError` when the edge does not exist. 130 | 131 | Debug output 132 | ------------ 133 | 134 | .. data:: ObjectGraph.debug 135 | 136 | The current debug level. 137 | 138 | .. method:: ObjectGraph.msg(level, text, \*args) 139 | 140 | Print a debug message at the current indentation level when the current 141 | debug level is *level* or less. 142 | 143 | .. method:: ObjectGraph.msgin(level, text, \*args) 144 | 145 | Print a debug message when the current debug level is *level* or less, 146 | and increase the indentation level. 147 | 148 | .. method:: ObjectGraph.msgout(level, text, \*args) 149 | 150 | Decrease the indentation level and print a debug message when the 151 | current debug level is *level* or less. 152 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [x-metadata] 2 | name = altgraph 3 | version = 0.17.4 4 | description = Python graph (network) package 5 | long_description_file = 6 | README.rst 7 | doc/changelog.rst 8 | 9 | author = Ronald Oussoren 10 | author_email = ronaldoussoren@mac.com 11 | maintainer = Ronald Oussoren 12 | maintainer_email = ronaldoussoren@mac.com 13 | url = https://altgraph.readthedocs.io 14 | download_url = http://pypi.python.org/pypi/altgraph 15 | license = MIT 16 | classifiers = 17 | Intended Audience :: Developers 18 | License :: OSI Approved :: MIT License 19 | Programming Language :: Python 20 | Programming Language :: Python :: 2 21 | Programming Language :: Python :: 2.7 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3.4 24 | Programming Language :: Python :: 3.5 25 | Programming Language :: Python :: 3.6 26 | Programming Language :: Python :: 3.7 27 | Programming Language :: Python :: 3.8 28 | Programming Language :: Python :: 3.9 29 | Programming Language :: Python :: 3.10 30 | Programming Language :: Python :: 3.11 31 | Programming Language :: Python :: 3.12 32 | Topic :: Software Development :: Libraries :: Python Modules 33 | Topic :: Scientific/Engineering :: Mathematics 34 | Topic :: Scientific/Engineering :: Visualization 35 | keywords = graph 36 | platforms = any 37 | packages = altgraph 38 | zip-safe = 1 39 | 40 | [bdist_wheel] 41 | universal = 1 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared setup file for simple python packages. Uses a setup.cfg that 3 | is the same as the distutils2 project, unless noted otherwise. 4 | 5 | It exists for two reasons: 6 | 1) This makes it easier to reuse setup.py code between my own 7 | projects 8 | 9 | 2) Easier migration to distutils2 when that catches on. 10 | 11 | Additional functionality: 12 | 13 | * Section metadata: 14 | requires-test: Same as 'tests_require' option for setuptools. 15 | 16 | """ 17 | 18 | import os 19 | import platform 20 | import re 21 | import sys 22 | from distutils import log 23 | from fnmatch import fnmatch 24 | 25 | from setuptools import Command, setup 26 | from setuptools.command import egg_info 27 | 28 | if sys.version_info[0] == 2: 29 | from ConfigParser import RawConfigParser, NoOptionError, NoSectionError 30 | else: 31 | from configparser import RawConfigParser, NoOptionError, NoSectionError 32 | 33 | ROOTDIR = os.path.dirname(os.path.abspath(__file__)) 34 | 35 | 36 | # 37 | # 38 | # 39 | # Parsing the setup.cfg and converting it to something that can be 40 | # used by setuptools.setup() 41 | # 42 | # 43 | # 44 | 45 | 46 | def eval_marker(value): 47 | """ 48 | Evaluate an distutils2 environment marker. 49 | 50 | This code is unsafe when used with hostile setup.cfg files, 51 | but that's not a problem for our own files. 52 | """ 53 | value = value.strip() 54 | 55 | class M: 56 | def __init__(self, **kwds): 57 | for k, v in kwds.items(): 58 | setattr(self, k, v) 59 | 60 | variables = { 61 | "python_version": "%d.%d" % (sys.version_info[0], sys.version_info[1]), 62 | "python_full_version": sys.version.split()[0], 63 | "os": M(name=os.name), 64 | "sys": M(platform=sys.platform), 65 | "platform": M(version=platform.version(), machine=platform.machine()), 66 | } 67 | 68 | return bool(eval(value, variables, variables)) 69 | 70 | return True 71 | 72 | 73 | def _opt_value(cfg, into, section, key, transform=None): 74 | try: 75 | v = cfg.get(section, key) 76 | if transform != _as_lines and ";" in v: 77 | v, marker = v.rsplit(";", 1) 78 | if not eval_marker(marker): 79 | return 80 | 81 | v = v.strip() 82 | 83 | if v: 84 | if transform: 85 | into[key] = transform(v.strip()) 86 | else: 87 | into[key] = v.strip() 88 | 89 | except (NoOptionError, NoSectionError): 90 | pass 91 | 92 | 93 | def _as_bool(value): 94 | if value.lower() in ("y", "yes", "on"): 95 | return True 96 | elif value.lower() in ("n", "no", "off"): 97 | return False 98 | elif value.isdigit(): 99 | return bool(int(value)) 100 | else: 101 | raise ValueError(value) 102 | 103 | 104 | def _as_list(value): 105 | return value.split() 106 | 107 | 108 | def _as_lines(value): 109 | result = [] 110 | for v in value.splitlines(): 111 | if ";" in v: 112 | v, marker = v.rsplit(";", 1) 113 | if not eval_marker(marker): 114 | continue 115 | 116 | v = v.strip() 117 | if v: 118 | result.append(v) 119 | else: 120 | result.append(v) 121 | return result 122 | 123 | 124 | def _map_requirement(value): 125 | m = re.search(r"(\S+)\s*(?:\((.*)\))?", value) 126 | name = m.group(1) 127 | version = m.group(2) 128 | 129 | if version is None: 130 | return name 131 | 132 | else: 133 | mapped = [] 134 | for v in version.split(","): 135 | v = v.strip() 136 | if v[0].isdigit(): 137 | # Checks for a specific version prefix 138 | m = v.rsplit(".", 1) 139 | mapped.append(">=%s,<%s.%s" % (v, m[0], int(m[1]) + 1)) 140 | 141 | else: 142 | mapped.append(v) 143 | return "%s %s" % (name, ",".join(mapped)) 144 | 145 | 146 | def _as_requires(value): 147 | requires = [] 148 | for req in value.splitlines(): 149 | if ";" in req: 150 | req, marker = req.rsplit(";", 1) 151 | if not eval_marker(marker): 152 | continue 153 | req = req.strip() 154 | 155 | if not req: 156 | continue 157 | requires.append(_map_requirement(req)) 158 | return requires 159 | 160 | 161 | def parse_setup_cfg(): 162 | cfg = RawConfigParser() 163 | r = cfg.read([os.path.join(ROOTDIR, "setup.cfg")]) 164 | if len(r) != 1: 165 | print("Cannot read 'setup.cfg'") 166 | sys.exit(1) 167 | 168 | metadata = { 169 | "name": cfg.get("x-metadata", "name"), 170 | "version": cfg.get("x-metadata", "version"), 171 | "description": cfg.get("x-metadata", "description"), 172 | } 173 | 174 | _opt_value(cfg, metadata, "x-metadata", "license") 175 | _opt_value(cfg, metadata, "x-metadata", "maintainer") 176 | _opt_value(cfg, metadata, "x-metadata", "maintainer_email") 177 | _opt_value(cfg, metadata, "x-metadata", "author") 178 | _opt_value(cfg, metadata, "x-metadata", "author_email") 179 | _opt_value(cfg, metadata, "x-metadata", "url") 180 | _opt_value(cfg, metadata, "x-metadata", "download_url") 181 | _opt_value(cfg, metadata, "x-metadata", "classifiers", _as_lines) 182 | _opt_value(cfg, metadata, "x-metadata", "platforms", _as_list) 183 | _opt_value(cfg, metadata, "x-metadata", "packages", _as_list) 184 | _opt_value(cfg, metadata, "x-metadata", "keywords", _as_list) 185 | 186 | try: 187 | v = cfg.get("x-metadata", "requires-dist") 188 | 189 | except (NoOptionError, NoSectionError): 190 | pass 191 | 192 | else: 193 | requires = _as_requires(v) 194 | if requires: 195 | metadata["install_requires"] = requires 196 | 197 | try: 198 | v = cfg.get("x-metadata", "requires-test") 199 | 200 | except (NoOptionError, NoSectionError): 201 | pass 202 | 203 | else: 204 | requires = _as_requires(v) 205 | if requires: 206 | metadata["tests_require"] = requires 207 | 208 | try: 209 | v = cfg.get("x-metadata", "long_description_file") 210 | except (NoOptionError, NoSectionError): 211 | pass 212 | 213 | else: 214 | parts = [] 215 | for nm in v.split(): 216 | fp = open(nm, "r") 217 | parts.append(fp.read()) 218 | fp.close() 219 | 220 | metadata["long_description"] = "\n\n".join(parts) 221 | metadata["long_description_content_type"] = "text/x-rst; charset=UTF-8" 222 | 223 | try: 224 | v = cfg.get("x-metadata", "zip-safe") 225 | except (NoOptionError, NoSectionError): 226 | pass 227 | 228 | else: 229 | metadata["zip_safe"] = _as_bool(v) 230 | 231 | try: 232 | v = cfg.get("x-metadata", "console_scripts") 233 | except (NoOptionError, NoSectionError): 234 | pass 235 | 236 | else: 237 | if "entry_points" not in metadata: 238 | metadata["entry_points"] = {} 239 | 240 | metadata["entry_points"]["console_scripts"] = v.splitlines() 241 | 242 | if sys.version_info[:2] <= (2, 6): 243 | try: 244 | metadata["tests_require"] += ", unittest2" 245 | except KeyError: 246 | metadata["tests_require"] = "unittest2" 247 | 248 | return metadata 249 | 250 | 251 | # 252 | # 253 | # 254 | # Definitions of custom commands 255 | # 256 | # 257 | # 258 | 259 | 260 | def recursiveGlob(root, pathPattern): 261 | """ 262 | Recursively look for files matching 'pathPattern'. Return a list 263 | of matching files/directories. 264 | """ 265 | result = [] 266 | 267 | for rootpath, _dirnames, filenames in os.walk(root): 268 | for fn in filenames: 269 | if fnmatch(fn, pathPattern): 270 | result.append(os.path.join(rootpath, fn)) 271 | return result 272 | 273 | 274 | def importExternalTestCases(unittest, pathPattern="test_*.py", root=".", package=None): 275 | """ 276 | Import all unittests in the PyObjC tree starting at 'root' 277 | """ 278 | 279 | testFiles = recursiveGlob(root, pathPattern) 280 | testModules = [ 281 | x[len(root) + 1 : -3].replace("/", ".") for x in testFiles # noqa: E203 282 | ] # noqa: E203 283 | if package is not None: 284 | testModules = [(package + "." + m) for m in testModules] 285 | 286 | suites = [] 287 | 288 | for modName in testModules: 289 | try: 290 | module = __import__(modName) 291 | except ImportError: 292 | print("SKIP %s: %s" % (modName, sys.exc_info()[1])) 293 | continue 294 | 295 | if "." in modName: 296 | for elem in modName.split(".")[1:]: 297 | module = getattr(module, elem) 298 | 299 | s = unittest.defaultTestLoader.loadTestsFromModule(module) 300 | suites.append(s) 301 | 302 | return unittest.TestSuite(suites) 303 | 304 | 305 | class my_egg_info(egg_info.egg_info): 306 | def run(self): 307 | egg_info.egg_info.run(self) 308 | 309 | path = os.path.join(self.egg_info, "PKG-INFO") 310 | 311 | with open(path, "r") as fp: 312 | contents = fp.read() 313 | 314 | try: 315 | before, after = contents.split("\n\n", 1) 316 | except ValueError: 317 | before = contents 318 | after = "" 319 | 320 | with open(path, "w") as fp: 321 | fp.write(before) 322 | fp.write( 323 | "\nProject-URL: Documentation, https://altgraph.readthedocs.io/en/latest/\n" # noqa: B950 324 | ) 325 | fp.write( 326 | "Project-URL: Issue tracker, https://github.com/ronaldoussoren/altgraph/issues\n" # noqa: B950 327 | ) 328 | fp.write( 329 | "Project-URL: Repository, https://github.com/ronaldoussoren/altgraph\n\n" # noqa: B950 330 | ) 331 | fp.write(after) 332 | 333 | 334 | class my_test(Command): 335 | description = "run test suite" 336 | user_options = [("verbosity=", None, "print what tests are run")] 337 | 338 | def initialize_options(self): 339 | self.verbosity = "1" 340 | 341 | def finalize_options(self): 342 | if isinstance(self.verbosity, str): 343 | self.verbosity = int(self.verbosity) 344 | 345 | def cleanup_environment(self): 346 | ei_cmd = self.get_finalized_command("egg_info") 347 | egg_name = ei_cmd.egg_name.replace("-", "_") 348 | 349 | to_remove = [] 350 | for dirname in sys.path: 351 | bn = os.path.basename(dirname) 352 | if bn.startswith(egg_name + "-"): 353 | to_remove.append(dirname) 354 | 355 | for dirname in to_remove: 356 | log.info("removing installed %r from sys.path before testing" % (dirname,)) 357 | sys.path.remove(dirname) 358 | 359 | def add_project_to_sys_path(self): 360 | from pkg_resources import normalize_path, add_activation_listener 361 | from pkg_resources import working_set, require 362 | 363 | self.reinitialize_command("egg_info") 364 | self.run_command("egg_info") 365 | self.reinitialize_command("build_ext", inplace=1) 366 | self.run_command("build_ext") 367 | 368 | # Check if this distribution is already on sys.path 369 | # and remove that version, this ensures that the right 370 | # copy of the package gets tested. 371 | 372 | self.__old_path = sys.path[:] 373 | self.__old_modules = sys.modules.copy() 374 | 375 | ei_cmd = self.get_finalized_command("egg_info") 376 | sys.path.insert(0, normalize_path(ei_cmd.egg_base)) 377 | sys.path.insert(1, os.path.dirname(__file__)) 378 | 379 | # Strip the namespace packages defined in this distribution 380 | # from sys.modules, needed to reset the search path for 381 | # those modules. 382 | 383 | nspkgs = getattr(self.distribution, "namespace_packages", None) 384 | if nspkgs is not None: 385 | for nm in nspkgs: 386 | del sys.modules[nm] 387 | 388 | # Reset pkg_resources state: 389 | add_activation_listener(lambda dist: dist.activate()) 390 | working_set.__init__() 391 | require("%s==%s" % (ei_cmd.egg_name, ei_cmd.egg_version)) 392 | 393 | def remove_from_sys_path(self): 394 | from pkg_resources import working_set 395 | 396 | sys.path[:] = self.__old_path 397 | sys.modules.clear() 398 | sys.modules.update(self.__old_modules) 399 | working_set.__init__() 400 | 401 | def run(self): 402 | import unittest 403 | 404 | # Ensure that build directory is on sys.path (py3k) 405 | 406 | self.cleanup_environment() 407 | self.add_project_to_sys_path() 408 | 409 | try: 410 | meta = self.distribution.metadata 411 | name = meta.get_name() 412 | test_pkg = name + "_tests" 413 | suite = importExternalTestCases(unittest, "test_*.py", test_pkg, test_pkg) 414 | 415 | runner = unittest.TextTestRunner(verbosity=self.verbosity) 416 | result = runner.run(suite) 417 | 418 | # Print out summary. This is a structured format that 419 | # should make it easy to use this information in scripts. 420 | summary = { 421 | "count": result.testsRun, 422 | "fails": len(result.failures), 423 | "errors": len(result.errors), 424 | "xfails": len(getattr(result, "expectedFailures", [])), 425 | "xpass": len(getattr(result, "expectedSuccesses", [])), 426 | "skip": len(getattr(result, "skipped", [])), 427 | } 428 | print("SUMMARY: %s" % (summary,)) 429 | if summary["fails"] or summary["errors"]: 430 | sys.exit(1) 431 | 432 | finally: 433 | self.remove_from_sys_path() 434 | 435 | 436 | # 437 | # 438 | # 439 | # And finally run the setuptools main entry point. 440 | # 441 | # 442 | # 443 | 444 | metadata = parse_setup_cfg() 445 | 446 | setup(cmdclass={"test": my_test, "egg_info": my_egg_info}, **metadata) 447 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py37,py38,py39,py310,py311,py312,flake8,coverage-report 3 | 4 | [testenv] 5 | commands = {envbindir}/python -m coverage run --parallel setup.py test 6 | deps = coverage 7 | 8 | [testenv:black] 9 | basepython = python3.7 10 | deps = black 11 | skip_install = true 12 | commands = 13 | {envbindir}/python -m black --target-version py36 altgraph altgraph_tests 14 | 15 | [testenv:isort] 16 | basepython = python3.7 17 | deps = 18 | isort 19 | skip_install = true 20 | commands = 21 | {envbindir}/python -m isort altgraph altgraph_tests 22 | 23 | [testenv:flake8] 24 | basepython = python3.7 25 | deps = 26 | flake8 27 | flake8-bugbear 28 | flake8-deprecated 29 | flake8-comprehensions 30 | flake8-isort 31 | flake8-quotes 32 | flake8-mutable 33 | flake8-todo 34 | skip_install = True 35 | commands = 36 | {envbindir}/python -m flake8 altgraph 37 | 38 | 39 | [testenv:coverage-report] 40 | deps = coverage 41 | skip_install = true 42 | commands = 43 | coverage combine 44 | coverage html 45 | coverage report 46 | 47 | [coverage:run] 48 | branch = True 49 | source = altgraph 50 | 51 | [coverage:report] 52 | sort = Cover 53 | 54 | [coverage:paths] 55 | source = 56 | altgraph 57 | .tox/*/lib/python*/site-packages/altgraph 58 | 59 | [flake8] 60 | max-line-length = 80 61 | select = C,E,F,W,B,B950,T,Q,M 62 | ignore = E501,W503 63 | inline-quotes = double 64 | multiline-quotes = double 65 | docstring-quotes = double 66 | 67 | [isort] 68 | multi_line_output=3 69 | include_trailing_comma=True 70 | force_grid_wrap=0 71 | use_parentheses=True 72 | line_length=88 73 | --------------------------------------------------------------------------------