diff --git a/NEWS.md b/NEWS.md index de80f730..37ee5954 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,9 @@ * Suppress a spurious internal warning upon reloading a module, caused by a dependent module being imported more than once (#363). +## New feature + +* Add `box::get_exports()` function to provide functionality similar to `base::getNamespaceExports()`. # box 1.2.0 diff --git a/R/get-exports.r b/R/get-exports.r new file mode 100644 index 00000000..c427633f --- /dev/null +++ b/R/get-exports.r @@ -0,0 +1,80 @@ +#' List exports of a module or package +#' +#' \code{box::get_exports} supports reflection on {box} modules. This is the {box} version of +#' {base::getNamespaceExports}. +#' +#' @usage \special{box::get_exports(prefix/mod, \dots)} +#' @usage \special{box::get_exports(pkg, \dots)} +#' @usage \special{box::get_exports(alias = prefix/mod, \dots)} +#' @usage \special{box::get_exports(alias = pkg, \dots)} +#' @usage \special{box::get_exports(prefix/mod[attach_list], \dots)} +#' @usage \special{box::get_exports(pkg[attach_list], \dots)} +#' +#' @param prefix/mod a qualified module name +#' @param pkg a package name +#' @param alias an alias name +#' @param attach_list a list of names to attached, optionally witha aliases of +#' the form \code{alias = name}; or the special placeholder name \code{\dots} +#' @param \dots further import declarations +#' @return \code{box::get_exports} returns a list of attached packages, modules, and functions. +#' +#' @examples +#' # Set the module search path for the example module +#' old_opts = options(box.path = system.file(package = 'box')) +#' +#' # Basic usage +#' box::get_exports(mod/hello_world) +#' +#' # Using an alias +#' box::get_exports(world = mod/hello_world) +#' +#' # Attaching exported names +#' box::get_exports(mod/hello_world[hello]) +#' +#' # Attach everything, give `hello` an alias: +#' box::get_exports(mod/hello_world[hi = hello, ...]) +#' +#' # Reset the module search path +#' on.exit(options(old_opts)) +#' +#' @seealso +#' \code{\link[=use]{box::use}} give information about importing modules or packages +#' +#' @export +get_exports = function (...) { + caller = parent.frame() + call = match.call() + imports = call[-1L] + aliases = names(imports) %||% character(length(imports)) + unlist( + map(get_one, imports, aliases, list(caller), use_call = list(sys.call())), + recursive = FALSE + ) +} + +#' Get a module or package's exports without loading into the environment +#' +#' @param declaration an unevaluated use declaration expression without the +#' surrounding \code{use} call +#' @param alias the use alias, if given, otherwise \code{NULL} +#' @param caller the client’s calling environment (parent frame) +#' @param use_call the \code{use} call which is invoking this code +#' @return \code{get_one} return a list of functions exported. +#' @keywords internal +get_one = function (declaration, alias, caller, use_call) { + if (declaration %==% quote(expr =) && alias %==% '') return() + + spec = parse_spec(declaration, alias) + info = find_mod(spec, caller) + mod_ns = load_mod(info) + mod_exports = mod_exports(info, spec, mod_ns) + + exports = attach_list(spec, names(mod_exports)) + + if (is.null(exports)) { + exports = list(names(mod_exports)) + names(exports) = spec$alias + } + + return(exports) +} diff --git a/tests/testthat/test-get-exports.r b/tests/testthat/test-get-exports.r new file mode 100644 index 00000000..9cad41ef --- /dev/null +++ b/tests/testthat/test-get-exports.r @@ -0,0 +1,146 @@ +context('get exports') + +test_that('returns all functions of a whole package attached', { + results = get_exports(stringr) + + expected_output = getNamespaceExports('stringr') + + expect_contains(results[['stringr']], expected_output) +}) + +test_that('returns all functions of a whole packaged attached and aliased', { + results = get_exports(alias = stringr) + + expected_output = getNamespaceExports('stringr') + + expect_named(results, c('alias')) + expect_contains(results[['alias']], expected_output) +}) + +test_that('return all functions of a package attached by three dots', { + results = get_exports(stringr[...]) + expected_output = getNamespaceExports('stringr') + + expect_contains(unname(results), expected_output) +}) + +test_that('returns attached functions from packages', { + results = get_exports(stringr[str_pad, str_trim]) + expected_output = c('str_pad', 'str_trim') + names(expected_output) = c('str_pad', 'str_trim') + expect_named(results, names(expected_output)) + expect_setequal(unname(results), unname(expected_output)) +}) + +test_that('returns aliased attached functions from packages', { + results = get_exports(stringr[alias_1 = str_pad, alias_2 = str_trim]) + expected_output = c('str_pad', 'str_trim') + names(expected_output) = c('alias_1', 'alias_2') + expect_named(results, names(expected_output)) + expect_setequal(unname(results), unname(expected_output)) +}) + +test_that('throws an error on unknown function attached from package', { + expect_error(get_exports(stringr[unknown_function])) +}) + +test_that('returns all functions of a whole module attached', { + results = get_exports(mod/a) + + expected_output = c( + 'double', + 'modname', + 'get_modname', + 'get_modname2', + 'get_counter', + 'inc', + '%or%', + '+.string', + 'which', + 'encoding_test', + '.hidden', + '%.%', + '%x.%', + '%.x%', + '%x.x%', + '%foo.bar', + '%%.%%', + '%a%.class%' + ) + + expect_setequal(results[['a']], expected_output) +}) + +test_that('returns all functions of a whole module attached', { + results = get_exports(alias = mod/a) + + expected_output = c( + 'double', + 'modname', + 'get_modname', + 'get_modname2', + 'get_counter', + 'inc', + '%or%', + '+.string', + 'which', + 'encoding_test', + '.hidden', + '%.%', + '%x.%', + '%.x%', + '%x.x%', + '%foo.bar', + '%%.%%', + '%a%.class%' + ) + + expect_named(results, c('alias')) + expect_setequal(results[['alias']], expected_output) +}) + +test_that('return all functions of a module attached by three dots', { + results = get_exports(mod/a[...]) + expected_output = c( + 'double', + 'modname', + 'get_modname', + 'get_modname2', + 'get_counter', + 'inc', + '%or%', + '+.string', + 'which', + 'encoding_test', + '.hidden', + '%.%', + '%x.%', + '%.x%', + '%x.x%', + '%foo.bar', + '%%.%%', + '%a%.class%' + ) + + expect_contains(unname(results), expected_output) +}) + +test_that('returns attached functions from modules', { + results = get_exports(mod/a[get_modname, `%or%`]) + expected_output = c('get_modname', '%or%') + names(expected_output) = c('get_modname', '%or%') + expect_named(results, names(expected_output)) + expect_setequal(unname(results), unname(expected_output)) +}) + +test_that('returns attached aliased functions from modules', { + results = get_exports(mod/a[alias_1 = get_modname, alias_2 = `%or%`]) + expected_output = c('get_modname', '%or%') + names(expected_output) = c('alias_1', 'alias_2') + expect_named(results, names(expected_output)) + expect_setequal(unname(results), unname(expected_output)) +}) + +test_that('throws an error on unknown function attached from module', { + expect_error(get_exports(mod/a[unknown_function])) +})