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

A proposed function to provide base::getNamespaceExports() for {box} #369

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
80 changes: 80 additions & 0 deletions R/get-exports.r
Original file line number Diff line number Diff line change
@@ -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)
}
146 changes: 146 additions & 0 deletions tests/testthat/test-get-exports.r
Original file line number Diff line number Diff line change
@@ -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]))
})