Skip to content

refactor(settings): move the settings mechanism into the framework; SM009 catches dynamic imports (#500) - #564

Merged
antosubash merged 11 commits into
mainfrom
refactor/settings-framework-500
Oct 21, 2026
Merged

antosubash merged 11 commits into
mainfrom
refactor/settings-framework-500

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

Closes #500.

The framework couldn't boot without the settings plugin, but reached into it only through importlib strings and raw SQL that SM009 couldn't see. Importing the plugin normally wasn't an option, because settings already depends on simple_module_hosting. So the mechanism moves down into the framework, and the plugin keeps the admin UI and the write path.

What Now in
register_module_settings, ModuleSettingsRegistry simple_module_core.module_settings
Hydration (env → DB → default), field helpers, OverrideSource protocol simple_module_core.settings_hydrate
Secret-field masking simple_module_core.secret_fields
Setting model, table and column constants, read-only override queries, hydrate_settings_sync simple_module_db.stored_settings
  • Hosting: _lifespan, _hydrate_step and _settings_registration import statically. _preapp_config and _secret_key build their SQL from the shared column constants instead of f-strings.
  • No migration: the settings_setting table is unchanged. The migration environment and the test schema collect it explicitly, so it exists even when the plugin is turned off.
  • With settings disabled: registration, hydration and secret-key storage now keep working; only the admin pages go away. A new test covers a boot without the plugin.
  • SM009: it now flags importlib.import_module, import_module, __import__ and find_spec calls whose literal name, or fixed f-string prefix, is a plugin package. Aliased imports count too. The repo itself has 0 hits.
  • Back-compat:
    • settings.registration, settings.module_registry, settings.hydrate, settings.models, settings._secrets and the moved names in store and constants re-export the same objects. There is exactly one Setting class.
    • The first three emit a DeprecationWarning, to be removed no earlier than 0.1.0.
    • hydrate_settings keeps its store= keyword.
  • In-repo callers: the 7 modules that reached register_module_settings through importlib now import it normally. Everything in the repo uses the new paths.
  • Scaffold: the make new-module and smpy templates now use DbBackedSettings plus register_module_settings.
    • Behaviour change: newly scaffolded modules no longer read their settings from env vars, per the repo's "configuration lives in the DB" rule.
  • Other behaviour changes (in the CHANGELOG):
    • The pre-app settings read now always runs. Outside development, an unreachable database refuses the boot even when the plugin isn't installed.
    • An out-of-tree host that never installed settings will see autogenerate emit settings_setting.

Decisions from review

  • Secret key without the plugin: a missing settings table still means "fresh install, use an ephemeral key", now even without the plugin. This matches main's behaviour when the plugin is installed and the table is missing.
  • Left as is:

Verification

  • Local CI:
    • make lint passes.
    • make test-py: 4995 passed, 22 skipped.
    • make test-js: 668 passed.
    • alembic check reports no new operations; its only output is a users_access_token.expires_at type difference that main reports too.
    • A live boot with Settings, Branding and Mail disabled served /health 200.
    • Flaky test: test_bg_outbox_eager_platform.py::test_eager_publish_from_a_plain_session_is_delivered failed once in each of two earlier full runs, with a 3 s wait_for timeout. It passed on its own 3 out of 3 times and in the later full runs, so it looks load-related, but that isn't proven.
  • Code review: clean after 2 passes. A Codex review found no regressions.
  • Browser QA of /admin/settings/ and /admin/doctor/: 9 passed, 0 P0–P2. It covered all 9 module cards, an edit that persists after reload, SMTP password masking, field errors on invalid values, and Doctor reporting no SM009. There were two P3 observations, both of which already happen on main: public_base_url accepts a non-URL, and a raw override accepts an unknown key.
  • Reports:

Report links are Claude Artifacts and start private — the author can share them from claude.ai/code/artifacts if reviewers need access.

Test plan

  • CI is green, including the migrations round trip (alembic check)
  • /admin/settings/ edits still persist, and secrets stay masked

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

…work (#500)

register_module_settings and ModuleSettingsRegistry move to
simple_module_core.module_settings (registry created lazily on app.state),
hydration to simple_module_core.settings_hydrate behind an OverrideSource
protocol, secret-field masking to simple_module_core.secret_fields, and the
Setting model, its constants, the read-only override queries and
hydrate_settings_sync to simple_module_db.stored_settings. The table and its
metadata owner are unchanged; no migration.

The hosting layer imports these statically: the lifespan hydrates through a
session-backed source, host settings always register, and the pre-app read
and secret-key bootstrap build their SQL from the shared column constants.
Boot works with the settings plugin disabled. build_module_metadata and the
test schema collect settings_setting without the plugin.

The plugin keeps its write path, admin UI and reload; its old modules
re-export the moved objects (registration, module_registry and hydrate with a
DeprecationWarning).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…e framework (#500)

The seven modules that reached settings.registration through importlib
(users, file_storage, site_lock, api_tokens, tenants, background_tasks,
keycloak) now import register_module_settings statically from
simple_module_core.module_settings; branding, mail, background_tasks' worker
and every test move off the deprecated plugin paths.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
Keep hydrate_settings' pre-#500 keyword name 'store' so keyword callers of
the settings.hydrate shim keep working, and attribute the shims'
DeprecationWarning to the importer instead of importlib's frozen frames.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: fb15944
Status: ✅  Deploy successful!
Preview URL: https://d1bdcd2e.simple-module-python.pages.dev
Branch Preview URL: https://refactor-settings-framework.simple-module-python.pages.dev

View logs

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review 🔄 Running since 2026-10-21T06:09:28.585932Z 5426955 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@antosubash
antosubash merged commit 3fc7498 into main Oct 21, 2026
16 checks passed
@antosubash
antosubash deleted the refactor/settings-framework-500 branch October 21, 2026 12:00
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.

Design: the settings plugin module has become de facto framework; make the dependency explicit

1 participant