From 34c2cae1b7bbaa42784cfc2f084e82d92f6ae098 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 6 Apr 2026 19:10:10 -0500 Subject: [PATCH 1/6] Reformat R/dev.R and R/release.R via rformat --- R/dev.R | 400 ++++++++++++++++++++++++------------------------ R/release.R | 427 ++++++++++++++++++++++++++-------------------------- 2 files changed, 408 insertions(+), 419 deletions(-) diff --git a/R/dev.R b/R/dev.R index c3bfbed..16fd29d 100644 --- a/R/dev.R +++ b/R/dev.R @@ -19,89 +19,88 @@ #' check(error_on = "error") # Only fail on errors, not warnings #' check(args = c("--as-cran", "--no-manual")) #' } -check <- function( - path = ".", - args = c("--as-cran", "--no-manual"), - error_on = c("warning", "error", "note") -) { - error_on <- match.arg(error_on) - - # Get absolute path - path <- normalizePath(path, mustWork = TRUE) - - # Get package name from DESCRIPTION - desc_file <- file.path(path, "DESCRIPTION") - if (!file.exists(desc_file)) { - stop("No DESCRIPTION file found in ", path, call. = FALSE) - } - - desc <- read.dcf(desc_file) - pkg_name <- desc[1, "Package"] - pkg_version <- desc[1, "Version"] - - # Create temp directory for build/check - tmp_dir <- tempfile("tinypkgr_check_") - dir.create(tmp_dir) - on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) - - # Build the package - message("Building ", pkg_name, "...") - build_cmd <- paste("R CMD build", shQuote(path)) - old_wd <- setwd(tmp_dir) - on.exit(setwd(old_wd), add = TRUE) - - build_result <- system(build_cmd, ignore.stdout = TRUE, ignore.stderr = TRUE) - if (build_result != 0) { - # Re-run to show errors - system(build_cmd) - stop("R CMD build failed", call. = FALSE) - } - - # Find the tarball - tarball <- paste0(pkg_name, "_", pkg_version, ".tar.gz") - if (!file.exists(tarball)) { - stop("Expected tarball not found: ", tarball, call. = FALSE) - } - - # Run R CMD check - message("Checking ", pkg_name, "...") - check_cmd <- paste("R CMD check", paste(args, collapse = " "), shQuote(tarball)) - check_result <- system(check_cmd) - - # Parse check results - check_dir <- paste0(pkg_name, ".Rcheck") - log_file <- file.path(check_dir, "00check.log") - - if (file.exists(log_file)) { - log <- readLines(log_file, warn = FALSE) - - # Count issues (format: "* checking ... NOTE" or "ERROR: ...") - errors <- sum(grepl("\\.\\.\\. ERROR$|^ERROR:", log)) - warnings <- sum(grepl("\\.\\.\\. WARNING$|^WARNING:", log)) - notes <- sum(grepl("\\.\\.\\. NOTE$|^NOTE:", log)) - - # Print summary - message("\n", pkg_name, " ", pkg_version, ": ", - errors, " error(s), ", warnings, " warning(s), ", notes, " note(s)") - - # Determine if we should error - should_error <- switch(error_on, - "note" = errors > 0 || warnings > 0 || notes > 0, - "warning" = errors > 0 || warnings > 0, - "error" = errors > 0 - ) - - if (should_error) { - stop("R CMD check found issues", call. = FALSE) +check <- function(path = ".", args = c("--as-cran", "--no-manual"), + error_on = c("warning", "error", "note")) { + error_on <- match.arg(error_on) + + # Get absolute path + path <- normalizePath(path, mustWork = TRUE) + + # Get package name from DESCRIPTION + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, call. = FALSE) } - invisible(TRUE) - } else { - if (check_result != 0) { - stop("R CMD check failed", call. = FALSE) + desc <- read.dcf(desc_file) + pkg_name <- desc[1, "Package"] + pkg_version <- desc[1, "Version"] + + # Create temp directory for build/check + tmp_dir <- tempfile("tinypkgr_check_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + + # Build the package + message("Building ", pkg_name, "...") + build_cmd <- paste("R CMD build", shQuote(path)) + old_wd <- setwd(tmp_dir) + on.exit(setwd(old_wd), add = TRUE) + + build_result <- system(build_cmd, ignore.stdout = TRUE, + ignore.stderr = TRUE) + if (build_result != 0) { + # Re-run to show errors + system(build_cmd) + stop("R CMD build failed", call. = FALSE) + } + + # Find the tarball + tarball <- paste0(pkg_name, "_", pkg_version, ".tar.gz") + if (!file.exists(tarball)) { + stop("Expected tarball not found: ", tarball, call. = FALSE) + } + + # Run R CMD check + message("Checking ", pkg_name, "...") + check_cmd <- paste("R CMD check", paste(args, collapse = " "), + shQuote(tarball)) + check_result <- system(check_cmd) + + # Parse check results + check_dir <- paste0(pkg_name, ".Rcheck") + log_file <- file.path(check_dir, "00check.log") + + if (file.exists(log_file)) { + log <- readLines(log_file, warn = FALSE) + + # Count issues (format: "* checking ... NOTE" or "ERROR: ...") + errors <- sum(grepl("\\.\\.\\. ERROR$|^ERROR:", log)) + warnings <- sum(grepl("\\.\\.\\. WARNING$|^WARNING:", log)) + notes <- sum(grepl("\\.\\.\\. NOTE$|^NOTE:", log)) + + # Print summary + message("\n", pkg_name, " ", pkg_version, ": ", + errors, " error(s), ", warnings, " warning(s), ", notes, " note(s)") + + # Determine if we should error + should_error <- switch(error_on, + "note" = errors > 0 || warnings > 0 || notes > 0, + "warning" = errors > 0 || warnings > 0, + "error" = errors > 0 + ) + + if (should_error) { + stop("R CMD check found issues", call. = FALSE) + } + + invisible(TRUE) + } else { + if (check_result != 0) { + stop("R CMD check failed", call. = FALSE) + } + invisible(TRUE) } - invisible(TRUE) - } } #' Install Package @@ -120,45 +119,42 @@ check <- function( #' install() #' install(quiet = FALSE) # Show full output #' } -install <- function( - path = ".", - quiet = TRUE -) { - # Get absolute path - path <- normalizePath(path, mustWork = TRUE) - - # Get package name from DESCRIPTION - desc_file <- file.path(path, "DESCRIPTION") - if (!file.exists(desc_file)) { - stop("No DESCRIPTION file found in ", path, call. = FALSE) - } - - desc <- read.dcf(desc_file) - pkg_name <- desc[1, "Package"] - - # Build command - cmd <- paste("R CMD INSTALL", shQuote(path)) - - # Run install (redirect output if quiet) - if (quiet) { - # Redirect both stdout and stderr - if (.Platform$OS.type == "windows") { - cmd_quiet <- paste(cmd, "> NUL 2>&1") +install <- function(path = ".", quiet = TRUE) { + # Get absolute path + path <- normalizePath(path, mustWork = TRUE) + + # Get package name from DESCRIPTION + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, call. = FALSE) + } + + desc <- read.dcf(desc_file) + pkg_name <- desc[1, "Package"] + + # Build command + cmd <- paste("R CMD INSTALL", shQuote(path)) + + # Run install (redirect output if quiet) + if (quiet) { + # Redirect both stdout and stderr + if (.Platform$OS.type == "windows") { + cmd_quiet <- paste(cmd, "> NUL 2>&1") + } else { + cmd_quiet <- paste(cmd, "> /dev/null 2>&1") + } + result <- system(cmd_quiet) + } else { + result <- system(cmd) + } + + if (result == 0) { + message("Installed ", pkg_name) + invisible(TRUE) } else { - cmd_quiet <- paste(cmd, "> /dev/null 2>&1") + message("Install failed for ", pkg_name) + invisible(FALSE) } - result <- system(cmd_quiet) - } else { - result <- system(cmd) - } - - if (result == 0) { - message("Installed ", pkg_name) - invisible(TRUE) - } else { - message("Install failed for ", pkg_name) - invisible(FALSE) - } } #' Load All Package Code @@ -178,44 +174,43 @@ install <- function( #' load_all() #' load_all(quiet = FALSE) # Show each file being sourced #' } -load_all <- function( - path = ".", - quiet = TRUE -) { - r_dir <- file.path(path, "R") +load_all <- function(path = ".", quiet = TRUE) { + r_dir <- file.path(path, "R") - if (!dir.exists(r_dir)) { - stop("No R/ directory found in ", path, call. = FALSE) - } + if (!dir.exists(r_dir)) { + stop("No R/ directory found in ", path, call. = FALSE) + } - r_files <- list.files(r_dir, pattern = "\\.[Rr]$", full.names = TRUE) + r_files <- list.files(r_dir, pattern = "\\.[Rr]$", full.names = TRUE) - if (length(r_files) == 0) { - message("No R files found.") - return(invisible(character())) - } + if (length(r_files) == 0) { + message("No R files found.") + return(invisible(character())) + } - # Create a new environment attached to the search path - pkg_env <- new.env(parent = globalenv()) + # Create a new environment attached to the search path + pkg_env <- new.env(parent = globalenv()) - for (f in r_files) { - if (!quiet) message("Sourcing ", basename(f)) - source(f, local = pkg_env) - } + for (f in r_files) { + if (!quiet) { + message("Sourcing ", basename(f)) + } + source(f, local = pkg_env) + } - # Attach the environment - # Use a name that won't conflict - env_name <- paste0("tinypkgr:", basename(normalizePath(path))) + # Attach the environment + # Use a name that won't conflict + env_name <- paste0("tinypkgr:", basename(normalizePath(path))) - # Detach if already attached - if (env_name %in% search()) { - detach(env_name, character.only = TRUE) - } + # Detach if already attached + if (env_name %in% search()) { + detach(env_name, character.only = TRUE) + } - attach(pkg_env, name = env_name) + attach(pkg_env, name = env_name) - message("Loaded ", length(r_files), " file(s) as '", env_name, "'") - invisible(r_files) + message("Loaded ", length(r_files), " file(s) as '", env_name, "'") + invisible(r_files) } #' Reload an Installed Package @@ -237,63 +232,62 @@ load_all <- function( #' reload() # Reinstall and reload current package #' reload(document = TRUE) # Document first (requires tinyrox) #' } -reload <- function( - path = ".", - document = FALSE, - quiet = TRUE -) { - # Get package name from DESCRIPTION - desc_file <- file.path(path, "DESCRIPTION") - if (!file.exists(desc_file)) { - stop("No DESCRIPTION file found in ", path, ". Is this an R package?", call. = FALSE) - } - - desc <- read.dcf(desc_file) - pkg_name <- desc[1, "Package"] - - # Document first if requested - if (document) { - if (requireNamespace("tinyrox", quietly = TRUE)) { - tinyrox::document(path) - } else { - warning("tinyrox not available, skipping documentation", call. = FALSE) +reload <- function(path = ".", document = FALSE, quiet = TRUE) { + # Get package name from DESCRIPTION + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, + ". Is this an R package?", call. = FALSE) + } + + desc <- read.dcf(desc_file) + pkg_name <- desc[1, "Package"] + + # Document first if requested + if (document) { + if (requireNamespace("tinyrox", quietly = TRUE)) { + tinyrox::document(path) + } else { + warning("tinyrox not available, skipping documentation", + call. = FALSE) + } + } + + # Unload package if loaded + pkg_loaded <- paste0("package:", pkg_name) + if (pkg_loaded %in% search()) { + tryCatch({ + detach(pkg_loaded, unload = TRUE, character.only = TRUE) + message("Unloaded ", pkg_name) + }, error = function(e) { + # Sometimes unload fails due to dependencies, just detach + tryCatch({ + detach(pkg_loaded, character.only = TRUE) + message("Detached ", pkg_name, " (could not fully unload)") + }, error = function(e2) { + message("Note: Could not detach ", pkg_name, ": ", e2$message) + }) + }) } - } - - # Unload package if loaded - pkg_loaded <- paste0("package:", pkg_name) - if (pkg_loaded %in% search()) { - tryCatch({ - detach(pkg_loaded, unload = TRUE, character.only = TRUE) - message("Unloaded ", pkg_name) - }, error = function(e) { - # Sometimes unload fails due to dependencies, just detach + + # Also unload namespace if still loaded + if (pkg_name %in% loadedNamespaces()) { tryCatch({ - detach(pkg_loaded, character.only = TRUE) - message("Detached ", pkg_name, " (could not fully unload)") - }, error = function(e2) { - message("Note: Could not detach ", pkg_name, ": ", e2$message) - }) - }) - } - - # Also unload namespace if still loaded - if (pkg_name %in% loadedNamespaces()) { - tryCatch({ - unloadNamespace(pkg_name) - }, error = function(e) { - # Ignore - will be handled by library() reload - }) - } - - # Reinstall - success <- install(path, quiet = quiet) - - if (success) { - # Reload - library(pkg_name, character.only = TRUE) - message("Reloaded ", pkg_name) - } - - invisible(success) + unloadNamespace(pkg_name) + }, error = function(e) { + # Ignore - will be handled by library() reload + }) + } + + # Reinstall + success <- install(path, quiet = quiet) + + if (success) { + # Reload + library(pkg_name, character.only = TRUE) + message("Reloaded ", pkg_name) + } + + invisible(success) } + diff --git a/R/release.R b/R/release.R index 95c8482..1711a9b 100644 --- a/R/release.R +++ b/R/release.R @@ -21,43 +21,40 @@ NULL #' build() #' build(dest_dir = tempdir()) #' } -build <- function( - path = ".", - dest_dir = "." -) { +build <- function(path = ".", dest_dir = ".") { + path <- normalizePath(path, mustWork = TRUE) + dest_dir <- normalizePath(dest_dir, mustWork = TRUE) - path <- normalizePath(path, mustWork = TRUE) - dest_dir <- normalizePath(dest_dir, mustWork = TRUE) - - desc_file <- file.path(path, "DESCRIPTION") - if (!file.exists(desc_file)) { - stop("No DESCRIPTION file found in ", path, call. = FALSE) - } + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, call. = FALSE) + } - desc <- read.dcf(desc_file) - pkg_name <- desc[1, "Package"] - pkg_version <- desc[1, "Version"] + desc <- read.dcf(desc_file) + pkg_name <- desc[1, "Package"] + pkg_version <- desc[1, "Version"] - message("Building ", pkg_name, " ", pkg_version, "...") + message("Building ", pkg_name, " ", pkg_version, "...") - old_wd <- getwd() - on.exit(setwd(old_wd), add = TRUE) - setwd(dest_dir) + old_wd <- getwd() + on.exit(setwd(old_wd), add = TRUE) + setwd(dest_dir) - cmd <- paste("R CMD build", shQuote(path)) - result <- system(cmd) + cmd <- paste("R CMD build", shQuote(path)) + result <- system(cmd) - if (result != 0) { - stop("R CMD build failed", call. = FALSE) - } + if (result != 0) { + stop("R CMD build failed", call. = FALSE) + } - tarball <- file.path(dest_dir, paste0(pkg_name, "_", pkg_version, ".tar.gz")) - if (!file.exists(tarball)) { - stop("Expected tarball not found: ", tarball, call. = FALSE) - } + tarball <- file.path(dest_dir, + paste0(pkg_name, "_", pkg_version, ".tar.gz")) + if (!file.exists(tarball)) { + stop("Expected tarball not found: ", tarball, call. = FALSE) + } - message("Built: ", tarball) - invisible(tarball) + message("Built: ", tarball) + invisible(tarball) } #' Get Package Maintainer @@ -75,52 +72,49 @@ build <- function( #' maintainer() #' } maintainer <- function(path = ".") { - desc_file <- file.path(path, "DESCRIPTION") - if (!file.exists(desc_file)) { - stop("No DESCRIPTION file found in ", path, call. = FALSE) - } - - desc <- read.dcf(desc_file) - - # Try Authors@R first - if ("Authors@R" %in% colnames(desc)) { - authors_r <- desc[1, "Authors@R"] - # Parse the R expression - authors <- tryCatch( - eval(parse(text = authors_r)), - error = function(e) NULL - ) + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, call. = FALSE) + } - if (!is.null(authors)) { - # Find the maintainer (cre role) - if (inherits(authors, "person")) { - for (i in seq_along(authors)) { - auth <- authors[i] - if ("cre" %in% auth$role) { - return(list( - name = paste(auth$given, auth$family), - email = auth$email - )) - } + desc <- read.dcf(desc_file) + + # Try Authors@R first + if ("Authors@R" %in% colnames(desc)) { + authors_r <- desc[1, "Authors@R"] + # Parse the R expression + authors <- tryCatch( + eval(parse(text = authors_r)), + error = function(e) NULL + ) + + if (!is.null(authors)) { + # Find the maintainer (cre role) + if (inherits(authors, "person")) { + for (i in seq_along(authors)) { + auth <- authors[i] + if ("cre" %in% auth$role) { + return(list( + name = paste(auth$given, auth$family), + email = auth$email + )) + } + } + } } - } } - } - - # Fall back to Maintainer field - if ("Maintainer" %in% colnames(desc)) { - maint <- desc[1, "Maintainer"] - # Parse "Name " format - match <- regmatches(maint, regexec("^(.+?)\\s*<(.+)>$", maint)) [[1]] - if (length(match) == 3) { - return(list( - name = trimws(match[2]), - email = trimws(match[3]) - )) + + # Fall back to Maintainer field + if ("Maintainer" %in% colnames(desc)) { + maint <- desc[1, "Maintainer"] + # Parse "Name " format + match <- regmatches(maint, regexec("^(.+?)\\s*<(.+)>$", maint))[[1]] + if (length(match) == 3) { + return(list(name = trimws(match[2]), email = trimws(match[3]))) + } } - } - stop("Could not determine maintainer from DESCRIPTION", call. = FALSE) + stop("Could not determine maintainer from DESCRIPTION", call. = FALSE) } #' Check Package on Windows via win-builder @@ -141,43 +135,41 @@ maintainer <- function(path = ".") { #' check_win_devel() #' check_win_devel(r_version = "release") #' } -check_win_devel <- function( - path = ".", - r_version = c("devel", "release", "oldrelease") -) { - r_version <- match.arg(r_version) - - # Build the package - tmp_dir <- tempfile("tinypkgr_winbuild_") - dir.create(tmp_dir) - on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) - - tarball <- build(path, dest_dir = tmp_dir) - - # Get maintainer email for confirmation - maint <- maintainer(path) - message("Results will be emailed to: ", maint$email) - - # FTP URL based on R version - ftp_dir <- switch(r_version, - "devel" = "R-devel", - "release" = "R-release", - "oldrelease" = "R-oldrelease" - ) - ftp_url <- paste0("ftp://win-builder.r-project.org/", ftp_dir, "/") - - message("Uploading to win-builder (", r_version, ")...") - - # Upload via FTP using curl - result <- tryCatch({ - curl::curl_upload(tarball, ftp_url) - TRUE +check_win_devel <- function(path = ".", + r_version = c("devel", "release", "oldrelease")) { + r_version <- match.arg(r_version) + + # Build the package + tmp_dir <- tempfile("tinypkgr_winbuild_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + + tarball <- build(path, dest_dir = tmp_dir) + + # Get maintainer email for confirmation + maint <- maintainer(path) + message("Results will be emailed to: ", maint$email) + + # FTP URL based on R version + ftp_dir <- switch(r_version, + "devel" = "R-devel", + "release" = "R-release", + "oldrelease" = "R-oldrelease" + ) + ftp_url <- paste0("ftp://win-builder.r-project.org/", ftp_dir, "/") + + message("Uploading to win-builder (", r_version, ")...") + + # Upload via FTP using curl + result <- tryCatch({ + curl::curl_upload(tarball, ftp_url) + TRUE }, error = function(e) { - stop("FTP upload failed: ", e$message, call. = FALSE) + stop("FTP upload failed: ", e$message, call. = FALSE) }) - message("Upload complete. Check your email for results (usually within 30 minutes).") - invisible(TRUE) + message("Upload complete. Check your email for results (usually within 30 minutes).") + invisible(TRUE) } #' Submit Package to CRAN @@ -196,121 +188,124 @@ check_win_devel <- function( #' \dontrun{ #' submit_cran() #' } -submit_cran <- function( - path = ".", - comments = "cran-comments.md" -) { - path <- normalizePath(path, mustWork = TRUE) - - # Get package info - desc_file <- file.path(path, "DESCRIPTION") - desc <- read.dcf(desc_file) - pkg_name <- desc[1, "Package"] - pkg_version <- desc[1, "Version"] - - # Get maintainer - maint <- maintainer(path) - - message("Package: ", pkg_name, " ", pkg_version) - message("Maintainer: ", maint$name, " <", maint$email, ">") - - # Confirm email - response <- readline(paste0("Is your email address correct? (y/n): ")) - if (!tolower(response) %in% c("y", "yes")) { - message("Submission cancelled. Update the maintainer email in DESCRIPTION.") - return(invisible(FALSE)) - } - - # Read comments - comment_text <- "" - comments_path <- file.path(path, comments) - if (!is.null(comments) && file.exists(comments_path)) { - comment_text <- paste(readLines(comments_path, warn = FALSE), collapse = "\n") - message("Including comments from: ", comments) - } - - # Build the package - tmp_dir <- tempfile("tinypkgr_cran_") - dir.create(tmp_dir) - on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) - - tarball <- build(path, dest_dir = tmp_dir) - tarball_size <- file.info(tarball) $size - message("Package size: ", format(structure(tarball_size, class = "object_size"), - units = "auto")) - - # Final confirmation - response <- readline(paste0("Ready to submit ", pkg_name, " ", pkg_version, - " to CRAN? (y/n): ")) - if (!tolower(response) %in% c("y", "yes")) { - message("Submission cancelled.") - return(invisible(FALSE)) - } - - # CRAN submission URL - cran_url <- "https://xmpalantir.wu.ac.at/cransubmit/index2.php" - - message("Uploading to CRAN...") - - # Submit - response <- tryCatch({ - h <- curl::new_handle() - curl::handle_setform(h, - name = maint$name, - email = maint$email, - uploaded_file = curl::form_file(tarball, type = "application/x-gzip"), - comment = comment_text, - upload = "Upload package" - ) - curl::curl_fetch_memory(cran_url, handle = h) +submit_cran <- function(path = ".", comments = "cran-comments.md") { + path <- normalizePath(path, mustWork = TRUE) + + # Get package info + desc_file <- file.path(path, "DESCRIPTION") + desc <- read.dcf(desc_file) + pkg_name <- desc[1, "Package"] + pkg_version <- desc[1, "Version"] + + # Get maintainer + maint <- maintainer(path) + + message("Package: ", pkg_name, " ", pkg_version) + message("Maintainer: ", maint$name, " <", maint$email, ">") + + # Confirm email + response <- readline(paste0("Is your email address correct? (y/n): ")) + if (!tolower(response) %in% c("y", "yes")) { + message("Submission cancelled. Update the maintainer email in DESCRIPTION.") + return(invisible(FALSE)) + } + + # Read comments + comment_text <- "" + comments_path <- file.path(path, comments) + if (!is.null(comments) && file.exists(comments_path)) { + comment_text <- paste(readLines(comments_path, warn = FALSE), + collapse = "\n") + message("Including comments from: ", comments) + } + + # Build the package + tmp_dir <- tempfile("tinypkgr_cran_") + dir.create(tmp_dir) + on.exit(unlink(tmp_dir, recursive = TRUE), add = TRUE) + + tarball <- build(path, dest_dir = tmp_dir) + tarball_size <- file.info(tarball)$size + message("Package size: ", format(structure(tarball_size, + class = "object_size"), + units = "auto")) + + # Final confirmation + response <- readline(paste0("Ready to submit ", pkg_name, " ", pkg_version, + " to CRAN? (y/n): ")) + if (!tolower(response) %in% c("y", "yes")) { + message("Submission cancelled.") + return(invisible(FALSE)) + } + + # CRAN submission URL + cran_url <- "https://xmpalantir.wu.ac.at/cransubmit/index2.php" + + message("Uploading to CRAN...") + + # Submit + response <- tryCatch({ + h <- curl::new_handle() + curl::handle_setform(h, + name = maint$name, + email = maint$email, + uploaded_file = curl::form_file(tarball, + type = "application/x-gzip"), + comment = comment_text, + upload = "Upload package" + ) + curl::curl_fetch_memory(cran_url, handle = h) }, error = function(e) { - stop("Upload failed: ", e$message, call. = FALSE) + stop("Upload failed: ", e$message, call. = FALSE) }) - # Check response - if (response$status_code == 200) { - # Parse response to get package ID for confirmation - response_text <- rawToChar(response$content) - - # Look for the package ID in the response - id_match <- regmatches(response_text, - regexec('name="pkg_id"[^>]*value="([^"]+)"', response_text)) [[1]] - - if (length(id_match) >= 2) { - pkg_id <- id_match[2] - - # Submit confirmation - h2 <- curl::new_handle() - curl::handle_setform(h2, - pkg_id = pkg_id, - name = maint$name, - email = maint$email, - policy_check = "1", - submit = "Submit package" - ) - confirm_response <- curl::curl_fetch_memory(cran_url, handle = h2) - - if (confirm_response$status_code == 200) { - message("\nSubmission uploaded successfully.") - message("Check your email (", maint$email, ") for a confirmation link.") - message("You must click the link to complete the submission.") + # Check response + if (response$status_code == 200) { + # Parse response to get package ID for confirmation + response_text <- rawToChar(response$content) + + # Look for the package ID in the response + id_match <- regmatches(response_text, + regexec('name="pkg_id"[^>]*value="([^"]+)"', response_text))[[1]] + + if (length(id_match) >= 2) { + pkg_id <- id_match[2] + + # Submit confirmation + h2 <- curl::new_handle() + curl::handle_setform(h2, + pkg_id = pkg_id, + name = maint$name, + email = maint$email, + policy_check = "1", + submit = "Submit package" + ) + confirm_response <- curl::curl_fetch_memory(cran_url, handle = h2) + + if (confirm_response$status_code == 200) { + message("\nSubmission uploaded successfully.") + message("Check your email (", maint$email, + ") for a confirmation link.") + message("You must click the link to complete the submission.") + return(invisible(TRUE)) + } + } + + # If we got here, something went wrong with confirmation + message("\nPackage uploaded, but confirmation step may have failed.") + message("Check your email for further instructions.") return(invisible(TRUE)) - } - } - # If we got here, something went wrong with confirmation - message("\nPackage uploaded, but confirmation step may have failed.") - message("Check your email for further instructions.") - return(invisible(TRUE)) - - } else if (response$status_code == 404) { - # CRAN might be in maintenance mode - response_text <- rawToChar(response$content) - message("CRAN submission system returned 404.") - message("The system may be in maintenance mode. Try again later.") - return(invisible(FALSE)) - - } else { - stop("Submission failed with status: ", response$status_code, call. = FALSE) - } + } else if (response$status_code == 404) { + # CRAN might be in maintenance mode + response_text <- rawToChar(response$content) + message("CRAN submission system returned 404.") + message("The system may be in maintenance mode. Try again later.") + return(invisible(FALSE)) + + } else { + stop("Submission failed with status: ", response$status_code, + call. = FALSE) + } } + From 0576d5b906948c00172bcda2c2e19ce1c5dea37d Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 6 Apr 2026 19:10:21 -0500 Subject: [PATCH 2/6] Add create_package() and bump to 0.2.0 New create_package() scaffolds a tinyverse-flavored R package: DESCRIPTION with Authors@R (optional ORCID), NAMESPACE, .Rbuildignore, NEWS.md, tests/tinytest.R entry point, and an optional starter hello() function with matching tinytest test. Pure base R, no new Imports. --- DESCRIPTION | 2 +- NAMESPACE | 1 + NEWS.md | 4 + R/create.R | 171 ++++++++++++++++++++++++++++++++++++ README.md | 10 +++ cran-comments.md | 5 +- inst/tinytest/test_create.R | 70 +++++++++++++++ man/create_package.Rd | 52 +++++++++++ 8 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 R/create.R create mode 100644 inst/tinytest/test_create.R create mode 100644 man/create_package.Rd diff --git a/DESCRIPTION b/DESCRIPTION index b9e623d..411cf68 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: tinypkgr Title: Minimal R Package Development Utilities -Version: 0.1.0 +Version: 0.2.0 Authors@R: person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"), comment = c(ORCID = "0009-0005-4248-604X")) diff --git a/NAMESPACE b/NAMESPACE index 3b29f4f..868cb06 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(build) export(check) export(check_win_devel) +export(create_package) export(install) export(load_all) export(maintainer) diff --git a/NEWS.md b/NEWS.md index 9320bbb..6edea25 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# tinypkgr 0.2.0 + +* New `create_package()` for scaffolding a tinyverse-flavored R package: DESCRIPTION with Authors@R, NAMESPACE, .Rbuildignore, NEWS.md, tests/tinytest.R entry point, and an optional starter `hello()` function with matching tinytest test. + # tinypkgr 0.1.0 * Initial CRAN release. diff --git a/R/create.R b/R/create.R new file mode 100644 index 0000000..5b0f41a --- /dev/null +++ b/R/create.R @@ -0,0 +1,171 @@ +#' Create a New R Package +#' +#' Scaffolds a minimal tinyverse-flavored R package: DESCRIPTION with Authors@R, +#' NAMESPACE, .Rbuildignore, NEWS.md, a tinytest entry point, and (optionally) +#' a starter function plus matching test. +#' +#' @param name Package name. Must start with a letter and contain only letters, +#' numbers, and dots. +#' @param path Directory in which to create the package. The package itself +#' will be created at `file.path(path, name)`. Default is the current +#' directory. +#' @param title One-line title in title case (no period). +#' @param description Paragraph describing what the package does. +#' @param author Author full name (e.g., "First Last"). +#' @param email Author email address. Also used as the maintainer. +#' @param orcid Optional ORCID identifier (e.g., "0000-0000-0000-0000"). If +#' supplied, added as `comment = c(ORCID = ...)` in Authors@R. +#' @param license License string for the License field. Default "GPL-3". +#' @param example_fn If TRUE (default), write a starter `R/hello.R` with a +#' tinyrox-documented `hello()` function and a matching tinytest test. +#' +#' @return Path to the created package directory (invisibly). +#' +#' @export +#' +#' @examples +#' \donttest{ +#' tmp <- tempfile("tinypkgr_create_") +#' dir.create(tmp) +#' create_package("mypkg", path = tmp, +#' author = "First Last", +#' email = "you@example.com") +#' unlink(tmp, recursive = TRUE) +#' } +create_package <- function(name, path = ".", + title = "What The Package Does (One Line, Title Case)", + description = "A paragraph describing what the package does.", + author, email, orcid = NULL, license = "GPL-3", + example_fn = TRUE) { + if (missing(name) || !is.character(name) || length(name) != 1) { + stop("'name' must be a single character string", call. = FALSE) + } + if (!valid_pkg_name(name)) { + stop("Invalid package name: ", name, + ". Must start with a letter and contain only letters, numbers, and dots.", + call. = FALSE) + } + if (missing(author) || !is.character(author) || length(author) != 1) { + stop("'author' must be a single character string", call. = FALSE) + } + if (missing(email) || !is.character(email) || length(email) != 1) { + stop("'email' must be a single character string", call. = FALSE) + } + + path <- normalizePath(path, mustWork = TRUE) + pkg_dir <- file.path(path, name) + if (file.exists(pkg_dir)) { + stop("Directory already exists: ", pkg_dir, call. = FALSE) + } + + dir.create(pkg_dir) + dir.create(file.path(pkg_dir, "R")) + dir.create(file.path(pkg_dir, "man")) + dir.create(file.path(pkg_dir, "tests")) + dir.create(file.path(pkg_dir, "inst", "tinytest"), recursive = TRUE) + + # DESCRIPTION + authors_r <- build_authors_r(author, email, orcid) + desc_lines <- c( + paste0("Package: ", name), + paste0("Title: ", title), + "Version: 0.0.0.9000", + "Authors@R:", + paste0(" ", authors_r), + paste0("Description: ", description), + paste0("License: ", license), + "Encoding: UTF-8", + "Suggests:", + " tinytest" + ) + writeLines(desc_lines, file.path(pkg_dir, "DESCRIPTION")) + + # NAMESPACE + ns_lines <- "# Generated by tinyrox: do not edit by hand" + if (example_fn) { + ns_lines <- c(ns_lines, "", "export(hello)") + } + writeLines(ns_lines, file.path(pkg_dir, "NAMESPACE")) + + # .Rbuildignore + writeLines(c( + "^\\.github$", + "^CLAUDE\\.md$", + "^cran-comments\\.md$" + ), file.path(pkg_dir, ".Rbuildignore")) + + # NEWS.md + writeLines(c( + paste0("# ", name, " 0.0.0.9000"), + "", + "* Initial development version." + ), file.path(pkg_dir, "NEWS.md")) + + # tests/tinytest.R entry point + writeLines(c( + "if (requireNamespace(\"tinytest\", quietly = TRUE)) {", + paste0(" tinytest::test_package(\"", name, "\")"), + "}" + ), file.path(pkg_dir, "tests", "tinytest.R")) + + # Optional starter function and test + if (example_fn) { + writeLines(c( + "#' Say Hello", + "#'", + "#' Returns a greeting.", + "#'", + "#' @param who Name to greet.", + "#'", + "#' @return A character string.", + "#'", + "#' @export", + "#'", + "#' @examples", + "#' hello(\"world\")", + "hello <- function(who = \"world\") {", + " paste0(\"Hello, \", who, \"!\")", + "}" + ), file.path(pkg_dir, "R", "hello.R")) + + writeLines(c( + paste0("expect_equal(", name, + "::hello(\"world\"), \"Hello, world!\")"), + paste0("expect_equal(", name, "::hello(\"R\"), \"Hello, R!\")") + ), file.path(pkg_dir, "inst", "tinytest", "test_hello.R")) + } + + message("Created package '", name, "' at ", pkg_dir) + invisible(pkg_dir) +} + +# Validate a package name against R's naming rules. +valid_pkg_name <- function(name) { + grepl("^[a-zA-Z][a-zA-Z0-9.]*[a-zA-Z0-9]$", name) +} + +# Build an Authors@R person() call as a single-line string. +build_authors_r <- function(author, email, orcid) { + parts <- strsplit(trimws(author), "\\s+")[[1]] + if (length(parts) >= 2) { + given <- paste(parts[-length(parts)], collapse = " ") + family <- parts[length(parts)] + person_str <- paste0( + "person(\"", given, "\", \"", family, + "\", email = \"", email, + "\", role = c(\"aut\", \"cre\")" + ) + } else { + person_str <- paste0( + "person(given = \"", author, + "\", email = \"", email, + "\", role = c(\"aut\", \"cre\")" + ) + } + if (!is.null(orcid)) { + person_str <- paste0(person_str, + ", comment = c(ORCID = \"", orcid, "\")") + } + paste0(person_str, ")") +} + diff --git a/README.md b/README.md index 9652442..a237ab0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ remotes::install_github("cornball-ai/tinypkgr") ## Usage +### Create a package + +```r +tinypkgr::create_package("mypkg", + author = "First Last", + email = "you@example.com", + orcid = "0000-0000-0000-0000") +``` + ### Development ```r @@ -49,6 +58,7 @@ submit_cran() | Function | Purpose | |----------|---------| +| `create_package()` | Scaffold a new tinyverse-flavored package | | `install()` | R CMD INSTALL wrapper | | `load_all()` | Source R/ files for dev | | `reload()` | Reinstall and reload | diff --git a/cran-comments.md b/cran-comments.md index 44b5988..8887b4d 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,8 +1,11 @@ ## R CMD check results -0 errors | 0 warnings | 1 note +0 errors | 0 warnings | 2 notes * This is a new submission. +* `load_all()` uses `attach()` deliberately to expose package symbols on the + search path for interactive development, mirroring `devtools::load_all()`. + This is intentional and documented. ## Test environments diff --git a/inst/tinytest/test_create.R b/inst/tinytest/test_create.R new file mode 100644 index 0000000..4052a0d --- /dev/null +++ b/inst/tinytest/test_create.R @@ -0,0 +1,70 @@ +# Tests for create_package() + +tmp_root <- tempfile("tinypkgr_create_") +dir.create(tmp_root) + +# Basic creation with all defaults + example function +pkg_dir <- tinypkgr::create_package( + "foopkg", + path = tmp_root, + author = "First Last", + email = "first@example.com", + orcid = "0009-0005-4248-604X" +) + +expect_true(dir.exists(pkg_dir)) +expect_true(file.exists(file.path(pkg_dir, "DESCRIPTION"))) +expect_true(file.exists(file.path(pkg_dir, "NAMESPACE"))) +expect_true(file.exists(file.path(pkg_dir, ".Rbuildignore"))) +expect_true(file.exists(file.path(pkg_dir, "NEWS.md"))) +expect_true(file.exists(file.path(pkg_dir, "tests", "tinytest.R"))) +expect_true(file.exists(file.path(pkg_dir, "R", "hello.R"))) +expect_true(file.exists(file.path(pkg_dir, "inst", "tinytest", "test_hello.R"))) +expect_true(dir.exists(file.path(pkg_dir, "man"))) + +# DESCRIPTION parses and has expected fields +desc <- read.dcf(file.path(pkg_dir, "DESCRIPTION")) +expect_equal(unname(desc[1, "Package"]), "foopkg") +expect_equal(unname(desc[1, "Version"]), "0.0.0.9000") +expect_equal(unname(desc[1, "License"]), "GPL-3") +expect_true("Authors@R" %in% colnames(desc)) + +# Authors@R parses and ORCID is included +authors <- eval(parse(text = desc[1, "Authors@R"])) +expect_true(inherits(authors, "person")) +expect_equal(authors$given, "First") +expect_equal(authors$family, "Last") +expect_equal(authors$email, "first@example.com") +expect_equal(unname(authors$comment["ORCID"]), "0009-0005-4248-604X") + +# example_fn = FALSE skips starter files +pkg_dir2 <- tinypkgr::create_package( + "barpkg", + path = tmp_root, + author = "Solo", + email = "solo@example.com", + example_fn = FALSE +) +expect_false(file.exists(file.path(pkg_dir2, "R", "hello.R"))) +expect_false(file.exists(file.path(pkg_dir2, "inst", "tinytest", "test_hello.R"))) + +# Single-name author works +desc2 <- read.dcf(file.path(pkg_dir2, "DESCRIPTION")) +authors2 <- eval(parse(text = desc2[1, "Authors@R"])) +expect_equal(authors2$given, "Solo") +expect_true(is.null(authors2$comment)) + +# Invalid names error +expect_error(tinypkgr::create_package("1pkg", path = tmp_root, + author = "A B", email = "a@b.com")) +expect_error(tinypkgr::create_package("my-pkg", path = tmp_root, + author = "A B", email = "a@b.com")) +expect_error(tinypkgr::create_package(".foo", path = tmp_root, + author = "A B", email = "a@b.com")) + +# Existing directory errors +expect_error(tinypkgr::create_package("foopkg", path = tmp_root, + author = "A B", email = "a@b.com")) + +# Cleanup +unlink(tmp_root, recursive = TRUE) diff --git a/man/create_package.Rd b/man/create_package.Rd new file mode 100644 index 0000000..2ed7fec --- /dev/null +++ b/man/create_package.Rd @@ -0,0 +1,52 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{create_package} +\alias{create_package} +\title{Create a New R Package} +\usage{ +create_package(name, path = ".", + title = "What The Package Does (One Line, Title Case)", + description = "A paragraph describing what the package does.", + author, email, orcid = NULL, license = "GPL-3", example_fn = TRUE) +} +\arguments{ +\item{name}{Package name. Must start with a letter and contain only letters, +numbers, and dots.} + +\item{path}{Directory in which to create the package. The package itself +will be created at `file.path(path, name)`. Default is the current +directory.} + +\item{title}{One-line title in title case (no period).} + +\item{description}{Paragraph describing what the package does.} + +\item{author}{Author full name (e.g., "First Last").} + +\item{email}{Author email address. Also used as the maintainer.} + +\item{orcid}{Optional ORCID identifier (e.g., "0000-0000-0000-0000"). If +supplied, added as `comment = c(ORCID = ...)` in Authors@R.} + +\item{license}{License string for the License field. Default "GPL-3".} + +\item{example_fn}{If TRUE (default), write a starter `R/hello.R` with a +tinyrox-documented `hello()` function and a matching tinytest test.} +} +\value{ +Path to the created package directory (invisibly). +} +\description{ +Scaffolds a minimal tinyverse-flavored R package: DESCRIPTION with Authors@R, +NAMESPACE, .Rbuildignore, NEWS.md, a tinytest entry point, and (optionally) +a starter function plus matching test. +} +\examples{ +\donttest{ +tmp <- tempfile("tinypkgr_create_") +dir.create(tmp) +create_package("mypkg", path = tmp, + author = "First Last", + email = "you@example.com") +unlink(tmp, recursive = TRUE) +} +} From 5c3e1f540799f85c241be3d405e4c8025167ac06 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 6 Apr 2026 20:01:51 -0500 Subject: [PATCH 3/6] Add use_version() to bump DESCRIPTION + NEWS.md --- NAMESPACE | 1 + NEWS.md | 1 + R/use.R | 92 ++++++++++++++++++++++++++++++++ README.md | 1 + inst/tinytest/test_use_version.R | 60 +++++++++++++++++++++ man/use_version.Rd | 31 +++++++++++ 6 files changed, 186 insertions(+) create mode 100644 R/use.R create mode 100644 inst/tinytest/test_use_version.R create mode 100644 man/use_version.Rd diff --git a/NAMESPACE b/NAMESPACE index 868cb06..50c521d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(load_all) export(maintainer) export(reload) export(submit_cran) +export(use_version) importFrom(curl,curl_fetch_memory) importFrom(curl,curl_upload) diff --git a/NEWS.md b/NEWS.md index 6edea25..0d4d298 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # tinypkgr 0.2.0 * New `create_package()` for scaffolding a tinyverse-flavored R package: DESCRIPTION with Authors@R, NAMESPACE, .Rbuildignore, NEWS.md, tests/tinytest.R entry point, and an optional starter `hello()` function with matching tinytest test. +* New `use_version()` bumps the DESCRIPTION Version field and prepends a matching NEWS.md section header. Supports `patch`, `minor`, `major`, and `dev` bumps. # tinypkgr 0.1.0 diff --git a/R/use.R b/R/use.R new file mode 100644 index 0000000..ba393d7 --- /dev/null +++ b/R/use.R @@ -0,0 +1,92 @@ +#' Bump Package Version +#' +#' Increments the Version field in DESCRIPTION and prepends a new section +#' header to NEWS.md (if present) so the two never drift apart. +#' +#' @param which Which component to bump: "patch" (0.2.0 -> 0.2.1), +#' "minor" (0.2.0 -> 0.3.0), "major" (0.2.0 -> 1.0.0), or +#' "dev" (0.2.0 -> 0.2.0.9000, or 0.2.0.9000 -> 0.2.0.9001). +#' @param path Path to package root directory. +#' +#' @return The new version string (invisibly). +#' +#' @export +#' +#' @examples +#' \donttest{ +#' tmp <- tempfile("tinypkgr_use_") +#' dir.create(tmp) +#' create_package("foo", path = tmp, +#' author = "First Last", email = "f@example.com") +#' use_version("patch", path = file.path(tmp, "foo")) +#' unlink(tmp, recursive = TRUE) +#' } +use_version <- function(which = c("patch", "minor", "major", "dev"), + path = ".") { + which <- match.arg(which) + path <- normalizePath(path, mustWork = TRUE) + desc_file <- file.path(path, "DESCRIPTION") + if (!file.exists(desc_file)) { + stop("No DESCRIPTION file found in ", path, call. = FALSE) + } + + desc_lines <- readLines(desc_file, warn = FALSE) + ver_idx <- grep("^Version:", desc_lines) + if (length(ver_idx) != 1) { + stop("Could not find Version field in DESCRIPTION", call. = FALSE) + } + current <- trimws(sub("^Version:", "", desc_lines[ver_idx])) + new_version <- bump_version(current, which) + desc_lines[ver_idx] <- paste0("Version: ", new_version) + writeLines(desc_lines, desc_file) + + pkg_name <- read.dcf(desc_file)[1, "Package"] + message("Bumped ", pkg_name, ": ", current, " -> ", new_version) + + # Update NEWS.md only on release-style bumps + news_file <- file.path(path, "NEWS.md") + if (file.exists(news_file) && which != "dev") { + news <- readLines(news_file, warn = FALSE) + header <- paste0("# ", pkg_name, " ", new_version) + new_news <- c(header, "", "* ", "", news) + writeLines(new_news, news_file) + message("Added NEWS.md header for ", new_version) + } + + invisible(new_version) +} + +# Bump a version string by component. +bump_version <- function(current, which) { + parts <- strsplit(current, ".", fixed = TRUE)[[1]] + if (which == "dev") { + if (length(parts) == 4) { + parts[4] <- as.character(as.integer(parts[4]) + 1) + } else if (length(parts) == 3) { + parts <- c(parts, "9000") + } else { + stop("Cannot bump dev version from: ", current, call. = FALSE) + } + } else { + if (length(parts) == 4) { + parts <- parts[1:3] + } + if (length(parts) != 3) { + stop("Cannot parse version: ", current, call. = FALSE) + } + nums <- as.integer(parts) + if (which == "patch") { + nums[3] <- nums[3] + 1 + } else if (which == "minor") { + nums[2] <- nums[2] + 1 + nums[3] <- 0 + } else if (which == "major") { + nums[1] <- nums[1] + 1 + nums[2] <- 0 + nums[3] <- 0 + } + parts <- as.character(nums) + } + paste(parts, collapse = ".") +} + diff --git a/README.md b/README.md index a237ab0..eb27375 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ submit_cran() | Function | Purpose | |----------|---------| | `create_package()` | Scaffold a new tinyverse-flavored package | +| `use_version()` | Bump DESCRIPTION version + NEWS.md header | | `install()` | R CMD INSTALL wrapper | | `load_all()` | Source R/ files for dev | | `reload()` | Reinstall and reload | diff --git a/inst/tinytest/test_use_version.R b/inst/tinytest/test_use_version.R new file mode 100644 index 0000000..0c4921a --- /dev/null +++ b/inst/tinytest/test_use_version.R @@ -0,0 +1,60 @@ +# Tests for use_version() + +# bump_version() unit tests +bump <- tinypkgr:::bump_version +expect_equal(bump("0.2.0", "patch"), "0.2.1") +expect_equal(bump("0.2.0", "minor"), "0.3.0") +expect_equal(bump("0.2.0", "major"), "1.0.0") +expect_equal(bump("0.2.0", "dev"), "0.2.0.9000") +expect_equal(bump("0.2.0.9000", "dev"), "0.2.0.9001") +expect_equal(bump("0.2.0.9001", "dev"), "0.2.0.9002") +# Release bump from a dev version strips the dev suffix +expect_equal(bump("0.2.0.9001", "patch"), "0.2.1") +expect_equal(bump("0.2.0.9001", "minor"), "0.3.0") +expect_equal(bump("0.2.0.9001", "major"), "1.0.0") +# Garbage +expect_error(bump("foo", "patch")) +expect_error(bump("0.2", "patch")) + +# End-to-end on a temp package +tmp <- tempfile("tinypkgr_useversion_") +dir.create(tmp) +pkg <- tinypkgr::create_package("vpkg", path = tmp, + author = "A B", email = "a@b.com") +desc_file <- file.path(pkg, "DESCRIPTION") + +# Set a known release version to test from +desc_lines <- readLines(desc_file) +desc_lines[grep("^Version:", desc_lines)] <- "Version: 0.1.0" +writeLines(desc_lines, desc_file) + +# Patch bump updates DESCRIPTION and NEWS.md +v <- tinypkgr::use_version("patch", path = pkg) +expect_equal(v, "0.1.1") +expect_equal(unname(read.dcf(desc_file)[1, "Version"]), "0.1.1") +news <- readLines(file.path(pkg, "NEWS.md")) +expect_true(grepl("^# vpkg 0.1.1", news[1])) + +# Minor bump +v <- tinypkgr::use_version("minor", path = pkg) +expect_equal(v, "0.2.0") + +# Major bump +v <- tinypkgr::use_version("major", path = pkg) +expect_equal(v, "1.0.0") + +# Dev bump does NOT touch NEWS.md +news_before <- readLines(file.path(pkg, "NEWS.md")) +v <- tinypkgr::use_version("dev", path = pkg) +expect_equal(v, "1.0.0.9000") +news_after <- readLines(file.path(pkg, "NEWS.md")) +expect_equal(news_before, news_after) + +# Successive dev bumps +v <- tinypkgr::use_version("dev", path = pkg) +expect_equal(v, "1.0.0.9001") + +# Missing DESCRIPTION +expect_error(tinypkgr::use_version("patch", path = tempdir())) + +unlink(tmp, recursive = TRUE) diff --git a/man/use_version.Rd b/man/use_version.Rd new file mode 100644 index 0000000..0cd4906 --- /dev/null +++ b/man/use_version.Rd @@ -0,0 +1,31 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{use_version} +\alias{use_version} +\title{Bump Package Version} +\usage{ +use_version(which = c("patch", "minor", "major", "dev"), path = ".") +} +\arguments{ +\item{which}{Which component to bump: "patch" (0.2.0 -> 0.2.1), +"minor" (0.2.0 -> 0.3.0), "major" (0.2.0 -> 1.0.0), or +"dev" (0.2.0 -> 0.2.0.9000, or 0.2.0.9000 -> 0.2.0.9001).} + +\item{path}{Path to package root directory.} +} +\value{ +The new version string (invisibly). +} +\description{ +Increments the Version field in DESCRIPTION and prepends a new section +header to NEWS.md (if present) so the two never drift apart. +} +\examples{ +\donttest{ +tmp <- tempfile("tinypkgr_use_") +dir.create(tmp) +create_package("foo", path = tmp, + author = "First Last", email = "f@example.com") +use_version("patch", path = file.path(tmp, "foo")) +unlink(tmp, recursive = TRUE) +} +} From 4411ea48ab0d5e0153cf8a7afb8ecd1d9a00915e Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 6 Apr 2026 20:04:50 -0500 Subject: [PATCH 4/6] Add use_github_action() to write r-ci workflow --- NAMESPACE | 1 + NEWS.md | 1 + R/use.R | 80 ++++++++++++++++++++++++++ README.md | 1 + inst/tinytest/test_use_github_action.R | 36 ++++++++++++ man/use_github_action.Rd | 28 +++++++++ 6 files changed, 147 insertions(+) create mode 100644 inst/tinytest/test_use_github_action.R create mode 100644 man/use_github_action.Rd diff --git a/NAMESPACE b/NAMESPACE index 50c521d..0f6a290 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(load_all) export(maintainer) export(reload) export(submit_cran) +export(use_github_action) export(use_version) importFrom(curl,curl_fetch_memory) diff --git a/NEWS.md b/NEWS.md index 0d4d298..3d4b74c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ * New `create_package()` for scaffolding a tinyverse-flavored R package: DESCRIPTION with Authors@R, NAMESPACE, .Rbuildignore, NEWS.md, tests/tinytest.R entry point, and an optional starter `hello()` function with matching tinytest test. * New `use_version()` bumps the DESCRIPTION Version field and prepends a matching NEWS.md section header. Supports `patch`, `minor`, `major`, and `dev` bumps. +* New `use_github_action()` writes a `.github/workflows/ci.yaml` from the r-ci template (Ubuntu + macOS) and adds `^\.github$` to `.Rbuildignore`. # tinypkgr 0.1.0 diff --git a/R/use.R b/R/use.R index ba393d7..4ed5191 100644 --- a/R/use.R +++ b/R/use.R @@ -56,6 +56,86 @@ use_version <- function(which = c("patch", "minor", "major", "dev"), invisible(new_version) } +#' Add a GitHub Actions CI Workflow +#' +#' Writes `.github/workflows/ci.yaml` using the r-ci template (Ubuntu and +#' macOS, via `eddelbuettel/github-actions/r-ci@master`). Adds `^\.github$` +#' to `.Rbuildignore` if not already present. +#' +#' @param path Path to package root directory. +#' +#' @return Path to the created YAML file (invisibly). +#' +#' @export +#' +#' @examples +#' \donttest{ +#' tmp <- tempfile("tinypkgr_use_") +#' dir.create(tmp) +#' create_package("foo", path = tmp, +#' author = "First Last", email = "f@example.com") +#' use_github_action(path = file.path(tmp, "foo")) +#' unlink(tmp, recursive = TRUE) +#' } +use_github_action <- function(path = ".") { + path <- normalizePath(path, mustWork = TRUE) + workflows_dir <- file.path(path, ".github", "workflows") + dir.create(workflows_dir, recursive = TRUE, showWarnings = FALSE) + yaml_file <- file.path(workflows_dir, "ci.yaml") + if (file.exists(yaml_file)) { + stop("File already exists: ", yaml_file, call. = FALSE) + } + + yaml_lines <- c( + "name: ci", + "", + "on:", + " push:", + " pull_request:", + "", + "env:", + " _R_CHECK_FORCE_SUGGESTS_: \"false\"", + "", + "jobs:", + " ci:", + " strategy:", + " matrix:", + " include:", + " - {os: macos-latest}", + " - {os: ubuntu-latest}", + "", + " runs-on: ${{ matrix.os }}", + "", + " steps:", + " - uses: actions/checkout@v6", + "", + " - name: Setup", + " uses: eddelbuettel/github-actions/r-ci@master", + "", + " - name: Dependencies", + " run: ./run.sh install_deps", + "", + " - name: Test", + " run: ./run.sh run_tests" + ) + writeLines(yaml_lines, yaml_file) + message("Created ", yaml_file) + + # Make sure .github is in .Rbuildignore + rbi <- file.path(path, ".Rbuildignore") + rbi_entry <- "^\\.github$" + if (file.exists(rbi)) { + lines <- readLines(rbi, warn = FALSE) + if (!rbi_entry %in% lines) { + writeLines(c(lines, rbi_entry), rbi) + } + } else { + writeLines(rbi_entry, rbi) + } + + invisible(yaml_file) +} + # Bump a version string by component. bump_version <- function(current, which) { parts <- strsplit(current, ".", fixed = TRUE)[[1]] diff --git a/README.md b/README.md index eb27375..354e9aa 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ submit_cran() |----------|---------| | `create_package()` | Scaffold a new tinyverse-flavored package | | `use_version()` | Bump DESCRIPTION version + NEWS.md header | +| `use_github_action()` | Write `.github/workflows/ci.yaml` (r-ci) | | `install()` | R CMD INSTALL wrapper | | `load_all()` | Source R/ files for dev | | `reload()` | Reinstall and reload | diff --git a/inst/tinytest/test_use_github_action.R b/inst/tinytest/test_use_github_action.R new file mode 100644 index 0000000..f76df8a --- /dev/null +++ b/inst/tinytest/test_use_github_action.R @@ -0,0 +1,36 @@ +# Tests for use_github_action() + +tmp <- tempfile("tinypkgr_useghaction_") +dir.create(tmp) +pkg <- tinypkgr::create_package("ghapkg", path = tmp, + author = "A B", email = "a@b.com") + +yaml <- tinypkgr::use_github_action(path = pkg) +expect_true(file.exists(yaml)) +expect_equal(basename(yaml), "ci.yaml") +expect_equal(basename(dirname(yaml)), "workflows") + +content <- readLines(yaml) +expect_true(any(grepl("^name: ci", content))) +expect_true(any(grepl("eddelbuettel/github-actions/r-ci@master", content))) +expect_true(any(grepl("macos-latest", content))) +expect_true(any(grepl("ubuntu-latest", content))) + +# .Rbuildignore already had ^\.github$ from create_package, no duplication +rbi <- readLines(file.path(pkg, ".Rbuildignore")) +expect_equal(sum(rbi == "^\\.github$"), 1) + +# Calling again errors +expect_error(tinypkgr::use_github_action(path = pkg)) + +# Works on a package without an .Rbuildignore +bare <- file.path(tmp, "bare") +dir.create(bare) +writeLines(c("Package: bare", "Version: 0.0.0.9000", + "Title: Bare", "Description: x.", "License: GPL-3"), + file.path(bare, "DESCRIPTION")) +tinypkgr::use_github_action(path = bare) +expect_true(file.exists(file.path(bare, ".Rbuildignore"))) +expect_true("^\\.github$" %in% readLines(file.path(bare, ".Rbuildignore"))) + +unlink(tmp, recursive = TRUE) diff --git a/man/use_github_action.Rd b/man/use_github_action.Rd new file mode 100644 index 0000000..bee86d1 --- /dev/null +++ b/man/use_github_action.Rd @@ -0,0 +1,28 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{use_github_action} +\alias{use_github_action} +\title{Add a GitHub Actions CI Workflow} +\usage{ +use_github_action(path = ".") +} +\arguments{ +\item{path}{Path to package root directory.} +} +\value{ +Path to the created YAML file (invisibly). +} +\description{ +Writes `.github/workflows/ci.yaml` using the r-ci template (Ubuntu and +macOS, via `eddelbuettel/github-actions/r-ci@master`). Adds `^\.github$` +to `.Rbuildignore` if not already present. +} +\examples{ +\donttest{ +tmp <- tempfile("tinypkgr_use_") +dir.create(tmp) +create_package("foo", path = tmp, + author = "First Last", email = "f@example.com") +use_github_action(path = file.path(tmp, "foo")) +unlink(tmp, recursive = TRUE) +} +} From f4e8d4584588a59b06d551739fca822eb3e42d6b Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Mon, 6 Apr 2026 20:56:35 -0500 Subject: [PATCH 5/6] Drop create_package() and switch dev versioning - Remove create_package() in favor of pkgKitten::kitten(). - Fix bump_version("dev"): increment a 4th digit starting at 1 instead of using the .9000 convention. - Rewrite test_use_version and test_use_github_action to use inline minimal package scaffolds. --- NAMESPACE | 1 - NEWS.md | 3 +- R/create.R | 171 ------------------------- R/use.R | 23 +--- README.md | 8 +- inst/tinytest/test_create.R | 70 ---------- inst/tinytest/test_use_github_action.R | 34 +++-- inst/tinytest/test_use_version.R | 65 ++++++---- man/create_package.Rd | 52 -------- man/use_github_action.Rd | 9 +- man/use_version.Rd | 12 +- 11 files changed, 76 insertions(+), 372 deletions(-) delete mode 100644 R/create.R delete mode 100644 inst/tinytest/test_create.R delete mode 100644 man/create_package.Rd diff --git a/NAMESPACE b/NAMESPACE index 0f6a290..2d38652 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,7 +3,6 @@ export(build) export(check) export(check_win_devel) -export(create_package) export(install) export(load_all) export(maintainer) diff --git a/NEWS.md b/NEWS.md index 3d4b74c..eb250dc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,6 @@ # tinypkgr 0.2.0 -* New `create_package()` for scaffolding a tinyverse-flavored R package: DESCRIPTION with Authors@R, NAMESPACE, .Rbuildignore, NEWS.md, tests/tinytest.R entry point, and an optional starter `hello()` function with matching tinytest test. -* New `use_version()` bumps the DESCRIPTION Version field and prepends a matching NEWS.md section header. Supports `patch`, `minor`, `major`, and `dev` bumps. +* New `use_version()` bumps the DESCRIPTION Version field and prepends a matching NEWS.md section header. Supports `patch`, `minor`, `major`, and `dev` bumps. (For package skeleton creation, use `pkgKitten::kitten()`.) * New `use_github_action()` writes a `.github/workflows/ci.yaml` from the r-ci template (Ubuntu + macOS) and adds `^\.github$` to `.Rbuildignore`. # tinypkgr 0.1.0 diff --git a/R/create.R b/R/create.R deleted file mode 100644 index 5b0f41a..0000000 --- a/R/create.R +++ /dev/null @@ -1,171 +0,0 @@ -#' Create a New R Package -#' -#' Scaffolds a minimal tinyverse-flavored R package: DESCRIPTION with Authors@R, -#' NAMESPACE, .Rbuildignore, NEWS.md, a tinytest entry point, and (optionally) -#' a starter function plus matching test. -#' -#' @param name Package name. Must start with a letter and contain only letters, -#' numbers, and dots. -#' @param path Directory in which to create the package. The package itself -#' will be created at `file.path(path, name)`. Default is the current -#' directory. -#' @param title One-line title in title case (no period). -#' @param description Paragraph describing what the package does. -#' @param author Author full name (e.g., "First Last"). -#' @param email Author email address. Also used as the maintainer. -#' @param orcid Optional ORCID identifier (e.g., "0000-0000-0000-0000"). If -#' supplied, added as `comment = c(ORCID = ...)` in Authors@R. -#' @param license License string for the License field. Default "GPL-3". -#' @param example_fn If TRUE (default), write a starter `R/hello.R` with a -#' tinyrox-documented `hello()` function and a matching tinytest test. -#' -#' @return Path to the created package directory (invisibly). -#' -#' @export -#' -#' @examples -#' \donttest{ -#' tmp <- tempfile("tinypkgr_create_") -#' dir.create(tmp) -#' create_package("mypkg", path = tmp, -#' author = "First Last", -#' email = "you@example.com") -#' unlink(tmp, recursive = TRUE) -#' } -create_package <- function(name, path = ".", - title = "What The Package Does (One Line, Title Case)", - description = "A paragraph describing what the package does.", - author, email, orcid = NULL, license = "GPL-3", - example_fn = TRUE) { - if (missing(name) || !is.character(name) || length(name) != 1) { - stop("'name' must be a single character string", call. = FALSE) - } - if (!valid_pkg_name(name)) { - stop("Invalid package name: ", name, - ". Must start with a letter and contain only letters, numbers, and dots.", - call. = FALSE) - } - if (missing(author) || !is.character(author) || length(author) != 1) { - stop("'author' must be a single character string", call. = FALSE) - } - if (missing(email) || !is.character(email) || length(email) != 1) { - stop("'email' must be a single character string", call. = FALSE) - } - - path <- normalizePath(path, mustWork = TRUE) - pkg_dir <- file.path(path, name) - if (file.exists(pkg_dir)) { - stop("Directory already exists: ", pkg_dir, call. = FALSE) - } - - dir.create(pkg_dir) - dir.create(file.path(pkg_dir, "R")) - dir.create(file.path(pkg_dir, "man")) - dir.create(file.path(pkg_dir, "tests")) - dir.create(file.path(pkg_dir, "inst", "tinytest"), recursive = TRUE) - - # DESCRIPTION - authors_r <- build_authors_r(author, email, orcid) - desc_lines <- c( - paste0("Package: ", name), - paste0("Title: ", title), - "Version: 0.0.0.9000", - "Authors@R:", - paste0(" ", authors_r), - paste0("Description: ", description), - paste0("License: ", license), - "Encoding: UTF-8", - "Suggests:", - " tinytest" - ) - writeLines(desc_lines, file.path(pkg_dir, "DESCRIPTION")) - - # NAMESPACE - ns_lines <- "# Generated by tinyrox: do not edit by hand" - if (example_fn) { - ns_lines <- c(ns_lines, "", "export(hello)") - } - writeLines(ns_lines, file.path(pkg_dir, "NAMESPACE")) - - # .Rbuildignore - writeLines(c( - "^\\.github$", - "^CLAUDE\\.md$", - "^cran-comments\\.md$" - ), file.path(pkg_dir, ".Rbuildignore")) - - # NEWS.md - writeLines(c( - paste0("# ", name, " 0.0.0.9000"), - "", - "* Initial development version." - ), file.path(pkg_dir, "NEWS.md")) - - # tests/tinytest.R entry point - writeLines(c( - "if (requireNamespace(\"tinytest\", quietly = TRUE)) {", - paste0(" tinytest::test_package(\"", name, "\")"), - "}" - ), file.path(pkg_dir, "tests", "tinytest.R")) - - # Optional starter function and test - if (example_fn) { - writeLines(c( - "#' Say Hello", - "#'", - "#' Returns a greeting.", - "#'", - "#' @param who Name to greet.", - "#'", - "#' @return A character string.", - "#'", - "#' @export", - "#'", - "#' @examples", - "#' hello(\"world\")", - "hello <- function(who = \"world\") {", - " paste0(\"Hello, \", who, \"!\")", - "}" - ), file.path(pkg_dir, "R", "hello.R")) - - writeLines(c( - paste0("expect_equal(", name, - "::hello(\"world\"), \"Hello, world!\")"), - paste0("expect_equal(", name, "::hello(\"R\"), \"Hello, R!\")") - ), file.path(pkg_dir, "inst", "tinytest", "test_hello.R")) - } - - message("Created package '", name, "' at ", pkg_dir) - invisible(pkg_dir) -} - -# Validate a package name against R's naming rules. -valid_pkg_name <- function(name) { - grepl("^[a-zA-Z][a-zA-Z0-9.]*[a-zA-Z0-9]$", name) -} - -# Build an Authors@R person() call as a single-line string. -build_authors_r <- function(author, email, orcid) { - parts <- strsplit(trimws(author), "\\s+")[[1]] - if (length(parts) >= 2) { - given <- paste(parts[-length(parts)], collapse = " ") - family <- parts[length(parts)] - person_str <- paste0( - "person(\"", given, "\", \"", family, - "\", email = \"", email, - "\", role = c(\"aut\", \"cre\")" - ) - } else { - person_str <- paste0( - "person(given = \"", author, - "\", email = \"", email, - "\", role = c(\"aut\", \"cre\")" - ) - } - if (!is.null(orcid)) { - person_str <- paste0(person_str, - ", comment = c(ORCID = \"", orcid, "\")") - } - paste0(person_str, ")") -} - diff --git a/R/use.R b/R/use.R index 4ed5191..c5a4009 100644 --- a/R/use.R +++ b/R/use.R @@ -5,7 +5,7 @@ #' #' @param which Which component to bump: "patch" (0.2.0 -> 0.2.1), #' "minor" (0.2.0 -> 0.3.0), "major" (0.2.0 -> 1.0.0), or -#' "dev" (0.2.0 -> 0.2.0.9000, or 0.2.0.9000 -> 0.2.0.9001). +#' "dev" (0.2.0 -> 0.2.0.1, or 0.2.0.1 -> 0.2.0.2). #' @param path Path to package root directory. #' #' @return The new version string (invisibly). @@ -13,13 +13,9 @@ #' @export #' #' @examples -#' \donttest{ -#' tmp <- tempfile("tinypkgr_use_") -#' dir.create(tmp) -#' create_package("foo", path = tmp, -#' author = "First Last", email = "f@example.com") -#' use_version("patch", path = file.path(tmp, "foo")) -#' unlink(tmp, recursive = TRUE) +#' \dontrun{ +#' use_version("patch") +#' use_version("dev") #' } use_version <- function(which = c("patch", "minor", "major", "dev"), path = ".") { @@ -69,13 +65,8 @@ use_version <- function(which = c("patch", "minor", "major", "dev"), #' @export #' #' @examples -#' \donttest{ -#' tmp <- tempfile("tinypkgr_use_") -#' dir.create(tmp) -#' create_package("foo", path = tmp, -#' author = "First Last", email = "f@example.com") -#' use_github_action(path = file.path(tmp, "foo")) -#' unlink(tmp, recursive = TRUE) +#' \dontrun{ +#' use_github_action() #' } use_github_action <- function(path = ".") { path <- normalizePath(path, mustWork = TRUE) @@ -143,7 +134,7 @@ bump_version <- function(current, which) { if (length(parts) == 4) { parts[4] <- as.character(as.integer(parts[4]) + 1) } else if (length(parts) == 3) { - parts <- c(parts, "9000") + parts <- c(parts, "1") } else { stop("Cannot bump dev version from: ", current, call. = FALSE) } diff --git a/README.md b/README.md index 354e9aa..1558a36 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,10 @@ remotes::install_github("cornball-ai/tinypkgr") ## Usage -### Create a package +For creating a new package skeleton, use [pkgKitten](https://github.com/eddelbuettel/pkgkitten): ```r -tinypkgr::create_package("mypkg", - author = "First Last", - email = "you@example.com", - orcid = "0000-0000-0000-0000") +pkgKitten::kitten("mypkg") ``` ### Development @@ -58,7 +55,6 @@ submit_cran() | Function | Purpose | |----------|---------| -| `create_package()` | Scaffold a new tinyverse-flavored package | | `use_version()` | Bump DESCRIPTION version + NEWS.md header | | `use_github_action()` | Write `.github/workflows/ci.yaml` (r-ci) | | `install()` | R CMD INSTALL wrapper | diff --git a/inst/tinytest/test_create.R b/inst/tinytest/test_create.R deleted file mode 100644 index 4052a0d..0000000 --- a/inst/tinytest/test_create.R +++ /dev/null @@ -1,70 +0,0 @@ -# Tests for create_package() - -tmp_root <- tempfile("tinypkgr_create_") -dir.create(tmp_root) - -# Basic creation with all defaults + example function -pkg_dir <- tinypkgr::create_package( - "foopkg", - path = tmp_root, - author = "First Last", - email = "first@example.com", - orcid = "0009-0005-4248-604X" -) - -expect_true(dir.exists(pkg_dir)) -expect_true(file.exists(file.path(pkg_dir, "DESCRIPTION"))) -expect_true(file.exists(file.path(pkg_dir, "NAMESPACE"))) -expect_true(file.exists(file.path(pkg_dir, ".Rbuildignore"))) -expect_true(file.exists(file.path(pkg_dir, "NEWS.md"))) -expect_true(file.exists(file.path(pkg_dir, "tests", "tinytest.R"))) -expect_true(file.exists(file.path(pkg_dir, "R", "hello.R"))) -expect_true(file.exists(file.path(pkg_dir, "inst", "tinytest", "test_hello.R"))) -expect_true(dir.exists(file.path(pkg_dir, "man"))) - -# DESCRIPTION parses and has expected fields -desc <- read.dcf(file.path(pkg_dir, "DESCRIPTION")) -expect_equal(unname(desc[1, "Package"]), "foopkg") -expect_equal(unname(desc[1, "Version"]), "0.0.0.9000") -expect_equal(unname(desc[1, "License"]), "GPL-3") -expect_true("Authors@R" %in% colnames(desc)) - -# Authors@R parses and ORCID is included -authors <- eval(parse(text = desc[1, "Authors@R"])) -expect_true(inherits(authors, "person")) -expect_equal(authors$given, "First") -expect_equal(authors$family, "Last") -expect_equal(authors$email, "first@example.com") -expect_equal(unname(authors$comment["ORCID"]), "0009-0005-4248-604X") - -# example_fn = FALSE skips starter files -pkg_dir2 <- tinypkgr::create_package( - "barpkg", - path = tmp_root, - author = "Solo", - email = "solo@example.com", - example_fn = FALSE -) -expect_false(file.exists(file.path(pkg_dir2, "R", "hello.R"))) -expect_false(file.exists(file.path(pkg_dir2, "inst", "tinytest", "test_hello.R"))) - -# Single-name author works -desc2 <- read.dcf(file.path(pkg_dir2, "DESCRIPTION")) -authors2 <- eval(parse(text = desc2[1, "Authors@R"])) -expect_equal(authors2$given, "Solo") -expect_true(is.null(authors2$comment)) - -# Invalid names error -expect_error(tinypkgr::create_package("1pkg", path = tmp_root, - author = "A B", email = "a@b.com")) -expect_error(tinypkgr::create_package("my-pkg", path = tmp_root, - author = "A B", email = "a@b.com")) -expect_error(tinypkgr::create_package(".foo", path = tmp_root, - author = "A B", email = "a@b.com")) - -# Existing directory errors -expect_error(tinypkgr::create_package("foopkg", path = tmp_root, - author = "A B", email = "a@b.com")) - -# Cleanup -unlink(tmp_root, recursive = TRUE) diff --git a/inst/tinytest/test_use_github_action.R b/inst/tinytest/test_use_github_action.R index f76df8a..c49e6f1 100644 --- a/inst/tinytest/test_use_github_action.R +++ b/inst/tinytest/test_use_github_action.R @@ -1,11 +1,20 @@ # Tests for use_github_action() -tmp <- tempfile("tinypkgr_useghaction_") -dir.create(tmp) -pkg <- tinypkgr::create_package("ghapkg", path = tmp, - author = "A B", email = "a@b.com") +# Inline minimal scaffold (no create_package dependency) +tmp_pkg <- file.path(tempdir(), "ghapkg") +if (dir.exists(tmp_pkg)) unlink(tmp_pkg, recursive = TRUE) +dir.create(tmp_pkg) +writeLines(c( + "Package: ghapkg", + "Title: Test", + "Version: 0.0.1", + "Authors@R: person('A', 'B', email = 'a@b.com', role = c('aut', 'cre'))", + "Description: Test.", + "License: GPL-3" +), file.path(tmp_pkg, "DESCRIPTION")) +writeLines("^cran-comments\\.md$", file.path(tmp_pkg, ".Rbuildignore")) -yaml <- tinypkgr::use_github_action(path = pkg) +yaml <- tinypkgr::use_github_action(path = tmp_pkg) expect_true(file.exists(yaml)) expect_equal(basename(yaml), "ci.yaml") expect_equal(basename(dirname(yaml)), "workflows") @@ -16,21 +25,24 @@ expect_true(any(grepl("eddelbuettel/github-actions/r-ci@master", content))) expect_true(any(grepl("macos-latest", content))) expect_true(any(grepl("ubuntu-latest", content))) -# .Rbuildignore already had ^\.github$ from create_package, no duplication -rbi <- readLines(file.path(pkg, ".Rbuildignore")) +# .Rbuildignore got the .github entry appended +rbi <- readLines(file.path(tmp_pkg, ".Rbuildignore")) +expect_true("^\\.github$" %in% rbi) expect_equal(sum(rbi == "^\\.github$"), 1) # Calling again errors -expect_error(tinypkgr::use_github_action(path = pkg)) +expect_error(tinypkgr::use_github_action(path = tmp_pkg)) # Works on a package without an .Rbuildignore -bare <- file.path(tmp, "bare") +bare <- file.path(tempdir(), "bare_for_ghaction") +if (dir.exists(bare)) unlink(bare, recursive = TRUE) dir.create(bare) -writeLines(c("Package: bare", "Version: 0.0.0.9000", +writeLines(c("Package: bare", "Version: 0.0.0.1", "Title: Bare", "Description: x.", "License: GPL-3"), file.path(bare, "DESCRIPTION")) tinypkgr::use_github_action(path = bare) expect_true(file.exists(file.path(bare, ".Rbuildignore"))) expect_true("^\\.github$" %in% readLines(file.path(bare, ".Rbuildignore"))) -unlink(tmp, recursive = TRUE) +unlink(tmp_pkg, recursive = TRUE) +unlink(bare, recursive = TRUE) diff --git a/inst/tinytest/test_use_version.R b/inst/tinytest/test_use_version.R index 0c4921a..9442b09 100644 --- a/inst/tinytest/test_use_version.R +++ b/inst/tinytest/test_use_version.R @@ -5,56 +5,65 @@ bump <- tinypkgr:::bump_version expect_equal(bump("0.2.0", "patch"), "0.2.1") expect_equal(bump("0.2.0", "minor"), "0.3.0") expect_equal(bump("0.2.0", "major"), "1.0.0") -expect_equal(bump("0.2.0", "dev"), "0.2.0.9000") -expect_equal(bump("0.2.0.9000", "dev"), "0.2.0.9001") -expect_equal(bump("0.2.0.9001", "dev"), "0.2.0.9002") +expect_equal(bump("0.2.0", "dev"), "0.2.0.1") +expect_equal(bump("0.2.0.1", "dev"), "0.2.0.2") +expect_equal(bump("0.2.0.2", "dev"), "0.2.0.3") # Release bump from a dev version strips the dev suffix -expect_equal(bump("0.2.0.9001", "patch"), "0.2.1") -expect_equal(bump("0.2.0.9001", "minor"), "0.3.0") -expect_equal(bump("0.2.0.9001", "major"), "1.0.0") +expect_equal(bump("0.2.0.5", "patch"), "0.2.1") +expect_equal(bump("0.2.0.5", "minor"), "0.3.0") +expect_equal(bump("0.2.0.5", "major"), "1.0.0") # Garbage expect_error(bump("foo", "patch")) expect_error(bump("0.2", "patch")) -# End-to-end on a temp package -tmp <- tempfile("tinypkgr_useversion_") -dir.create(tmp) -pkg <- tinypkgr::create_package("vpkg", path = tmp, - author = "A B", email = "a@b.com") -desc_file <- file.path(pkg, "DESCRIPTION") +# End-to-end on a temp package (inline minimal scaffold) +tmp_pkg <- file.path(tempdir(), "vpkg") +if (dir.exists(tmp_pkg)) unlink(tmp_pkg, recursive = TRUE) +dir.create(tmp_pkg) +writeLines(c( + "Package: vpkg", + "Title: Test Package", + "Version: 0.1.0", + "Authors@R: person('A', 'B', email = 'a@b.com', role = c('aut', 'cre'))", + "Description: Test.", + "License: GPL-3" +), file.path(tmp_pkg, "DESCRIPTION")) +writeLines(c("# vpkg 0.1.0", "", "* Initial."), + file.path(tmp_pkg, "NEWS.md")) -# Set a known release version to test from -desc_lines <- readLines(desc_file) -desc_lines[grep("^Version:", desc_lines)] <- "Version: 0.1.0" -writeLines(desc_lines, desc_file) +desc_file <- file.path(tmp_pkg, "DESCRIPTION") # Patch bump updates DESCRIPTION and NEWS.md -v <- tinypkgr::use_version("patch", path = pkg) +v <- tinypkgr::use_version("patch", path = tmp_pkg) expect_equal(v, "0.1.1") expect_equal(unname(read.dcf(desc_file)[1, "Version"]), "0.1.1") -news <- readLines(file.path(pkg, "NEWS.md")) +news <- readLines(file.path(tmp_pkg, "NEWS.md")) expect_true(grepl("^# vpkg 0.1.1", news[1])) # Minor bump -v <- tinypkgr::use_version("minor", path = pkg) +v <- tinypkgr::use_version("minor", path = tmp_pkg) expect_equal(v, "0.2.0") # Major bump -v <- tinypkgr::use_version("major", path = pkg) +v <- tinypkgr::use_version("major", path = tmp_pkg) expect_equal(v, "1.0.0") # Dev bump does NOT touch NEWS.md -news_before <- readLines(file.path(pkg, "NEWS.md")) -v <- tinypkgr::use_version("dev", path = pkg) -expect_equal(v, "1.0.0.9000") -news_after <- readLines(file.path(pkg, "NEWS.md")) +news_before <- readLines(file.path(tmp_pkg, "NEWS.md")) +v <- tinypkgr::use_version("dev", path = tmp_pkg) +expect_equal(v, "1.0.0.1") +news_after <- readLines(file.path(tmp_pkg, "NEWS.md")) expect_equal(news_before, news_after) # Successive dev bumps -v <- tinypkgr::use_version("dev", path = pkg) -expect_equal(v, "1.0.0.9001") +v <- tinypkgr::use_version("dev", path = tmp_pkg) +expect_equal(v, "1.0.0.2") # Missing DESCRIPTION -expect_error(tinypkgr::use_version("patch", path = tempdir())) +empty_dir <- file.path(tempdir(), "empty_for_use_version") +if (dir.exists(empty_dir)) unlink(empty_dir, recursive = TRUE) +dir.create(empty_dir) +expect_error(tinypkgr::use_version("patch", path = empty_dir)) -unlink(tmp, recursive = TRUE) +unlink(tmp_pkg, recursive = TRUE) +unlink(empty_dir, recursive = TRUE) diff --git a/man/create_package.Rd b/man/create_package.Rd deleted file mode 100644 index 2ed7fec..0000000 --- a/man/create_package.Rd +++ /dev/null @@ -1,52 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{create_package} -\alias{create_package} -\title{Create a New R Package} -\usage{ -create_package(name, path = ".", - title = "What The Package Does (One Line, Title Case)", - description = "A paragraph describing what the package does.", - author, email, orcid = NULL, license = "GPL-3", example_fn = TRUE) -} -\arguments{ -\item{name}{Package name. Must start with a letter and contain only letters, -numbers, and dots.} - -\item{path}{Directory in which to create the package. The package itself -will be created at `file.path(path, name)`. Default is the current -directory.} - -\item{title}{One-line title in title case (no period).} - -\item{description}{Paragraph describing what the package does.} - -\item{author}{Author full name (e.g., "First Last").} - -\item{email}{Author email address. Also used as the maintainer.} - -\item{orcid}{Optional ORCID identifier (e.g., "0000-0000-0000-0000"). If -supplied, added as `comment = c(ORCID = ...)` in Authors@R.} - -\item{license}{License string for the License field. Default "GPL-3".} - -\item{example_fn}{If TRUE (default), write a starter `R/hello.R` with a -tinyrox-documented `hello()` function and a matching tinytest test.} -} -\value{ -Path to the created package directory (invisibly). -} -\description{ -Scaffolds a minimal tinyverse-flavored R package: DESCRIPTION with Authors@R, -NAMESPACE, .Rbuildignore, NEWS.md, a tinytest entry point, and (optionally) -a starter function plus matching test. -} -\examples{ -\donttest{ -tmp <- tempfile("tinypkgr_create_") -dir.create(tmp) -create_package("mypkg", path = tmp, - author = "First Last", - email = "you@example.com") -unlink(tmp, recursive = TRUE) -} -} diff --git a/man/use_github_action.Rd b/man/use_github_action.Rd index bee86d1..bffa56f 100644 --- a/man/use_github_action.Rd +++ b/man/use_github_action.Rd @@ -17,12 +17,7 @@ macOS, via `eddelbuettel/github-actions/r-ci@master`). Adds `^\.github$` to `.Rbuildignore` if not already present. } \examples{ -\donttest{ -tmp <- tempfile("tinypkgr_use_") -dir.create(tmp) -create_package("foo", path = tmp, - author = "First Last", email = "f@example.com") -use_github_action(path = file.path(tmp, "foo")) -unlink(tmp, recursive = TRUE) +\dontrun{ +use_github_action() } } diff --git a/man/use_version.Rd b/man/use_version.Rd index 0cd4906..4a3991d 100644 --- a/man/use_version.Rd +++ b/man/use_version.Rd @@ -8,7 +8,7 @@ use_version(which = c("patch", "minor", "major", "dev"), path = ".") \arguments{ \item{which}{Which component to bump: "patch" (0.2.0 -> 0.2.1), "minor" (0.2.0 -> 0.3.0), "major" (0.2.0 -> 1.0.0), or -"dev" (0.2.0 -> 0.2.0.9000, or 0.2.0.9000 -> 0.2.0.9001).} +"dev" (0.2.0 -> 0.2.0.1, or 0.2.0.1 -> 0.2.0.2).} \item{path}{Path to package root directory.} } @@ -20,12 +20,8 @@ Increments the Version field in DESCRIPTION and prepends a new section header to NEWS.md (if present) so the two never drift apart. } \examples{ -\donttest{ -tmp <- tempfile("tinypkgr_use_") -dir.create(tmp) -create_package("foo", path = tmp, - author = "First Last", email = "f@example.com") -use_version("patch", path = file.path(tmp, "foo")) -unlink(tmp, recursive = TRUE) +\dontrun{ +use_version("patch") +use_version("dev") } } From 371dedef741379ea1cad0be764acfd7dbb3e070b Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Tue, 7 Apr 2026 14:12:10 -0500 Subject: [PATCH 6/6] Refactor load_all() to not modify the search path load_all() now returns the populated environment and takes an optional env arg so callers can supply their own target. It no longer calls attach(), so tinypkgr's source is free of search-path side effects and R CMD check --as-cran is clean (0/0/0 aside from the new-submission NOTE). Users who want the previous auto-attach behavior can do it themselves: attach(tinypkgr::load_all(), name = "tinypkgr:mypkg") --- R/dev.R | 41 +++++++++++++++++++--------------------- cran-comments.md | 5 +---- inst/tinytest/test_dev.R | 20 ++++++++++++-------- man/load_all.Rd | 20 ++++++++++++++++---- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/R/dev.R b/R/dev.R index 16fd29d..4cac0d6 100644 --- a/R/dev.R +++ b/R/dev.R @@ -163,18 +163,29 @@ install <- function(path = ".", quiet = TRUE) { #' without requiring a full install. #' #' @param path Path to package root directory. +#' @param env Environment to source files into. Defaults to a fresh +#' environment whose parent is the global environment. #' @param quiet Logical. Suppress file sourcing messages? Default TRUE. #' -#' @return Character vector of sourced files (invisibly). +#' @return The environment into which files were sourced (invisibly). +#' Does not modify the search path. If you want search-path behavior, +#' call `attach()` yourself: +#' \preformatted{attach(tinypkgr::load_all(), name = "tinypkgr:mypkg")} +#' +#' @seealso \code{\link[pkgKitten]{kitten}} for scaffolding a new package. #' #' @export #' #' @examples #' \dontrun{ -#' load_all() -#' load_all(quiet = FALSE) # Show each file being sourced +#' e <- load_all() +#' e$my_function(123) +#' +#' # Or attach to the search path explicitly: +#' attach(load_all(), name = "tinypkgr:mypkg") #' } -load_all <- function(path = ".", quiet = TRUE) { +load_all <- function(path = ".", env = new.env(parent = globalenv()), + quiet = TRUE) { r_dir <- file.path(path, "R") if (!dir.exists(r_dir)) { @@ -185,32 +196,18 @@ load_all <- function(path = ".", quiet = TRUE) { if (length(r_files) == 0) { message("No R files found.") - return(invisible(character())) + return(invisible(env)) } - # Create a new environment attached to the search path - pkg_env <- new.env(parent = globalenv()) - for (f in r_files) { if (!quiet) { message("Sourcing ", basename(f)) } - source(f, local = pkg_env) + source(f, local = env) } - # Attach the environment - # Use a name that won't conflict - env_name <- paste0("tinypkgr:", basename(normalizePath(path))) - - # Detach if already attached - if (env_name %in% search()) { - detach(env_name, character.only = TRUE) - } - - attach(pkg_env, name = env_name) - - message("Loaded ", length(r_files), " file(s) as '", env_name, "'") - invisible(r_files) + message("Loaded ", length(r_files), " file(s)") + invisible(env) } #' Reload an Installed Package diff --git a/cran-comments.md b/cran-comments.md index 8887b4d..44b5988 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,11 +1,8 @@ ## R CMD check results -0 errors | 0 warnings | 2 notes +0 errors | 0 warnings | 1 note * This is a new submission. -* `load_all()` uses `attach()` deliberately to expose package symbols on the - search path for interactive development, mirroring `devtools::load_all()`. - This is intentional and documented. ## Test environments diff --git a/inst/tinytest/test_dev.R b/inst/tinytest/test_dev.R index e5d074b..54cfeca 100644 --- a/inst/tinytest/test_dev.R +++ b/inst/tinytest/test_dev.R @@ -25,15 +25,19 @@ if (at_home()) { expect_true(result) } -# Test load_all -files <- tinypkgr::load_all(tmp_pkg, quiet = TRUE) -expect_equal(length(files), 1) -expect_true(any(grepl("tinypkgr:testpkg", search()))) +# load_all returns an environment populated with sourced functions, +# without touching the search path. +e <- tinypkgr::load_all(tmp_pkg, quiet = TRUE) +expect_true(is.environment(e)) +expect_true("add" %in% ls(e)) +expect_equal(e$add(2, 3), 5) +expect_false(any(grepl("testpkg", search()))) -# Clean up search path -if ("tinypkgr:testpkg" %in% search()) { - detach("tinypkgr:testpkg", character.only = TRUE) -} +# Caller may supply their own env +target <- new.env() +e2 <- tinypkgr::load_all(tmp_pkg, env = target, quiet = TRUE) +expect_identical(e2, target) +expect_true("add" %in% ls(target)) # Clean up temp package unlink(tmp_pkg, recursive = TRUE) diff --git a/man/load_all.Rd b/man/load_all.Rd index 0c6fe35..5741f6f 100644 --- a/man/load_all.Rd +++ b/man/load_all.Rd @@ -3,15 +3,21 @@ \alias{load_all} \title{Load All Package Code} \usage{ -load_all(path = ".", quiet = TRUE) +load_all(path = ".", env = new.env(parent = globalenv()), quiet = TRUE) } \arguments{ \item{path}{Path to package root directory.} +\item{env}{Environment to source files into. Defaults to a fresh +environment whose parent is the global environment.} + \item{quiet}{Logical. Suppress file sourcing messages? Default TRUE.} } \value{ -Character vector of sourced files (invisibly). +The environment into which files were sourced (invisibly). + Does not modify the search path. If you want search-path behavior, + call `attach()` yourself: + \preformatted{attach(tinypkgr::load_all(), name = "tinypkgr:mypkg")} } \description{ Sources all R files in a package for interactive development, @@ -19,7 +25,13 @@ without requiring a full install. } \examples{ \dontrun{ -load_all() -load_all(quiet = FALSE) # Show each file being sourced +e <- load_all() +e$my_function(123) + +# Or attach to the search path explicitly: +attach(load_all(), name = "tinypkgr:mypkg") +} } +\seealso{ +\code{\link[pkgKitten]{kitten}} for scaffolding a new package. }