Skip to content

Commit 3211943

Browse files
filchristougdalle
andauthored
Formats outside @require + Package extensions (#51)
* Formats outside @require * Use package extensions * Add Aqua and JuliaFormatter * Fix Requires * Typos in README and Aqua test --------- Co-authored-by: Guillaume Dalle <22795598+gdalle@users.noreply.github.com>
1 parent 7242859 commit 3211943

29 files changed

+781
-697
lines changed

.JuliaFormatter.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
style = "blue"

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
version:
18-
- '1'
18+
- '1.6'
19+
- '1.9'
20+
- 'nightly'
1921
os:
2022
- ubuntu-latest
2123
arch:

Project.toml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
name = "GraphIO"
22
uuid = "aa1b3936-2fda-51b9-ab35-c553d3a640a2"
3-
version = "0.6.0"
3+
version = "0.7.0"
44

55
[deps]
66
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
77
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
88
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
99
SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"
1010

11+
[weakdeps]
12+
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
13+
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
14+
ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46"
15+
16+
[extensions]
17+
GraphIODOTExt = "ParserCombinator"
18+
GraphIOGEXFExt = "EzXML"
19+
GraphIOGMLExt = "ParserCombinator"
20+
GraphIOGraphMLExt = "EzXML"
21+
GraphIOLGCompressedExt = "CodecZlib"
22+
1123
[compat]
24+
CodecZlib = "0.7"
25+
DelimitedFiles = "1"
1226
EzXML = "1"
1327
Graphs = "1.4"
1428
ParserCombinator = "2.1"
@@ -17,11 +31,13 @@ SimpleTraits = "0.9"
1731
julia = "1"
1832

1933
[extras]
34+
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
2035
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
2136
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
2237
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
38+
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
2339
ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46"
2440
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2541

2642
[targets]
27-
test = ["CodecZlib", "Graphs", "EzXML", "ParserCombinator", "Test"]
43+
test = ["Aqua", "CodecZlib", "Graphs", "JuliaFormatter", "EzXML", "ParserCombinator", "Test"]

README.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22

33
[![Build Status](https://github.com/JuliaGraphs/GraphIO.jl/workflows/CI/badge.svg)](https://github.com/JuliaGraphs/GraphIO.jl/actions?query=workflow%3ACI+branch%3Amaster)
44
[![codecov.io](http://codecov.io/github/JuliaGraphs/GraphIO.jl/coverage.svg?branch=master)](http://codecov.io/github/JuliaGraphs/GraphIO.jl?branch=master)
5+
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)
6+
[![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)
57

68
GraphIO provides support to [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) for reading/writing graphs in various formats.
79

810
Currently, the following functionality is provided:
911

10-
Format | Read | Write | Multiple Graphs| Format Name | Comment |
11-
--------------|------|-------|----------------|--------------|----------|
12-
EdgeList | ✓ | ✓ | |EdgeListFormat| a simple list of sources and dests separated by whitespace and/or comma, one pair per line. |
13-
[GML] | ✓ | ✓ | ✓ |GMLFormat |
14-
[Graph6] | ✓ | ✓ | ✓ |Graph6Format |
15-
[GraphML] | ✓ | ✓ | ✓ |GraphMLFormat |
16-
[Pajek NET] | ✓ | ✓ | |NETFormat |
17-
[GEXF] | | ✓ | |GEXFFormat |
18-
[DOT] | ✓ | | ✓ |DOTFormat |
19-
[CDF] | ✓ | | |CDFFormat |
12+
| Format | Read | Write | Multiple Graphs | Format Name | Comment |
13+
| ----------- | ---- | ----- | --------------- | -------------- | ------------------------------------------------------------------------------------------- |
14+
| EdgeList | | | | EdgeListFormat | a simple list of sources and dests separated by whitespace and/or comma, one pair per line. |
15+
| [GML] | | | | GMLFormat | |
16+
| [Graph6] | | | | Graph6Format | |
17+
| [GraphML] | | | | GraphMLFormat | |
18+
| [Pajek NET] | | | | NETFormat | |
19+
| [GEXF] | | | | GEXFFormat | |
20+
| [DOT] | | | | DOTFormat | |
21+
| [CDF] | | | | CDFFormat | |
2022

2123

2224
Graphs are read using either the `loadgraph` function or, for formats that support multiple graphs in a single file,
@@ -28,6 +30,23 @@ For example, an edgelist file could be loaded as:
2830
graph = loadgraph("path_to_graph/my_edgelist.txt", "graph_key", EdgeListFormat())
2931
```
3032

33+
## Reading different graph types
34+
35+
All `*Format` types are readily accessible.
36+
However, in order to use some of them with `loadgraph`, additional packages are required.
37+
You may thus need to install and load the following dependencies before using parts of GraphIO.jl:
38+
- Reading [DOT] or [GML] files: do `using ParserCombinator`
39+
- Reading [GEXF] or [GraphML] files: do `using EzXML`
40+
- Reading [GML] files: do `using CodecZlib`
41+
42+
The current design avoids populating your environment with unnecessary dependencies.
43+
44+
> **_IMPLEMENTATION NOTE:_**
45+
> The current design uses package extensions, introduced in Julia v1.9.
46+
> At the moment, package extensions cannot conditionally load types, that is one of the main reasons why all `*Format` types are readily accessible.
47+
> However, the functionality of `loadgraph` is extended for the various types only when the appropriate dependencies are available.
48+
> We are searching for more intuitive ways to design this interface.
49+
3150
[CDF]: http://www2.ee.washington.edu/research/pstca/formats/cdf.txt
3251
[GML]: https://en.wikipedia.org/wiki/Graph_Modelling_Language
3352
[Graph6]: https://users.cecs.anu.edu.au/~bdm/data/formats.html

ext/GraphIODOTExt.jl

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
module GraphIODOTExt
2+
3+
using Graphs
4+
import Graphs: loadgraph, loadgraphs, savegraph
5+
6+
@static if isdefined(Base, :get_extension)
7+
using GraphIO
8+
using ParserCombinator
9+
import GraphIO.DOT.DOTFormat
10+
else # not required for julia >= v1.9
11+
using ..GraphIO
12+
using ..ParserCombinator
13+
import ..GraphIO.DOT.DOTFormat
14+
end
15+
16+
function savedot(io::IO, g::AbstractGraph, gname::String="")
17+
isdir = is_directed(g)
18+
println(io, (isdir ? "digraph " : "graph ") * gname * " {")
19+
for i in vertices(g)
20+
println(io, "\t" * string(i))
21+
end
22+
if isdir
23+
for u in vertices(g)
24+
out_nbrs = outneighbors(g, u)
25+
length(out_nbrs) == 0 && continue
26+
println(io, "\t" * string(u) * " -> {" * join(out_nbrs, ',') * "}")
27+
end
28+
else
29+
for e in edges(g)
30+
source = string(src(e))
31+
dest = string(dst(e))
32+
println(io, "\t" * source * " -- " * dest)
33+
end
34+
end
35+
println(io, "}")
36+
return 1
37+
end
38+
39+
function savedot_mult(io::IO, graphs::Dict)
40+
ng = 0
41+
for (gname, g) in graphs
42+
ng += savedot(io, g, gname)
43+
end
44+
return ng
45+
end
46+
47+
function _dot_read_one_graph(pg::Parsers.DOT.Graph)
48+
isdir = pg.directed
49+
nvg = length(Parsers.DOT.nodes(pg))
50+
nodedict = Dict(zip(collect(Parsers.DOT.nodes(pg)), 1:nvg))
51+
if isdir
52+
g = DiGraph(nvg)
53+
else
54+
g = Graph(nvg)
55+
end
56+
for es in Parsers.DOT.edges(pg)
57+
s = nodedict[es[1]]
58+
d = nodedict[es[2]]
59+
add_edge!(g, s, d)
60+
end
61+
return g
62+
end
63+
64+
function _name(pg::Parsers.DOT.Graph)
65+
return if pg.id !== nothing
66+
pg.id.id
67+
else
68+
Parsers.DOT.StringID(pg.directed ? "digraph" : "graph")
69+
end
70+
end
71+
72+
function loaddot(io::IO, gname::String)
73+
p = Parsers.DOT.parse_dot(read(io, String))
74+
for pg in p
75+
_name(pg) == gname && return _dot_read_one_graph(pg)
76+
end
77+
return error("Graph $gname not found")
78+
end
79+
80+
function loaddot_mult(io::IO)
81+
p = Parsers.DOT.parse_dot(read(io, String))
82+
graphs = Dict{String,AbstractGraph}()
83+
84+
for pg in p
85+
graphs[_name(pg)] = _dot_read_one_graph(pg)
86+
end
87+
return graphs
88+
end
89+
90+
loadgraph(io::IO, gname::String, ::DOTFormat) = loaddot(io, gname)
91+
loadgraphs(io::IO, ::DOTFormat) = loaddot_mult(io)
92+
savegraph(io::IO, g::AbstractGraph, gname::String, ::DOTFormat) = savedot(io, g, gname)
93+
savegraph(io::IO, d::Dict, ::DOTFormat) = savedot_mult(io, d)
94+
95+
end

ext/GraphIOGEXFExt.jl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module GraphIOGEXFExt
2+
3+
using Graphs
4+
import Graphs: loadgraph, loadgraphs, savegraph, AbstractGraph
5+
6+
@static if isdefined(Base, :get_extension)
7+
using GraphIO
8+
using EzXML
9+
import GraphIO.GEXF.GEXFFormat
10+
else # not required for julia >= v1.9
11+
using ..GraphIO
12+
using ..EzXML
13+
import ..GraphIO.GEXF.GEXFFormat
14+
end
15+
16+
"""
17+
savegexf(f, g, gname)
18+
19+
Write a graph `g` with name `gname` to an IO stream `io` in the
20+
[Gexf](http://gexf.net/format/) format. Return 1 (number of graphs written).
21+
"""
22+
function savegexf(io::IO, g::AbstractGraph, gname::String)
23+
xdoc = XMLDocument()
24+
xroot = setroot!(xdoc, ElementNode("gexf"))
25+
xroot["xmlns"] = "http://www.gexf.net/1.2draft"
26+
xroot["version"] = "1.2"
27+
xroot["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
28+
xroot["xsi:schemaLocation"] = "http://www.gexf.net/1.2draft/gexf.xsd"
29+
30+
xmeta = addelement!(xroot, "meta")
31+
addelement!(xmeta, "description", gname)
32+
xg = addelement!(xroot, "graph")
33+
strdir = is_directed(g) ? "directed" : "undirected"
34+
xg["defaultedgetype"] = strdir
35+
36+
xnodes = addelement!(xg, "nodes")
37+
for i in 1:nv(g)
38+
xv = addelement!(xnodes, "node")
39+
xv["id"] = "$(i-1)"
40+
end
41+
42+
xedges = addelement!(xg, "edges")
43+
m = 0
44+
for e in edges(g)
45+
xe = addelement!(xedges, "edge")
46+
xe["id"] = "$m"
47+
xe["source"] = "$(src(e)-1)"
48+
xe["target"] = "$(dst(e)-1)"
49+
m += 1
50+
end
51+
52+
prettyprint(io, xdoc)
53+
return 1
54+
end
55+
56+
savegraph(io::IO, g::AbstractGraph, gname::String, ::GEXFFormat) = savegexf(io, g, gname)
57+
58+
end

ext/GraphIOGMLExt.jl

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
module GraphIOGMLExt
2+
3+
using Graphs
4+
import Graphs: loadgraph, loadgraphs, savegraph
5+
6+
@static if isdefined(Base, :get_extension)
7+
using GraphIO
8+
using ParserCombinator
9+
import GraphIO.GML.GMLFormat
10+
else # not required for julia >= v1.9
11+
using ..GraphIO
12+
using ..ParserCombinator
13+
import ..GraphIO.GML.GMLFormat
14+
end
15+
16+
function _gml_read_one_graph(gs, dir)
17+
nodes = [x[:id] for x in gs[:node]]
18+
if dir
19+
g = DiGraph(length(nodes))
20+
else
21+
g = Graph(length(nodes))
22+
end
23+
mapping = Dict{Int,Int}()
24+
for (i, n) in enumerate(nodes)
25+
mapping[n] = i
26+
end
27+
sds = [(Int(x[:source]), Int(x[:target])) for x in gs[:edge]]
28+
for (s, d) in (sds)
29+
add_edge!(g, mapping[s], mapping[d])
30+
end
31+
return g
32+
end
33+
34+
function loadgml(io::IO, gname::String)
35+
p = Parsers.GML.parse_dict(read(io, String))
36+
for gs in p[:graph]
37+
dir = Bool(get(gs, :directed, 0))
38+
graphname = get(gs, :label, dir ? "digraph" : "graph")
39+
40+
(gname == graphname) && return _gml_read_one_graph(gs, dir)
41+
end
42+
return error("Graph $gname not found")
43+
end
44+
45+
function loadgml_mult(io::IO)
46+
p = Parsers.GML.parse_dict(read(io, String))
47+
graphs = Dict{String,AbstractGraph}()
48+
for gs in p[:graph]
49+
dir = Bool(get(gs, :directed, 0))
50+
graphname = get(gs, :label, dir ? "digraph" : "graph")
51+
graphs[graphname] = _gml_read_one_graph(gs, dir)
52+
end
53+
return graphs
54+
end
55+
56+
"""
57+
savegml(f, g, gname="graph")
58+
59+
Write a graph `g` with name `gname` to an IO stream `io` in the
60+
[GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) format. Return 1.
61+
"""
62+
function savegml(io::IO, g::AbstractGraph, gname::String="")
63+
println(io, "graph")
64+
println(io, "[")
65+
length(gname) > 0 && println(io, "label \"$gname\"")
66+
is_directed(g) && println(io, "directed 1")
67+
for i in 1:nv(g)
68+
println(io, "\tnode")
69+
println(io, "\t[")
70+
println(io, "\t\tid $i")
71+
println(io, "\t]")
72+
end
73+
for e in edges(g)
74+
s, t = Tuple(e)
75+
println(io, "\tedge")
76+
println(io, "\t[")
77+
println(io, "\t\tsource $s")
78+
println(io, "\t\ttarget $t")
79+
println(io, "\t]")
80+
end
81+
println(io, "]")
82+
return 1
83+
end
84+
85+
"""
86+
savegml_mult(io, graphs)
87+
Write a dictionary of (name=>graph) to an IO stream `io` Return number of graphs written.
88+
"""
89+
function savegml_mult(io::IO, graphs::Dict)
90+
ng = 0
91+
for (gname, g) in graphs
92+
ng += savegml(io, g, gname)
93+
end
94+
return ng
95+
end
96+
loadgraph(io::IO, gname::String, ::GMLFormat) = loadgml(io, gname)
97+
loadgraphs(io::IO, ::GMLFormat) = loadgml_mult(io)
98+
savegraph(io::IO, g::AbstractGraph, gname::String, ::GMLFormat) = savegml(io, g, gname)
99+
savegraph(io::IO, d::Dict, ::GMLFormat) = savegml_mult(io, d)
100+
101+
end

0 commit comments

Comments
 (0)