From 441ccf5181b5b13a5cce3cbe8a843c2949ddfbb1 Mon Sep 17 00:00:00 2001 From: Cevat Batuhan Tolon Date: Tue, 18 Aug 2026 01:18:57 +0300 Subject: [PATCH 1/2] release dynamic-config-py-web 0.1.0 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e9757..afb3946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ for an adapter and an adapter fix should not drag the wheels behind it. ## [Unreleased] +## 0.1.0 — 2026-08-18 + ### Added - **The shared core.** `Wiring` (load, watch, stop — idempotent, leased per From 79acb13fc4c480770a69b468609fcde2597626e9 Mon Sep 17 00:00:00 2001 From: Cevat Batuhan Tolon Date: Tue, 18 Aug 2026 01:30:36 +0300 Subject: [PATCH 2/2] the Django settings and the type overrides follow what is installed --- justfile | 26 +++++++++++++++++++++++--- pyproject.toml | 39 ++++++++++++++++++++++++++++++++++----- tests/django_bootstrap.py | 14 +++++++++++++- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/justfile b/justfile index bd2c9d1..39f303b 100644 --- a/justfile +++ b/justfile @@ -15,12 +15,32 @@ lint: ruff check . ruff format --check . -# The types a caller sees. The frameworks are `ignore_missing_imports`, so -# this runs with or without them — and checks against the real thing when -# they are there. +# The types a caller sees, in both environments CI checks — with the +# frameworks installed, and without any. They are different checks: a +# decorator is typed in one and `Any` in the other, so an override that +# satisfies only the environment you happen to have is how a green local +# run becomes a red `the types a caller sees`. types: + #!/usr/bin/env bash + set -euo pipefail + mypy --strict src/dynamic_config_web/ + # And again resolving imports against an environment with no framework + # in it. `--python-executable` is what makes that cheap: one venv, and + # the mypy you already have does the checking. + bare=$(mktemp -d) + trap 'rm -rf "$bare"' EXIT + python -m venv "$bare" + # `[dev]` and no framework — the same install CI's second run makes. + # It matters that it is `[dev]` rather than bare: `pytest` is in there, + # and `dynamic_config_web.pytest` decorates fixtures with it. + # `--python-executable` then points the mypy you already have at that + # environment, so no second toolchain is built. + "$bare/bin/pip" install --quiet -e ".[dev]" + echo "→ and with no framework installed" + mypy --strict --python-executable "$bare/bin/python" src/dynamic_config_web/ + # The shared half. Runs with no framework installed, which is the point: # the core is what has to work on every interpreter this package claims. core: diff --git a/pyproject.toml b/pyproject.toml index bd0e816..dca35cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -227,13 +227,39 @@ known-first-party = ["dynamic_config", "dynamic_config_web"] # floor this package supports is checked by running the suite on it, # not by asking a type checker to pretend. -# django-bolt registers routes with `Router.get(...)`, which is untyped — -# so every handler it decorates is "made untyped", which is a fact about -# django-bolt and not about this adapter. The handlers themselves are -# annotated; see the note in `router()` about why they answer `Any`. +# The adapters are type-checked twice: once with every framework installed, +# and once with none of them — the environment most users type-check in, and +# the one CI's second `mypy` run reproduces. Three of `--strict`'s rules +# cannot hold in both, because each depends on whether an optional import +# resolved: +# +# disallow_untyped_decorators `@app.get(...)` is typed when the framework +# is there and `Any` when it is not, so the +# handlers it decorates are "made untyped" +# only in the second environment. (For +# django-bolt it is untyped in *both*: +# `Router.get` carries no annotations.) +# warn_unused_ignores an ignore that silences a real complaint +# from the installed framework has nothing to +# silence when the import failed. +# disallow_subclassing_any `InitPlugin` is a class in one environment +# and `Any` in the other. +# +# Everything else stays strict, and the shared core — which imports no +# framework — keeps all three. [[tool.mypy.overrides]] -module = ["dynamic_config_web.django_bolt"] +module = [ + "dynamic_config_web.fastapi", + "dynamic_config_web.litestar", + "dynamic_config_web.flask", + "dynamic_config_web.quart", + "dynamic_config_web.robyn", + "dynamic_config_web.django_bolt", + "dynamic_config_web.django.*", +] disallow_untyped_decorators = false +warn_unused_ignores = false +disallow_subclassing_any = false # Every framework is optional by construction, so in any one environment # most of them are missing — including the environment that type-checks @@ -242,10 +268,13 @@ disallow_untyped_decorators = false # against the real thing. [[tool.mypy.overrides]] module = [ + "asgiref.*", "django.*", "django_bolt.*", + "fastapi.*", "flask.*", "litestar.*", + "ninja.*", "quart.*", "rest_framework.*", "robyn.*", diff --git a/tests/django_bootstrap.py b/tests/django_bootstrap.py index 56fd3de..944ee79 100644 --- a/tests/django_bootstrap.py +++ b/tests/django_bootstrap.py @@ -13,6 +13,7 @@ from __future__ import annotations +import importlib.util from typing import Any import django @@ -23,6 +24,11 @@ urlpatterns: list[Any] = [] +def _installed(*apps: str) -> list[str]: + """The subset of `apps` this interpreter can actually import.""" + return [app for app in apps if importlib.util.find_spec(app) is not None] + + def bootstrap() -> None: """Configures Django, once per process.""" if settings.configured: @@ -37,7 +43,13 @@ def bootstrap() -> None: # `ready()` reads `DYNAMIC_CONFIG`, and each case wires a different # configuration. `tests/test_django_app.py` exercises that path in a # subprocess, where a settings module can name one target. - INSTALLED_APPS=["rest_framework", "ninja"], + # Only the ones actually installed. Each CI row installs a single + # extra — `[drf]` brings `rest_framework`, `[ninja]` brings `ninja`, + # `[django]` brings neither — and naming an absent app here fails + # `django.setup()` for every Django case in that row, not just the + # one that needs it. A developer with all three installed would + # never see it. + INSTALLED_APPS=_installed("rest_framework", "ninja"), MIDDLEWARE=[ "dynamic_config_web.django.middleware.DynamicConfigMiddleware", ],