The goal of {cnd}
is to provide easy, customized classes
for your conditions
.
This makes setting up custom conditions quick and more useful.
You can install the current CRAN version of {cnd}
with
one of:
install.packages("cnd")
::pak("cran/cnd@*release")
pak::pak("jmbarbone/cnd@*release") pak
You can install the development version of {cnd}
from GitHub with:
::pak("jmbarbone/cnd") pak
But is recommended to specify a pre-release tag, if available.
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.
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
<- condition(
bad_value "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
<- function(x) {
foo 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.
<- condition(
bad_value2 "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:
<- function(x) {
foo 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
There are three things you can do to get the most out of
{cnd}
within your package.
registry
within your package"condition"
attribute to your
functionsA 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.
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.
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.
cnd_document()
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)`
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:
<- function() {
foo_call 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({
<- options(cnd.message.format = "simple", cnd.call = FALSE)
op 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:
<- condition("foo_condition", "Hello\nthere", type = "condition")
con <- function() cnd(con())
my_fun 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")
} )