Skip to content

GDAL Configuration System (#8) - #15

Merged
jimbrig merged 5 commits into
developfrom
feature/gdal_config
Jul 2, 2026
Merged

jimbrig merged 5 commits into
developfrom
feature/gdal_config

Conversation

@jimbrig

@jimbrig jimbrig commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the stateful GDAL configuration system designed in #8, structured as three tiers with GDAL itself as the single source of truth (nothing mirrored):

  • Value - gdal_config(): an inert, composable bundle of a gdal_config_opts payload (global channel) + path-bound gdal_vsi_opts (VSI channel), with full S3 conventions (new_gdal_config(), as_gdal_config() coercions, c() merge with later-wins semantics, cli format()/print()). as_gdal_args() renders the global channel as --config tokens and omits path-bound VSI options with a message (they cannot ride the CLI); flatten_vsi = TRUE opts into lossy flattening with a conflict warning.
  • State - gdal_config_set()/get()/unset()/reset() apply values to GDAL (set_config_option() / vsi_set_path_option()), track the session's active configuration (gdal_config_active()), and record priors on first touch so reset() always restores the true pre-package state. local_gdal_config()/with_gdal_config() provide exact scoped application (GDAL values and package state restored, even on error) - the recommended pattern around pipeline executions. gdal_config_unset(mode = c("reveal", "mask", "scrub")) makes the envvar-fallback asymmetry explicit (set is authoritative; unset merely reveals).
  • View - gdal_config_sitrep(): live provenance report (gdalvector / envvar / config_file / external / unset per key), probing the full known-option universe; an at_load baseline sitrep is stashed at package load ([Feature]: gdal_config_sitrep() - Configuration Situational Report #12, initial scope).

Also included:

  • gdalrc I/O ([Feature]: GDAL Configuration File (gdalrc) I/O #13): gdal_config_file_read()/write() with the full grammar ([configoptions], [directives], [credentials] with [.subsection]/path bindings -> gdal_vsi_opts values); as_gdal_config() bridges a parsed file into the configuration system; gdal_config_file() discovers/parses the file GDAL loaded at init. The config file is the only lossless serialization of a full configuration (path-bound credentials have no CLI representation).
  • Known options without unbacked hardcoded lists: runtime enumeration of VSI/network/credential options from the running GDAL build (.vsi_fs_options_tbl()), the curated per-driver config metadata, and a small curated GDAL_CORE_CONFIG_OPTS core list (the only part GDAL does not expose at runtime). Advisory (classed, non-blocking) unknown-key warnings.
  • Load-time defaults: fill-only, tracked, reversible - currently just GDAL_HTTP_USERAGENT (gdalvector/x.y.z), never overriding user env/config, opt-out via options(gdalvector.config_defaults = FALSE).
  • Cross-cutting: gdal_*_config() conditions, is_gdal_config*() predicates, shared cli_redact() (secrets never printed), corrected precedence notes in dev/ref/gdal_config_nuances_examples.R.

Closes #8. Closes #13. Part of #12 (initial scope; per-prefix credential resolution and diffing follow). Groundwork for #14 (presets compose via c.gdal_config).

Test plan

  • 549 tests passing (0 fail / 0 warn / 0 skip), including: value-class construction/coercion/merge, channel separation (path-bound vs unbound VSI), envvar asymmetry (override/reveal/scrub/mask), reset restore semantics for all prior states, scoped application incl. error paths and state restoration, sitrep provenance + redaction, gdalrc read/write round-trip incl. credentials, fill-only defaults + opt-out, unknown-key advisories, rejection of open/creation opts.
  • devtools::document() clean; NAMESPACE regenerated.

jimbrig added 2 commits July 2, 2026 13:38
Three-tier design with GDAL as the single source of truth:

- value: gdal_config() - composable bundle of global config opts +
  path-bound VSI opts, with constructor/coercions/c() merge/cli print
  and as_gdal_args() rendering (vsi omitted with message; flatten_vsi
  escape hatch with conflict warning)
- state: gdal_config_set/get/unset/reset + gdal_config_active();
  priors recorded on first touch for exact restore; unset modes
  (reveal/mask/scrub) encode the envvar-fallback asymmetry; scoped
  local_/with_gdal_config() restore values and state even on error
- view: gdal_config_sitrep() live provenance report (#12 initial
  scope) with at-load baseline stash

Also: gdalrc I/O (#13) with full grammar incl. [credentials] path
bindings and as_gdal_config() bridge; runtime-derived known-option
universe (VSI fs options + driver metadata + curated core list);
fill-only tracked GDAL_HTTP_USERAGENT load default with opt-out;
config conditions/predicates and shared cli_redact().

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a comprehensive GDAL configuration management system, including verbs for setting, getting, unsetting, and resetting configurations, scoped configurations, situational reports, and gdalrc file I/O. Feedback on the changes highlights that the internal .gdalrc_entries function introduces an undeclared dependency on the tidyr package, and suggests a dependency-free alternative using base R and already-imported packages.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread R/gdal_config_file.R
Comment on lines +253 to +281
.gdalrc_entries <- function(path) {
tibble::tibble(raw = stringr::str_trim(readLines(path, warn = FALSE))) |>
dplyr::filter(nzchar(.data$raw), !stringr::str_starts(.data$raw, stringr::fixed("#"))) |>
dplyr::mutate(
is_subsection = stringr::str_detect(.data$raw, "^\\[\\..+\\]$"),
is_section = !.data$is_subsection & stringr::str_detect(.data$raw, "^\\[.+\\]$"),
section = dplyr::if_else(
.data$is_section,
tolower(stringr::str_remove_all(.data$raw, "^\\[|\\]$")),
NA_character_
),
subsection = dplyr::if_else(
.data$is_subsection,
stringr::str_remove_all(.data$raw, "^\\[\\.|\\]$"),
NA_character_
)
) |>
tidyr::fill("section") |>
dplyr::group_by(.data$section) |>
tidyr::fill("subsection") |>
dplyr::ungroup() |>
dplyr::filter(!.data$is_section, !.data$is_subsection, stringr::str_detect(.data$raw, stringr::fixed("="))) |>
tidyr::separate_wider_delim("raw", "=", names = c("key", "value"), too_many = "merge") |>
dplyr::mutate(
key = stringr::str_trim(.data$key),
value = stringr::str_trim(.data$value)
) |>
dplyr::select("section", "subsection", "key", "value")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The internal .gdalrc_entries function introduces a dependency on the tidyr package (specifically tidyr::fill and tidyr::separate_wider_delim). However, tidyr is not imported in the package's NAMESPACE, which could lead to runtime failures if the package is used in an environment where tidyr is not installed. Even if it is installed, adding a heavy dependency like tidyr for a single internal helper function is unnecessary. We can easily replace these operations with lightweight, dependency-free base R and dplyr/stringr equivalents (which are already imported and used).

.gdalrc_entries <- function(path) {
  fill_down <- function(x) {
    if (length(x) <= 1L) return(x)
    Reduce(function(a, b) if (is.na(b)) a else b, x, accumulate = TRUE)
  }

  tibble::tibble(raw = stringr::str_trim(readLines(path, warn = FALSE))) |>
    dplyr::filter(nzchar(.data$raw), !stringr::str_starts(.data$raw, stringr::fixed("#"))) |>
    dplyr::mutate(
      is_subsection = stringr::str_detect(.data$raw, "^\\[\\..+\\]$"),
      is_section = !.data$is_subsection & stringr::str_detect(.data$raw, "^\\[.+\\]$"),
      section = dplyr::if_else(
        .data$is_section,
        tolower(stringr::str_remove_all(.data$raw, "^\\[|\\]$")),
        NA_character_
      ),
      subsection = dplyr::if_else(
        .data$is_subsection,
        stringr::str_remove_all(.data$raw, "^\\[\\.|\\]$"),
        NA_character_
      )
    ) |>
    dplyr::mutate(section = fill_down(section)) |>
    dplyr::group_by(.data$section) |>
    dplyr::mutate(subsection = fill_down(subsection)) |>
    dplyr::ungroup() |>
    dplyr::filter(!.data$is_section, !.data$is_subsection, stringr::str_detect(.data$raw, stringr::fixed("="))) |>
    dplyr::mutate(
      key = stringr::str_trim(sub("^([^=]+)=.*$", "\\1", .data$raw)),
      value = stringr::str_trim(sub("^[^=]+=(.*)$", "\\1", .data$raw))
    ) |>
    dplyr::select("section", "subsection", "key", "value")
}

Copilot AI 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.

Pull request overview

Implements a three-tier GDAL configuration system for gdalvector, providing (1) inert, composable configuration values (gdal_config), (2) stateful verbs to apply/restore configuration in-process, and (3) a provenance “sitrep” view plus gdalrc read/write support.

Changes:

  • Adds gdal_config value class (global + path-bound VSI channels), rendering (as_gdal_args()), composition (c()), and coercions.
  • Introduces stateful configuration verbs (set/get/unset/reset), scoped helpers (local_gdal_config() / with_gdal_config()), and a provenance report (gdal_config_sitrep()), including load-time defaults.
  • Adds gdalrc I/O (gdal_config_file_read()/write()/discover()) and extensive test coverage.

Reviewed changes

Copilot reviewed 10 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/testthat/test-gdal_config.R Adds comprehensive tests for value/state/view tiers, envvar modes, gdalrc round-tripping, and defaults.
R/zzz.R Runs config defaults initialization at package load after driver metadata init.
R/utils_predicates.R Expands predicate docs; updates is_vsi_path() behavior and adds config-related predicates.
R/utils_cli.R Adds cli_redact() utility for key-based secret redaction in printed output.
R/gdalvector-conditions.R Adds config-scoped condition wrappers (gdal_*_config).
R/gdal_vsi.R Adds lazy runtime enumeration/caching of VSI option metadata (.vsi_fs_options_tbl()).
R/gdal_config.R Implements the gdal_config system: value class, stateful verbs, scoped helpers, sitrep, defaults, and internal bookkeeping.
R/gdal_config_file.R Adds gdalrc parsing/serialization/discovery and coercion to gdal_config.
R/aaa.R Adds curated GDAL_VSI_PREFIXES and GDAL_CORE_CONFIG_OPTS lists for known-option coverage.
NAMESPACE Exports new APIs and registers new S3 methods.
man/*.Rd Adds/updates generated documentation for new config system APIs and predicates.
Files not reviewed (10)
  • man/as_gdal_args.Rd: Generated file
  • man/as_gdal_config_opts.Rd: Generated file
  • man/gdal_config.Rd: Generated file
  • man/gdal_config_active.Rd: Generated file
  • man/gdal_config_file.Rd: Generated file
  • man/gdal_config_set.Rd: Generated file
  • man/gdal_config_sitrep.Rd: Generated file
  • man/is_vsi_path.Rd: Generated file
  • man/predicates.Rd: Generated file
  • man/with_gdal_config.Rd: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/utils_predicates.R
Comment on lines 57 to 63
is_vsi_path <- function(x) {
if (!is.character(x) || length(x) != 1L || !nzchar(x)) {
return(FALSE)
}
all(startsWith(x, "/vsi"), grepl("^/vsi[a-z0-9_]+/", x, ignore.case = TRUE))
# all(startsWith(x, "/vsi"), grepl("^/vsi[a-z0-9_]+/", x, ignore.case = TRUE))
any(startsWith(x, GDAL_VSI_PREFIXES))
}
Comment thread R/utils_predicates.R Outdated
Comment thread R/utils_predicates.R Outdated
Comment thread R/gdal_config_file.R
Comment on lines +98 to +101
gdal_config_file_read <- function(path) {
check_path(path)
entries <- .gdalrc_entries(path)

Comment thread R/gdal_config.R Outdated
Comment on lines +628 to +631
#' Coverage spans keys pinned by the package, GDAL-relevant environment variables, config-file
#' entries, and a probe of the full known-option universe (runtime VSI metadata + driver metadata
#' + curated core names), so externally set in-memory values for any documented key are discovered
#' too. Secret-bearing values are redacted in printed output. A baseline sitrep taken at package
jimbrig added 2 commits July 2, 2026 14:00
- terminate the predicates roxygen topic with NULL so it no longer
  absorbs is_int64() (undocumented-argument warning in predicates.Rd)
- fix two typos (caes/Inheritence) and update inst/WORDLIST

R CMD check: 0 errors, 0 warnings, 0 notes
…tions

- restore pattern-based is_vsi_path() (hardcoded prefix list was
  case-sensitive and missed 19 of the 32 handlers the running build
  registers); GDAL_VSI_PREFIXES stays as the curated common set
- gdal_config_file_read() errors immediately (classed) on a directory
- fix sitrep roxygen line misparsed as a markdown list item
- per-function @importFrom tags for all ::-used externals (rlang
  pronouns/operators stay package-wide) and @returns content on the
  line below the tag, per package conventions
@jimbrig

jimbrig commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Review comments triaged (4df3393 + this commit):

Addressed:

  • Copilot / is_vsi_path(): valid catch - the hardcoded GDAL_VSI_PREFIXES check was case-sensitive and incomplete (the running GDAL 3.13 build registers 32 prefixes vs the curated 13, e.g. /vsis3_streaming/, /vsioss/, /vsiwebhdfs/, /vsi7z/). Restored the pattern-based check (^/vsi[a-z0-9_]+/, case-insensitive); GDAL_VSI_PREFIXES remains as the curated common set and gdalraster::vsi_get_fs_prefixes() is the runtime enumeration.
  • Copilot / gdal_config_file_read() on a directory: now errors immediately with a classed gdal_config_file_error instead of failing later in readLines().
  • Copilot / sitrep roxygen + line: rewrapped so markdown no longer interprets it as a list item (malformed \itemize{} in the generated Rd).
  • Copilot / doc typos (caes, Virtual File System's): fixed (the former already in 4df3393).

Rejected:

  • gemini-code-assist / "undeclared tidyr dependency": factually incorrect - tidyr has been in Imports since before this PR (used by gdal_drivers metadata assembly) and the helper now carries explicit @importFrom tidyr fill separate_wider_delim tags. The suggested hand-rolled Reduce()-based fill_down() replacement is strictly worse than the imported, tested tidyr::fill(); tidyverse packages are deliberately first-class dependencies in this package.

Also in this commit: a style pass aligning the new config files with package roxygen conventions (per-function @importFrom for every ::-used external function except the package-wide rlang pronouns/operators, @returns content on the line below the tag).

The commented block in gdal_sitrep.R listed various functions from gdalraster, sf, terra, vapour, and geos. This list appears to be a vestige of early development and is no longer needed, improving code hygiene.
@jimbrig
jimbrig merged commit e991fcc into develop Jul 2, 2026
6 of 7 checks passed
@jimbrig
jimbrig deleted the feature/gdal_config branch July 2, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: GDAL Configuration File (gdalrc) I/O [Feature]: GDAL Configuration

2 participants