From 9c72dda3669b6de56626bd4edbadb032eefb003e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABlle=20Salmon?= Date: Mon, 6 Jul 2026 15:26:35 +0200 Subject: [PATCH 1/4] feat!: automatically detect format in `read_graph()` and `write_graph()` --- R/foreign.R | 97 ++++++++++++++++------ tests/testthat/_snaps/foreign.md | 58 +++++++++++++ tests/testthat/test-foreign.R | 134 +++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 24 deletions(-) diff --git a/R/foreign.R b/R/foreign.R index d4a2e9c6d5c..6afc3b92bd1 100644 --- a/R/foreign.R +++ b/R/foreign.R @@ -303,19 +303,10 @@ write.graph.fromraw <- function(buffer, file) { #' @keywords graphs #' @family foreign #' @export + read_graph <- function( file, - format = c( - "edgelist", - "pajek", - "ncol", - "lgl", - "graphml", - "dimacs", - "graphdb", - "gml", - "dl" - ), + format = NULL, ... ) { if ( @@ -328,7 +319,41 @@ read_graph <- function( write.graph.fromraw(buffer, file) } - format <- igraph_match_arg(format) + file_extension <- tools::file_ext(file) + if (is.null(format)) { + format <- switch( + file_extension, + "net" = "pajek", + "gml" = "gml", + "graphml" = "graphml", + { + lifecycle::deprecate_soft( + "2.3.3", + sprintf( + "read_graph(format = 'must be non NULL for file extension %s')", + file_extension + ), + details = "Defaulting to `edgelist`" + ) + "edgelist" + } + ) + } + + format <- rlang::arg_match( + format, + c( + "edgelist", + "pajek", + "ncol", + "lgl", + "graphml", + "dimacs", + "graphdb", + "gml", + "dl" + ) + ) res <- switch( format, "pajek" = read.graph.pajek(file, ...), @@ -465,17 +490,7 @@ read_graph <- function( write_graph <- function( graph, file, - format = c( - "edgelist", - "pajek", - "ncol", - "lgl", - "graphml", - "dimacs", - "gml", - "dot", - "leda" - ), + format = NULL, ... ) { ensure_igraph(graph) @@ -491,7 +506,41 @@ write_graph <- function( tmpfile <- FALSE } - format <- igraph_match_arg(format) + file_extension <- tools::file_ext(file) + if (is.null(format)) { + format <- switch( + file_extension, + "net" = "pajek", + "gml" = "gml", + "graphml" = "graphml", + { + lifecycle::deprecate_soft( + "2.3.3", + sprintf( + "write_graph(format = 'must be non NULL for file extension %s')", + file_extension + ), + details = "Defaulting to `edgelist`" + ) + "edgelist" + } + ) + } + + format <- rlang::arg_match( + format, + c( + "edgelist", + "pajek", + "ncol", + "lgl", + "graphml", + "dimacs", + "gml", + "dot", + "leda" + ) + ) res <- switch( format, "pajek" = write.graph.pajek(graph, file, ...), diff --git a/tests/testthat/_snaps/foreign.md b/tests/testthat/_snaps/foreign.md index 5ff13f89152..7822a66ef60 100644 --- a/tests/testthat/_snaps/foreign.md +++ b/tests/testthat/_snaps/foreign.md @@ -42,6 +42,64 @@ Error in `write_graph()`: ! `format` must be one of "edgelist", "pajek", "ncol", "lgl", "graphml", "dimacs", "gml", "dot", or "leda", not "blop". +# reading GML file works with lesmis + + Code + read_graph(igraphdata::lesmis_gml(), format = "gml") + Output + IGRAPH UN-- 77 254 -- + + attr: id (v/n), name (v/c), value (e/n) + + edges (vertex names): + [1] Myriel --Napoleon Myriel --MlleBaptistine + [3] Myriel --MmeMagloire MlleBaptistine--MmeMagloire + [5] Myriel --CountessDeLo Myriel --Geborand + [7] Myriel --Champtercier Myriel --Cravatte + [9] Myriel --Count Myriel --OldMan + [11] Labarre --Valjean MmeMagloire --Valjean + [13] MlleBaptistine--Valjean Myriel --Valjean + [15] Valjean --Marguerite Valjean --MmeDeR + + ... omitted several edges + +# reading GraphML file works with lesmis + + Code + read_graph(igraphdata::lesmis_graphml(), format = "graphml") + Condition + Warning in `read_graph_graphml_impl()`: + Could not add vertex ids, there is already an 'id' vertex attribute. + Source: io/graphml.c:486 + Output + IGRAPH UN-- 77 254 -- + + attr: id (v/n), name (v/c), value (e/n) + + edges (vertex names): + [1] Myriel --Napoleon Myriel --MlleBaptistine + [3] Myriel --MmeMagloire MlleBaptistine--MmeMagloire + [5] Myriel --CountessDeLo Myriel --Geborand + [7] Myriel --Champtercier Myriel --Cravatte + [9] Myriel --Count Myriel --OldMan + [11] Labarre --Valjean MmeMagloire --Valjean + [13] MlleBaptistine--Valjean Myriel --Valjean + [15] Valjean --Marguerite Valjean --MmeDeR + + ... omitted several edges + +# reading Pajek file works with lesmis + + Code + read_graph(igraphdata::lesmis_pajek(), format = "pajek") + Output + IGRAPH UN-- 77 254 -- + + attr: id (v/c), name (v/c) + + edges (vertex names): + [1] Myriel --Napoleon Myriel --MlleBaptistine + [3] Myriel --MmeMagloire MlleBaptistine--MmeMagloire + [5] Myriel --CountessDeLo Myriel --Geborand + [7] Myriel --Champtercier Myriel --Cravatte + [9] Myriel --Count Myriel --OldMan + [11] Labarre --Valjean MmeMagloire --Valjean + [13] MlleBaptistine--Valjean Myriel --Valjean + [15] Valjean --Marguerite Valjean --MmeDeR + + ... omitted several edges + # graph_from_graphdb works Code diff --git a/tests/testthat/test-foreign.R b/tests/testthat/test-foreign.R index b1542f0334f..e17c1c44d78 100644 --- a/tests/testthat/test-foreign.R +++ b/tests/testthat/test-foreign.R @@ -57,6 +57,140 @@ test_that("writing graph in unsupported format", { expect_snapshot_igraph_error(write_graph(g, file, format = "blop")) }) +test_that("reading GML file works with lesmis", { + skip_if_not_installed("igraphdata") + + expect_snapshot(read_graph(igraphdata::lesmis_gml(), format = "gml")) +}) + +test_that("reading GraphML file works with lesmis", { + skip_if_no_graphml() + skip_if_not_installed("igraphdata") + + expect_snapshot(read_graph(igraphdata::lesmis_graphml(), format = "graphml")) +}) + +test_that("reading Pajek file works with lesmis", { + skip_if_not_installed("igraphdata") + + expect_snapshot(read_graph(igraphdata::lesmis_pajek(), format = "pajek")) +}) + +test_that("write/read Pajek round-trip preserves karate graph structure", { + skip_if_not_installed("igraphdata") + + data("karate", package = "igraphdata") + net_path <- withr::local_tempfile(fileext = ".net") + write_graph(karate, net_path, format = "pajek") + g <- read_graph(net_path, format = "pajek") + + expect_equal(vcount(g), vcount(karate)) + expect_equal(ecount(g), ecount(karate)) + expect_false(is_directed(g)) +}) + +test_that("write/read GraphML round-trip preserves UKfaculty graph structure", { + skip_if_no_graphml() + skip_if_not_installed("igraphdata") + + data("UKfaculty", package = "igraphdata") + graphml_path <- withr::local_tempfile(fileext = ".graphml") + write_graph(UKfaculty, graphml_path, format = "graphml") + g <- read_graph(graphml_path, format = "graphml") + + expect_equal(vcount(g), vcount(UKfaculty)) + expect_equal(ecount(g), ecount(UKfaculty)) + expect_true(is_directed(g)) + expect_true("Group" %in% vertex_attr_names(g)) + expect_true("weight" %in% edge_attr_names(g)) +}) + +test_that("write/read NCOL round-trip preserves karate edge weights", { + skip_if_not_installed("igraphdata") + + data("karate", package = "igraphdata") + # NCOL rejects spaces in vertex names; replace with underscores + V(karate)$name <- gsub(" ", "_", V(karate)$name) + ncol_path <- withr::local_tempfile(fileext = ".ncol") + write_graph(karate, ncol_path, format = "ncol") + g <- read_graph(ncol_path, format = "ncol") + + expect_equal(vcount(g), vcount(karate)) + expect_equal(ecount(g), ecount(karate)) + expect_false(is_directed(g)) + expect_true("name" %in% vertex_attr_names(g)) + expect_true("weight" %in% edge_attr_names(g)) +}) + +test_that("write_graph auto-detects GraphML format from .graphml extension", { + skip_if_no_graphml() + skip_if_not_installed("igraphdata") + + data("UKfaculty", package = "igraphdata") + graphml_path <- withr::local_tempfile(fileext = ".graphml") + write_graph(UKfaculty, graphml_path) + g <- read_graph(graphml_path, format = "graphml") + + expect_equal(vcount(g), vcount(UKfaculty)) + expect_equal(ecount(g), ecount(UKfaculty)) +}) + +test_that("write_graph auto-detects Pajek format from .net extension", { + skip_if_not_installed("igraphdata") + + data("karate", package = "igraphdata") + net_path <- withr::local_tempfile(fileext = ".net") + write_graph(karate, net_path) + g <- read_graph(net_path, format = "pajek") + + expect_equal(vcount(g), vcount(karate)) + expect_equal(ecount(g), ecount(karate)) +}) + +test_that("write_graph auto-detects Pajek format for lesmis from .net extension", { + skip_if_not_installed("igraphdata") + + g_orig <- read_graph(igraphdata::lesmis_pajek(), format = "pajek") + net_path <- withr::local_tempfile(fileext = ".net") + write_graph(g_orig, net_path) + g <- read_graph(net_path, format = "pajek") + + expect_equal(vcount(g), vcount(g_orig)) + expect_equal(ecount(g), ecount(g_orig)) + expect_false(is_directed(g)) +}) + +test_that("write_graph auto-detects GraphML format for lesmis from .graphml extension", { + skip_if_no_graphml() + skip_if_not_installed("igraphdata") + + # lesmis GraphML has an 'id' vertex attribute; igraph warns it cannot add its own + g_orig <- suppressWarnings(read_graph(igraphdata::lesmis_graphml(), format = "graphml")) + graphml_path <- withr::local_tempfile(fileext = ".graphml") + write_graph(g_orig, graphml_path) + g <- suppressWarnings(read_graph(graphml_path, format = "graphml")) + + expect_equal(vcount(g), vcount(g_orig)) + expect_equal(ecount(g), ecount(g_orig)) + expect_false(is_directed(g)) +}) + +test_that("write_graph defaults to edgelist with deprecation for unknown extension", { + skip_if_not_installed("igraphdata") + + data("karate", package = "igraphdata") + txt_path <- withr::local_tempfile(fileext = ".txt") + withr::local_options(lifecycle_verbosity = "warning") + expect_warning( + write_graph(karate, txt_path), + "Defaulting to `edgelist`" + ) + g <- read_graph(txt_path, format = "edgelist", directed = FALSE) + + expect_equal(vcount(g), vcount(karate)) + expect_equal(ecount(g), ecount(karate)) +}) + test_that("graph_from_graphdb works", { # FIXME: Need to fix ingestion code on Windows skip_on_os("windows") From 05cef88b59fc9493667f77c3209e67c701017d86 Mon Sep 17 00:00:00 2001 From: maelle Date: Mon, 6 Jul 2026 13:35:59 +0000 Subject: [PATCH 2/4] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/28795056455 --- man/read_graph.Rd | 7 +------ man/write_graph.Rd | 8 +------- tests/testthat/test-foreign.R | 5 ++++- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/man/read_graph.Rd b/man/read_graph.Rd index 15f4f2fa85b..dd83f1a85d8 100644 --- a/man/read_graph.Rd +++ b/man/read_graph.Rd @@ -10,12 +10,7 @@ \alias{UCINET} \title{Reading foreign file formats} \usage{ -read_graph( - file, - format = c("edgelist", "pajek", "ncol", "lgl", "graphml", "dimacs", "graphdb", "gml", - "dl"), - ... -) +read_graph(file, format = NULL, ...) } \arguments{ \item{file}{The connection to read from. This can be a local file, or a diff --git a/man/write_graph.Rd b/man/write_graph.Rd index 05758654e8a..8ec253de274 100644 --- a/man/write_graph.Rd +++ b/man/write_graph.Rd @@ -4,13 +4,7 @@ \alias{write_graph} \title{Writing the graph to a file in some format} \usage{ -write_graph( - graph, - file, - format = c("edgelist", "pajek", "ncol", "lgl", "graphml", "dimacs", "gml", "dot", - "leda"), - ... -) +write_graph(graph, file, format = NULL, ...) } \arguments{ \item{graph}{The graph to export.} diff --git a/tests/testthat/test-foreign.R b/tests/testthat/test-foreign.R index e17c1c44d78..b49f7a3be52 100644 --- a/tests/testthat/test-foreign.R +++ b/tests/testthat/test-foreign.R @@ -165,7 +165,10 @@ test_that("write_graph auto-detects GraphML format for lesmis from .graphml exte skip_if_not_installed("igraphdata") # lesmis GraphML has an 'id' vertex attribute; igraph warns it cannot add its own - g_orig <- suppressWarnings(read_graph(igraphdata::lesmis_graphml(), format = "graphml")) + g_orig <- suppressWarnings(read_graph( + igraphdata::lesmis_graphml(), + format = "graphml" + )) graphml_path <- withr::local_tempfile(fileext = ".graphml") write_graph(g_orig, graphml_path) g <- suppressWarnings(read_graph(graphml_path, format = "graphml")) From 1028a3846e981bb26a69729d29d3980ee648549f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABlle=20Salmon?= Date: Mon, 6 Jul 2026 15:40:42 +0200 Subject: [PATCH 3/4] test for `read_graph()` --- tests/testthat/test-foreign.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/testthat/test-foreign.R b/tests/testthat/test-foreign.R index b49f7a3be52..f47abbcc6b3 100644 --- a/tests/testthat/test-foreign.R +++ b/tests/testthat/test-foreign.R @@ -147,20 +147,20 @@ test_that("write_graph auto-detects Pajek format from .net extension", { expect_equal(ecount(g), ecount(karate)) }) -test_that("write_graph auto-detects Pajek format for lesmis from .net extension", { +test_that("read_graph auto-detects Pajek format for lesmis from .net extension", { skip_if_not_installed("igraphdata") g_orig <- read_graph(igraphdata::lesmis_pajek(), format = "pajek") net_path <- withr::local_tempfile(fileext = ".net") - write_graph(g_orig, net_path) - g <- read_graph(net_path, format = "pajek") + write_graph(g_orig, net_path, format = "pajek") + g <- read_graph(net_path) expect_equal(vcount(g), vcount(g_orig)) expect_equal(ecount(g), ecount(g_orig)) expect_false(is_directed(g)) }) -test_that("write_graph auto-detects GraphML format for lesmis from .graphml extension", { +test_that("read_graph auto-detects GraphML format for lesmis from .graphml extension", { skip_if_no_graphml() skip_if_not_installed("igraphdata") @@ -170,8 +170,8 @@ test_that("write_graph auto-detects GraphML format for lesmis from .graphml exte format = "graphml" )) graphml_path <- withr::local_tempfile(fileext = ".graphml") - write_graph(g_orig, graphml_path) - g <- suppressWarnings(read_graph(graphml_path, format = "graphml")) + write_graph(g_orig, graphml_path, format = "graphml") + g <- suppressWarnings(read_graph(graphml_path)) expect_equal(vcount(g), vcount(g_orig)) expect_equal(ecount(g), ecount(g_orig)) From 45baa3e971fe34f7a470ac3ad2a9371e0bf967ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABlle=20Salmon?= Date: Mon, 6 Jul 2026 16:34:06 +0200 Subject: [PATCH 4/4] ouch --- DESCRIPTION | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1205ce5e01d..16777bd2d61 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,6 +40,7 @@ Imports: cli, graphics, grDevices, + igraphdata (>= 1.0.1.9003), lifecycle, magrittr, Matrix, @@ -53,7 +54,6 @@ Suggests: callr, decor, digest, - igraphdata, knitr, rgl (>= 1.3.14), rmarkdown, @@ -87,3 +87,5 @@ Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace", "igraph.r2cdocs::docs_rd", "devtag::dev_roclet")) SystemRequirements: libxml2 (optional), glpk (>= 4.57, optional) Config/roxygen2/version: 8.0.0.9000 +Remotes: + igraph/igraphdata