Skip to content

Commit

Permalink
Formats outside @require + Package extensions (#51)
Browse files Browse the repository at this point in the history
* Formats outside @require

* Use package extensions

* Add Aqua and JuliaFormatter

* Fix Requires

* Typos in README and Aqua test

---------

Co-authored-by: Guillaume Dalle <[email protected]>
  • Loading branch information
filchristou and gdalle authored Jun 23, 2023
1 parent 7242859 commit 3211943
Show file tree
Hide file tree
Showing 29 changed files with 781 additions and 697 deletions.
1 change: 1 addition & 0 deletions .JuliaFormatter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
style = "blue"
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ jobs:
fail-fast: false
matrix:
version:
- '1'
- '1.6'
- '1.9'
- 'nightly'
os:
- ubuntu-latest
arch:
Expand Down
20 changes: 18 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
name = "GraphIO"
uuid = "aa1b3936-2fda-51b9-ab35-c553d3a640a2"
version = "0.6.0"
version = "0.7.0"

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

[weakdeps]
CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
ParserCombinator = "fae87a5f-d1ad-5cf0-8f61-c941e1580b46"

[extensions]
GraphIODOTExt = "ParserCombinator"
GraphIOGEXFExt = "EzXML"
GraphIOGMLExt = "ParserCombinator"
GraphIOGraphMLExt = "EzXML"
GraphIOLGCompressedExt = "CodecZlib"

[compat]
CodecZlib = "0.7"
DelimitedFiles = "1"
EzXML = "1"
Graphs = "1.4"
ParserCombinator = "2.1"
Expand All @@ -17,11 +31,13 @@ SimpleTraits = "0.9"
julia = "1"

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

[targets]
test = ["CodecZlib", "Graphs", "EzXML", "ParserCombinator", "Test"]
test = ["Aqua", "CodecZlib", "Graphs", "JuliaFormatter", "EzXML", "ParserCombinator", "Test"]
39 changes: 29 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

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

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

Currently, the following functionality is provided:

Format | Read | Write | Multiple Graphs| Format Name | Comment |
--------------|------|-------|----------------|--------------|----------|
EdgeList | ✓ | ✓ | |EdgeListFormat| a simple list of sources and dests separated by whitespace and/or comma, one pair per line. |
[GML] | ✓ | ✓ | ✓ |GMLFormat |
[Graph6] | ✓ | ✓ | ✓ |Graph6Format |
[GraphML] | ✓ | ✓ | ✓ |GraphMLFormat |
[Pajek NET] | ✓ | ✓ | |NETFormat |
[GEXF] | | ✓ | |GEXFFormat |
[DOT] | ✓ | | ✓ |DOTFormat |
[CDF] | ✓ | | |CDFFormat |
| Format | Read | Write | Multiple Graphs | Format Name | Comment |
| ----------- | ---- | ----- | --------------- | -------------- | ------------------------------------------------------------------------------------------- |
| EdgeList | | | | EdgeListFormat | a simple list of sources and dests separated by whitespace and/or comma, one pair per line. |
| [GML] | | | | GMLFormat | |
| [Graph6] | | | | Graph6Format | |
| [GraphML] | | | | GraphMLFormat | |
| [Pajek NET] | | | | NETFormat | |
| [GEXF] | | | | GEXFFormat | |
| [DOT] | | | | DOTFormat | |
| [CDF] | | | | CDFFormat | |


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

## Reading different graph types

All `*Format` types are readily accessible.
However, in order to use some of them with `loadgraph`, additional packages are required.
You may thus need to install and load the following dependencies before using parts of GraphIO.jl:
- Reading [DOT] or [GML] files: do `using ParserCombinator`
- Reading [GEXF] or [GraphML] files: do `using EzXML`
- Reading [GML] files: do `using CodecZlib`

The current design avoids populating your environment with unnecessary dependencies.

> **_IMPLEMENTATION NOTE:_**
> The current design uses package extensions, introduced in Julia v1.9.
> At the moment, package extensions cannot conditionally load types, that is one of the main reasons why all `*Format` types are readily accessible.
> However, the functionality of `loadgraph` is extended for the various types only when the appropriate dependencies are available.
> We are searching for more intuitive ways to design this interface.
[CDF]: http://www2.ee.washington.edu/research/pstca/formats/cdf.txt
[GML]: https://en.wikipedia.org/wiki/Graph_Modelling_Language
[Graph6]: https://users.cecs.anu.edu.au/~bdm/data/formats.html
Expand Down
95 changes: 95 additions & 0 deletions ext/GraphIODOTExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module GraphIODOTExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph

@static if isdefined(Base, :get_extension)
using GraphIO
using ParserCombinator
import GraphIO.DOT.DOTFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..ParserCombinator
import ..GraphIO.DOT.DOTFormat
end

function savedot(io::IO, g::AbstractGraph, gname::String="")
isdir = is_directed(g)
println(io, (isdir ? "digraph " : "graph ") * gname * " {")
for i in vertices(g)
println(io, "\t" * string(i))
end
if isdir
for u in vertices(g)
out_nbrs = outneighbors(g, u)
length(out_nbrs) == 0 && continue
println(io, "\t" * string(u) * " -> {" * join(out_nbrs, ',') * "}")
end
else
for e in edges(g)
source = string(src(e))
dest = string(dst(e))
println(io, "\t" * source * " -- " * dest)
end
end
println(io, "}")
return 1
end

function savedot_mult(io::IO, graphs::Dict)
ng = 0
for (gname, g) in graphs
ng += savedot(io, g, gname)
end
return ng
end

function _dot_read_one_graph(pg::Parsers.DOT.Graph)
isdir = pg.directed
nvg = length(Parsers.DOT.nodes(pg))
nodedict = Dict(zip(collect(Parsers.DOT.nodes(pg)), 1:nvg))
if isdir
g = DiGraph(nvg)
else
g = Graph(nvg)
end
for es in Parsers.DOT.edges(pg)
s = nodedict[es[1]]
d = nodedict[es[2]]
add_edge!(g, s, d)
end
return g
end

function _name(pg::Parsers.DOT.Graph)
return if pg.id !== nothing
pg.id.id
else
Parsers.DOT.StringID(pg.directed ? "digraph" : "graph")
end
end

function loaddot(io::IO, gname::String)
p = Parsers.DOT.parse_dot(read(io, String))
for pg in p
_name(pg) == gname && return _dot_read_one_graph(pg)
end
return error("Graph $gname not found")
end

function loaddot_mult(io::IO)
p = Parsers.DOT.parse_dot(read(io, String))
graphs = Dict{String,AbstractGraph}()

for pg in p
graphs[_name(pg)] = _dot_read_one_graph(pg)
end
return graphs
end

loadgraph(io::IO, gname::String, ::DOTFormat) = loaddot(io, gname)
loadgraphs(io::IO, ::DOTFormat) = loaddot_mult(io)
savegraph(io::IO, g::AbstractGraph, gname::String, ::DOTFormat) = savedot(io, g, gname)
savegraph(io::IO, d::Dict, ::DOTFormat) = savedot_mult(io, d)

end
58 changes: 58 additions & 0 deletions ext/GraphIOGEXFExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module GraphIOGEXFExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph, AbstractGraph

@static if isdefined(Base, :get_extension)
using GraphIO
using EzXML
import GraphIO.GEXF.GEXFFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..EzXML
import ..GraphIO.GEXF.GEXFFormat
end

"""
savegexf(f, g, gname)
Write a graph `g` with name `gname` to an IO stream `io` in the
[Gexf](http://gexf.net/format/) format. Return 1 (number of graphs written).
"""
function savegexf(io::IO, g::AbstractGraph, gname::String)
xdoc = XMLDocument()
xroot = setroot!(xdoc, ElementNode("gexf"))
xroot["xmlns"] = "http://www.gexf.net/1.2draft"
xroot["version"] = "1.2"
xroot["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
xroot["xsi:schemaLocation"] = "http://www.gexf.net/1.2draft/gexf.xsd"

xmeta = addelement!(xroot, "meta")
addelement!(xmeta, "description", gname)
xg = addelement!(xroot, "graph")
strdir = is_directed(g) ? "directed" : "undirected"
xg["defaultedgetype"] = strdir

xnodes = addelement!(xg, "nodes")
for i in 1:nv(g)
xv = addelement!(xnodes, "node")
xv["id"] = "$(i-1)"
end

xedges = addelement!(xg, "edges")
m = 0
for e in edges(g)
xe = addelement!(xedges, "edge")
xe["id"] = "$m"
xe["source"] = "$(src(e)-1)"
xe["target"] = "$(dst(e)-1)"
m += 1
end

prettyprint(io, xdoc)
return 1
end

savegraph(io::IO, g::AbstractGraph, gname::String, ::GEXFFormat) = savegexf(io, g, gname)

end
101 changes: 101 additions & 0 deletions ext/GraphIOGMLExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module GraphIOGMLExt

using Graphs
import Graphs: loadgraph, loadgraphs, savegraph

@static if isdefined(Base, :get_extension)
using GraphIO
using ParserCombinator
import GraphIO.GML.GMLFormat
else # not required for julia >= v1.9
using ..GraphIO
using ..ParserCombinator
import ..GraphIO.GML.GMLFormat
end

function _gml_read_one_graph(gs, dir)
nodes = [x[:id] for x in gs[:node]]
if dir
g = DiGraph(length(nodes))
else
g = Graph(length(nodes))
end
mapping = Dict{Int,Int}()
for (i, n) in enumerate(nodes)
mapping[n] = i
end
sds = [(Int(x[:source]), Int(x[:target])) for x in gs[:edge]]
for (s, d) in (sds)
add_edge!(g, mapping[s], mapping[d])
end
return g
end

function loadgml(io::IO, gname::String)
p = Parsers.GML.parse_dict(read(io, String))
for gs in p[:graph]
dir = Bool(get(gs, :directed, 0))
graphname = get(gs, :label, dir ? "digraph" : "graph")

(gname == graphname) && return _gml_read_one_graph(gs, dir)
end
return error("Graph $gname not found")
end

function loadgml_mult(io::IO)
p = Parsers.GML.parse_dict(read(io, String))
graphs = Dict{String,AbstractGraph}()
for gs in p[:graph]
dir = Bool(get(gs, :directed, 0))
graphname = get(gs, :label, dir ? "digraph" : "graph")
graphs[graphname] = _gml_read_one_graph(gs, dir)
end
return graphs
end

"""
savegml(f, g, gname="graph")
Write a graph `g` with name `gname` to an IO stream `io` in the
[GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) format. Return 1.
"""
function savegml(io::IO, g::AbstractGraph, gname::String="")
println(io, "graph")
println(io, "[")
length(gname) > 0 && println(io, "label \"$gname\"")
is_directed(g) && println(io, "directed 1")
for i in 1:nv(g)
println(io, "\tnode")
println(io, "\t[")
println(io, "\t\tid $i")
println(io, "\t]")
end
for e in edges(g)
s, t = Tuple(e)
println(io, "\tedge")
println(io, "\t[")
println(io, "\t\tsource $s")
println(io, "\t\ttarget $t")
println(io, "\t]")
end
println(io, "]")
return 1
end

"""
savegml_mult(io, graphs)
Write a dictionary of (name=>graph) to an IO stream `io` Return number of graphs written.
"""
function savegml_mult(io::IO, graphs::Dict)
ng = 0
for (gname, g) in graphs
ng += savegml(io, g, gname)
end
return ng
end
loadgraph(io::IO, gname::String, ::GMLFormat) = loadgml(io, gname)
loadgraphs(io::IO, ::GMLFormat) = loadgml_mult(io)
savegraph(io::IO, g::AbstractGraph, gname::String, ::GMLFormat) = savegml(io, g, gname)
savegraph(io::IO, d::Dict, ::GMLFormat) = savegml_mult(io, d)

end
Loading

0 comments on commit 3211943

Please sign in to comment.