Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
^CRAN-SUBMISSION$
^\.claude$
^dev_notes$
^dev$
60 changes: 60 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Runs R CMD check on every push and pull request. Based on the canonical
# workflow at https://github.com/r-lib/actions/tree/v2/examples
#
# The R-hub workflow alongside this one stays manual: it covers the unusual
# platforms (valgrind, gcc-san, older toolchains) that are worth a look before
# a CRAN submission but not on every commit.

on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:

name: R-CMD-check

permissions: read-all

jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}

name: ${{ matrix.config.os }} (${{ matrix.config.r }})

strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes

steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-pandoc@v2

- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true
# rWCVPdata (Suggests) isn't on CRAN; this is the repository named in
# DESCRIPTION's Additional_repositories
extra-repositories: 'https://matildabrown.github.io/drat'

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check

- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual", "--compact-vignettes=gs+qpdf")'
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ Suggests:
data.table,
nanoparquet,
rmarkdown,
rWCVP,
rWCVPdata,
sf,
terra,
Expand Down
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
bases are answered separately in native_status_coordinates and native_status_names.

## Bug fixes
* NSR_metadata() stopped with an error ("'names' attribute [3] must be the same length
as the vector [0]") when the NSR API could not be reached, which broke the package's
examples and vignette on CRAN's Windows check machines and led to its archival. It
now gives a message and returns NULL, like the package's other API functions. Tests
now put every exported API function through an unreachable API, an HTTP error and an
empty reply, with the request mocked.
* NSR_metadata() had its sources and version elements named the wrong way round:
metadata$version held the sources and metadata$sources the version.
* NSR() sent any input in a single request, although the API accepts at most 5,000
Expand Down
4 changes: 2 additions & 2 deletions R/NSR_core.R
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ nsr_core <- function(url = "https://nsrapi.xyz/nsr_wsb.php",

#Return NULL if API isn't working

if(!exists("results_json")){
if(!exists("results_json", inherits = FALSE)){
return(invisible(NULL))
}

Expand All @@ -92,7 +92,7 @@ nsr_core <- function(url = "https://nsrapi.xyz/nsr_wsb.php",

#Convert to data.frame if things worked

if(!exists("results_raw")){
if(!exists("results_raw", inherits = FALSE)){
return(invisible(NULL))
}else{
results <- as.data.frame(results_raw)
Expand Down
26 changes: 18 additions & 8 deletions R/NSR_metadata.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#'Returns metadata on NSR including version and citation information
#' @param bibtex_file Optional output file for writing bibtex citations.
#' @param ... Additional arguments passed to internal functions.
#' @return List containing: (1) bibtex-formatted citation information, (2) information about NSR data sources, and (3) NSR version information.
#' @return List containing: (1) bibtex-formatted citation information, (2) information about NSR data sources, and (3) NSR version information. If any of these can't be retrieved (e.g. the API is unreachable), a message is given and NULL is returned.
#' @note This function provides citation information in bibtex format that can be used with reference manager software (e.g. Paperpile, Zotero). Please remember to cite both the sources and the NSR, as the NSR couldn't exist without these sources!
#' @note This function is a wrapper that returns the output of the functions NSR_citations, NSR_sources, and NSR_version.
#' @export
Expand All @@ -13,14 +13,24 @@
#'
NSR_metadata <- function(bibtex_file=NULL, ...){

getters <- list(citations = NSR_citations,
sources = NSR_sources,
version = NSR_version)

output <- list()

output[[1]] <- NSR_citations(...)
output[[2]] <- NSR_sources(...)
output[[3]] <- NSR_version(...)

names(output) <- c("citations", "sources", "version")


for(element in names(getters)){

output[[element]] <- getters[[element]](...)

#If the API didn't work, return a null (stopping here avoids repeating the failed request)
if(is.null(output[[element]])){
message("NSR metadata could not be retrieved.")
return(invisible(NULL))
}

}

#Write the bibtex information if a file is specified
if(!is.null(bibtex_file)){writeLines(text = output$citations$citation, con = bibtex_file)}

Expand Down
32 changes: 28 additions & 4 deletions R/local_build.R
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ NSR_local_build <- function(sources = "wcvp",
"The checklist tables are written, but until this raster exists no query ",
"can reach a WCVP opinion: coordinates cannot be placed in a WCVP region, ",
"and division names resolve to GADM, which needs the link table below. ",
"Install 'rWCVPdata' (or 'rWCVP'), then re-run NSR_local_build(overwrite = TRUE).",
"Install 'rWCVPdata', then re-run NSR_local_build(overwrite = TRUE).",
call. = FALSE)
}
}
Expand Down Expand Up @@ -276,16 +276,40 @@ nsr_match_poldiv <- function(country, state = NULL, bb) {
out
}

#' TNRS_local(), which TNRS added in 0.4.0
#'
#' Internal. Looked up when it's needed rather than called as TNRS::TNRS_local(), so the
#' package installs and passes R CMD check with an older TNRS: only the offline build
#' needs it.
#' @keywords internal
#' @noRd
nsr_tnrs_local <- function() {
nsr_need("TNRS", what = "Resolving names against WCVP")
if (!nsr_has_export("TNRS", "TNRS_local")) {
stop("Resolving names against WCVP needs TNRS 0.4.0 or later, for TNRS_local(); the ",
"installed version is ", utils::packageVersion("TNRS"), ". Install the newer ",
"version with remotes::install_github(\"EnquistLab/RTNRS\").", call. = FALSE)
}
getExportedValue("TNRS", "TNRS_local")
}

#' Whether a package exports a function
#' @keywords internal
#' @noRd
nsr_has_export <- function(pkg, name) {
name %in% getNamespaceExports(pkg)
}

#' Resolve checklist names to WCVP accepted species
#'
#' Internal. The id returned is the SPECIES' id, not an infraspecific one, so every
#' source keys on the same unit as POWO after its infraspecifics are rolled up.
#' @keywords internal
#' @noRd
nsr_resolve_names <- function(names_in, quiet = FALSE, species_index = NULL) {
nsr_need("TNRS", what = "Resolving names against WCVP")
r <- TNRS::TNRS_local(taxonomic_names = names_in, sources = "wcvp", matches = "best",
build_missing = FALSE, quiet = quiet)
tnrs_local <- nsr_tnrs_local()
r <- tnrs_local(taxonomic_names = names_in, sources = "wcvp", matches = "best",
build_missing = FALSE, quiet = quiet)
acc <- r$Accepted_species
acc[is.na(acc) | !nzchar(acc)] <- NA_character_
# prefer the species' own id where we know it, so subspecies land on their species
Expand Down
8 changes: 6 additions & 2 deletions R/local_import.R
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,12 @@ nsr_species_of <- function(ids, nm, max_steps = 3L) {
#' @noRd
nsr_find_wcvp_zip <- function() {
if (!requireNamespace("TNRS", quietly = TRUE)) return(NULL)
d <- try(TNRS:::tnrs_cache_dir(), silent = TRUE)
if (inherits(d, "try-error") || !dir.exists(d)) return(NULL)
# TNRS's own rule for its cache directory (its tnrs_cache_dir(), which isn't exported)
d <- getOption("TNRS.cache_dir")
if (is.null(d) && !is.null(getNamespace("tools")[["R_user_dir"]])) {
d <- tools::R_user_dir("TNRS", which = "cache")
}
if (is.null(d) || !dir.exists(d)) return(NULL)
f <- list.files(d, pattern = "^wcvp.*\\.zip$", full.names = TRUE)
if (!length(f)) NULL else f[order(file.mtime(f), decreasing = TRUE)][1]
}
Expand Down
9 changes: 3 additions & 6 deletions R/local_regions.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,9 @@ nsr_wgsrpd_polygons <- function() {
p <- try(rWCVPdata::wgsrpd3, silent = TRUE)
if (!inherits(p, "try-error")) return(sf::st_as_sf(p))
}
if (requireNamespace("rWCVP", quietly = TRUE)) {
e <- new.env()
utils::data("wgsrpd3", package = "rWCVP", envir = e)
return(sf::st_as_sf(get("wgsrpd3", envir = e)))
}
stop("The WGSRPD level-3 polygons need the 'rWCVPdata' (or 'rWCVP') package.", call. = FALSE)
stop("The WGSRPD level-3 polygons need the 'rWCVPdata' package, installed with ",
"install.packages(\"rWCVPdata\", repos = c(\"https://matildabrown.github.io/drat\", ",
"getOption(\"repos\")))", call. = FALSE)
}

#' Spatial links between region systems
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# RNSR

[![R-CMD-check](https://github.com/EnquistLab/RNSR/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/EnquistLab/RNSR/actions/workflows/R-CMD-check.yaml)

R package containing tools to access the Native Status Resolver API and associated functionality

## Description
Expand Down
58 changes: 46 additions & 12 deletions cran-comments.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
## Resubmission of an archived package

NSR was archived on 2026-07-28 because check errors were not corrected in time. This
submission fixes their cause.

The errors were on the Windows flavors (r-devel, r-release and r-oldrel), in the examples
and in re-building the vignette. Those machines could not reach the NSR API, and
NSR_metadata() then stopped with "'names' attribute [3] must be the same length as the
vector [0]" instead of failing gracefully: it assigned NULL results into a list and then
named three elements.

What we changed:

* NSR_metadata() now gives an informative message and returns NULL when any part of the
metadata can't be retrieved, as the package's other API functions already did.
* New tests (tests/testthat/test-NSR_api_failure.R) run every exported function that uses
the API under an unreachable API, an HTTP error, an empty reply and no internet access,
with the request mocked so no network is needed. Each must give a message and return
NULL, with no error or warning. These tests fail on the archived version with the
error CRAN reported.
* We rendered the vignette with the API made unreachable, and it builds, showing the
messages in place of results.
* Tests that need the live API are skipped on CRAN or replay recorded responses (vcr).

This version also adds an offline resolver, NSR_local(), whose data are built on the
user's machine by NSR_local_build(). Nothing derived from the checklists ships with the
package, and the packages it needs are in Suggests. One of them, rWCVPdata, is not on
CRAN; it is available from the repository given in Additional_repositories, and it is
used only conditionally, through requireNamespace(). We dropped rWCVP from Suggests,
since it has been archived on CRAN.

NSR_local_build() resolves names with TNRS_local(), which TNRS (Suggests) adds in
version 0.4.0; CRAN currently has 0.3.6. The function is looked up at run time, after
checking that the installed TNRS exports it, and an informative error says which
version is needed if it doesn't. The package installs and passes R CMD check with
TNRS 0.3.6, and nothing outside the offline build uses TNRS.

## Test environments
* local "Windows" "10 x64" "build 17763", R 4.4.1
* win-builder
* macOS builder
* ubuntu-latest on GitHub
* macos-13 on GitHub
* macos-latest on GitHub
* windows-latest on GitHub

## R CMD check results
* local Windows 11 x64, R 4.4.1

0 errors √ | 0 warnings √ | 0 notes √
## R CMD check results

0 errors | 0 warnings | 1 note

## Notes
* win-builder returns two notes: (1) "CRAN-pack does not know about .github", which is not returned otherwise (.github is needed for rhub and is in the .Rbuildignore); and (2) "This is a new release.""
* checking CRAN incoming feasibility ... NOTE
- New submission. Package was archived on CRAN: addressed above.
- Suggests or Enhances not in mainstream repositories: rWCVPdata. It is available
using the Additional_repositories specification, as above.

* Updated the Description per CRAN feedback.
(Locally there is a second NOTE, "unable to verify current time", which comes from the
local network rather than the package.)
84 changes: 84 additions & 0 deletions dev/release_checks.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Pre-release checklist for NSR. Source the sections one at a time from the
# package root. This folder is in .Rbuildignore and never ships.
#
# The GitHub Actions workflow (.github/workflows/R-CMD-check.yaml) already runs
# R CMD check on Linux/macOS/Windows for every push, so the steps here are the
# ones CRAN asks for beyond that: the --as-cran incoming checks, the API-down
# checks that got 0.1.0 archived, URL and spelling checks, tarball size, and
# the remote CRAN-like builders.

# 1. Bookkeeping ------------------------------------------------------------
# Bump Version in DESCRIPTION, add an entry to NEWS, update cran-comments.md
# (its Test environments should list the runs below once they pass).
# usethis::use_version("patch") # or "minor" / "major"
# usethis::use_release_issue() # tickable checklist as a GitHub issue

# 2. Regenerate docs and figures --------------------------------------------
devtools::document()

# The README figures are drawn as SVG; the PNGs are rendered from them. Only
# needed after editing an SVG.
for (svg in list.files("man/figures", "\\.svg$", full.names = TRUE)) {
img <- magick::image_read(rsvg::rsvg_png(svg, width = 2000))
img <- magick::image_quantize(img, max = 256, dither = FALSE)
magick::image_write(img, sub("\\.svg$", ".png", svg), format = "png")
}

# 3. Local checks -----------------------------------------------------------
devtools::test()
chk <- devtools::check(cran = TRUE, incoming = TRUE, remote = TRUE)
stopifnot(length(chk$errors) == 0, length(chk$warnings) == 0)
# Expected NOTE: "New submission / Package was archived on CRAN", and
# rWCVPdata (Suggests) not in mainstream repositories but available through
# Additional_repositories. Both are explained in cran-comments.md.
# "unable to verify current time" is the local network, not the package.

# URLs in Rd files, README and DESCRIPTION.
urlchecker::url_check()

# Spelling. There is no inst/WORDLIST yet: fix real typos, then run
# update_wordlist() to accept the remaining terms (taxon names, NSR, WCVP...).
spelling::spell_check_package()
# spelling::update_wordlist() # accept the current list

# Tarball must stay comfortably under CRAN's 5 MB limit. man/figures ships.
tb <- devtools::build(path = tempdir(), quiet = TRUE)
cat(basename(tb), round(file.size(tb) / 1024), "KB\n")
stopifnot(file.size(tb) < 4 * 1024^2)

# 4. With the API down ------------------------------------------------------
# 0.1.0 was archived because CRAN's Windows machines couldn't reach the NSR
# API and NSR_metadata() stopped with an error. CRAN policy: functions that
# use the internet must fail gracefully (a message, no error or warning).
# These tests mock every exported API function through an unreachable API,
# an HTTP error, an empty reply and no internet access.
devtools::test(filter = "api_failure")

# The vignette calls the API too, and is rebuilt during CRAN's checks. Render
# it with the API unreachable: it must build, showing messages, not stop.
devtools::load_all()
testthat::with_mocked_bindings(
rmarkdown::render("vignettes/NSR.Rmd", output_dir = tempdir(), quiet = TRUE),
check_internet = function() TRUE,
POST = function(...) stop("Could not resolve host: nsrapi.xyz"),
.package = "NSR")

# 5. Remote builders (results arrive by email / GitHub) ---------------------
# All three Windows flavors failed for 0.1.0, so run all three.
devtools::check_win_devel() # CRAN's Windows R-devel builder
devtools::check_win_release()
devtools::check_win_oldrelease()
# R-hub v2 runs in this repo's own GitHub Actions (.github/workflows/rhub.yaml);
# rhub::rhub_doctor() passed on 2026-10-19. The platforms below are the
# CRAN-flavoured set for a pure-R package: the three OS runners, current and
# patched R, the alternative-BLAS Fedora build, no-Suggests, and \donttest
# examples. The call dispatches the workflow and prints the run URL; the
# package must be committed and pushed first, since the run checks what is on
# GitHub.
rhub::rhub_check(platforms = c("linux", "windows", "macos",
"ubuntu-release", "ubuntu-next", "atlas",
"nosuggests", "donttest"))

# 6. Submit -----------------------------------------------------------------
# devtools::release() # interactive checklist, then uploads
# After acceptance: usethis::use_github_release(); usethis::use_dev_version()
2 changes: 1 addition & 1 deletion man/NSR_metadata.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading