diff --git a/.lintr b/.lintr index c2e2122..820dbf7 100644 --- a/.lintr +++ b/.lintr @@ -1,3 +1,5 @@ -linters: linters_with_defaults() # see vignette("lintr") +linters: linters_with_defaults( + indentation_linter = NULL + ) encoding: "UTF-8" exclusions: list("tests/spelling.R") diff --git a/DESCRIPTION b/DESCRIPTION index 971a36b..f9cc428 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: echo Title: Echo Code Evaluations -Version: 0.1.0.9000 +Version: 0.1.0.9001 Authors@R: person(given = "Jordan Mark", family = "Barbone", @@ -21,7 +21,7 @@ Suggests: testthat (>= 3.0.0) Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.0 URL: https://github.com/jmbarbone/echo, https://jmbarbone.github.io/echo/ BugReports: https://github.com/jmbarbone/echo/issues diff --git a/NEWS.md b/NEWS.md index 0ac8a86..b2e4427 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ + # echo 0.1.0.9000 +* `echo()` gains new `progress` param to print a progress bar, similar to `utils::txtProgressBar(style = 3)` [#2](https://github.com/jmbarbone/echo/issues/2) +* `echo()` now handles single expressions better (e.g., `echo({ letters })` and `echo(letters)` should produce the same result) +* `echo()` gains new `expr` param to evaluate an `expression` object instead of `expr` or `file` * Added a `NEWS.md` file to track changes to the package. diff --git a/R/echo-classes.R b/R/echo-classes.R index 8c601b5..89bb990 100644 --- a/R/echo-classes.R +++ b/R/echo-classes.R @@ -1,9 +1,17 @@ -echo_echo <- function(x, level = "NUL") { +echo_echo <- function(x, level = "NUL", p = NULL) { if (echo_get_level() > level) { return(invisible()) } + if (inherits(p, "echo_progress")) { + p$flush() + on.exit(p$show(), add = TRUE) + } + + op <- options(width = getOption("echo.width", getOption("width", 80L))) + on.exit(options(op), add = TRUE) + print(x) } @@ -36,32 +44,32 @@ print.echo_err <- function(x, ...) { catln(paste0(time(), "[ERR] #> ", x)) } -echo_exp <- function(x) { - echo_class("exp", x) +echo_exp <- function(x, p = NULL) { + echo_class("exp", x, p = p) } -echo_out <- function(x) { - echo_class("out", x) +echo_out <- function(x, p = NULL) { + echo_class("out", x, p = p) } -echo_msg <- function(x) { - echo_class("msg", x) +echo_msg <- function(x, p = NULL) { + echo_class("msg", x, p = p) } -echo_wrn <- function(x) { - echo_class("wrn", x) +echo_wrn <- function(x, p = NULL) { + echo_class("wrn", x, p = p) } -echo_err <- function(x) { - echo_class("err", x) +echo_err <- function(x, p = NULL) { + echo_class("err", x, p = p) } -echo_class <- function(class, text) { +echo_class <- function(class, text, p = NULL) { stopifnot(toupper(class) %in% level_levels()) x <- structure( text, class = c("echo_echo", paste0("echo_", class), "list") ) - echo_echo(x, level = toupper(class)) + echo_echo(x, level = toupper(class), p = p) } diff --git a/R/echo.R b/R/echo.R index 0c4a359..cb48234 100644 --- a/R/echo.R +++ b/R/echo.R @@ -18,18 +18,21 @@ #' Timestamps are printed in UTC by default. To control this, set the option #' value, such as `options(echo.timezone = "EST")`. #' -#' @param expr Expression to evaluate; should be written with curly braces (see -#' examples) +#' @param expr Expression to evaluate. This can be a single expression. +#' expressions within braces (i.e., `{ ... }`). #' @param log A connection or file name for outputs; defaults to `stdout()` #' @param msg Logical, if `FALSE` does not output a message; defaults to `TRUE` #' @param level Sets the echo level (see details); defaults to `0L` #' @param file File path to evaluate (like [base::source()]). If `file` is not #' `NULL`, then `expr` must be missing` +#' @param exprs Expressions to evaluate. This can be a single `expression`. +#' @param progress Logical, if `TRUE` shows a progress bar; defaults to `FALSE` #' @returns Nothing, called for side-effects #' @examples #' # make sure to use braces for expr -#' echo(letters, level = 0) # bad -#' echo({letters}, level = 0) # good +#' echo({ letters }, level = 0) # good +#' echo({letters}, level = 0) # still good +#' echo(letters, level = 0) # also good #' #' try(echo( #' expr = { @@ -43,7 +46,6 @@ #' level = 0 #' )) #' -#' #' # Parse lines in a file instead #' try(echo(file = system.file("example-script.R", package = "echo"))) #' @@ -60,13 +62,15 @@ echo <- function( log = echo_get_log(), #> stdout() msg = echo_get_msg(), #> TRUE level = echo_get_level(), - file = NULL + file = NULL, + exprs = NULL, + progress = getOption("echo.progress", FALSE) ) { op <- options( echo.msg = msg, echo.log = log, echo.level = level, - width = max(getOption("width") - 37, 30) + echo.width = max(getOption("width") - 37, 30) ) on.exit(options(op), add = TRUE) @@ -77,43 +81,65 @@ echo <- function( stop("If 'file' is not NULL, 'expr' must be missing", call. = FALSE) } expr <- parse(file) + } else if (!is.null(exprs)) { + if (!missing(expr)) { + stop("If 'exprs' is not NULL, 'expr' must be missing", call. = FALSE) + } + stopifnot(inherits(exprs, "expression")) + expr <- as.expression(exprs) + } else { + expr <- as.list(substitute(expr)) + if (identical(expr[[1]], quote(`{`))) { + expr <- expr[-1] + } + } + + if (progress) { + progress <- echo_progress } else { - expr <- as.list(substitute(expr))[-1] + progress <- echo_null } - # TODO add functions for other controls + progress$reset() + progress$total <- length(expr) for (ex in expr) { - evaluate(ex, env = env) + evaluate(ex, env = env, progress = progress) } invisible() } -evaluate <- function(expr, env = env) { +evaluate <- function(expr, env = env, progress = echo_null) { dep <- deparse1(expr) + progress$flush() echo_exp(dep) + progress$show() + # FIXME include progress$flush(), $show() in echo_echo() output <- utils::capture.output(value <- tryCatch( eval(as.expression(expr), envir = env), message = function(e) { - echo_msg(conditionMessage(e)) + echo_msg(conditionMessage(e), p = progress) tryInvokeRestart("muffleMessage") }, warning = function(e) { - echo_wrn(conditionMessage(e)) + echo_wrn(conditionMessage(e), p = progress) tryInvokeRestart("muffleWarning") }, error = function(e) { - echo_err(conditionMessage(e)) + progress$flush() + echo_err(conditionMessage(e), p = progress) + progress$show() stop("Error in ", dep, "\n ", conditionMessage(e), call. = FALSE) } )) - if (is.null(value) && identical(output, character())) { - utils::flush.console() - } else { + if (!(is.null(value) && identical(output, character()))) { + progress$flush() echo_out(output) } + progress$add(1L) + progress$show() invisible(value) } diff --git a/R/progress.R b/R/progress.R new file mode 100644 index 0000000..71b1e4c --- /dev/null +++ b/R/progress.R @@ -0,0 +1,182 @@ + +#' Echo progress env +#' +#' Environments for handling the progress bars +#' +#' @details Two progress bar environments are created. `echo_progress` is the +#' standard environment which produces a progress bar similar to +#' [utils::txtProgressBar()] with `style = 3`. `echo_null` doesn't produce +#' any outputs but has the same methods as `echo_progress` so that it can be +#' used in place of `echo_progress` when `progress = FALSE`. +#' +#' @section: fields +#' - `self`: The environment itself +#' - `at`: The current progress (`int`) +#' - `total`: The total value the progress bar goes to (`int`) +#' +#' @section: methods Methods which begin with `.` are conceptually _private_ +#' methods and only used internally for _public_ methods to work. The _private_ +#' methods are not available in `echo_null`. +#' +#' - `reset()`: Reset the progress bar +#' - `set(x)`: Sets the progress bar to `x` +#' - `add(x)`: Increment the progress bar by `x` +#' - `show()`: Show the current progress (print) +#' - `flush()`: Flush the progress bar (removes it) +#' - `.width()`: Get the width of the progress bar +#' - `.percent()`: Get the percentage of progress completed +#' - `.progress()`: Get the current character length of progress bar (for width) +#' - `.remaining()`: Get the remaining character length of the progress bar +#' +#' @examples +#' # shorter alias +#' pb <- echo_progress +#' # run within braces to execute together +#' { +#' # starts +#' pb$reset() +#' pb$show() +#' pb$add(10) +#' +#' # processing +#' Sys.sleep(2) +#' +#' pb$flush() +#' print(letters) +#' pb$add(15) +#' pb$show() +#' +#' # processing +#' Sys.sleep(2) +#' +#' pb$flush() +#' print(state.abb) +#' pb$add(20) +#' pb$show() +#' +#' # processing +#' Sys.sleep(2) +#' +#' pb$flush() +#' pb$set(100) +#' pb$show() +#' +#' pb$reset() +#' cat("Done!") +#' } +#' @keywords internal +#' @name echo_progress_env +#' @noRd +NULL + +#' @rdname echo_progress_env +#' @noRd +echo_progress <- new.env(hash = FALSE) +local(envir = echo_progress, { + self <- echo_progress + at <- 0L + total <- 100L + + reset <- function() { + self$at <- 0L + self$.width() + invisible(self) + } + + set <- function(x) { + self$at <- x + invisible(self) + } + + add <- function(x) { + x <- as.integer(x) + if (is.na(x)) { + return() + } + + self$at <- self$at + x + invisible(self) + } + + show <- function() { + if (self$at == 0L) { + return(invisible()) + } + + cat( + " |", + strrep("=", self$.progress()), + strrep(" ", self$.remaining()), + "|", + format(floor(self$.percent() * 100L), width = 4L), + "%", + strrep(" ", getOption("width") - self$.width()), + if (self$at == self$total) "\n", + sep = "" + ) + flush.console() + } + + flush <- function() { + if (self$at == 0L) { + return(invisible()) + } + w <- getOption("width") + bs <- strrep("\b", w) + cat(bs) + cat("\r", strrep(" ", w), sep = "") + cat(bs) + } + + # 'internal' methods + + .width <- function(x = getOption("width")) { + max(20L, as.integer(x) - 10L) + } + + .percent <- function() { + self$at / self$total + } + + .progress <- function() { + floor(self$.percent() * self$.width()) + } + + .remaining <- function() { + self$.width() - self$.progress() + } +}) +class(echo_progress) <- c("echo_progress", "environment") +lockBinding("self", echo_progress) +lockBinding("reset", echo_progress) +lockBinding("set", echo_progress) +lockBinding("add", echo_progress) +lockBinding("show", echo_progress) +lockBinding("flush", echo_progress) +lockBinding(".width", echo_progress) +lockBinding(".percent", echo_progress) +lockBinding(".progress", echo_progress) +lockBinding(".remaining", echo_progress) +lockEnvironment(echo_progress) + +#' @rdname echo_progress_env +#' @noRd +echo_null <- new.env(hash = FALSE) +local(envir = echo_null, { + self <- parent.frame() + total <- 0L + reset <- function(...) invisible() + set <- function(...) invisible() + add <- function(...) invisible() + show <- function(...) invisible() + flush <- function(...) invisible() +}) + +class(echo_null) <- c("echo_null", "environment") +lockBinding("self", echo_null) +lockBinding("reset", echo_null) +lockBinding("set", echo_null) +lockBinding("add", echo_null) +lockBinding("show", echo_null) +lockBinding("flush", echo_null) +lockEnvironment(echo_null) diff --git a/echo.Rproj b/echo.Rproj index 69fafd4..7a1f5b9 100644 --- a/echo.Rproj +++ b/echo.Rproj @@ -2,7 +2,7 @@ Version: 1.0 RestoreWorkspace: No SaveWorkspace: No -AlwaysSaveHistory: Default +AlwaysSaveHistory: No EnableCodeIndexing: Yes UseSpacesForTab: Yes diff --git a/man/echo-package.Rd b/man/echo-package.Rd index e9f3494..d0ef837 100644 --- a/man/echo-package.Rd +++ b/man/echo-package.Rd @@ -3,7 +3,6 @@ \docType{package} \name{echo-package} \alias{echo-package} -\alias{_PACKAGE} \alias{echo_package} \title{echo: Echo Code Evaluations} \description{ diff --git a/man/echo.Rd b/man/echo.Rd index a4135ff..935f845 100644 --- a/man/echo.Rd +++ b/man/echo.Rd @@ -9,12 +9,14 @@ echo( log = echo_get_log(), msg = echo_get_msg(), level = echo_get_level(), - file = NULL + file = NULL, + exprs = NULL, + progress = getOption("echo.progress", FALSE) ) } \arguments{ -\item{expr}{Expression to evaluate; should be written with curly braces (see -examples)} +\item{expr}{Expression to evaluate. This can be a single expression. +expressions within braces (i.e., \code{{ ... }}).} \item{log}{A connection or file name for outputs; defaults to \code{stdout()}} @@ -24,6 +26,10 @@ examples)} \item{file}{File path to evaluate (like \code{\link[base:source]{base::source()}}). If \code{file} is not \code{NULL}, then \code{expr} must be missing`} + +\item{exprs}{Expressions to evaluate. This can be a single \code{expression}.} + +\item{progress}{Logical, if \code{TRUE} shows a progress bar; defaults to \code{FALSE}} } \value{ Nothing, called for side-effects @@ -50,8 +56,9 @@ value, such as \code{options(echo.timezone = "EST")}. } \examples{ # make sure to use braces for expr -echo(letters, level = 0) # bad -echo({letters}, level = 0) # good +echo({ letters }, level = 0) # good +echo({letters}, level = 0) # still good +echo(letters, level = 0) # also good try(echo( expr = { @@ -65,7 +72,6 @@ try(echo( level = 0 )) - # Parse lines in a file instead try(echo(file = system.file("example-script.R", package = "echo"))) diff --git a/tests/testthat/test-echo.R b/tests/testthat/test-echo.R index 23f319f..c3e3a3f 100644 --- a/tests/testthat/test-echo.R +++ b/tests/testthat/test-echo.R @@ -1,20 +1,24 @@ test_that("echo() works", { - res <- utils::capture.output(echo({ print(1) }, level = 0)) # nolint: brace_linter, line_length_linter. - expect_identical( - substr(res, 23, nchar(res)), - c("[EXP] print(1)", "[OUT] #> [1] 1") - ) + # nolint next: brace_linter + res <- utils::capture.output(echo({ print(1) }, level = 0)) + obj <- substr(res, 23, nchar(res)) + exp <- c("[EXP] print(1)", "[OUT] #> [1] 1") + expect_identical(obj, exp) expect_error( - echo( - exprs = { - print(NULL) - invisible(1) - message("message") - warning("warning") - stop("error") - }, - log = NULL + expect_output( + expect_message( + echo( + expr = { + print(NULL) + invisible(1) + message("message") + warning("warning") + stop("error") + }, + log = NULL + ) + ) ) ) @@ -27,4 +31,9 @@ test_that("echo() works", { echo({ 1 }, file = tempfile()), # nolint: brace_linter. "must be missing" ) + + # progress still produces outputs + exprs <- expression(print(1), print(2)) + expect_silent(echo(exprs = exprs, level = "NUL", progress = FALSE)) + expect_output(echo(exprs = exprs, level = "NUL", progress = TRUE)) })