Skip to content

fix(data-io): bind lazy datasets into the namespace at load - #775

Open
lbm364dl wants to merge 1 commit into
mainfrom
claude/lazydata-not-attached-641
Open

fix(data-io): bind lazy datasets into the namespace at load#775
lbm364dl wants to merge 1 commit into
mainfrom
claude/lazydata-not-attached-641

Conversation

@lbm364dl

Copy link
Copy Markdown
Collaborator

What was wrong

Confirmed on origin/main with a normally installed package (R CMD INSTALL
into a temp lib, fresh Rscript --vanilla, no library(whep)):

rows in whep::polities: 753
resolve_polity_label: object 'polity_label_aliases' not found
add_polity_code: object 'polity_area_crosswalk' not found
get_polity_geometries: object 'polities' not found
polities in namespace: FALSE

Mechanism, exactly as diagnosed in #641: with LazyData: true, loadNamespace()
lazy-loads data/Rdata.rdb into ns$.__NAMESPACE__.$lazydata, not into the
namespace. Package code is evaluated in the namespace, whose parents are
imports → base → globalenv, so a bare polities can only be found via the
search path — which holds the datasets only once library(whep) has attached
the package env. whep::polities works because :: falls back to the lazydata
environment. All 100 lazy-loaded objects (56 topics) are invisible to package
code in the loaded-but-not-attached state, and 55 of the 56 datasets are
referenced by bare name in R/.

What changed

New R/zzz.R (25 lines, no call sites touched — options 1 and 3 in the issue
would have rewritten ~667 references across 67 files):

  • .onLoad() binds each lazydata name that the namespace does not already
    define into the namespace with delayedAssign(). The namespace is still
    unlocked while .onLoad() runs, so this is a legal assignment; sealing
    afterwards just locks the promises.
  • The bindings stay lazy — verified with
    rlang::env_binding_are_lazy(asNamespace("whep"), c("polities", "biomass_coefs"))
    TRUE TRUE right after loadNamespace("whep"), so nothing is eagerly
    loaded and memory behaviour is unchanged.
  • Existing namespace objects are never shadowed (setdiff against ls(ns)),
    and identical(whep::polities, get("polities", asNamespace("whep"))) is
    TRUE.
  • library(whep) and pkgload::load_all() are unaffected (under load_all()
    the datasets are already in the namespace, so the binder is a no-op).

Classification: mechanical — a packaging/visibility fix. No published value
changes.

Verification

tests/testthat/test_zzz.R: four unit tests of the binder (binds, keeps lazy,
never shadows, no-op without a lazydata env) plus an invariant that every object
in the package's lazydata environment is reachable from the namespace — the
guard that would have caught this.

Before the fix, running the new test file against the installed
(unpatched) package, i.e. the R CMD check state:

── Failure ('test_zzz.R:64:3'): every shipped dataset is reachable from the namespace ──
`missing` (`actual`) not equal to character() (`expected`).
     actual                | expected
 [1] "amg_h_by_input_type" -
 [2] "animals_codes"       -
 [3] "biomass_coefs"       -
 ... and 90 more ...
[ FAIL 5 | WARN 0 | SKIP 0 | PASS 0 ]

After the fix, same installed-package run:

[ FAIL 0 | WARN 0 | SKIP 0 | PASS 9 ] Done!

and the issue's reproduction script:

rows in whep::polities: 753
resolve_polity_label: OK
add_polity_code: OK
get_polity_geometries: OK
polities in namespace: TRUE

Also run:

  • devtools::test(filter="zzz")FAIL 0 | PASS 9
  • devtools::test(filter="polities|gapfilling|datasets")FAIL 0 | SKIP 1 | PASS 1080
  • rcmdcheck::rcmdcheck(build_args="--no-build-vignettes", args=c("--no-tests","--ignore-vignettes"))
    0 errors | 0 warnings | 0 notes (with _R_CHECK_FORCE_SUGGESTS_=false;
    archive and RSQLite are not installed on this machine)
  • air format ., lintr::lint() on both changed files → no lints
  • No roxygen, no new exported object, no new NSE symbol, so man/,
    _pkgdown.yml and utils::globalVariables() are untouched.

Closes #641.

🤖 Generated with Claude Code

With `LazyData: true` the shipped datasets are lazy-loaded into the
namespace's `.__NAMESPACE__.$lazydata` environment, which `library(whep)`
puts on the search path but which is not in the namespace itself. Package
code is evaluated in the namespace, whose parents are imports -> base ->
globalenv, so a bare reference to `polities` or `polity_area_crosswalk`
only resolved through the search path -- i.e. only when the package was
attached. `whep::f()` from a script without `library(whep)` aborted with
`object 'polities' not found`, and 55 of the 56 datasets are referenced by
bare name somewhere in `R/`.

`.onLoad()` now copies every lazydata name that the namespace does not
already define into the namespace with `delayedAssign()`, so the bindings
stay promises and the data is still loaded on first use only. Existing
namespace objects are never shadowed, and `whep::<dataset>` keeps
returning the same object.

Nothing caught this because `tests/testthat.R` attaches the package,
examples run attached under `R CMD check`, and `pkgload::load_all()` puts
the datasets in the namespace directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbm364dl lbm364dl self-assigned this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exported functions fail when whep is loaded but not attached: LazyData datasets are not in the namespace

1 participant