Skip to content

Commit 7acfc45

Browse files
committed
Addressed Krastanov's feedback
1 parent 8778ca9 commit 7acfc45

6 files changed

Lines changed: 100 additions & 74 deletions

File tree

CondaPkg.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[deps]
2+
networkx = ""

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ version = "0.1.0"
44
authors = ["Jash <jashambaliya1@gmail.com>"]
55

66
[deps]
7+
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
78
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
89
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
910

1011
[compat]
12+
CondaPkg = "0.2"
1113
Graphs = "1"
1214
PythonCall = "0.9"
1315
julia = "1.10"

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
`NetworkX.jl` is a Julia wrapper around Python's `networkx` built on `PythonCall.jl`.
44
The current milestone is intentionally narrow: constructors plus the basic `Graphs.jl` API needed to pass `GraphsInterfaceChecker`.
55

6+
`networkx` is declared as a package dependency and is automatically installed via `CondaPkg.jl` — no manual Python setup required.
7+
68
## Features
79

8-
- Wrap `networkx.Graph`/`networkx.DiGraph` as `Graphs.AbstractGraph`
9-
- Convert between `Graphs.jl` graph types and Python `networkx` objects
10+
- Wrap `networkx.Graph`/`networkx.DiGraph` as `Graphs.AbstractGraph` using the `NetworkXGraph` / `NetworkXDiGraph` constructors
11+
- Convert between `Graphs.jl` graph types and Python `networkx` objects via `networkx_graph`
12+
- Access the raw Python `networkx` module through `NetworkX.PythonNetworkX`
1013
- Validate interface conformance with `GraphsInterfaceChecker.jl`
1114
- Stress-test multi-threaded use of independent graphs to catch Python/GIL integration regressions
1215

@@ -16,18 +19,25 @@ The current milestone is intentionally narrow: constructors plus the basic `Grap
1619
using Graphs
1720
using NetworkX
1821

19-
g = path_graph(5)
22+
# Access the raw Python networkx module
23+
nx = NetworkX.PythonNetworkX.networkx
2024

21-
# Convert Graphs.jl -> Python networkx
22-
pyg = networkx_graph(g)
25+
# Create a Python networkx graph and wrap it as a Graphs.jl-compatible graph
26+
pyg = nx.path_graph(5)
27+
gw = NetworkXGraph(pyg) # undirected
28+
nv(gw) == 5 # true
2329

24-
# Wrap networkx -> Graphs.jl compatible graph
25-
gw = wrap_networkx(pyg)
30+
pydg = nx.DiGraph()
31+
pydg.add_edges_from([(1, 2), (2, 3)])
32+
dgw = NetworkXDiGraph(pydg) # directed
2633

27-
nv(gw) == nv(g)
34+
# Convert a Graphs.jl graph to a Python networkx object
35+
g = path_graph(5)
36+
pyg2 = networkx_graph(g)
37+
gw2 = NetworkXGraph(pyg2)
38+
nv(gw2) == nv(g) # true
2839
```
2940

3041
## Notes
3142

32-
- Python package `networkx` must be available in the Python environment used by `PythonCall.jl`.
3343
- No graph algorithms are implemented in this package at this stage.

src/NetworkX.jl

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
11
module NetworkX
22

33
using Graphs
4-
using PythonCall: Py, pybuiltins, pyconvert, pyimport
4+
using PythonCall: Py, pynew, pycopy!, pybuiltins, pyconvert, pyimport
55

66
export AbstractNetworkXGraph,
77
NetworkXGraph,
88
NetworkXDiGraph,
99
networkx_graph,
10-
wrap_networkx,
1110
refresh_index!
1211

12+
"""
13+
NetworkX.PythonNetworkX
14+
15+
Sub-module providing direct access to the Python `networkx` package.
16+
Use this namespace when you need raw Python networkx objects or algorithms
17+
that are not yet wrapped by the Julia API.
18+
19+
# Example
20+
```julia
21+
using NetworkX
22+
nx = NetworkX.PythonNetworkX.networkx
23+
pyg = nx.complete_graph(5)
24+
```
25+
"""
26+
module PythonNetworkX
27+
using PythonCall: pynew, pycopy!, pyimport
28+
29+
"""The raw Python `networkx` module."""
30+
const networkx = pynew()
31+
32+
function __init__()
33+
pycopy!(networkx, pyimport("networkx"))
34+
end
35+
end # module PythonNetworkX
36+
1337
"""
1438
AbstractNetworkXGraph{T} <: Graphs.AbstractGraph{T}
1539
@@ -19,8 +43,17 @@ abstract type AbstractNetworkXGraph{T<:Integer} <: Graphs.AbstractGraph{T} end
1943

2044
"""
2145
NetworkXGraph{T}(pygraph)
46+
NetworkXGraph(pygraph)
2247
23-
Wrapper for an undirected NetworkX graph.
48+
Wrap an undirected Python `networkx.Graph` as a `Graphs.AbstractGraph`.
49+
50+
# Example
51+
```julia
52+
using NetworkX
53+
nx = NetworkX.PythonNetworkX.networkx
54+
pyg = nx.path_graph(5)
55+
gw = NetworkXGraph(pyg)
56+
```
2457
"""
2558
mutable struct NetworkXGraph{T<:Integer} <: AbstractNetworkXGraph{T}
2659
pygraph::Py
@@ -30,23 +63,25 @@ end
3063

3164
"""
3265
NetworkXDiGraph{T}(pygraph)
33-
34-
Wrapper for a directed NetworkX graph.
66+
NetworkXDiGraph(pygraph)
67+
68+
Wrap a directed Python `networkx.DiGraph` as a `Graphs.AbstractGraph`.
69+
70+
# Example
71+
```julia
72+
using NetworkX
73+
nx = NetworkX.PythonNetworkX.networkx
74+
pyg = nx.DiGraph()
75+
pyg.add_edges_from([(1, 2), (2, 3)])
76+
gw = NetworkXDiGraph(pyg)
77+
```
3578
"""
3679
mutable struct NetworkXDiGraph{T<:Integer} <: AbstractNetworkXGraph{T}
3780
pygraph::Py
3881
nodes::Vector{Any}
3982
node_to_index::Dict{Any,T}
4083
end
4184

42-
const _NX = Ref{Py}()
43-
44-
_nx() = isassigned(_NX) ? _NX[] : (_NX[] = pyimport("networkx"))
45-
_pylist(x) = pybuiltins.list(x)
46-
_nodes(pygraph::Py) = pyconvert(Vector{Any}, _pylist(pygraph.nodes()))
47-
_edges(pygraph::Py) = pyconvert(Vector{Tuple{Any,Any}}, _pylist(pygraph.edges()))
48-
_neighbors(pyiter) = pyconvert(Vector{Any}, _pylist(pyiter))
49-
5085
function _node_to_index(nodes::Vector{Any}, ::Type{T}) where {T<:Integer}
5186
mapping = Dict{Any,T}()
5287
for (i, node) in enumerate(nodes)
@@ -56,9 +91,8 @@ function _node_to_index(nodes::Vector{Any}, ::Type{T}) where {T<:Integer}
5691
end
5792

5893
function refresh_index!(g::AbstractNetworkXGraph{T}) where {T<:Integer}
59-
nodes = _nodes(g.pygraph)
60-
g.nodes = nodes
61-
g.node_to_index = _node_to_index(nodes, T)
94+
g.nodes = pyconvert(Vector{Any}, pybuiltins.list(g.pygraph.nodes()))
95+
g.node_to_index = _node_to_index(g.nodes, T)
6296
return g
6397
end
6498

@@ -85,34 +119,23 @@ end
85119

86120
NetworkXDiGraph(pygraph::Py) = NetworkXDiGraph{Int}(pygraph)
87121

88-
"""
89-
wrap_networkx(pygraph; T=Int)
90-
91-
Wrap a NetworkX Python graph object as a `Graphs.AbstractGraph` implementation.
92-
"""
93-
function wrap_networkx(pygraph::Py; T::Type{<:Integer}=Int)
94-
return pyconvert(Bool, pygraph.is_directed()) ? NetworkXDiGraph{T}(pygraph) :
95-
NetworkXGraph{T}(pygraph)
96-
end
97-
98122
"""
99123
networkx_graph(g)
100124
101125
Convert a `Graphs.AbstractGraph` to a Python NetworkX graph object.
126+
Returns the underlying Python object for `AbstractNetworkXGraph` wrappers,
127+
or creates a new Python networkx graph for any other `Graphs.AbstractGraph`.
102128
"""
103129
networkx_graph(g::AbstractNetworkXGraph) = g.pygraph
104130

105131
function networkx_graph(g::Graphs.AbstractGraph)
106-
nx = _nx()
132+
nx = PythonNetworkX.networkx
107133
pyg = Graphs.is_directed(g) ? nx.DiGraph() : nx.Graph()
108134
pyg.add_nodes_from(collect(Graphs.vertices(g)))
109135
pyg.add_edges_from([(Graphs.src(e), Graphs.dst(e)) for e in Graphs.edges(g)])
110136
return pyg
111137
end
112138

113-
wrap_networkx(g::Graphs.AbstractGraph{T}) where {T<:Integer} =
114-
wrap_networkx(networkx_graph(g); T=T)
115-
116139
Graphs.is_directed(::Type{<:NetworkXGraph}) = false
117140
Graphs.is_directed(::NetworkXGraph) = false
118141
Graphs.is_directed(::Type{<:NetworkXDiGraph}) = true
@@ -127,9 +150,6 @@ Graphs.eltype(::Type{G}) where {T<:Integer,G<:AbstractNetworkXGraph{T}} = T
127150
Graphs.eltype(::AbstractNetworkXGraph{T}) where {T<:Integer} = T
128151

129152
_node(g::AbstractNetworkXGraph, v::Integer) = g.nodes[Int(v)]
130-
_label_to_vertex(g::AbstractNetworkXGraph{T}, label) where {T<:Integer} =
131-
g.node_to_index[label]::T
132-
_label_to_vertex(g::Graphs.AbstractGraph, label) = label
133153

134154
function Graphs.has_edge(g::AbstractNetworkXGraph, s, d)
135155
Graphs.has_vertex(g, s) || return false
@@ -138,7 +158,7 @@ function Graphs.has_edge(g::AbstractNetworkXGraph, s, d)
138158
end
139159

140160
function _mapped_neighbors(g::AbstractNetworkXGraph{T}, pyiter) where {T<:Integer}
141-
py_ns = _neighbors(pyiter)
161+
py_ns = pyconvert(Vector{Any}, pybuiltins.list(pyiter))
142162
return T[g.node_to_index[n] for n in py_ns]
143163
end
144164

@@ -160,12 +180,14 @@ function Graphs.inneighbors(g::NetworkXDiGraph{T}, v) where {T<:Integer}
160180
end
161181

162182
function Graphs.edges(g::AbstractNetworkXGraph{T}) where {T<:Integer}
183+
py_edges = pyconvert(Vector{Tuple{Any,Any}}, pybuiltins.list(g.pygraph.edges()))
163184
return Graphs.Edge{T}[
164-
Graphs.Edge{T}(g.node_to_index[u], g.node_to_index[v]) for (u, v) in _edges(g.pygraph)
185+
Graphs.Edge{T}(g.node_to_index[u], g.node_to_index[v]) for (u, v) in py_edges
165186
]
166187
end
167188

168-
Graphs.has_self_loops(g::AbstractNetworkXGraph) = pyconvert(Int, _nx().number_of_selfloops(g.pygraph)) > 0
189+
Graphs.has_self_loops(g::AbstractNetworkXGraph) =
190+
pyconvert(Int, PythonNetworkX.networkx.number_of_selfloops(g.pygraph)) > 0
169191

170192
function Graphs.add_vertex!(g::AbstractNetworkXGraph{T}) where {T<:Integer}
171193
new_index = T(Graphs.nv(g) + 1)
@@ -234,25 +256,19 @@ function Graphs.rem_vertices!(g::AbstractNetworkXGraph{T}, vs; keep_order::Bool=
234256
end
235257

236258
function Graphs.squash(g::AbstractNetworkXGraph{T}) where {T<:Integer}
237-
copyg = wrap_networkx(g.pygraph.copy(); T=Int)
259+
copyg = typeof(g)(g.pygraph.copy())
238260
copyg.nodes = copy(g.nodes)
239261
_refresh_index_from_nodes!(copyg)
240262
return copyg, collect(Graphs.vertices(g))
241263
end
242264

243-
Graphs.zero(::Type{<:NetworkXGraph{T}}) where {T<:Integer} = wrap_networkx(_nx().Graph(); T=T)
265+
Graphs.zero(::Type{<:NetworkXGraph{T}}) where {T<:Integer} =
266+
NetworkXGraph{T}(PythonNetworkX.networkx.Graph())
244267
Graphs.zero(::Type{<:NetworkXDiGraph{T}}) where {T<:Integer} =
245-
wrap_networkx(_nx().DiGraph(); T=T)
246-
247-
function Base.copy(g::NetworkXGraph{T}) where {T<:Integer}
248-
copyg = NetworkXGraph{T}(g.pygraph.copy())
249-
copyg.nodes = copy(g.nodes)
250-
copyg.node_to_index = copy(g.node_to_index)
251-
return copyg
252-
end
268+
NetworkXDiGraph{T}(PythonNetworkX.networkx.DiGraph())
253269

254-
function Base.copy(g::NetworkXDiGraph{T}) where {T<:Integer}
255-
copyg = NetworkXDiGraph{T}(g.pygraph.copy())
270+
function Base.copy(g::AbstractNetworkXGraph{T}) where {T<:Integer}
271+
copyg = typeof(g)(g.pygraph.copy())
256272
copyg.nodes = copy(g.nodes)
257273
copyg.node_to_index = copy(g.node_to_index)
258274
return copyg

test/Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[deps]
2-
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
32
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
43
GraphsInterfaceChecker = "3bef136c-15ff-4091-acbb-1a4aafe67608"
54
Interfaces = "85a1e053-f937-4924-92a5-1367d23b7b87"

test/runtests.jl

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
using Test
2-
using CondaPkg
32
using Base.Threads: nthreads
43

5-
CondaPkg.add("networkx")
6-
CondaPkg.resolve()
7-
84
using Graphs
95
using GraphsInterfaceChecker
106
using Interfaces
117
using NetworkX
128
using PythonCall
139

1410
@testset "NetworkX.jl" begin
15-
nx = pyimport("networkx")
11+
nx = NetworkX.PythonNetworkX.networkx
1612

1713
@testset "Constructors and basic API" begin
1814
pyg = nx.Graph()
1915
pyg.add_edges_from([(10, 20), (20, 30), (30, 40)])
20-
gw = wrap_networkx(pyg)
16+
gw = NetworkXGraph(pyg)
2117
@test gw isa NetworkXGraph
2218
@test !is_directed(gw)
2319
@test nv(gw) == 4
@@ -31,7 +27,7 @@ using PythonCall
3127

3228
pydg = nx.DiGraph()
3329
pydg.add_edges_from([(1, 2), (2, 3), (4, 2)])
34-
dgw = wrap_networkx(pydg)
30+
dgw = NetworkXDiGraph(pydg)
3531
@test dgw isa NetworkXDiGraph
3632
@test is_directed(dgw)
3733
@test nv(dgw) == 4
@@ -41,7 +37,8 @@ using PythonCall
4137

4238
# Graphs.jl -> networkx -> wrapper roundtrip on basic structure.
4339
g = path_graph(5)
44-
gw2 = wrap_networkx(networkx_graph(g))
40+
pyg2 = networkx_graph(g)
41+
gw2 = NetworkXGraph(pyg2)
4542
@test nv(gw2) == 5
4643
@test ne(gw2) == 4
4744
@test has_edge(gw2, 2, 3)
@@ -54,7 +51,7 @@ using PythonCall
5451
PythonCall.GIL.@unlock Threads.@threads for i in eachindex(results)
5552
pyg = PythonCall.GIL.@lock nx.Graph()
5653
PythonCall.GIL.@lock pyg.add_edges_from([(1, 2), (2, 3), (3, 4), (4, 5), (5, 5 + i)])
57-
gw = PythonCall.GIL.@lock wrap_networkx(pyg)
54+
gw = PythonCall.GIL.@lock NetworkXGraph(pyg)
5855
ok = gw isa NetworkXGraph
5956
ok &= PythonCall.GIL.@lock nv(gw) == 6
6057
ok &= PythonCall.GIL.@lock ne(gw) == 5
@@ -63,7 +60,7 @@ using PythonCall
6360

6461
pydg = PythonCall.GIL.@lock nx.DiGraph()
6562
PythonCall.GIL.@lock pydg.add_edges_from([(1, 2), (2, 3), (3, 1)])
66-
dgw = PythonCall.GIL.@lock wrap_networkx(pydg)
63+
dgw = PythonCall.GIL.@lock NetworkXDiGraph(pydg)
6764
ok &= dgw isa NetworkXDiGraph
6865
ok &= PythonCall.GIL.@lock is_directed(dgw)
6966
ok &= PythonCall.GIL.@lock outneighbors(dgw, 2) == [3]
@@ -82,16 +79,16 @@ using PythonCall
8279
dg1 = nx.DiGraph()
8380
dg1.add_edges_from([(1, 2), (2, 3), (3, 4)])
8481
dg2 = nx.complete_graph(4, create_using=nx.DiGraph())
85-
test_ugraphs = [wrap_networkx(ug1), wrap_networkx(ug2)]
86-
test_dgraphs = [wrap_networkx(dg1), wrap_networkx(dg2)]
82+
test_ugraphs = [NetworkXGraph(ug1), NetworkXGraph(ug2)]
83+
test_dgraphs = [NetworkXDiGraph(dg1), NetworkXDiGraph(dg2)]
8784

8885
@test Interfaces.test(AbstractGraphInterface, NetworkXGraph, test_ugraphs)
8986
@test Interfaces.test(AbstractGraphInterface, NetworkXDiGraph, test_dgraphs)
9087
end
9188

9289
@testset "Deletion preserves wrapper order" begin
9390
pyg = nx.path_graph(4)
94-
gw = wrap_networkx(pyg)
91+
gw = NetworkXGraph(pyg)
9592
@test rem_vertex!(gw, 2)
9693
@test gw.nodes == Any[0, 3, 2]
9794
@test gw.node_to_index == Dict{Any,Int}(0 => 1, 3 => 2, 2 => 3)
@@ -105,19 +102,19 @@ using PythonCall
105102
@test gw_squash.node_to_index == gw.node_to_index
106103
@test vmap == [1, 2, 3]
107104

108-
gw_batch = wrap_networkx(nx.path_graph(4))
105+
gw_batch = NetworkXGraph(nx.path_graph(4))
109106
@test rem_vertex!(gw_batch, 2)
110107
@test rem_vertices!(gw_batch, [3]) == [1, 2, 0]
111108
@test gw_batch.nodes == Any[0, 3]
112109
@test gw_batch.node_to_index == Dict{Any,Int}(0 => 1, 3 => 2)
113110

114-
dg = wrap_networkx(nx.DiGraph([(1, 2), (2, 3), (3, 4)]))
111+
dg = NetworkXDiGraph(nx.DiGraph([(1, 2), (2, 3), (3, 4)]))
115112
@test rem_vertex!(dg, 2)
116113
@test reverse(dg).nodes == dg.nodes
117114
end
118115

119116
@testset "Duplicate edges are rejected" begin
120-
gw = wrap_networkx(nx.path_graph(3))
117+
gw = NetworkXGraph(nx.path_graph(3))
121118
@test !add_edge!(gw, 1, 2)
122119
@test ne(gw) == 2
123120
end

0 commit comments

Comments
 (0)