The goal of cnd is to provide easy, customized classes for your conditions
.
This makes setting up custom conditions quick and more useful.
Installation
You can install the current CRAN version of cnd with one of:
install.packages("cnd")
pak::pak("cran/cnd@*release")
pak::pak("jmbarbone/cnd@*release")
You can install the development version of cnd from GitHub with:
pak::pak("jmbarbone/cnd")
But is recommended to specify a pre-release tag, if available.
Conditions, in general
conditions
are special objects that R will use for both signaling and messaging, primarily within the context of stop()
, warning()
, and message()
.
format(stop)[7:10]
#> [1] " message <- conditionMessage(cond)"
#> [2] " call <- conditionCall(cond)"
#> [3] " .Internal(.signalCondition(cond, message, call))"
#> [4] " .Internal(.dfltStop(message, call))"
format(warning)[9:14]
#> [1] " message <- conditionMessage(cond)"
#> [2] " call <- conditionCall(cond)"
#> [3] " withRestarts({"
#> [4] " .Internal(.signalCondition(cond, message, call))"
#> [5] " .Internal(.dfltWarn(message, call))"
#> [6] " }, muffleWarning = function() NULL)"
format(message)[13:19]
#> [1] " defaultHandler <- function(c) {"
#> [2] " cat(conditionMessage(c), file = stderr(), sep = \"\")"
#> [3] " }"
#> [4] " withRestarts({"
#> [5] " signalCondition(cond)"
#> [6] " defaultHandler(cond)"
#> [7] " }, muffleMessage = function() NULL)"
All functions accept a condition
object as their first argument, which contains both the classes that will be signaled and a message that will be sent to the stderr()
. You’ll notice, too, that warning()
and message()
call withRestarts()
, which contain signalCondition()
call. By default, these three functions create condition
objects with an extra class of "error"
, "warning"
, or "message"
, respectively. cnd inserts itself into these processes by allowing users to define new condition objects to be signaled and with greater control over messaging.
Example
The workhorse of cnd is condition()
, which is a special function of class cnd::condition_progenitor
, which returns other special functions of class cnd::condition_generator
. The cnd::condition_generator
objects return condition
s.
library(cnd)
condition
#> cnd::condition_progenitor
#>
#> generator
#> $ class : <symbol>
#> $ message : NULL
#> $ type : <language> c("condition", "message", "warning", "error")
#> $ package : <language> get_package()
#> $ exports : NULL
#> $ help : NULL
#> $ registry : <symbol> package
#> $ register : <language> !is.null(registry)
#>
#> condition(s)
#> cnd:as_character_cnd_error/error
#> cnd:condition_message_generator/error
#> cnd:condition_overwrite/warning
#> cnd:invalid_condition/error
#> cnd:invalid_condition_message/error
#> cnd:match_arg/error
#> cnd:no_package_exports/warning
#>
#> For a list of conditions: `cnd::conditions()`
Note:
condition
is of mode “function” but does not retain “function” as a class.condition
also has several conditions which can be signaled directly or indirectly.
Use condition()
to create a generator, then use that generator within your functions:
# cnd::condition_generator
bad_value <- condition(
"bad_value",
message = "Value has to be better",
type = "error"
)
bad_value
#> cnd::condition_generator
#> bad_value/error
# condition
bad_value()
#> bad_value/error
#> (bad_value/cnd::condition/error/condition)
#> Value has to be better
foo <- function(x) {
if (x < 0) {
stop(bad_value())
}
x
}
foo(-1)
#> Error in foo(): <bad_value>
#> Value has to be better
The resulting cnd::condition_generator
object can also take parameters that are used in creating a custom message.
bad_value2 <- condition(
"bad_value2",
message = function(x) {
sprintf("`x` must be `>=0`. A value of `%s` is no good", format(x))
},
type = "error"
)
# a 'generator' is also printed, with formals
bad_value2
#> cnd::condition_generator
#> bad_value2/error
#>
#> generator
#> $ x : <symbol>
# pass a value to the args to generate the condition message
bad_value2(0)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `0` is no good
bad_value2(-1)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `-1` is no good
# note: this does not provide any tests, so you may produce non-nonsensical messages
bad_value2(10)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `10` is no good
# now when used in your function:
foo <- function(x) {
if (x < 0) {
stop(bad_value2(x))
}
x
}
foo(-1.2)
#> Error in foo(): <bad_value2>
#> `x` must be `>=0`. A value of `-1.2` is no good
Your package
There are three things you can do to get the most out of cnd within your package.
- Creating a
registry
within your package
- Assigning a
"condition"
attribute to your functions
- Documenting your conditions
Registry
A registry
is a new environment that will store all of your conditions. This environment must exist within your package, an cnd will be able to find this and use it to connect your conditions to your functions and to other outputs.
Simple add cnd_registry()
to an R/
script in your package. If you are going to save an store conditions as objects (recommended) then you should ensure that the cnd_registry()
call is made before any conditions are created.
NOTE
cnd_registry()
is designed to useassign()
within your package environment. Please read the documentation to ensure the environment is not masked by other objects.
NOTE By default,
condition(registry = )
will pick up on theregistry
object within your package when you create your conditions and functions are loaded. However, interactive use may not provide the same results. See the examples incnd_create_registry()
for an example of how to create a new registry and assign conditions to the registry.
Assigning conditions
condition()
has an argument for exports
, which you can set to any function which you want to relate with any specific conditions.
If you add any functions to the exports
option, package
must be set. By default, package
should be set to your development package, so you don’t need to explicitly assign it every time.
In your package, add cnd_exports()
to an R/
script. This should be executed after all your conditions and their functions are created. This will add a new "conditions"
attribute to your functions as well as a new "cnd::conditioned_function"
class. The new class specifically updates the print()
method to show the conditions assigned to the function.
Documentation
When you’ve created your conditions and assigned them to your functions, you may also want to provide documentation. Or, rather, you should always provide documentation.
cnd_document()
will create a new {package}-cnd-conditions.R
file for all conditions you have assigned to your package. Simply run the command when developing to generate a file listing all conditions. You can also include this after your call to cnd_exports()
to ensure that all conditions are documented. The file is written for roxygen2 to generate the Rd
files for your package.
If you want to include other information directly within your roxygen comments, you can use the cnd_section()
function to grab all the conditions from a single functions and print out roxygen-friendly section information:
cat(cnd_section(cnd))
#>
#> Conditions are generated through the [`{cnd}`][cnd::cnd-package] package.
#> The following conditions are associated with this function:
#>
#> \describe{
#>
#> \item{[`cnd:cond_cnd_class/error`][cnd-cnd-conditions]}{
#> [cnd()] simple calls the appropriate function: [stop()], [warning()], or [message()] based on the `type` parameter from [cnd::condition()].
#> }
#>
#> }
#>
#> For more conditions, see: [cnd-cnd-conditions]
Typically, you may want to use this as such:
#' @section Conditions:
#' `r cnd_section(my_function)`
Retrieval
You can retrieve any conditions
that are created with conditions()
. By default this will list all conditions
loaded, but can be filtered by specific packages.
conditions("cnd", type = "warning")
#> [[1]]
#> cnd::condition_generator
#> cnd:cnd_document_conditions/warning
#>
#> exports
#> cnd::cnd_document()
#>
#> [[2]]
#> cnd::condition_generator
#> cnd:condition_overwrite/warning
#>
#> generator
#> $ old : <symbol>
#> $ new : <symbol>
#>
#> exports
#> cnd::condition()
#>
#> [[3]]
#> cnd::condition_generator
#> cnd:conditions_dots/warning
#>
#> help
#> The `...` parameter in [conditions()] is meant for convenience. Only a single argument is allowed. Other parameters must be named explicitly. For example: ```r # Instead of this conditions("class", "package") # "package" is ignored with a warning # Do this conditions(class = "class", package = "package") ```
#>
#> exports
#> cnd::conditions()
#>
#> [[4]]
#> cnd::condition_generator
#> cnd:no_package_exports/warning
#>
#> help
#> The `exports` parameter requires a `package`
#>
#> exports
#> cnd::condition()
cnd()
cnd()
is a special function which will use the appropriate signaling and handling function based on type of condition
provided. When the condition’s type is "error"
or "warning"
, cnd()
passes these directly through stop()
and warning()
, respectively. Both of these functions have .Internal()
calls (i.e., .dfltStop()
and .dfltWarn()
), which makes would make them difficult to replicate. However, message()
does not, and thus an equivalent wrapper is internally used which also controls for formatting:
foo_call <- function() {
condition("foo_condition", "two\nlines", type = "message")()
}
# provides a character(2) vector output:
conditionMessage(foo_call())
#> [1] "<foo_condition>\ntwo\nlines"
message()
uses a handler which simply collapses the message vector into a single string. Because of this, the lines are not always neatly separated:
message(foo_call())
#> <foo_condition>
#> two
#> lines
By contrast, the handlers invoked in cnd()
will recognize each element as a separate line for the output. Also, the default is to provide more information about the call, in a different format:
cnd(foo_call())
#> <foo_condition>
#> two
#> lines
To get the a simpler message, you can use the options()
function to change the cnd.message.format
option to "simple"
.
local({
op <- options(cnd.message.format = "simple", cnd.call = FALSE)
on.exit(options(op))
cnd(foo_call())
})
#> <foo_condition>
#> two
#> lines
Currently
message()
and thereforecnd()
send message conditions to thestderr()
, thus usually giving them an colored text.
Another benefit in using cnd(condition)
is being able to control for messages printed to the stdout()
. Using cat()
can sometimes create noise that you’d rather suppress. Because cnd()
uses an internal handler for message
and condition
types, a condition is signaled with singalCondition()
, which can then be caught with calling handlers, using a provided "muffleCondition"
restart:
con <- condition("foo_condition", "Hello\nthere", type = "condition")
my_fun <- function() cnd(con())
my_fun() # note the classes inside (...)
#> foo_condition/condition
#> (foo_condition/cnd::condition/condition)
#> Hello
#> there
withCallingHandlers(
my_fun(),
foo_condition = function(c) {
tryInvokeRestart("muffleCondition")
}
)