├── .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 |
--------------------------------------------------------------------------------