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

export(name, attach = FALSE): Export, but don't attach #165

Open
HenrikBengtsson opened this issue Jun 26, 2024 · 6 comments
Open

export(name, attach = FALSE): Export, but don't attach #165

HenrikBengtsson opened this issue Jun 26, 2024 · 6 comments

Comments

@HenrikBengtsson
Copy link
Owner

Background

Some package functions are not intended for end-users, but are instead meant to be imported by other packages. When attaching a package via library(), all the exported objects are attached and therefore put on the search() path.

Idea

Let a package decided which exported objects to not be attached by specifying them in optional character vector .packageExclude. This avoids cluttering the search() path with objects that are not intended for end-users, yet making them available via the :: form.

For example, consider package somepkg declaring functions:

hello <- function() message("hello world")
bye <- function() message("bye bye")

and exporting them in NAMESPACE as:

export("hello")
export("bye")

These functions can be called as:

> somepkg::hello()
hello world
> somepkg::bye()
bye bye

and

> library(somepkg)
> hello()
hello world
> bye()
bye bye

However, the package can choose to exclude bye() from the attached namespace such that it is only available via the first form by specifying:

.packageExclude <- c("bye")

If so, then we will have:

> somepkg::hello()
hello world
> somepkg::bye()
bye bye

and

> library(somepkg)
> hello()
hello world
> bye()
Error: object 'bye' not found

Implementation

Implementing this is straightforward, because we can leverage the exclude argument that library() and require() already supports. All we need to do is to append any objects specified in the optional .packageExclude character vector. Here's a patch:

diff --git src/library/base/R/attach.R src/library/base/R/attach.R
index d6c324c305..c0b6cb9f69 100644
--- src/library/base/R/attach.R
+++ src/library/base/R/attach.R
@@ -43,7 +43,8 @@ attach <- function(what, pos = 2L, name = deparse1(substitute(what), backtick=FA
     {
         dont.mind <- c("last.dump", "last.warning", ".Last.value",
                        ".Random.seed", ".Last.lib", ".onDetach",
-                       ".packageName", ".noGenerics", ".required",
+                       ".packageName", ".packageExclude",
+                       ".noGenerics", ".required",
                        ".no_S3_generics", ".Depends", ".requireCachedGenerics")
         sp <- search()
         for (i in seq_along(sp)) {
diff --git src/library/base/R/library.R src/library/base/R/library.R
index 027ae047b4..facc0047a7 100644
--- src/library/base/R/library.R
+++ src/library/base/R/library.R
@@ -168,7 +168,8 @@ function(package, help, pos = 2, lib.loc = NULL, character.only = FALSE,
     {
         dont.mind <- c("last.dump", "last.warning", ".Last.value",
                        ".Random.seed", ".Last.lib", ".onDetach",
-                       ".packageName", ".noGenerics", ".required",
+                       ".packageName", ".packageExclude",
+                       ".noGenerics", ".required",
                        ".no_S3_generics", ".Depends", ".requireCachedGenerics")
         sp <- search()
         lib.pos <- which(sp == pkgname)
@@ -378,6 +379,7 @@ function(package, help, pos = 2, lib.loc = NULL, character.only = FALSE,
 		tt <- tryCatch({
                     attr(package, "LibPath") <- which.lib.loc
                     ns <- loadNamespace(package, lib.loc)
+                    exclude <- c(exclude, ns[[".packageExclude"]])
                     env <- attachNamespace(ns, pos = pos, deps,
                                            exclude, include.only)
 		}, error = function(e) {
@HenrikBengtsson
Copy link
Owner Author

HenrikBengtsson commented Jul 10, 2024

@DavisVaughan, @lionel-, @georgestagg suggest that this would be better declared in the NAMESPACE file, e.g.

export(name, attach = FALSE)

where the default behavior is:

export(name, attach = TRUE)

A patch for this is:

diff --git src/library/base/R/namespace.R src/library/base/R/namespace.R
index 49bf222c5b..6a7f885ca8 100644
--- src/library/base/R/namespace.R
+++ src/library/base/R/namespace.R
@@ -139,6 +139,9 @@ attachNamespace <- function(ns, pos = 2L, depends = NULL, exclude, include.only)
     on.exit(Sys.unsetenv("_R_NS_LOAD_"), add = TRUE)
     runHook(".onAttach", ns, dirname(nspath), nsname)
 
+    ## update 'exclude' by 'excludes' in namespace
+    exclude <- c(exclude, .getNamespaceInfo(ns, "excludes"))
+    
     ## adjust variables for 'exclude', 'include.only' arguments
     if (! missing(exclude) && length(exclude) > 0)
         rm(list = exclude, envir = env)
@@ -617,6 +620,7 @@ loadNamespace <- function (package, lib.loc = NULL,
         }
         addNamespaceDynLibs(env, nsInfo$dynlibs)
         setNamespaceInfo(env, "nativeRoutines", nativeRoutines)
+        setNamespaceInfo(env, "excludes", nsInfo$excludes)
 
         ## used in e.g. utils::assignInNamespace
         Sys.setenv("_R_NS_LOAD_" = package)
@@ -812,6 +816,7 @@ loadNamespace <- function (package, lib.loc = NULL,
         }
 	if(lev > 2L) message("--- processing exports for ", dQuote(package))
         namespaceExport(ns, exports)
+
 	if(lev > 2L) message("--- sealing exports for ", dQuote(package))
         sealNamespace(ns)
         runUserHook(package, pkgpath)
@@ -1303,6 +1308,7 @@ parseNamespaceFile <- function(package, package.lib, mustExist = TRUE)
         stop(gettextf("package %s has no 'NAMESPACE' file", sQuote(package)),
              domain = NA)
     else directives <- NULL
+    excludes <- character()
     exports <- character()
     exportPatterns <- character()
     exportClasses <- character()
@@ -1332,6 +1338,12 @@ parseNamespaceFile <- function(package, package.lib, mustExist = TRUE)
             as.character(eval(eval(call("substitute", cc, as.list(vars))),
                               .GlobalEnv))
         }
+        evalToLogical <- function(cc) {
+            vars <- all.vars(cc)
+            names(vars) <- vars
+            as.logical(eval(eval(call("substitute", cc, as.list(vars))),
+                                 .GlobalEnv))
+        }
         switch(as.character(e[[1L]]),
                "if" = if (eval(e[[2L]], .GlobalEnv))
                parseDirective(e[[3L]])
@@ -1345,9 +1357,16 @@ parseNamespaceFile <- function(package, package.lib, mustExist = TRUE)
                        names(dynlibs)[length(dynlibs)] <<- asChar(e[[2L]])
                },
                export = {
+                   attach <- e$attach
+                   e$attach <- NULL
                    exp <- e[-1L]
                    exp <- structure(asChar(exp), names = names(exp))
                    exports <<- c(exports, exp)
+                   if (!is.null(attach)) {
+                       attach <- evalToLogical(attach)
+                       if (!attach)
+                           excludes <<- c(excludes, exp)
+                   }
                },
                exportPattern = {
                    pat <- asChar(e[-1L])
@@ -1521,7 +1540,7 @@ parseNamespaceFile <- function(package, package.lib, mustExist = TRUE)
 
     ## need to preserve the names on dynlibs, so unique() is not appropriate.
     dynlibs <- dynlibs[!duplicated(dynlibs)]
-    list(imports = imports, exports = exports,
+    list(imports = imports, exports = exports, excludes = excludes,
          exportPatterns = unique(exportPatterns),
          importClasses = importClasses, importMethods = importMethods,
          exportClasses = unique(exportClasses),

FWIW, one way this could be added to roxygen2 might be:

#' @export
#' @noattach

@HenrikBengtsson HenrikBengtsson changed the title Package not attaching all exported objects ("protected objects") export(name, attach = FALSE): Export, but don't attach Jul 10, 2024
@DavisVaughan
Copy link

This might be more useful as an IDE feature. With the new declare() syntax, a function could say something like

declare(ark(autocomplete = FALSE))

and that would allow it to be ignored from autocompletions like when a user types dplyr::

This would actually be useful for two cases:

  • The original case Henrik mentions, where developers want to hide internal functions
  • A new thing I thought of, where developers want to deprecate an existing function by hiding it from autocompletions, making it harder for users to find

@HenrikBengtsson
Copy link
Owner Author

Interesting - I've seen declare(), but I don't know where it's heading.

That said, there's also the masking problem, which can be avoided if we don't attach.

@t-kalinowski
Copy link

t-kalinowski commented Sep 4, 2024

I hope that declare() will be reserved for type hints, while interactive features like autocomplete are managed through other mechanisms, such as NAMESPACE or roxygen tags.

Another approach could be defining a special symbol in a package namespace—for example, a character vector of symbol names bound to a specific symbol, like .exclude_attach or .autocomplete_omit.

@lionel-
Copy link

lionel- commented Sep 9, 2024

or roxygen tags.

The roxygen tags can generate declare() annotations, possibly in sidecar files (e.g. .rtype).

Another approach could be defining a special symbol in a package namespace—for example, a character vector of symbol names bound to a specific symbol, like .exclude_attach or .autocomplete_omit.

I think it's better to use declarative syntax (via declare() or NAMESPACE) that can be examined without the use of an R runtime. You could restrict the declaration to a subset of R code but if it looks too much like R code it will likely be confusing for users when they use equivalent but syntactically invalid declarations.

@t-kalinowski
Copy link

t-kalinowski commented Sep 9, 2024

@lionel- To avoid derailing the conversation here, I've opened a new issue: #169.

without the use of an R runtime.

This seems like a significant requirement. Can you elaborate on what motivates it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants