Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addressing ropensci initial review check #321

Merged
merged 8 commits into from
Aug 30, 2023
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@
^codecov\.yml$
^cran-comments\.md$
^CRAN-SUBMISSION$
^codemeta\.json$
4 changes: 0 additions & 4 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by roxygen2: do not edit by hand

S3method(.export,default)
S3method(.export,rio_arff)
S3method(.export,rio_clipboard)
S3method(.export,rio_csv)
Expand Down Expand Up @@ -32,7 +31,6 @@ S3method(.export,rio_xml)
S3method(.export,rio_xpt)
S3method(.export,rio_yml)
S3method(.export,rio_zsav)
S3method(.import,default)
S3method(.import,rio_arff)
S3method(.import,rio_clipboard)
S3method(.import,rio_csv)
Expand Down Expand Up @@ -77,8 +75,6 @@ S3method(characterize,data.frame)
S3method(characterize,default)
S3method(factorize,data.frame)
S3method(factorize,default)
export(.export)
export(.import)
export(characterize)
export(convert)
export(export)
Expand Down
233 changes: 116 additions & 117 deletions R/arg_reconcile.R
Original file line number Diff line number Diff line change
@@ -1,124 +1,123 @@
#' @title Reconcile an argument list to any function signature.
#'
#' @description Adapt an argument list to a function excluding arguments that
#' will not be recognized by it, redundant arguments, and un-named
#' arguments.
#'
#' @param fun A function to which an argument list needs to be adapted. Use
#' the unquoted name of the function. If it's in a different
#' package then the fully qualified unquoted name (e.g.
#' \code{utils::read.table})
#' @param ... An arbitrary list of named arguments (unnamed ones will be
#' ignored). Arguments in \code{.args} are overridden by
#' arguments of the same name (if any) in \code{...}
#' @param .args A list or \code{alist} of named arguments, to be merged
#' with \code{...}. Arguments in \code{.args} are overridden by
#' arguments of the same name (if any) in \code{...}
#' @param .docall If set to \code{TRUE} will not only clean up the arguments
#' but also execute \code{fun} with those arguments
#' (\code{FALSE} by default) and return the results
#' @param .include Whitelist. If not empty, only arguments named here will be
#' permitted, and only if they satisfy the conditions implied by
#' the other arguments. Evaluated before \code{.remap}.
#' @param .exclude Blacklist. If not empty, arguments named here will be removed
#' even if they satisfy the conditions implied by the other
#' arguments. Evaluated before \code{.remap}.
#' @param .remap An optional named character vector or named list of character
#' values for standardizing arguments that play the same role
#' but have different names in different functions. Evaluated
#' after \code{.exclude} and \code{.include}.
#' @param .warn Whether to issue a warning message (default) when invalid
#' arguments need to be discarded.
#' @param .error If specified, should be the object to return in the event of
#' error. This object will have the error as its
#' \code{error} attribute. If not specified an ordinary error is
#' thrown with an added hint on the documentation to read for
#' troubleshooting. Ignored if \code{.docall} is \code{FALSE}.
#' The point of doing this is fault-tolerance-- if this function
#' is part of a lengthy process where you want to document an
#' error but keep going, you can set \code{.error} to some
#' object of a compatible type. That object will be returned in
#' the event of error and will have as its \code{"error"}
#' attribute the error object.
#' @param .finish A function to run on the result before returning it. Ignored
#' if \code{.docall} is \code{FALSE}.
#'
#' @return Either a named list or the result of calling \code{fun} with the
#' supplied arguments
#'
arg_reconcile <- function(fun, ..., .args = alist(), .docall = FALSE,
.include = c(), .exclude= c(), .remap = list(),
## @title Reconcile an argument list to any function signature.
##
## @description Adapt an argument list to a function excluding arguments that
## will not be recognized by it, redundant arguments, and un-named
## arguments.
##
## @param fun A function to which an argument list needs to be adapted. Use
## the unquoted name of the function. If it's in a different
## package then the fully qualified unquoted name (e.g.
## \code{utils::read.table})
## @param ... An arbitrary list of named arguments (unnamed ones will be
## ignored). Arguments in \code{.args} are overridden by
## arguments of the same name (if any) in \code{...}
## @param .args A list or \code{alist} of named arguments, to be merged
## with \code{...}. Arguments in \code{.args} are overridden by
## arguments of the same name (if any) in \code{...}
## @param .docall If set to \code{TRUE} will not only clean up the arguments
## but also execute \code{fun} with those arguments
## (\code{FALSE} by default) and return the results
## @param .include Whitelist. If not empty, only arguments named here will be
## permitted, and only if they satisfy the conditions implied by
## the other arguments. Evaluated before \code{.remap}.
## @param .exclude Blacklist. If not empty, arguments named here will be removed
## even if they satisfy the conditions implied by the other
## arguments. Evaluated before \code{.remap}.
## @param .remap An optional named character vector or named list of character
## values for standardizing arguments that play the same role
## but have different names in different functions. Evaluated
## after \code{.exclude} and \code{.include}.
## @param .warn Whether to issue a warning message (default) when invalid
## arguments need to be discarded.
## @param .error If specified, should be the object to return in the event of
## error. This object will have the error as its
## \code{error} attribute. If not specified an ordinary error is
## thrown with an added hint on the documentation to read for
## troubleshooting. Ignored if \code{.docall} is \code{FALSE}.
## The point of doing this is fault-tolerance-- if this function
## is part of a lengthy process where you want to document an
## error but keep going, you can set \code{.error} to some
## object of a compatible type. That object will be returned in
## the event of error and will have as its \code{"error"}
## attribute the error object.
## @param .finish A function to run on the result before returning it. Ignored
## if \code{.docall} is \code{FALSE}.
##
## @return Either a named list or the result of calling \code{fun} with the
## supplied arguments
##
arg_reconcile <- function(fun, ..., .args = alist(), .docall = FALSE,
.include = c(), .exclude= c(), .remap = list(),
.warn = TRUE, .error = "default", .finish = identity) {
# capture the formal arguments of the target function
frmls <- formals(fun)
# both freeform and an explicit list
args <- match.call(expand.dots = FALSE)[["..."]]
if (isTRUE(.docall)) {
for (ii in names(args)) {
try(args[[ii]] <- eval(args[[ii]], parent.frame()))
## capture the formal arguments of the target function
frmls <- formals(fun)
## both freeform and an explicit list
args <- match.call(expand.dots = FALSE)[["..."]]
if (isTRUE(.docall)) {
for (ii in names(args)) {
try(args[[ii]] <- eval(args[[ii]], parent.frame()))
}
}
}
# get rid of duplicate arguments, with freeform arguments
dupes <- names(args)[duplicated(names(args))]
for (ii in dupes) {
args[which(names(args) == ii)[-1]] <- NULL
}
# Merge ... with .args
args <- c(args, .args)
# Apply whitelist and blacklist. This step also removes duplicates _between_
# the freeform (...) and pre-specified (.args) arguments, with ... versions
# taking precedence over the .args versions. This is a consequence of the
# intersect() and setdiff() operations and works even if there is no blacklist
# nor whitelist
if (!missing(.include)) {
args <- args[intersect(names(args), .include)]
}
args <- args[setdiff(names(args), .exclude)]
# if any remappings of one argument to another are specified, perform them
for (ii in names(.remap)) {
if (!.remap[[ii]] %in% names(args) && ii %in% names(args)) {
args[[.remap[[ii]] ]] <- args[[ii]]
## get rid of duplicate arguments, with freeform arguments
dupes <- names(args)[duplicated(names(args))]
for (ii in dupes) {
args[which(names(args) == ii)[-1]] <- NULL
}
}
# remove any unnamed arguments
args[names(args) == ""] <- NULL
# if the target function doesn't have "..." as an argument, check to make sure
# only recognized arguments get passed, optionally with a warning
if (!"..." %in% names(frmls)) {
unused <- setdiff(names(args), names(frmls))
if (length(unused)>0){
if (isTRUE(.warn)) {
warning("The following arguments were ignored for ",
deparse(substitute(fun)), ":\n", paste(unused, collapse = ", "))
}
args <- args[intersect(names(args), names(frmls))]
## Merge ... with .args
args <- c(args, .args)
## Apply whitelist and blacklist. This step also removes duplicates _between_
## the freeform (...) and pre-specified (.args) arguments, with ... versions
## taking precedence over the .args versions. This is a consequence of the
## intersect() and setdiff() operations and works even if there is no blacklist
## nor whitelist
if (!missing(.include)) {
args <- args[intersect(names(args), .include)]
}
}
# the final, cleaned-up arguments either get returned as a list or used on the
# function, depending on how .docall is set
if (!isTRUE(.docall)) {
return(args)
} else {
# run the function and return the result case
oo <- try(do.call(fun, args), silent = TRUE)
if (!inherits(oo, "try-error")) {
return(.finish(oo))
args <- args[setdiff(names(args), .exclude)]
## if any remappings of one argument to another are specified, perform them
for (ii in names(.remap)) {
if (!.remap[[ii]] %in% names(args) && ii %in% names(args)) {
args[[.remap[[ii]] ]] <- args[[ii]]
}
}
## remove any unnamed arguments
args[names(args) == ""] <- NULL
## if the target function doesn't have "..." as an argument, check to make sure
## only recognized arguments get passed, optionally with a warning
if (!"..." %in% names(frmls)) {
unused <- setdiff(names(args), names(frmls))
if (length(unused)>0) {
if (isTRUE(.warn)) {
warning("The following arguments were ignored for ",
deparse(substitute(fun)), ":\n", paste(unused, collapse = ", "))
}
args <- args[intersect(names(args), names(frmls))]
}
}
## the final, cleaned-up arguments either get returned as a list or used on the
## function, depending on how .docall is set
if (!isTRUE(.docall)) {
return(args)
} else {
# construct an informative error... eventually there will be more
# detailed info here
errorhint <- paste('\nThis error was generated by: ',
deparse(match.call()$fun),
'\nWith the following arguments:\n',
gsub('^list\\(|\\)$', '',
paste(deparse(args, control=c('delayPromises')),
collapse='\n')))
if (missing(.error)) {
stop(attr(oo, "condition")$message, errorhint)
} else {
attr(.error, "error") <- oo
return(.error)
}
## run the function and return the result case
oo <- try(do.call(fun, args), silent = TRUE)
if (!inherits(oo, "try-error")) {
return(.finish(oo))
} else {
## construct an informative error... eventually there will be more
## detailed info here
errorhint <- paste("\nThis error was generated by: ",
deparse(match.call()$fun),
"\nWith the following arguments:\n",
gsub("^list\\(|\\)$", "",
paste(deparse(args, control=c("delayPromises")),
collapse = "\n")))
if (missing(.error)) {
stop(attr(oo, "condition")$message, errorhint)
} else {
attr(.error, "error") <- oo
return(.error)
}
}
}
}
}

9 changes: 5 additions & 4 deletions R/characterize.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@
#' @param coerce_character A logical indicating whether to additionally coerce character columns to factor (in \code{factorize}). Default \code{FALSE}.
#' @param \dots additional arguments passed to methods
#' @details \code{characterize} converts a vector with a \code{labels} attribute of named levels into a character vector. \code{factorize} does the same but to factors. This can be useful at two stages of a data workflow: (1) importing labelled data from metadata-rich file formats (e.g., Stata or SPSS), and (2) exporting such data to plain text files (e.g., CSV) in a way that preserves information.
#' @return a character vector (for \code{characterize}) or factor vector (for \code{factorize})
#' @examples
#' # vector method
#' x <- structure(1:4, labels = c("A" = 1, "B" = 2, "C" = 3))
#' characterize(x)
#' factorize(x)
#'
#'
#' # data frame method
#' x <- data.frame(v1 = structure(1:4, labels = c("A" = 1, "B" = 2, "C" = 3)),
#' v2 = structure(c(1,0,0,1), labels = c("foo" = 0, "bar" = 1)))
#' str(factorize(x))
#' str(characterize(x))
#'
#'
#' # comparison of exported file contents
#' import(export(x, csv_file <- tempfile(fileext = ".csv")))
#' import(export(factorize(x), csv_file))
#'
#'
#' # cleanup
#' unlink(csv_file)
#'
#'
#' @seealso \code{\link{gather_attrs}}
#' @export
characterize <- function(x, ...) {
Expand Down
20 changes: 10 additions & 10 deletions R/export.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#' @param \dots Additional arguments for the underlying export functions. This can be used to specify non-standard arguments. See examples.
#' @return The name of the output file as a character string (invisibly).
#' @details This function exports a data frame or matrix into a file with file format based on the file extension (or the manually specified format, if \code{format} is specified).
#'
#'
#' The output file can be to a compressed directory, simply by adding an appropriate additional extensiont to the \code{file} argument, such as: \dQuote{mtcars.csv.tar}, \dQuote{mtcars.csv.zip}, or \dQuote{mtcars.csv.gz}.
#'
#' \code{export} supports many file formats. See the documentation for the underlying export functions for optional arguments that can be passed via \code{...}
Expand All @@ -33,7 +33,7 @@
#' \item Apache Arrow Parquet (.parquet), using \code{\link[arrow]{write_parquet}}
#' \item Feather R/Python interchange format (.feather), using \code{\link[feather]{write_feather}}
#' \item Fast storage (.fst), using \code{\link[fst]{write.fst}}
#' \item JSON (.json), using \code{\link[jsonlite]{toJSON}}. In this case, \code{x} can be a variety of R objects, based on class mapping conventions in this paper: \href{https://arxiv.org/abs/1403.2805}{https://arxiv.org/abs/1403.2805}.
#' \item JSON (.json), using \code{\link[jsonlite]{toJSON}}. In this case, \code{x} can be a variety of R objects, based on class mapping conventions in this paper: \href{https://arxiv.org/abs/1403.2805}{https://arxiv.org/abs/1403.2805}.
#' \item Matlab (.mat), using \code{\link[rmatio]{write.mat}}
#' \item OpenDocument Spreadsheet (.ods), using \code{\link[readODS]{write_ods}}. (Currently only single-sheet exports are supported.)
#' \item HTML (.html), using a custom method based on \code{\link[xml2]{xml_add_child}} to create a simple HTML table and \code{\link[xml2]{write_xml}} to write to disk.
Expand All @@ -43,9 +43,9 @@
#' }
#'
#' When exporting a data set that contains label attributes (e.g., if imported from an SPSS or Stata file) to a plain text file, \code{\link{characterize}} can be a useful pre-processing step that records value labels into the resulting file (e.g., \code{export(characterize(x), "file.csv")}) rather than the numeric values.
#'
#'
#' Use \code{\link{export_list}} to export a list of dataframes to separate files.
#'
#'
#' @examples
#' library("datasets")
#' # specify only `file` argument
Expand All @@ -61,7 +61,7 @@
#' f2 %in% tempdir()
#' export(mtcars, format = "stata")
#' "mtcars.dta" %in% dir()
#'
#'
#' setwd(wd)
#' }
#' # specify `file` and `format` to override default format
Expand All @@ -87,14 +87,14 @@
#' \dontrun{
#' ## export a single data frame
#' export(mtcars, f9 <- tempfile(fileext = ".xlsx"))
#'
#'
#' ## export NAs to Excel as missing via args passed to `...`
#' mtcars$drat <- NA_real_
#' mtcars %>% export(f10 <- tempfile(fileext = ".xlsx"), keepNA = TRUE)
#'
#'
#' ## export a list of data frames as worksheets
#' export(list(a = mtcars, b = iris), f11 <- tempfile(fileext = ".xlsx"))
#'
#'
#' ## export, adding a new sheet to an existing workbook
#' export(iris, f12 <- tempfile(fileext = ".xlsx"), which = "iris")
#' }
Expand All @@ -116,7 +116,7 @@
#' # unlink(f11)
#' # unlink(f12)
#' # unlink(f13)
#' @seealso \code{\link{.export}}, \code{\link{characterize}}, \code{\link{import}}, \code{\link{convert}}, \code{\link{export_list}}
#' @seealso \code{\link{characterize}}, \code{\link{import}}, \code{\link{convert}}, \code{\link{export_list}}
#' @importFrom haven labelled
#' @export
export <- function(x, file, format, ...) {
Expand Down Expand Up @@ -155,7 +155,7 @@ export <- function(x, file, format, ...) {
} else if (is.matrix(x)) {
x <- as.data.frame(x)
}

class(file) <- c(paste0("rio_", fmt), class(file))
.export(file = file, x = x, ...)

Expand Down
Loading