Skip to content

ENH: two-tier runtime config (permanent set_config + scoped config_context) - #773

Merged
d-chambers merged 3 commits into
devfrom
config-two-tier
Jul 25, 2026
Merged

ENH: two-tier runtime config (permanent set_config + scoped config_context)#773
d-chambers merged 3 commits into
devfrom
config-two-tier

Conversation

@d-chambers

@d-chambers d-chambers commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Second extraction from the free-threaded work (follows #772). It splits the runtime configuration API into two clear tiers so config is safe and predictable under free-threaded (GIL-disabled) execution, and makes Spool.map propagate config to workers.

This supersedes the config portion of the integration PR #763 with a much smaller surface: no generator/async decorator machinery, just a global base plus a ContextVar override.

What changed

  • set_config(...) now sets the process-wide base permanently (visible from every thread and task) and returns the new config. It is no longer a context manager. The read-modify-write is serialized under a lock so concurrent updates can't lose fields.
  • config_context(...) is a new context manager for temporary, thread/task-local overrides. The override lives in a ContextVar, so concurrent overrides in different threads are isolated and never clobber one another, and it restores on block exit.
  • get_config() returns the ContextVar override when set, else the global base.
  • Spool.map(...) captures the config active at the call and re-applies it in each worker (threads, and pickled into processes), so workers observe the caller's config. Previously process-pool workers silently saw defaults — this is strictly better than the old global singleton.

Migration

with dc.set_config(...)with dc.config_context(...). All internal usages, docs, and tests are migrated. Broad-scoped test/benchmark fixtures (session/module) intentionally stay on the permanent tier so their overrides remain visible to worker threads and forked processes — a module-scoped config_context would leak its ContextVar override across modules.

Validation

  • Full suite: 8164 passed, 89 skipped, 2 xfailed.
  • New test_config.py covers: permanent visibility across threads, scoped restore/stacking, concurrent scoped overrides staying isolated, and Spool.map seeing call-time config under both thread and process pools.
  • config.py doctests pass; project pre-commit clean on changed files.
  • Independent Codex review: its findings (unlocked global read-modify-write; a benchmark fixture still using with set_config; runtime-dependent thread-inheritance docs; making the map wrapper's config required) were all addressed.
  • Second review round addressed: unknown config field names now raise (extra="forbid"), the global config lock is reinstalled after a fork so a lock held at fork time cannot deadlock a child, config_context is annotated -> Iterator[DascoreConfig], and the legacy-DASDAE error message shows the full with statement plus the permanent alternative.

Part of the free-threading work; supersedes the config parts of #763.

Changelog

  • added: runtime configuration — dc.set_config(...) sets the process-wide base permanently and returns the new config, and dc.config_context(...) applies thread- and task-local overrides that restore on exit. Spool.map(...) re-applies the caller's config in each worker, and an unknown field name raises.

Checklist

I have (if applicable):

  • referenced the related integration PR.
  • documented the new behavior with docstrings and a changelog entry.
  • included tests. See testing guidelines.
  • added the "ready_for_review" tag once the PR is ready to be reviewed.

Summary by CodeRabbit

  • New Features

    • Added config_context for temporary, thread- and task-local configuration overrides.
    • Updated configuration handling to support persistent global settings and isolated scoped changes.
    • Spool processing now preserves the caller’s active configuration in worker execution.
  • Bug Fixes

    • Improved configuration consistency across concurrent operations and worker pools.
  • Documentation

    • Updated configuration and migration guidance for the new APIs.

…ntext)

Split the runtime configuration API into two clear tiers so it is safe and
predictable under free-threaded execution:

- set_config(...) now sets the process-wide base permanently (visible from
  every thread and task) and returns the new config; it is no longer a
  context manager.
- config_context(...) is a new context manager for temporary, thread/task-
  local overrides. It stores the override in a ContextVar, so concurrent
  overrides in different threads are isolated and never clobber one another,
  and it restores on block exit.
- get_config() returns the ContextVar override when set, else the global base.

Spool.map(...) captures the config active at the call and re-applies it in
each worker via config_context, so both thread- and process-pool workers see
the caller's config (process workers previously saw defaults).

Migrated all internal `with set_config(...)` usages to config_context, and
kept broad-scoped test fixtures (session/module) on the permanent base so
their overrides remain visible to worker threads and forked processes.
…, wrapper

- set_config/reset_config now serialize the global read-modify-write under a
  module lock so concurrent updates cannot lose fields or return another
  thread's config.
- benchmarks/test_io_benchmarks.py module fixture uses permanent save/set/
  restore instead of `with set_config(...)` (no longer a context manager).
- config_context docstring: thread inheritance is runtime-dependent
  (sys.flags.thread_inherit_context), and an inherited copy is not undone on
  block exit.
- _MapFuncWrapper takes config as a required arg (its only caller always
  supplies it), strengthening the call-time binding invariant.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Runtime configuration model

Layer / File(s) Summary
Configuration API and state model
dascore/config.py, dascore/__init__.py
set_config now permanently updates the process-wide configuration, while config_context provides scoped thread/task-local overrides.
Spool worker configuration binding
dascore/utils/misc.py
Spool.map(...) captures the active configuration at call time and reapplies it during worker execution.
Runtime integrations and migration documentation
dascore/examples.py, dascore/io/dasdae/_compat.py, docs/...
Example loading, legacy unpickling guidance, changelog content, and tutorials use or document config_context.
Fixtures and configuration behavior validation
benchmarks/..., tests/...
Fixtures and tests are updated for permanent configuration setters, scoped overrides, concurrency isolation, and worker propagation.

Possibly related PRs

  • DASDAE/dascore#757: Both changes update the legacy DASDAE unpickling configuration gate.
  • DASDAE/dascore#763: Both changes modify the runtime configuration and ContextVar-based scoping behavior.

Suggested labels: documentation

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the new two-tier runtime config API and the switch to permanent set_config plus scoped config_context.
Description check ✅ Passed The PR description includes the required Description, Migration, Validation, and Checklist sections and matches the template well.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch config-two-tier

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the documentation Improvements or additions to documentation label Jul 24, 2026

@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: 9e601e2513

ℹ️ 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 dascore/config.py Outdated


@contextmanager
def config_context(new_config: DascoreConfig | None = None, **kwargs):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Annotate the public config context return type

Because this newly exported public function has no return annotation, type checkers cannot infer that with config_context(...) as config binds a DascoreConfig; annotate the generator return type, such as Iterator[DascoreConfig], to preserve typing for the new public API. .agents/agents.mdL75-L80

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 805ec70config_context is now annotated -> Iterator[DascoreConfig].

Comment thread dascore/config.py
# overrides from `config_context(...)` live in a ContextVar so concurrent
# blocks stay isolated per thread/task and never clobber one another.
_GLOBAL_CONFIG: DascoreConfig = DascoreConfig()
_GLOBAL_CONFIG_LOCK = Lock()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reinitialize the config lock after a fork

On supported POSIX Python versions where a process pool uses fork, if a worker is forked while another thread is inside set_config() or reset_config(), it inherits _GLOBAL_CONFIG_LOCK in the locked state with no surviving owner; any subsequent config mutation in that worker then hangs indefinitely. Register an after-fork handler that replaces this lock in the child, or use a synchronization design that cannot carry a held thread lock across a fork.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. 805ec70 registers os.register_at_fork(after_in_child=_reinit_config_lock), which rebinds _GLOBAL_CONFIG_LOCK to a fresh Lock in the child, so an inherited held lock can no longer wedge config changes there. _GLOBAL_CONFIG itself needs nothing: the rebind in set_config is a single atomic store, so a forked child sees either the old or the new config, never a partial one. Covered by test_fork_handler_replaces_held_lock.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f4fae61) to head (805ec70).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #773   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          164       164           
  Lines        17356     17373   +17     
=========================================
+ Hits         17356     17373   +17     
Flag Coverage Δ
network 48.31% <70.00%> (+0.04%) ⬆️
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
dascore/config.py (1)

182-195: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reject unknown config kwargs in set_config/config_context.

DascoreConfig does not set extra="forbid", and Pydantic v2’s default is extra="ignore", so _build_config() can swallow typo’d overrides like dc.set_config(dispplay_float_precision=5) instead of raising. Add extra="forbid" to DascoreConfig’s model_config and/or validate kwargs against allowed fields before applying them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dascore/config.py` around lines 182 - 195, Update DascoreConfig’s
model_config to forbid extra fields, ensuring unknown keyword overrides passed
through _build_config (including set_config/config_context) raise validation
errors instead of being ignored. Preserve existing valid field overrides and
full DascoreConfig replacement behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dascore/io/dasdae/_compat.py`:
- Line 67: Update the error guidance string in the compatibility handling code
to show the activated context-manager syntax, including the trailing colon: with
dc.config_context(allow_dasdae_format_unpickle=True):. Preserve the existing
option name and surrounding guidance.

---

Nitpick comments:
In `@dascore/config.py`:
- Around line 182-195: Update DascoreConfig’s model_config to forbid extra
fields, ensuring unknown keyword overrides passed through _build_config
(including set_config/config_context) raise validation errors instead of being
ignored. Preserve existing valid field overrides and full DascoreConfig
replacement behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b5887e2f-5002-4f07-8acc-2eb06c64d09f

📥 Commits

Reviewing files that changed from the base of the PR and between f4fae61 and 9e601e2.

📒 Files selected for processing (26)
  • benchmarks/test_io_benchmarks.py
  • dascore/__init__.py
  • dascore/config.py
  • dascore/examples.py
  • dascore/io/dasdae/_compat.py
  • dascore/utils/misc.py
  • docs/changelog.qmd
  • docs/tutorial/configuration.qmd
  • docs/tutorial/patch.qmd
  • docs/tutorial/remote_patches.qmd
  • tests/conftest.py
  • tests/test_io/test_dasdae/test_dasdae.py
  • tests/test_io/test_index/test_index_edge_cases.py
  • tests/test_io/test_index/test_plan.py
  • tests/test_io/test_indexer.py
  • tests/test_io/test_io_core.py
  • tests/test_io/test_remote_common_io.py
  • tests/test_io/test_remote_http.py
  • tests/test_io/test_remote_memory.py
  • tests/test_proc/test_rolling.py
  • tests/test_utils/test_config.py
  • tests/test_utils/test_display.py
  • tests/test_utils/test_downloader.py
  • tests/test_utils/test_io_utils.py
  • tests/test_utils/test_patch_utils.py
  • tests/test_utils/test_progress.py

Comment thread dascore/io/dasdae/_compat.py Outdated
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

✅ Documentation built:
👉 Download
Note: You must be logged in to github and a DASDAE member to access the link.

- Set extra="forbid" on DascoreConfig so misspelled overrides raise
  instead of being silently dropped.
- Reinstall the config lock after a fork so a lock held by another
  thread at fork time cannot deadlock config changes in the child.
- Annotate config_context's generator return type.
- Show the full with-statement (and the permanent alternative) in the
  legacy DASDAE unpickle error message.
- Document the two tiers in the configuration tutorial.
- Expose the conftest permanent-config helper as a fixture so the
  remote common-IO module fixture stops hand-rolling save/restore, and
  have reset_config delegate to set_config.
@d-chambers

Copy link
Copy Markdown
Contributor Author

Addressed the second review round in 805ec70:

  • extra="forbid" on DascoreConfig (CodeRabbit nitpick) — typo'd overrides such as set_config(dispplay_float_precision=5) now raise instead of being silently ignored. Changelog notes the behavior change; new test test_unknown_field_raises.
  • Fork-safe config lock (Codex P2) — os.register_at_fork(after_in_child=...) reinstalls _GLOBAL_CONFIG_LOCK in the child so a lock held at fork time cannot deadlock config changes there. New test test_fork_handler_replaces_held_lock.
  • Iterator[DascoreConfig] return annotation on config_context (Codex P1).
  • Error message for legacy DASDAE unpickling now shows the full with dc.config_context(...): statement plus the permanent dc.set_config(...) alternative (CodeRabbit).
  • Minor tidy while in here: reset_config delegates to set_config, the conftest permanent-config helper is exposed as a permanent_config fixture so test_remote_common_io no longer hand-rolls save/restore, dascore/utils/misc.py config imports moved to module scope, and the configuration tutorial documents the two tiers.

Local validation: full suite 8166 passed / 89 skipped / 2 xfailed; doctests for config.py, utils/misc.py, examples.py pass; pre-commit clean on changed files.

@d-chambers d-chambers added the ready_for_review PR is ready for review label Jul 25, 2026
@d-chambers
d-chambers merged commit 1cb682f into dev Jul 25, 2026
29 checks passed
@d-chambers
d-chambers deleted the config-two-tier branch July 25, 2026 10:53
@d-chambers d-chambers removed the ready_for_review PR is ready for review label Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant