Skip to content

fix: make ggExpress an optional (Suggests) dependency for qbarplot() - #46

Merged
vertesy merged 3 commits into
devfrom
fix/qbarplot-undeclared-dependency
Aug 28, 2026
Merged

fix: make ggExpress an optional (Suggests) dependency for qbarplot()#46
vertesy merged 3 commits into
devfrom
fix/qbarplot-undeclared-dependency

Conversation

@vertesy

@vertesy vertesy commented Aug 28, 2026

Copy link
Copy Markdown
Owner

What was the bug?

df.remove.empty.rows.and.columns() (an exported function) calls bare qbarplot(...) when plot_stats = TRUE. qbarplot is defined in the ggExpress package, which is not declared anywhere in DESCRIPTION (Depends: Stringendo only).

This is the most severe of the three undeclared-dependency bugs found in this audit batch, because plot_stats defaults to TRUE — the crash fires on ordinary, argument-free use, not just an opt-in code path.

Why couldn't ggExpress just become a normal Import?

Per the package's own README.md, ggExpress is downstream of CodeAndRoll2 — it's built on top of it, not the other way around. Declaring it as a hard Depends/Imports here would create a circular package dependency (CodeAndRoll2ggExpressCodeAndRoll2), which R does not support cleanly.

The fix

Made ggExpress a soft, optional dependency:

  • Guard the call: if (requireNamespace("ggExpress", quietly = TRUE)) { ggExpress::qbarplot(...) } else { message(...) } — if ggExpress is installed, the plot renders exactly as before; if not, the function now prints one informative message and skips the plot instead of crashing.
  • Declared ggExpress under Suggests: in DESCRIPTION (and the upstream Development/config.R source it's generated from) — Suggests doesn't force install order and carries no circularity risk.
  • Updated the roxygen docs for plot_stats and ... to note the optional dependency.

What was the impact of the old (wrong) behavior?

Any call to df.remove.empty.rows.and.columns(df) — with no other arguments, since plot_stats = TRUE is the default — threw:

Error in qbarplot(...) : could not find function "qbarplot"

unless the caller happened to have ggExpress separately installed and attached.

Testing

df <- data.frame(a = c(1,0,3), b = c(0,0,0), c = c(4,5,6), row.names = c("r1","r2","r3"))
df.remove.empty.rows.and.columns(df)  # ggExpress not installed in the test environment

Before: Error in qbarplot(...) : could not find function "qbarplot".
After:

[1] "33.3% or 1 of 3 cols are empty/removed."
[1] "0% or 0 of 3 rows are empty/removed."
Package 'ggExpress' is not installed; skipping the removal-fraction plot (plot_stats = TRUE). Install it with remotes::install_github('vertesy/ggExpress') to enable this plot.
   a c
r1 1 4
r2 0 5
r3 3 6

— completes successfully with a clear message instead of crashing, and the data-frame-filtering logic (the function's core job) is unaffected either way.

  • R CMD build + R CMD check (with _R_CHECK_FORCE_SUGGESTS_=false, since the sandbox doesn't have the now-optional ggExpress installed — the same env var a maintainer would use for any newly-added optional Suggests not yet present locally): the "no visible global function definition for 'qbarplot'" NOTE is gone; no new NOTEs/WARNINGs/ERRORs introduced by this change (remaining output is pre-existing and unrelated — the other two undeclared-dependency crashes tracked in separate PRs, and long-standing missing base-package importFrom entries).
  • Regenerated man/df.remove.empty.rows.and.columns.Rd via roxygen2::roxygenise(".").
  • No testthat/automated tests added, per repo convention.

Generated by Claude Code

df.remove.empty.rows.and.columns() called bare qbarplot(), which is
defined in the ggExpress package -- not a declared dependency of
CodeAndRoll2 (DESCRIPTION only declares Depends: Stringendo). Unlike
the other two undeclared-dependency bugs fixed in this batch, this
one crashed on the function's own *default* arguments
(plot_stats = TRUE by default), not just an opt-in code path, making
it the most severe of the three.

ggExpress cannot simply become a hard Import here: per the package's
own README, ggExpress is downstream of CodeAndRoll2 (built on top of
it), so a hard dependency in this direction would create a circular
package dependency.

Fixed by guarding the call with
requireNamespace("ggExpress", quietly = TRUE): if ggExpress is
installed, the plot renders exactly as before via
ggExpress::qbarplot(...); if not, the function now prints an
informative message and skips the plot instead of erroring. Declared
ggExpress under Suggests in DESCRIPTION/config.R (a soft dependency
that doesn't force install order or risk circularity) and updated
the roxygen docs to note this.

Verified: df.remove.empty.rows.and.columns(df) with default arguments
(ggExpress not installed in the test environment) now completes
successfully with a clear message, instead of
"could not find function 'qbarplot'".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15d220edc7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Development/config.R
vertesy pushed a commit that referenced this pull request Aug 28, 2026
Per repo convention (AGENTS.md: bump Development/config.R by 0.0.1 for
a substantial code change) and reviewer feedback, so this fix is
distinguishable by version from the broken 2.8.1 release.

Note: this PR and its siblings in the same batch (#45, #46, #47, #48,
#49) each independently bump from the same 2.8.1 base to keep them
mergeable in any order; whichever merges first "claims" 2.8.2, and
the others may need a trivial one-line renumber at merge time
depending on actual merge order.
vertesy pushed a commit that referenced this pull request Aug 28, 2026
Per repo convention (AGENTS.md: bump Development/config.R by 0.0.1 for
a substantial code change) and reviewer feedback.

Note: this PR and its siblings in the same batch (#44, #46, #47, #48,
#49) each independently bump from the same 2.8.1 base with a distinct
target version to keep them mergeable in any order; a trivial one-line
renumber may be needed at merge time depending on actual merge order.
Per repo convention (AGENTS.md: bump Development/config.R by 0.0.1 for
a substantial code change) and reviewer feedback.

Note: this PR and its siblings in the same batch (#44, #45, #47, #48,
#49) each independently bump from the same 2.8.1 base with a distinct
target version to keep them mergeable in any order; a trivial one-line
renumber may be needed at merge time depending on actual merge order.
vertesy pushed a commit that referenced this pull request Aug 28, 2026
…ions

Follow-up to the seq_along/seq_len sweep in this PR: reorder.list()
and intermingle2lists() both initialized their accumulator as
list(NA) (a length-1 sentinel list). Previously, the 1:length(x)
version of their loop bound would crash on zero-length input before
this sentinel value could ever leak into the return value. Now that
the loop correctly does nothing for zero-length input
(seq_along(character(0)) / seq_len(0) both being empty), the
uninitialized sentinel list(NA) was returned as-is instead of the
correct empty list().

Fixed by initializing Lout <- list() in both functions -- list
elements are still assigned via Lout[[x]] <- ..., which auto-extends
a list on out-of-bounds assignment, so normal (non-empty) behavior is
unaffected.

Verified:
- intermingle2lists(list(a=1,b=2), list(c=3,d=4)) unchanged.
- intermingle2lists(list(), list()) now returns list() instead of
  list(NA).
- reorder.list(list(b=2,a=1,c=3)) unchanged.
- reorder.list(list(), namesOrdered = character(0)) now returns
  list() instead of list(NA).

Also bumps Development/config.R/DESCRIPTION to 2.8.5 per repo
convention and reviewer feedback (a distinct number from sibling
PRs #44/#45/#46/#48/#49 in this batch, all bumping from the same
2.8.1 base, so they stay mergeable in any order).
vertesy pushed a commit that referenced this pull request Aug 28, 2026
Reviewer feedback on the previous fix: CodeAndRoll2::zero.omit only
works if the package has been formally installed and its namespace
can be loaded. But README.md explicitly documents an alternative,
supported installation method -- sourcing R/CodeAndRoll2.R directly
(no install) -- and in that mode there is no "CodeAndRoll2" namespace
for :: to resolve against, so both functions would still crash with
zero.omit = TRUE for anyone using that method.

Replaced CodeAndRoll2::zero.omit with
get("zero.omit", mode = "function"). get()'s mode argument makes it
skip non-function bindings while searching up the enclosing scope
chain -- so it walks straight past the local logical zero.omit
argument (the actual shadowing problem) and finds the real function,
regardless of whether it's sitting in a loaded package's namespace or
in globalenv() from a direct source() call. Verified both scenarios:

- Package loaded via library(CodeAndRoll2): unchanged, works as
  before.
- File sourced directly (source("R/CodeAndRoll2.R"), package not
  installed/attached): as.list.df.by.col(dtf, zero.omit = TRUE) now
  also works, where CodeAndRoll2::zero.omit would have failed with
  "could not find function" (no such namespace loaded).

Also bumps Development/config.R/DESCRIPTION to 2.8.6 per repo
convention and reviewer feedback (a distinct number from sibling
PRs #44/#45/#46/#47/#49 in this batch, all bumping from the same
2.8.1 base, so they stay mergeable in any order).
vertesy pushed a commit that referenced this pull request Aug 28, 2026
Per repo convention (AGENTS.md: bump Development/config.R by 0.0.1 for
a substantial code change) -- applying proactively, matching the same
reviewer feedback already given on sibling PRs #44/#45/#46/#47/#48.

Note: this PR and its siblings in the same batch each independently
bump from the same 2.8.1 base with a distinct target version to keep
them mergeable in any order; a trivial one-line renumber may be
needed at merge time depending on actual merge order.
@vertesy

vertesy commented Aug 28, 2026

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: vertesy <5101911+vertesy@users.noreply.github.com>

Copilot AI commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts by merging dev into this branch in commit 84df5c4. Kept dev's current version/signature changes and preserved this PR's optional ggExpress fix.

@vertesy
vertesy merged commit 5c9694b into dev Aug 28, 2026
@vertesy
vertesy deleted the fix/qbarplot-undeclared-dependency branch August 28, 2026 15:54
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.

3 participants