kececilayout 0.5.0__py3-none-any.whl → 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/conf.py +97 -0
- kececilayout/__init__.py +17 -1
- kececilayout/_version.py +4 -3
- kececilayout/kececi_layout.py +176 -3
- {kececilayout-0.5.0.dist-info → kececilayout-0.5.2.dist-info}/METADATA +749 -25
- kececilayout-0.5.2.dist-info/RECORD +10 -0
- {kececilayout-0.5.0.dist-info → kececilayout-0.5.2.dist-info}/WHEEL +1 -1
- kececilayout-0.5.2.dist-info/licenses/LICENSE +661 -0
- {kececilayout-0.5.0.dist-info → kececilayout-0.5.2.dist-info}/top_level.txt +2 -0
- tests/test_sample.py +261 -0
- kececilayout-0.5.0.dist-info/RECORD +0 -8
- kececilayout-0.5.0.dist-info/licenses/LICENSE +0 -21
tests/test_sample.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Advanced tests for the kececilayout library.
|
|
4
|
+
|
|
5
|
+
This test suite uses pytest to verify the functionality of layout calculations,
|
|
6
|
+
graph type compatibility, error handling, and drawing function routing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
import sys
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
|
|
14
|
+
# Import the module to be tested
|
|
15
|
+
# Assume the code is in a file named `kececilayout_lib.py` in the same directory
|
|
16
|
+
# or properly installed in the environment.
|
|
17
|
+
# Note: Before testing, it's recommended to clean up the provided code by
|
|
18
|
+
# removing duplicate functions (e.g., keep only one canonical `kececi_layout` function).
|
|
19
|
+
# We will test against the most feature-complete versions of the functions.
|
|
20
|
+
from kececilayout import (
|
|
21
|
+
find_max_node_id,
|
|
22
|
+
kececi_layout, # Assuming this is the main, multi-library compatible function
|
|
23
|
+
to_networkx,
|
|
24
|
+
draw_kececi,
|
|
25
|
+
_draw_internal # Also test the internal router
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Pytest markers to skip tests if optional libraries are not installed
|
|
29
|
+
nx = pytest.importorskip("networkx")
|
|
30
|
+
ig = pytest.importorskip("igraph")
|
|
31
|
+
rx = pytest.importorskip("rustworkx")
|
|
32
|
+
nk = pytest.importorskip("networkit")
|
|
33
|
+
gg = pytest.importorskip("graphillion")
|
|
34
|
+
plt = pytest.importorskip("matplotlib.pyplot")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --- Test Fixtures: Reusable Test Data ---
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def nx_graph_simple():
|
|
41
|
+
"""A simple, predictable NetworkX graph with 5 nodes."""
|
|
42
|
+
return nx.path_graph(5)
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def ig_graph_simple():
|
|
46
|
+
"""A simple igraph graph, equivalent to the NetworkX path graph."""
|
|
47
|
+
return ig.Graph.TupleList([(0, 1), (1, 2), (2, 3), (3, 4)], directed=False)
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def rx_graph_simple():
|
|
51
|
+
"""A simple Rustworkx graph."""
|
|
52
|
+
g = rx.PyGraph()
|
|
53
|
+
g.add_nodes_from(range(5))
|
|
54
|
+
g.add_edges_from([(0, 1, None), (1, 2, None), (2, 3, None), (3, 4, None)])
|
|
55
|
+
return g
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def nk_graph_simple():
|
|
59
|
+
"""A simple NetworKit graph."""
|
|
60
|
+
g = nk.graph.Graph(5)
|
|
61
|
+
g.addEdge(0, 1)
|
|
62
|
+
g.addEdge(1, 2)
|
|
63
|
+
g.addEdge(2, 3)
|
|
64
|
+
g.addEdge(3, 4)
|
|
65
|
+
return g
|
|
66
|
+
|
|
67
|
+
# --- Test Classes for Organization ---
|
|
68
|
+
|
|
69
|
+
class TestFindMaxNodeId:
|
|
70
|
+
"""Tests for the find_max_node_id helper function."""
|
|
71
|
+
|
|
72
|
+
def test_empty_edges(self):
|
|
73
|
+
"""Should return 0 for an empty list of edges."""
|
|
74
|
+
assert find_max_node_id([]) == 0
|
|
75
|
+
|
|
76
|
+
def test_standard_edges(self):
|
|
77
|
+
"""Should find the max ID in a standard list."""
|
|
78
|
+
edges = [(1, 2), (5, 3), (4, 0)]
|
|
79
|
+
assert find_max_node_id(edges) == 5
|
|
80
|
+
|
|
81
|
+
def test_with_malformed_data(self, capsys):
|
|
82
|
+
"""Should handle TypeError and return 0, printing a warning."""
|
|
83
|
+
edges = [1, 2, 3] # Not a list of tuples
|
|
84
|
+
assert find_max_node_id(edges) == 0
|
|
85
|
+
captured = capsys.readouterr()
|
|
86
|
+
assert "Warning: Edge format was unexpected" in captured.out
|
|
87
|
+
|
|
88
|
+
class TestKececiLayout:
|
|
89
|
+
"""Comprehensive tests for the main kececi_layout function."""
|
|
90
|
+
|
|
91
|
+
def test_empty_graph(self, nx_graph_simple):
|
|
92
|
+
"""Should return an empty dict for an empty graph."""
|
|
93
|
+
empty_graph = nx.Graph()
|
|
94
|
+
assert kececi_layout(empty_graph) == {}
|
|
95
|
+
|
|
96
|
+
def test_single_node_graph(self):
|
|
97
|
+
"""A single node should be positioned at the origin (0,0)."""
|
|
98
|
+
g = nx.Graph()
|
|
99
|
+
g.add_node("A")
|
|
100
|
+
pos = kececi_layout(g)
|
|
101
|
+
assert pos == {"A": (0.0, 0.0)}
|
|
102
|
+
|
|
103
|
+
def test_invalid_primary_direction(self, nx_graph_simple):
|
|
104
|
+
"""Should raise ValueError for an invalid primary_direction."""
|
|
105
|
+
with pytest.raises(ValueError, match="Invalid primary_direction"):
|
|
106
|
+
kececi_layout(nx_graph_simple, primary_direction='diagonal')
|
|
107
|
+
|
|
108
|
+
def test_invalid_secondary_start_vertical(self, nx_graph_simple):
|
|
109
|
+
"""Should raise ValueError for an invalid secondary_start in vertical mode."""
|
|
110
|
+
with pytest.raises(ValueError, match="Invalid secondary_start for vertical"):
|
|
111
|
+
kececi_layout(nx_graph_simple, primary_direction='top_down', secondary_start='up')
|
|
112
|
+
|
|
113
|
+
def test_invalid_secondary_start_horizontal(self, nx_graph_simple):
|
|
114
|
+
"""Should raise ValueError for an invalid secondary_start in horizontal mode."""
|
|
115
|
+
with pytest.raises(ValueError, match="Invalid secondary_start for horizontal"):
|
|
116
|
+
kececi_layout(nx_graph_simple, primary_direction='left-to-right', secondary_start='left')
|
|
117
|
+
|
|
118
|
+
def test_unsupported_graph_type(self):
|
|
119
|
+
"""Should raise TypeError for an unsupported object."""
|
|
120
|
+
class UnknownGraph:
|
|
121
|
+
pass
|
|
122
|
+
with pytest.raises(TypeError, match="Unsupported graph type"):
|
|
123
|
+
kececi_layout(UnknownGraph())
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize("expanding, expected_x_coords", [
|
|
126
|
+
(True, [0.0, 1.0, -1.0, 2.0, -2.0]), # Expanding v4 style
|
|
127
|
+
(False, [0.0, 1.0, -1.0, 1.0, -1.0]) # Parallel style
|
|
128
|
+
])
|
|
129
|
+
def test_expanding_parameter(self, nx_graph_simple, expanding, expected_x_coords):
|
|
130
|
+
"""Test the effect of the 'expanding' parameter on coordinates."""
|
|
131
|
+
pos = kececi_layout(nx_graph_simple,
|
|
132
|
+
primary_direction='top_down',
|
|
133
|
+
secondary_start='right',
|
|
134
|
+
expanding=expanding)
|
|
135
|
+
|
|
136
|
+
assert len(pos) == 5
|
|
137
|
+
# Check X coordinates (secondary axis)
|
|
138
|
+
x_coords = [pos[i][0] for i in sorted(pos.keys())]
|
|
139
|
+
np.testing.assert_allclose(x_coords, expected_x_coords)
|
|
140
|
+
|
|
141
|
+
# Check Y coordinates (primary axis should be sequential)
|
|
142
|
+
y_coords = [pos[i][1] for i in sorted(pos.keys())]
|
|
143
|
+
np.testing.assert_allclose(y_coords, [0.0, -1.0, -2.0, -3.0, -4.0])
|
|
144
|
+
|
|
145
|
+
def test_known_output_left_to_right(self):
|
|
146
|
+
"""Verify the exact coordinates for a known horizontal layout."""
|
|
147
|
+
g = nx.path_graph(4) # Nodes 0, 1, 2, 3
|
|
148
|
+
pos = kececi_layout(g,
|
|
149
|
+
primary_direction='left-to-right',
|
|
150
|
+
secondary_start='up',
|
|
151
|
+
expanding=True,
|
|
152
|
+
primary_spacing=2.0,
|
|
153
|
+
secondary_spacing=0.5)
|
|
154
|
+
|
|
155
|
+
expected_pos = {
|
|
156
|
+
0: (0.0, 0.0), # x=primary, y=secondary
|
|
157
|
+
1: (2.0, 0.5), # y = 1 * ceil(1/2) * +1 * 0.5 = 0.5
|
|
158
|
+
2: (4.0, -0.5), # y = 1 * ceil(2/2) * -1 * 0.5 = -0.5
|
|
159
|
+
3: (6.0, 1.0) # y = 1 * ceil(3/2) * +1 * 0.5 = 1.0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
assert pos.keys() == expected_pos.keys()
|
|
163
|
+
for node in pos:
|
|
164
|
+
np.testing.assert_allclose(pos[node], expected_pos[node], atol=1e-7)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestGraphCompatibility:
|
|
168
|
+
"""Ensures layout and conversion functions work with all supported libraries."""
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("graph_fixture", [
|
|
171
|
+
"nx_graph_simple", "ig_graph_simple", "rx_graph_simple", "nk_graph_simple"
|
|
172
|
+
])
|
|
173
|
+
def test_kececi_layout_on_all_types(self, graph_fixture, request):
|
|
174
|
+
"""Check that kececi_layout runs without error and returns a valid position dict."""
|
|
175
|
+
graph = request.getfixturevalue(graph_fixture)
|
|
176
|
+
pos = kececi_layout(graph)
|
|
177
|
+
assert isinstance(pos, dict)
|
|
178
|
+
assert len(pos) == 5
|
|
179
|
+
# All values in the dict should be tuples of length 2 (x, y)
|
|
180
|
+
assert all(isinstance(v, tuple) and len(v) == 2 for v in pos.values())
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize("graph_fixture", [
|
|
183
|
+
"ig_graph_simple", "rx_graph_simple", "nk_graph_simple"
|
|
184
|
+
])
|
|
185
|
+
def test_to_networkx_conversion(self, graph_fixture, request):
|
|
186
|
+
"""Verify that conversion to NetworkX preserves graph structure."""
|
|
187
|
+
original_graph = request.getfixturevalue(graph_fixture)
|
|
188
|
+
nx_graph = to_networkx(original_graph)
|
|
189
|
+
|
|
190
|
+
assert isinstance(nx_graph, nx.Graph)
|
|
191
|
+
assert nx_graph.number_of_nodes() == 5
|
|
192
|
+
assert nx_graph.number_of_edges() == 4
|
|
193
|
+
# Check one edge to be sure
|
|
194
|
+
assert nx_graph.has_edge(2, 3)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class TestDrawingFunctions:
|
|
198
|
+
"""
|
|
199
|
+
Tests the user-facing drawing functions, primarily using mocking to avoid
|
|
200
|
+
generating plots during automated tests.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
@pytest.fixture(autouse=True)
|
|
204
|
+
def close_plots(self):
|
|
205
|
+
"""Fixture to close any plots created during tests."""
|
|
206
|
+
yield
|
|
207
|
+
plt.close('all')
|
|
208
|
+
|
|
209
|
+
def test_draw_kececi_invalid_style(self, nx_graph_simple):
|
|
210
|
+
"""Should raise ValueError for an unknown style."""
|
|
211
|
+
with pytest.raises(ValueError, match="Invalid style"):
|
|
212
|
+
draw_kececi(nx_graph_simple, style="nonexistent_style")
|
|
213
|
+
|
|
214
|
+
def test_draw_kececi_3d_on_2d_axis(self, nx_graph_simple):
|
|
215
|
+
"""Should raise ValueError if style is '3d' but axis is 2D."""
|
|
216
|
+
fig, ax = plt.subplots()
|
|
217
|
+
with pytest.raises(ValueError, match="requires an axis with 'projection=\"3d\"'"):
|
|
218
|
+
draw_kececi(nx_graph_simple, style='3d', ax=ax)
|
|
219
|
+
|
|
220
|
+
def test_draw_kececi_creates_axis(self, nx_graph_simple, mocker):
|
|
221
|
+
"""Should create a new figure and axis if none is provided."""
|
|
222
|
+
mock_add_subplot = mocker.patch('matplotlib.figure.Figure.add_subplot')
|
|
223
|
+
draw_kececi(nx_graph_simple)
|
|
224
|
+
mock_add_subplot.assert_called_once()
|
|
225
|
+
|
|
226
|
+
def test_draw_internal_routing(self, nx_graph_simple, mocker):
|
|
227
|
+
"""
|
|
228
|
+
Verify that draw_kececi correctly calls _draw_internal with the
|
|
229
|
+
right parameters by mocking the internal function.
|
|
230
|
+
"""
|
|
231
|
+
# ÖNEMLİ: Fonksiyonu import etmek yerine, tam yolunu string olarak veriyoruz.
|
|
232
|
+
# Python, test çalışırken bu yoldaki fonksiyonu bizim için izleyecek.
|
|
233
|
+
mock_internal_draw = mocker.patch('kececilayout._draw_internal')
|
|
234
|
+
|
|
235
|
+
# kececi_layout'u da mock'layalım ki sadece yönlendirmeyi test edelim.
|
|
236
|
+
mocker.patch('kececilayout.kececi_layout', return_value={0:(0,0)})
|
|
237
|
+
|
|
238
|
+
fig, ax = plt.subplots()
|
|
239
|
+
|
|
240
|
+
# Genel (public) fonksiyon olan draw_kececi'yi çağırıyoruz.
|
|
241
|
+
draw_kececi(nx_graph_simple, ax=ax, style='curved',
|
|
242
|
+
expanding=False, node_size=500, primary_direction='bottom-up')
|
|
243
|
+
|
|
244
|
+
# ŞİMDİ DOĞRULAMA:
|
|
245
|
+
# _draw_internal fonksiyonumuz beklendiği gibi çağrıldı mı?
|
|
246
|
+
mock_internal_draw.assert_called_once()
|
|
247
|
+
|
|
248
|
+
# Çağrılırken verilen argümanları kontrol edelim.
|
|
249
|
+
# kwargs, çağrının ikinci argümanıdır (args, kwargs).
|
|
250
|
+
call_kwargs = mock_internal_draw.call_args[1]
|
|
251
|
+
|
|
252
|
+
# Yerleşim (layout) parametreleri doğru aktarıldı mı?
|
|
253
|
+
assert call_kwargs['expanding'] is False
|
|
254
|
+
assert call_kwargs['primary_direction'] == 'bottom-up'
|
|
255
|
+
|
|
256
|
+
# Çizim (drawing) parametreleri doğru aktarıldı mı?
|
|
257
|
+
assert call_kwargs['node_size'] == 500
|
|
258
|
+
|
|
259
|
+
if __name__ == "__main__":
|
|
260
|
+
pytest.main([__file__]) # Testleri direkt çalıştırırsa
|
|
261
|
+
sys.exit(0)
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
kececilayout/__init__.py,sha256=WZDKbFvlkCaZABEwVKD29h0qWusAy9mZjNz3wV2NpSE,4864
|
|
2
|
-
kececilayout/_version.py,sha256=nWgc2GU0hFtB3l6RQG2NdpHaZ2cKN71Eo1Lcp06c6z0,813
|
|
3
|
-
kececilayout/kececi_layout.py,sha256=0HlIovC4bqJvt-CmiT3eDjDMJ3gh1WM8YM7GIOVrpBw,89250
|
|
4
|
-
kececilayout-0.5.0.dist-info/licenses/LICENSE,sha256=NJZsJEbQuKzxn1mWPWCbRx8jRUqGS22thl8wwuRQJ9c,1071
|
|
5
|
-
kececilayout-0.5.0.dist-info/METADATA,sha256=Db5_GA-E2XWV1ZolOlpX0g4ePbCtAjW7pl79a74JDx8,76108
|
|
6
|
-
kececilayout-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
kececilayout-0.5.0.dist-info/top_level.txt,sha256=OBzN_wm4q-iwSkeACF4E8ET_LFLJKBTldSH3D1jG2hA,13
|
|
8
|
-
kececilayout-0.5.0.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Mehmet Keçeci
|
|
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 deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|