civilytics_load_fonts() always errors: '<<-' to a locked namespace binding #18
Closed
opened 2026-07-28 10:58:25 -04:00 by jared
·
1 comment
No Branch/Tag Specified
Labels
Clear labels
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
question
wontfix
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
Something isn't working
Improvements or additions to documentation
This issue or pull request already exists
New feature or request
Good for newcomers
Extra attention is needed
This doesn't seem right
Kodor should process this issue
Kodor has written a feature proposal
Kodor should implement a fix (assigned to Kodor)
Kodor's work or failure needs Jared's review
Kodor has already triaged this issue (skip)
Further information is requested
This will not be worked on
needs
human
Cannot move without a person -- a decision, a check an agent cannot make, something outside the repo
origin
client
Came from a client ask
origin
obligation
Created by a change elsewhere
origin
review
Came from human review
origin
roborev
Promoted from a roborev finding
type
chore
Maintenance with no behaviour change
type
debt
Owed work -- docs, tests, cleanup a change obligated
type
decision
Needs a decision before work can proceed
type
defect
Something is wrong
type
feature
New capability
ws
helpers
Analysis and workflow helpers
ws
logo
Logo and branded output composition
ws
packaging
Package infrastructure and release
ws
quarto
Quarto themes and publishing templates
ws
theme
Themes, palettes, and fonts
Assign a task to kodor
Kodor thinks this needs a feature.
Kodor should fix this
Kodor thinks the user is ready to review this.
Kodor is done with this issue.
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Civilytics/civilyticsR#18
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
civilytics_load_fonts()errors on every call:The fonts themselves load fine — the failure is the last statement in the
function, so
showtext_auto()has already run by the time it throws. But becausethe error propagates, any script that calls it dies, and any script that sources
a themes file calling it dies too.
Reproduction
Reproduces in a plain
Rscript -e, insidesource(), and interactively. It isnot conditional on anything in the calling environment.
Cause
The function ends with:
.cv_fonts_loadedis a package-level object, so when the namespace seals at loadtime R locks both the environment and its bindings.
<<-resolves to that lockedbinding and cannot write to it.
Minimal demonstration of the mechanism, and of the fix:
Locking applies to the binding, not to the contents of an environment the
binding points at — which is why the environment-based idiom is the standard way
to hold mutable package state.
The flag is never read
Worth deciding before fixing:
.cv_fonts_loadedis not exported, and no functionin the package reads it.
civilytics_load_fonts()is the only reference:So it reads as a guard flag whose read side was never written (or was removed).
That gives two reasonable fixes.
Suggested fix
Option A — drop the line. If idempotency was never actually wanted, the
one-line fix is to delete
.cv_fonts_loaded <<- TRUEand the object it writes to.sysfonts::font_add_google()is already safe to call repeatedly.Option B — make the guard real. If the intent was to avoid re-downloading
fonts on every call, hold the state in an environment and actually check it:
I'd lean to B, since
font_add_google()hits the network and the guard thenearns its keep.
Two adjacent notes
font_add_google()requires network access. On a machine that already hasInter / Libre Franklin / Source Serif 4 / JetBrains Mono installed system-wide,
the download is unnecessary — ggplot2 resolves them by family name via
systemfonts. Falling back to the installed copy whensystemfonts::system_fonts()already has the family would make the functionwork offline and finish faster.
showtext_auto()is a global side effect. It switches all device textrendering to showtext for the rest of the session, which changes how
ggsave()output looks in unrelated plots. Worth documenting on the helppage, or gating behind an argument.
Workaround
Wrapping the call makes it non-fatal, since the fonts are registered before the
error is raised:
Environment
Follow-up after digging further — the impact is narrower than the original report implies, but sharper, and it lands on the recovery path the package itself recommends.
Why
.onLoadgets away with it.onLoad()callscivilytics_load_fonts()and it succeeds, because R seals anamespace after
.onLoadruns. The<<-therefore writes fine at load time:So on a normal machine nothing looks wrong: the fonts register, the flag is set,
.civilytics_fonts_failedstaysFALSE, and no startup message appears. Only asubsequent, manual call hits the locked binding.
The bite
That subsequent manual call is exactly what the package tells users to make.
.onLoadwraps the call intryCatchand sets a flag;.onAttachthen says:For an offline user the sequence is:
.onLoad→font_add_google()fails on no network →tryCatchcatches it →.civilytics_fonts_failed = TRUE.onAttachprints the message abovecivilytics_load_fonts()→ fonts register fine → then dies oncannot change value of locked binding for '.cv_fonts_loaded'So the documented remedy fails with an error that says nothing about fonts or
networks. Worth noting the fonts are registered by the time it throws — the
error is the last statement — so
try(civilytics_load_fonts(), silent = TRUE)genuinely works as a workaround.
Scope of the fix
<<-appears exactly once in the package, so this is isolated to the one function:Suggested addition to the fix
Beyond moving the flag into an environment (Option B in the original report), the
.onLoadhandler is worth tightening: it currently attributes any error to aGoogle Fonts failure. Distinguishing a genuine network failure from an internal
one would have surfaced this immediately rather than hiding it behind a
misleading message —