Skip to content

load_all() drops exports of generics re-exported from base #339

Description

@SNAnalyst

Summary

A package that provides S3 methods for a base generic can export the generic's
name so that pkg::plot(x) works without the package being attached. This is
export(plot) with no object named plot in the namespace; R resolves the
export through the namespace's parent chain, and pkg::plot becomes base::plot.

When the package is installed, this works exactly as intended. Under
load_all(), setup_ns_exports() removes such names from the package's exports,
so pkg::plot does not exist. load_all() and an installed package therefore
disagree about the package's API.

This is not the same as #31. I'm not arguing the warning is wrong — I understand
the position there that plot is genuinely "exported but not in the package
namespace". The point here is the second half: after warning, setup_ns_exports()
drops the export, and that is what makes load_all() diverge from an installed
package. A developer working under load_all() sees pkg::plot() fail with an
error that does not occur for their users.

Reprex

No roxygen2 involved; hand-written NAMESPACE.

basegen/
  DESCRIPTION      # Package: basegen, plus the usual required fields
  NAMESPACE
  R/plot.R
# NAMESPACE
S3method(plot,widget)
export(plot)
# R/plot.R
plot.widget <- function(x, ...) invisible("widget method ran")

Installed, this behaves as intended:

"plot" %in% getNamespaceExports("basegen")
#> TRUE
identical(basegen::plot, base::plot)
#> TRUE
basegen::plot(structure(1, class = "widget"))
#> "widget method ran"

Under load_all():

pkgload::load_all("basegen", export_all = FALSE)
#> Warning: Objects listed as exports, but not present in namespace:
#> • plot

"plot" %in% getNamespaceExports("basegen")
#> FALSE

basegen::plot(structure(1, class = "widget"))
#> Error: 'plot' is not an exported object from 'namespace:basegen'

Mechanism

setup_ns_exports() (R/namespace-env.R, current dev) keeps only names it finds
in the namespace env or the imports env, and does not follow the parent chain to
base:

ns_and_imports <- c(env_names(nsenv), env_names(imports_env(package)))
extra_exports  <- setdiff(exports, ns_and_imports)
...
exports <- intersect(ns_and_imports, exports)   # base-inherited exports dropped here

Consistently, a re-export from a normal package is fine, because the object
lands in the imports env:

# NAMESPACE:  export(head)  +  importFrom(utils, head)
pkgload::load_all("basegen", export_all = FALSE)
"head" %in% getNamespaceExports("basegen")   #> TRUE
"plot" %in% getNamespaceExports("basegen")   #> FALSE

So base is the special case: the object it re-exports is reachable through the
parent chain but is neither in the namespace env nor the imports env.

No workaround

The obvious one, importing from base, is rejected by R itself:

# NAMESPACE:  export(plot)  +  importFrom(base, plot)
pkgload::load_all("basegen")
#> Error: operation not allowed on base namespace
install.packages("basegen", repos = NULL, type = "source")
#> Error in asNamespace(ns, base.OK = FALSE) : operation not allowed on base namespace

@rawNamespace export(plot) produces the same NAMESPACE line and the same
outcome. So a package in this position cannot make load_all() match an
installed build.

Suggestion

Would setup_ns_exports() be willing to keep exports that resolve in the
namespace's parent chain — e.g. exists(name, envir = nsenv, inherits = TRUE),
or at minimum names that resolve in baseenv() — rather than dropping them? That
would make load_all() agree with an installed package for re-exported base
generics, independently of whether the warning is kept.

Happy to send a PR if that direction is acceptable.

Context

This bit us in practice: working under load_all(), a contributor saw
pkg::plot() fail and removed the @export tag to "fix" it, which then broke the
installed package for every user. Reduced example above.

pkgload 1.5.3 (current CRAN); dev setup_ns_exports() is unchanged. R 4.6.1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions