diff --git a/DESCRIPTION b/DESCRIPTION index 1303ba86..d6fe876e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: migraph Title: Inferential Methods for Multimodal and Other Networks -Version: 1.6.8 +Version: 1.6.9 Description: A set of tools for testing networks. It includes functions for univariate and multivariate conditional uniform graph and quadratic assignment procedure testing, and network regression. @@ -69,4 +69,4 @@ Config/Needs/website: Config/testthat/parallel: true Config/testthat/edition: 3 Config/testthat/start-first: tutorials_migraph -Config/roxygen2/version: 8.0.0 +Config/roxygen2/version: 8.1.0 diff --git a/NAMESPACE b/NAMESPACE index ff838e49..ef13f541 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -34,28 +34,38 @@ export(test_permutation) export(test_random) export(tidy) importFrom(autograph,ag_base) -importFrom(dplyr,as_tibble) -importFrom(dplyr,left_join) -importFrom(dplyr,select) -importFrom(dplyr,tibble) +importFrom(dplyr, + as_tibble, + left_join, + select, + tibble +) importFrom(ergm,as.rlebdm) -importFrom(furrr,furrr_options) -importFrom(furrr,future_map_dfr) +importFrom(furrr, + furrr_options, + future_map_dfr +) importFrom(future,plan) -importFrom(generics,glance) -importFrom(generics,tidy) -importFrom(manynet,bind_node_attributes) -importFrom(manynet,generate_configuration) -importFrom(manynet,generate_random) -importFrom(manynet,is_complex) -importFrom(manynet,is_directed) -importFrom(manynet,play_diffusion) -importFrom(manynet,to_subgraph) +importFrom(generics, + glance, + tidy +) +importFrom(manynet, + bind_node_attributes, + generate_configuration, + generate_random, + is_complex, + is_directed, + play_diffusion, + to_subgraph +) importFrom(netrics,net_by_heterophily) importFrom(purrr,flatten) -importFrom(stats,as.formula) -importFrom(stats,binomial) -importFrom(stats,df.residual) -importFrom(stats,glm.fit) -importFrom(stats,pchisq) -importFrom(stats,quantile) +importFrom(stats, + as.formula, + binomial, + df.residual, + glm.fit, + pchisq, + quantile +) diff --git a/NEWS.md b/NEWS.md index 653d3b8c..445be3f1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +# migraph 1.6.9 + +## Package + +- Improved stocnet version check, consolidating into `{migraph}` so that it runs once rather than three times + - Also consults each package's GitHub repository, so a fix that is released but not yet published on CRAN is reported; where both sources agree, CRAN is recommended, since it needs no compiler or `{remotes}` + - Results are cached for seven days in `tools::R_user_dir("migraph", "cache")`, so most sessions make no network request at all + - Check is skipped in non-interactive sessions, fails silently when offline or when no repository is configured, and can be disabled with `options(snet_check_version = FALSE)` + +## Learning + +- Improved empty `run_tute()` listing of tutorials to suggest manynet>autograph>netrics>migraph sequence + # migraph 1.6.8 2026-07-31 diff --git a/R/tutorial_run.R b/R/tutorial_run.R index 18381806..12e573dd 100644 --- a/R/tutorial_run.R +++ b/R/tutorial_run.R @@ -18,7 +18,7 @@ #' @name tutorials NULL -stocnet <- c("manynet", "migraph", "autograph", "netrics") +stocnet <- c("manynet", "autograph", "netrics", "migraph") #' @rdname tutorials #' @export @@ -34,7 +34,6 @@ run_tute <- function(tute) { silent = TRUE) |> dplyr::select(1:3) }) dplyr::bind_rows(tutelist) |> - dplyr::arrange(dplyr::across(dplyr::any_of("name"))) |> print() manynet::snet_info("You can run a tutorial by typing e.g `run_tute('tutorial1')` or `run_tute('Data')` into the console.") } else { diff --git a/R/zzz.R b/R/zzz.R new file mode 100644 index 00000000..32520076 --- /dev/null +++ b/R/zzz.R @@ -0,0 +1,146 @@ +# nocov start + +# The stocnet packages used to each check CRAN for a newer version of themselves +# on attach. Since migraph Depends on manynet, autograph, and netrics, that meant +# three blocking round-trips per session. The check now lives here only, covers +# the whole stack at once, and is cached so it runs at most weekly. + +snet_pkgs <- c("migraph", "manynet", "autograph", "netrics") + +# How long to trust a cached result before checking again. +snet_check_interval <- 7 + +snet_cache_file <- function() { + file.path(tools::R_user_dir("migraph", which = "cache"), "version-check.rds") +} + +snet_read_cache <- function() { + f <- snet_cache_file() + if (!file.exists(f)) return(NULL) + out <- tryCatch(readRDS(f), error = function(e) NULL) + if (!is.list(out) || is.null(out$date) || !inherits(out$date, "Date")) return(NULL) + if (as.numeric(Sys.Date() - out$date) >= snet_check_interval) return(NULL) + out +} + +snet_write_cache <- function(behind) { + f <- snet_cache_file() + # Failure to cache is not worth bothering the user about; the check simply + # runs again next session. + tryCatch({ + dir.create(dirname(f), recursive = TRUE, showWarnings = FALSE) + saveRDS(list(date = Sys.Date(), behind = behind), f) + }, error = function(e) NULL, warning = function(w) NULL) +} + +# Versions available on CRAN. Returns a named character vector, or NULL when no +# repository is configured (as under a bare Rscript) or CRAN is unreachable. +snet_cran_versions <- function() { + repos <- getOption("repos") + if (is.null(repos) || !length(repos) || any(repos == "@CRAN@")) return(NULL) + tryCatch({ + ap <- utils::available.packages() + have <- intersect(snet_pkgs, rownames(ap)) + if (!length(have)) return(NULL) + out <- ap[have, "Version"] + names(out) <- have + out + }, error = function(e) NULL, warning = function(w) NULL) +} + +# Versions on the release branch of each GitHub repo. Much cheaper than the CRAN +# index: four ~2KB files from a CDN rather than the full package database. +snet_github_versions <- function() { + tryCatch({ + # Generous: fetching all four takes about 0.4s on a working connection. + old <- options(timeout = 2) + on.exit(options(old), add = TRUE) + out <- character() + for (p in snet_pkgs) { + url <- paste0("https://raw.githubusercontent.com/stocnet/", p, + "/main/DESCRIPTION") + txt <- tryCatch(readLines(url, warn = FALSE), error = function(e) NULL) + # If the first request fails the host is unreachable, so give up rather + # than waiting out the timeout once per package. Attach should never + # stall on a bad network. + if (is.null(txt)) { + if (!length(out)) return(NULL) else next + } + line <- grep("^Version:", txt, value = TRUE) + if (!length(line)) next + out[[p]] <- trimws(sub("^Version:", "", line[1])) + } + if (!length(out)) NULL else out + }, error = function(e) NULL, warning = function(w) NULL) +} + +# Which packages are behind, and where the newer version lives. When CRAN and +# GitHub agree, prefer CRAN: it is the binary install and needs no compiler. +snet_check_versions <- function() { + cran <- snet_cran_versions() + gh <- snet_github_versions() + if (is.null(cran) && is.null(gh)) return(NULL) + out <- list() + for (p in snet_pkgs) { + installed <- tryCatch(utils::packageVersion(p), error = function(e) NULL) + if (is.null(installed)) next + cv <- if (!is.null(cran) && p %in% names(cran)) utils::packageVersion(cran[[p]]) else NULL + gv <- if (!is.null(gh) && p %in% names(gh)) utils::packageVersion(gh[[p]]) else NULL + if (!is.null(cv) && cv > installed) { + out[[p]] <- list(version = as.character(cv), source = "CRAN") + } else if (!is.null(gv) && gv > installed) { + out[[p]] <- list(version = as.character(gv), source = "GitHub") + } + } + out +} + +snet_report_outdated <- function(behind) { + if (!length(behind)) return(invisible(NULL)) + pkgs <- names(behind) + vers <- vapply(behind, function(x) x$version, character(1)) + from_cran <- pkgs[vapply(behind, function(x) x$source, character(1)) == "CRAN"] + from_gh <- setdiff(pkgs, from_cran) + + packageStartupMessage( + "Newer version", if (length(pkgs) > 1) "s" else "", " available: ", + paste0(pkgs, " ", vers, collapse = ", "), ".") + + # Deliberately printed rather than prompted for. `utils::menu()` reads from + # stdin, and when stdin is at EOF while `interactive()` is still TRUE it loops + # forever, hanging the session on `library()`. Attach should never block on + # input, so name the command and let the user run it. + if (length(from_cran)) { + packageStartupMessage("Update from CRAN with:\n update.packages(c(", + paste0('"', from_cran, '"', collapse = ", "), "))") + } + if (length(from_gh)) { + packageStartupMessage( + "Not yet on CRAN. Install from GitHub with:\n remotes::install_github(c(", + paste0('"stocnet/', from_gh, '"', collapse = ", "), "))") + } + invisible(NULL) +} + +.onAttach <- function(...) { + + if (!interactive()) return() + if (!isTRUE(getOption("snet_check_version", TRUE))) return() + + cached <- snet_read_cache() + if (!is.null(cached)) { + snet_report_outdated(cached$behind) + return(invisible(NULL)) + } + + behind <- snet_check_versions() + # NULL means the check could not run (offline, no repo); don't cache that, so + # it is retried next session rather than suppressed for a week. + if (is.null(behind)) return(invisible(NULL)) + + snet_write_cache(behind) + snet_report_outdated(behind) + +} + +# nocov end diff --git a/cran-comments.md b/cran-comments.md index 0f40d750..31b9e985 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -8,3 +8,15 @@ ## R CMD check results 0 errors | 0 warnings | 0 notes + +## User filespace and internet access + +On attach in interactive sessions only, this version checks whether the installed stocnet packages +are outdated, and caches the result for seven days in `tools::R_user_dir("migraph", "cache")`. This +replaces a check that previously ran on every attach in each of three dependencies, so it reduces +both network use and startup time. + +The check queries CRAN and the packages' public GitHub repositories. It is wrapped in `tryCatch()` +with a short timeout, fails silently when offline or when no repository is configured, is skipped +entirely in non-interactive sessions, and can be disabled with `options(snet_check_version = FALSE)`. +The package is fully functional if the cache directory is absent or unwritable. diff --git a/tests/testthat/test-measure_over.R b/tests/testthat/test-measure_over.R index 6684d7c9..862f017f 100644 --- a/tests/testthat/test-measure_over.R +++ b/tests/testthat/test-measure_over.R @@ -4,7 +4,20 @@ test_that("over_waves works", { }) test_that("over_membership works", { - res <- over_membership(fict_potter, netrics::net_by_assortativity, - membership = netrics::node_in_regular(fict_potter)) - expect_equal(unname(unlist(c(res))), c(0.490201713,NaN)) + # A fixed two-block network with an explicit membership, so that the + # expectations do not move when upstream clustering methods change. + # Block A is a triad plus a pendant, block B a path, joined by one cross tie. + el <- rbind(c(1,2), c(1,3), c(1,4), c(2,3), c(5,6), c(6,7), c(7,8), c(4,5)) + mat <- matrix(0, 8, 8) + mat[el] <- 1 + mat[el[, 2:1]] <- 1 + memb <- rep(c("A","B"), each = 4) + # Densities check the blocks are split as expected, the cross tie excluded: + # 4 of 6 possible ties within A, 3 of 6 within B. + expect_equal(unname(unlist(c(over_membership(mat, netrics::net_by_density, + membership = memb)))), + c(4/6, 3/6)) + expect_equal(unname(unlist(c(over_membership(mat, netrics::net_by_assortativity, + membership = memb)))), + c(-5/7, -1/2)) })