From 4ea9bca39e29e944a4517931d751526c9d4fee44 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:23:57 -0700 Subject: [PATCH 01/11] Add feature flag system (mobius._flags) Lightweight runtime feature flags for toggling behaviour via environment variables or programmatic assignment, with a scoped override context manager for tests. src/mobius/_flags.py: - Flags dataclass singleton; each field reads MOBIUS_ at import time - _env_bool(): case-insensitive bool from env var with fallback default - list_flags(): snapshot dict of all current flag values - override_flags(**kwargs): context manager that restores values on exit, even if an exception is raised (exception-safe) - Initial flag: suppress_dedup_warning (default True) - Docstring explains how to add new flags src/mobius/_flags_test.py: 23 tests covering defaults, all truthy/falsy env-var strings, unknown values, programmatic override, context manager (single, nested, exception safety), and list_flags() snapshot semantics. src/mobius/__init__.py: export flags and override_flags in public API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- src/mobius/__init__.py | 3 ++ src/mobius/_flags.py | 100 ++++++++++++++++++++++++++++++++++++ src/mobius/_flags_test.py | 104 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 src/mobius/_flags.py create mode 100644 src/mobius/_flags_test.py diff --git a/src/mobius/__init__.py b/src/mobius/__init__.py index 8508185b4..247f658ee 100644 --- a/src/mobius/__init__.py +++ b/src/mobius/__init__.py @@ -31,7 +31,9 @@ "build_diffusers_pipeline", "build_from_module", "components", + "flags", "models", + "override_flags", "registry", "tasks", ] @@ -63,6 +65,7 @@ ) from mobius._constants import OPSET_VERSION from mobius._diffusers_builder import build_diffusers_pipeline +from mobius._flags import flags, override_flags from mobius._model_package import ModelPackage from mobius._registry import ( ModelRegistration, diff --git a/src/mobius/_flags.py b/src/mobius/_flags.py new file mode 100644 index 000000000..c65064298 --- /dev/null +++ b/src/mobius/_flags.py @@ -0,0 +1,100 @@ +# Copyright (c) ONNX Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Runtime feature flags for mobius. + +Flags control experimental or environment-specific behaviour. Each flag can be +set via an environment variable (``MOBIUS_``) or programmatically +by assigning to the :data:`flags` singleton. + +Environment variable values are read once at import time. Valid truthy strings +are ``1``, ``true``, ``yes``; falsy are ``0``, ``false``, ``no`` +(case-insensitive). Any other value falls back to the default. + +**Adding new flags:** add a field to :class:`Flags` with a +``dataclasses.field(default_factory=...)`` that calls :func:`_env_bool`. + +Example:: + + from mobius import flags, override_flags + + # Check a flag + if flags.suppress_dedup_warning: + ... + + # Programmatic override (persists until changed) + flags.suppress_dedup_warning = False + + # Scoped override for tests + with override_flags(suppress_dedup_warning=False): + ... +""" + +from __future__ import annotations + +import dataclasses +import os +from contextlib import contextmanager +from typing import Iterator + + +def _env_bool(name: str, default: bool) -> bool: + """Read a boolean from an environment variable. + + Returns *default* if the variable is unset or has an unrecognised value. + """ + val = os.environ.get(name, "") + if val.lower() in ("1", "true", "yes"): + return True + if val.lower() in ("0", "false", "no"): + return False + return default + + +@dataclasses.dataclass +class Flags: + """Runtime feature flags singleton. + + Each flag maps to a ``MOBIUS_`` environment variable read at + import time. Flags can be overridden programmatically at any point or + scoped temporarily with :func:`override_flags`. + """ + + # Suppress spurious "has no constant value" warnings from the + # initializer-deduplication pass. These are expected noise when + # optimisation passes run before weights are loaded. + # Set MOBIUS_SUPPRESS_DEDUP_WARNING=0 to see all warnings. + suppress_dedup_warning: bool = dataclasses.field( + default_factory=lambda: _env_bool("MOBIUS_SUPPRESS_DEDUP_WARNING", True) + ) + + +# Global singleton — import and use this directly. +flags = Flags() + + +def list_flags() -> dict[str, bool]: + """Return the current value of all flags as a plain dict snapshot.""" + return dataclasses.asdict(flags) + + +@contextmanager +def override_flags(**kwargs: bool) -> Iterator[None]: + """Temporarily override one or more flags within a ``with`` block. + + Restores the original values on exit, even if an exception is raised. + Intended for use in tests. + + Example:: + + with override_flags(suppress_dedup_warning=False): + build(model_id) + """ + old = {k: getattr(flags, k) for k in kwargs} + for k, v in kwargs.items(): + setattr(flags, k, v) + try: + yield + finally: + for k, v in old.items(): + setattr(flags, k, v) diff --git a/src/mobius/_flags_test.py b/src/mobius/_flags_test.py new file mode 100644 index 000000000..fd3215857 --- /dev/null +++ b/src/mobius/_flags_test.py @@ -0,0 +1,104 @@ +# Copyright (c) ONNX Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the feature flag system.""" + +from __future__ import annotations + +import pytest + +from mobius._flags import Flags, flags, list_flags, override_flags + + +class TestDefaultValues: + """Flags have the expected defaults when no env vars are set.""" + + def test_suppress_dedup_warning_default_on(self, monkeypatch): + monkeypatch.delenv("MOBIUS_SUPPRESS_DEDUP_WARNING", raising=False) + f = Flags() + assert f.suppress_dedup_warning is True + + +class TestEnvVarOverride: + """Flags are read from environment variables at construction time.""" + + @pytest.mark.parametrize("val", ["1", "true", "True", "TRUE", "yes", "YES"]) + def test_truthy_values(self, monkeypatch, val): + monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) + f = Flags() + assert f.suppress_dedup_warning is True + + @pytest.mark.parametrize("val", ["0", "false", "False", "FALSE", "no", "NO"]) + def test_falsy_values(self, monkeypatch, val): + monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) + f = Flags() + assert f.suppress_dedup_warning is False + + def test_unknown_value_falls_back_to_default(self, monkeypatch): + monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", "maybe") + f = Flags() + assert f.suppress_dedup_warning is True # default + + +class TestProgrammaticOverride: + """Flags can be assigned directly on the singleton.""" + + def test_assign_and_restore(self): + original = flags.suppress_dedup_warning + try: + flags.suppress_dedup_warning = not original + assert flags.suppress_dedup_warning is not original + finally: + flags.suppress_dedup_warning = original + + +class TestOverrideFlagsContextManager: + """override_flags() restores original values on exit.""" + + def test_single_flag_restored(self): + original = flags.suppress_dedup_warning + with override_flags(suppress_dedup_warning=not original): + assert flags.suppress_dedup_warning is not original + assert flags.suppress_dedup_warning is original + + def test_restored_on_exception(self): + original = flags.suppress_dedup_warning + with pytest.raises(RuntimeError): + with override_flags(suppress_dedup_warning=not original): + raise RuntimeError("boom") + assert flags.suppress_dedup_warning is original + + def test_nested_overrides(self): + original = flags.suppress_dedup_warning + with override_flags(suppress_dedup_warning=not original): + assert flags.suppress_dedup_warning is not original + with override_flags(suppress_dedup_warning=original): + assert flags.suppress_dedup_warning is original + assert flags.suppress_dedup_warning is not original + assert flags.suppress_dedup_warning is original + + def test_context_manager_yields_none(self): + with override_flags(suppress_dedup_warning=True) as result: + assert result is None + + +class TestListFlags: + """list_flags() returns a plain dict of all current flag values.""" + + def test_returns_dict(self): + result = list_flags() + assert isinstance(result, dict) + + def test_contains_suppress_dedup_warning(self): + assert "suppress_dedup_warning" in list_flags() + + def test_values_match_singleton(self): + result = list_flags() + assert result["suppress_dedup_warning"] == flags.suppress_dedup_warning + + def test_returns_snapshot_not_live_view(self): + """list_flags() returns a copy, not a live reference.""" + snapshot = list_flags() + original = flags.suppress_dedup_warning + with override_flags(suppress_dedup_warning=not original): + assert snapshot["suppress_dedup_warning"] == original From 5fee5b42ce951492893bd3881cd7c10f6b187639 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:44:29 -0700 Subject: [PATCH 02/11] Address PR #55 review comments 1. Docstring accuracy: clarify env vars are read at Flags() construction time; the global singleton is constructed at import time (not import-time env var reading per se). 2. Per-field docstrings: added a string-literal docstring after the suppress_dedup_warning field for documentation generation. 3. Auto-generated flags table: class-level docstring now includes a list-table of all available flags with env var, default, and description. 4. Return type: list_flags() now returns dict[str, object] instead of dict[str, bool] (dataclasses.asdict returns dict[str, Any]; tighten to object for mypy strict compatibility). 5. Unknown-flag validation: override_flags() now raises ValueError with a helpful message listing available flags when an unknown key is passed. 6. Wire suppress_dedup_warning into _builder.py: _optimize() now only applies _suppress_dedup_empty_initializer_warnings() when the flag is True, making the flag actually functional. 7. Import style in tests: changed from individual symbol imports to module-level import (from mobius import _flags) so test code clearly shows where each symbol originates. 8. Unreachable code: replaced pytest.raises nested-with pattern with try/except in test_restored_on_exception; replaced double-with in test_unknown_flag_raises_value_error with single combined with statement (SIM117). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- src/mobius/_builder.py | 6 ++- src/mobius/_flags.py | 58 ++++++++++++++++++++++------- src/mobius/_flags_test.py | 78 +++++++++++++++++++++------------------ 3 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/mobius/_builder.py b/src/mobius/_builder.py index d00727379..c801b888a 100644 --- a/src/mobius/_builder.py +++ b/src/mobius/_builder.py @@ -34,6 +34,7 @@ from mobius._configs import ( BaseModelConfig, ) +from mobius._flags import flags from mobius._model_package import ModelPackage from mobius._registry import registry from mobius._weight_loading import _download_weights @@ -121,7 +122,10 @@ def call(self, model: ir.Model) -> ir.passes.PassResult: def _optimize(model: ir.Model) -> None: """Apply default optimization passes to a model in-place.""" pass_ = ir.passes.PassManager(_DEFAULT_PASSES, steps=2) - with _suppress_dedup_empty_initializer_warnings(): + if flags.suppress_dedup_warning: + with _suppress_dedup_empty_initializer_warnings(): + pass_(model) + else: pass_(model) diff --git a/src/mobius/_flags.py b/src/mobius/_flags.py index c65064298..2380a1235 100644 --- a/src/mobius/_flags.py +++ b/src/mobius/_flags.py @@ -7,12 +7,16 @@ set via an environment variable (``MOBIUS_``) or programmatically by assigning to the :data:`flags` singleton. -Environment variable values are read once at import time. Valid truthy strings -are ``1``, ``true``, ``yes``; falsy are ``0``, ``false``, ``no`` -(case-insensitive). Any other value falls back to the default. +Environment variable values are read each time a :class:`Flags` instance is +constructed. The global :data:`flags` singleton is constructed at import time, +so env vars should be set before importing mobius. Valid truthy strings are +``1``, ``true``, ``yes``; falsy are ``0``, ``false``, ``no`` +(case-insensitive). Any other value falls back to the field default. **Adding new flags:** add a field to :class:`Flags` with a -``dataclasses.field(default_factory=...)`` that calls :func:`_env_bool`. +``dataclasses.field(default_factory=...)`` that calls :func:`_env_bool`, +plus a docstring string literal immediately after the field for documentation +generation. Example:: @@ -34,8 +38,8 @@ import dataclasses import os +from collections.abc import Iterator from contextlib import contextmanager -from typing import Iterator def _env_bool(name: str, default: bool) -> bool: @@ -55,25 +59,43 @@ def _env_bool(name: str, default: bool) -> bool: class Flags: """Runtime feature flags singleton. - Each flag maps to a ``MOBIUS_`` environment variable read at - import time. Flags can be overridden programmatically at any point or - scoped temporarily with :func:`override_flags`. + Each flag maps to a ``MOBIUS_`` environment variable read when + a :class:`Flags` instance is constructed. The global :data:`flags` + singleton is constructed at import time. Flags can be overridden + programmatically at any point or scoped temporarily with + :func:`override_flags`. + + **Available flags** + + .. list-table:: + :header-rows: 1 + + * - Flag + - Env var + - Default + - Description + * - ``suppress_dedup_warning`` + - ``MOBIUS_SUPPRESS_DEDUP_WARNING`` + - ``True`` + - Suppress "has no constant value" warnings from the initializer + deduplication pass. """ - # Suppress spurious "has no constant value" warnings from the - # initializer-deduplication pass. These are expected noise when - # optimisation passes run before weights are loaded. - # Set MOBIUS_SUPPRESS_DEDUP_WARNING=0 to see all warnings. suppress_dedup_warning: bool = dataclasses.field( default_factory=lambda: _env_bool("MOBIUS_SUPPRESS_DEDUP_WARNING", True) ) + """Suppress "has no constant value" warnings from the initializer-deduplication pass. + + These warnings are expected noise when optimisation passes run before weights + are loaded. Set ``MOBIUS_SUPPRESS_DEDUP_WARNING=0`` to see all warnings. + """ # Global singleton — import and use this directly. flags = Flags() -def list_flags() -> dict[str, bool]: +def list_flags() -> dict[str, object]: """Return the current value of all flags as a plain dict snapshot.""" return dataclasses.asdict(flags) @@ -85,11 +107,21 @@ def override_flags(**kwargs: bool) -> Iterator[None]: Restores the original values on exit, even if an exception is raised. Intended for use in tests. + Raises: + ValueError: If any key in *kwargs* is not a known flag name. + Example:: with override_flags(suppress_dedup_warning=False): build(model_id) """ + valid = {f.name for f in dataclasses.fields(Flags)} + unknown = sorted(set(kwargs) - valid) + if unknown: + available = ", ".join(sorted(valid)) + raise ValueError( + f"Unknown flag name(s): {', '.join(unknown)}. Available flags: {available}" + ) old = {k: getattr(flags, k) for k in kwargs} for k, v in kwargs.items(): setattr(flags, k, v) diff --git a/src/mobius/_flags_test.py b/src/mobius/_flags_test.py index fd3215857..7120f6f96 100644 --- a/src/mobius/_flags_test.py +++ b/src/mobius/_flags_test.py @@ -7,7 +7,7 @@ import pytest -from mobius._flags import Flags, flags, list_flags, override_flags +from mobius import _flags class TestDefaultValues: @@ -15,7 +15,7 @@ class TestDefaultValues: def test_suppress_dedup_warning_default_on(self, monkeypatch): monkeypatch.delenv("MOBIUS_SUPPRESS_DEDUP_WARNING", raising=False) - f = Flags() + f = _flags.Flags() assert f.suppress_dedup_warning is True @@ -25,18 +25,18 @@ class TestEnvVarOverride: @pytest.mark.parametrize("val", ["1", "true", "True", "TRUE", "yes", "YES"]) def test_truthy_values(self, monkeypatch, val): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) - f = Flags() + f = _flags.Flags() assert f.suppress_dedup_warning is True @pytest.mark.parametrize("val", ["0", "false", "False", "FALSE", "no", "NO"]) def test_falsy_values(self, monkeypatch, val): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) - f = Flags() + f = _flags.Flags() assert f.suppress_dedup_warning is False def test_unknown_value_falls_back_to_default(self, monkeypatch): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", "maybe") - f = Flags() + f = _flags.Flags() assert f.suppress_dedup_warning is True # default @@ -44,61 +44,67 @@ class TestProgrammaticOverride: """Flags can be assigned directly on the singleton.""" def test_assign_and_restore(self): - original = flags.suppress_dedup_warning + original = _flags.flags.suppress_dedup_warning try: - flags.suppress_dedup_warning = not original - assert flags.suppress_dedup_warning is not original + _flags.flags.suppress_dedup_warning = not original + assert _flags.flags.suppress_dedup_warning is not original finally: - flags.suppress_dedup_warning = original + _flags.flags.suppress_dedup_warning = original class TestOverrideFlagsContextManager: """override_flags() restores original values on exit.""" def test_single_flag_restored(self): - original = flags.suppress_dedup_warning - with override_flags(suppress_dedup_warning=not original): - assert flags.suppress_dedup_warning is not original - assert flags.suppress_dedup_warning is original + original = _flags.flags.suppress_dedup_warning + with _flags.override_flags(suppress_dedup_warning=not original): + assert _flags.flags.suppress_dedup_warning is not original + assert _flags.flags.suppress_dedup_warning is original def test_restored_on_exception(self): - original = flags.suppress_dedup_warning - with pytest.raises(RuntimeError): - with override_flags(suppress_dedup_warning=not original): - raise RuntimeError("boom") - assert flags.suppress_dedup_warning is original + original = _flags.flags.suppress_dedup_warning + exc = RuntimeError("boom") + try: + with _flags.override_flags(suppress_dedup_warning=not original): + raise exc + except RuntimeError: + pass + assert _flags.flags.suppress_dedup_warning is original def test_nested_overrides(self): - original = flags.suppress_dedup_warning - with override_flags(suppress_dedup_warning=not original): - assert flags.suppress_dedup_warning is not original - with override_flags(suppress_dedup_warning=original): - assert flags.suppress_dedup_warning is original - assert flags.suppress_dedup_warning is not original - assert flags.suppress_dedup_warning is original - - def test_context_manager_yields_none(self): - with override_flags(suppress_dedup_warning=True) as result: - assert result is None + original = _flags.flags.suppress_dedup_warning + with _flags.override_flags(suppress_dedup_warning=not original): + assert _flags.flags.suppress_dedup_warning is not original + with _flags.override_flags(suppress_dedup_warning=original): + assert _flags.flags.suppress_dedup_warning is original + assert _flags.flags.suppress_dedup_warning is not original + assert _flags.flags.suppress_dedup_warning is original + + def test_unknown_flag_raises_value_error(self): + with ( + pytest.raises(ValueError, match="Unknown flag"), + _flags.override_flags(nonexistent_flag=True), + ): + pass # pragma: no cover class TestListFlags: """list_flags() returns a plain dict of all current flag values.""" def test_returns_dict(self): - result = list_flags() + result = _flags.list_flags() assert isinstance(result, dict) def test_contains_suppress_dedup_warning(self): - assert "suppress_dedup_warning" in list_flags() + assert "suppress_dedup_warning" in _flags.list_flags() def test_values_match_singleton(self): - result = list_flags() - assert result["suppress_dedup_warning"] == flags.suppress_dedup_warning + result = _flags.list_flags() + assert result["suppress_dedup_warning"] == _flags.flags.suppress_dedup_warning def test_returns_snapshot_not_live_view(self): """list_flags() returns a copy, not a live reference.""" - snapshot = list_flags() - original = flags.suppress_dedup_warning - with override_flags(suppress_dedup_warning=not original): + snapshot = _flags.list_flags() + original = _flags.flags.suppress_dedup_warning + with _flags.override_flags(suppress_dedup_warning=not original): assert snapshot["suppress_dedup_warning"] == original From 7bab523bd0f8a03a2c8ecab06cce3fb78e990f22 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:47:33 -0700 Subject: [PATCH 03/11] Add thread-safety note to override_flags and docs/feature-flags.md Thread safety note: - override_flags() docstring now explains it is not thread-safe (TOCTOU between save-old and restore-old), but safe for pytest -n auto because xdist workers are separate processes with independent flag singletons. Docs: - docs/feature-flags.md: covers available flags table, env var usage, programmatic override, override_flags() in tests (with thread-safety callout), list_flags(), and how to add new flags. - docs/index.md: added feature-flags to the toctree. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- docs/feature-flags.md | 103 ++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + src/mobius/_flags.py | 7 +++ 3 files changed, 111 insertions(+) create mode 100644 docs/feature-flags.md diff --git a/docs/feature-flags.md b/docs/feature-flags.md new file mode 100644 index 000000000..860859c0b --- /dev/null +++ b/docs/feature-flags.md @@ -0,0 +1,103 @@ +# Feature Flags + +mobius exposes a small set of runtime feature flags that control experimental +or environment-specific behaviour. Flags live in `mobius._flags` and are +accessible through the public API. + +## Available flags + +| Flag | Environment variable | Default | Description | +|------|---------------------|---------|-------------| +| `suppress_dedup_warning` | `MOBIUS_SUPPRESS_DEDUP_WARNING` | `True` | Suppress "has no constant value" warnings from the initializer-deduplication pass. These are expected noise before weights are loaded. Set to `False` to surface all deduplication warnings. | + +## Setting flags via environment variables + +Environment variables are read when the global `flags` singleton is constructed +at import time. Set them before importing mobius (e.g., in a shell or `.env`): + +```bash +# Disable warning suppression to see all deduplication warnings +export MOBIUS_SUPPRESS_DEDUP_WARNING=0 + +python -c "import mobius; mobius.build('Qwen/Qwen2.5-0.5B-Instruct')" +``` + +Accepted truthy values: `1`, `true`, `yes` (case-insensitive). +Accepted falsy values: `0`, `false`, `no` (case-insensitive). +Any other value falls back to the field default. + +## Setting flags programmatically + +Assign directly to the `flags` singleton at any point after import: + +```python +import mobius + +# Disable warning suppression +mobius.flags.suppress_dedup_warning = False + +# Re-enable it +mobius.flags.suppress_dedup_warning = True +``` + +## Using `override_flags()` in tests + +For tests that need a temporary flag value, use the `override_flags` context +manager. It restores the original values on exit, even if the test raises: + +```python +import mobius +from mobius import override_flags + +def test_build_with_warnings(tmp_path): + with override_flags(suppress_dedup_warning=False): + pkg = mobius.build("Qwen/Qwen2.5-0.5B-Instruct") + # suppress_dedup_warning is restored here +``` + +`override_flags` raises `ValueError` for unknown flag names, catching typos +early: + +```python +# Raises: ValueError: Unknown flag name(s): typo. Available flags: suppress_dedup_warning +with override_flags(typo=True): + ... +``` + +> **Thread safety:** `override_flags` is not thread-safe — concurrent calls +> in different threads may interleave the save/restore cycle. For pytest, +> this is safe with `pytest -n auto` because xdist workers are separate +> processes with independent flag singletons. + +## Listing all flags + +```python +import mobius + +print(mobius.list_flags()) +# {'suppress_dedup_warning': True} +``` + +## Adding new flags + +1. Add a field to the `Flags` dataclass in `src/mobius/_flags.py`: + + ```python + @dataclasses.dataclass + class Flags: + suppress_dedup_warning: bool = dataclasses.field( + default_factory=lambda: _env_bool("MOBIUS_SUPPRESS_DEDUP_WARNING", True) + ) + """Suppress deduplication pass warnings (see above).""" + + my_new_flag: bool = dataclasses.field( + default_factory=lambda: _env_bool("MOBIUS_MY_NEW_FLAG", False) + ) + """Short description of what my_new_flag controls.""" + ``` + +2. Wire the flag into the code path it controls. + +3. Add the flag to the table at the top of this page. + +4. Add tests in `src/mobius/_flags_test.py` following the existing patterns. diff --git a/docs/index.md b/docs/index.md index 1cd631cca..026fc614c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,4 +14,5 @@ models/index design test-architecture ai-model-support-strategy +feature-flags ``` diff --git a/src/mobius/_flags.py b/src/mobius/_flags.py index 2380a1235..207115d13 100644 --- a/src/mobius/_flags.py +++ b/src/mobius/_flags.py @@ -107,6 +107,13 @@ def override_flags(**kwargs: bool) -> Iterator[None]: Restores the original values on exit, even if an exception is raised. Intended for use in tests. + .. note:: + **Thread safety:** ``override_flags`` is not thread-safe — concurrent + calls in different threads may interleave the save/restore cycle + (TOCTOU). For pytest, this is safe when running with ``-n auto`` + because xdist spawns separate worker *processes* (not threads), so + each worker has its own copy of the flag singleton. + Raises: ValueError: If any key in *kwargs* is not a known flag name. From d132441ff5233fa7ad6343b77ee5b51bb4f731dd Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:51:12 -0700 Subject: [PATCH 04/11] Auto-generate docs/feature-flags.md from Flags dataclass Replace the manually-written docs page with an auto-generated one. - scripts/generate_flags_docs.py: introspects Flags via dataclasses.fields() and AST-parses _flags.py to extract env var names, defaults, and per-field docstrings from string literals after each field definition - docs/feature-flags.md: regenerated from the script - .github/workflows/check-flags-docs.yml: CI check that fails if the committed page is out of date with the Flags dataclass Developers add a new flag by adding a dataclass field + docstring in _flags.py, then running 'python scripts/generate_flags_docs.py'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- .github/workflows/check-flags-docs.yml | 39 ++++ docs/feature-flags.md | 64 +++---- scripts/generate_flags_docs.py | 236 +++++++++++++++++++++++++ 3 files changed, 301 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/check-flags-docs.yml create mode 100644 scripts/generate_flags_docs.py diff --git a/.github/workflows/check-flags-docs.yml b/.github/workflows/check-flags-docs.yml new file mode 100644 index 000000000..5b4404bc1 --- /dev/null +++ b/.github/workflows/check-flags-docs.yml @@ -0,0 +1,39 @@ +name: Check flags docs + +on: + pull_request: + paths: + - "src/mobius/_flags.py" + - "docs/feature-flags.md" + - "scripts/generate_flags_docs.py" + +permissions: + contents: read + +jobs: + check: + name: Verify feature-flags docs are up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install -e '.[testing]' -q + + - name: Regenerate docs + run: python scripts/generate_flags_docs.py + + - name: Check for diff + run: | + if ! git diff --exit-code docs/feature-flags.md; then + echo "" + echo "❌ docs/feature-flags.md is out of date." + echo " Run 'python scripts/generate_flags_docs.py' and commit the result." + exit 1 + fi + echo "✅ docs/feature-flags.md is up to date." diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 860859c0b..86dd1de8c 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -1,14 +1,17 @@ + + # Feature Flags -mobius exposes a small set of runtime feature flags that control experimental -or environment-specific behaviour. Flags live in `mobius._flags` and are +mobius exposes runtime feature flags that control experimental or +environment-specific behaviour. Flags live in `mobius._flags` and are accessible through the public API. ## Available flags | Flag | Environment variable | Default | Description | |------|---------------------|---------|-------------| -| `suppress_dedup_warning` | `MOBIUS_SUPPRESS_DEDUP_WARNING` | `True` | Suppress "has no constant value" warnings from the initializer-deduplication pass. These are expected noise before weights are loaded. Set to `False` to surface all deduplication warnings. | +| `suppress_dedup_warning` | `MOBIUS_SUPPRESS_DEDUP_WARNING` | `true` | Suppress "has no constant value" warnings from the initializer-deduplication pass. These warnings are expected noise when optimisation passes run before weights are loaded. Set ``MOBIUS_SUPPRESS_DEDUP_WARNING=0`` to see all warnings. | ## Setting flags via environment variables @@ -16,14 +19,14 @@ Environment variables are read when the global `flags` singleton is constructed at import time. Set them before importing mobius (e.g., in a shell or `.env`): ```bash -# Disable warning suppression to see all deduplication warnings +# Example: disable warning suppression to see all deduplication warnings export MOBIUS_SUPPRESS_DEDUP_WARNING=0 python -c "import mobius; mobius.build('Qwen/Qwen2.5-0.5B-Instruct')" ``` -Accepted truthy values: `1`, `true`, `yes` (case-insensitive). -Accepted falsy values: `0`, `false`, `no` (case-insensitive). +Accepted truthy values: `1`, `true`, `yes` (case-insensitive).\ +Accepted falsy values: `0`, `false`, `no` (case-insensitive).\ Any other value falls back to the field default. ## Setting flags programmatically @@ -33,11 +36,8 @@ Assign directly to the `flags` singleton at any point after import: ```python import mobius -# Disable warning suppression -mobius.flags.suppress_dedup_warning = False - -# Re-enable it -mobius.flags.suppress_dedup_warning = True +mobius.flags.suppress_dedup_warning = False # disable +mobius.flags.suppress_dedup_warning = True # re-enable ``` ## Using `override_flags()` in tests @@ -46,7 +46,6 @@ For tests that need a temporary flag value, use the `override_flags` context manager. It restores the original values on exit, even if the test raises: ```python -import mobius from mobius import override_flags def test_build_with_warnings(tmp_path): @@ -55,19 +54,12 @@ def test_build_with_warnings(tmp_path): # suppress_dedup_warning is restored here ``` -`override_flags` raises `ValueError` for unknown flag names, catching typos -early: - -```python -# Raises: ValueError: Unknown flag name(s): typo. Available flags: suppress_dedup_warning -with override_flags(typo=True): - ... -``` +`override_flags` raises `ValueError` for unknown flag names, catching typos early. > **Thread safety:** `override_flags` is not thread-safe — concurrent calls -> in different threads may interleave the save/restore cycle. For pytest, -> this is safe with `pytest -n auto` because xdist workers are separate -> processes with independent flag singletons. +> in different threads may interleave the save/restore cycle (TOCTOU). For +> pytest, this is safe with `pytest -n auto` because xdist workers run in +> separate processes with independent flag singletons. ## Listing all flags @@ -75,29 +67,25 @@ with override_flags(typo=True): import mobius print(mobius.list_flags()) -# {'suppress_dedup_warning': True} ``` ## Adding new flags -1. Add a field to the `Flags` dataclass in `src/mobius/_flags.py`: +1. Add a field to `Flags` in `src/mobius/_flags.py`: ```python - @dataclasses.dataclass - class Flags: - suppress_dedup_warning: bool = dataclasses.field( - default_factory=lambda: _env_bool("MOBIUS_SUPPRESS_DEDUP_WARNING", True) - ) - """Suppress deduplication pass warnings (see above).""" - - my_new_flag: bool = dataclasses.field( - default_factory=lambda: _env_bool("MOBIUS_MY_NEW_FLAG", False) - ) - """Short description of what my_new_flag controls.""" + my_new_flag: bool = dataclasses.field( + default_factory=lambda: _env_bool("MOBIUS_MY_NEW_FLAG", False) + ) + """Short description of what my_new_flag controls.""" ``` 2. Wire the flag into the code path it controls. -3. Add the flag to the table at the top of this page. +3. Regenerate this page: + + ```bash + python scripts/generate_flags_docs.py + ``` -4. Add tests in `src/mobius/_flags_test.py` following the existing patterns. +4. Add tests in `src/mobius/_flags_test.py` following existing patterns. diff --git a/scripts/generate_flags_docs.py b/scripts/generate_flags_docs.py new file mode 100644 index 000000000..3e2556554 --- /dev/null +++ b/scripts/generate_flags_docs.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# Copyright (c) ONNX Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Auto-generate docs/feature-flags.md from the Flags dataclass. + +Run this script whenever a flag is added or modified:: + + python scripts/generate_flags_docs.py + +The generated file is committed to the repository. A CI check (see +``.github/workflows/check-flags-docs.yml``) verifies it is up to date. + +Extraction strategy +------------------- +For each field in :class:`mobius._flags.Flags`: + +* **Name** — from ``dataclasses.fields()``. +* **Env var** — parsed from the ``_env_bool("ENV_VAR", default)`` call + inside the field's ``default_factory`` lambda via the source AST. +* **Default** — the second argument to ``_env_bool`` in the same AST node. +* **Docstring** — the string-literal ``Expr`` node immediately following the + field's ``AnnAssign`` node in the class body. +""" + +from __future__ import annotations + +import ast +import inspect +import sys +import textwrap +from pathlib import Path + +# Resolve paths relative to repo root (this script lives in scripts/). +_REPO_ROOT = Path(__file__).parent.parent +_OUTPUT = _REPO_ROOT / "docs" / "feature-flags.md" + +# Add src/ to the path so we can import mobius without an editable install. +sys.path.insert(0, str(_REPO_ROOT / "src")) + +from mobius._flags import Flags # noqa: E402 (after sys.path manipulation) + + +# --------------------------------------------------------------------------- +# AST extraction helpers +# --------------------------------------------------------------------------- + + +def _parse_class_ast(cls: type) -> ast.ClassDef: + """Return the ``ClassDef`` AST node for *cls*.""" + source = textwrap.dedent(inspect.getsource(cls)) + tree = ast.parse(source) + return next(n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)) + + +def _extract_field_docstrings(class_def: ast.ClassDef) -> dict[str, str]: + """Map field names → per-field docstring (string literal after the field).""" + docs: dict[str, str] = {} + body = class_def.body + for i, node in enumerate(body): + if not (isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)): + continue + name = node.target.id + if i + 1 < len(body): + nxt = body[i + 1] + if ( + isinstance(nxt, ast.Expr) + and isinstance(nxt.value, ast.Constant) + and isinstance(nxt.value.value, str) + ): + docs[name] = nxt.value.value.strip() + return docs + + +def _extract_env_info(class_def: ast.ClassDef) -> dict[str, tuple[str, bool]]: + """Map field names → (env_var_name, default_value) from _env_bool calls.""" + info: dict[str, tuple[str, bool]] = {} + for node in class_def.body: + if not (isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)): + continue + name = node.target.id + if node.value is None: + continue + # Walk node.value looking for Lambda → _env_bool(env_var, default) + for sub in ast.walk(node.value): + if not isinstance(sub, ast.Lambda): + continue + body = sub.body + if ( + isinstance(body, ast.Call) + and isinstance(body.func, ast.Name) + and body.func.id == "_env_bool" + and len(body.args) >= 2 + and isinstance(body.args[0], ast.Constant) + and isinstance(body.args[1], ast.Constant) + ): + env_var: str = body.args[0].value + default: bool = bool(body.args[1].value) + info[name] = (env_var, default) + break + return info + + +# --------------------------------------------------------------------------- +# Markdown generation +# --------------------------------------------------------------------------- + +_STATIC_PREAMBLE = """\ + + +# Feature Flags + +mobius exposes runtime feature flags that control experimental or +environment-specific behaviour. Flags live in `mobius._flags` and are +accessible through the public API. + +""" + +_STATIC_BODY = """ +## Setting flags via environment variables + +Environment variables are read when the global `flags` singleton is constructed +at import time. Set them before importing mobius (e.g., in a shell or `.env`): + +```bash +# Example: disable warning suppression to see all deduplication warnings +export MOBIUS_SUPPRESS_DEDUP_WARNING=0 + +python -c "import mobius; mobius.build('Qwen/Qwen2.5-0.5B-Instruct')" +``` + +Accepted truthy values: `1`, `true`, `yes` (case-insensitive).\\ +Accepted falsy values: `0`, `false`, `no` (case-insensitive).\\ +Any other value falls back to the field default. + +## Setting flags programmatically + +Assign directly to the `flags` singleton at any point after import: + +```python +import mobius + +mobius.flags.suppress_dedup_warning = False # disable +mobius.flags.suppress_dedup_warning = True # re-enable +``` + +## Using `override_flags()` in tests + +For tests that need a temporary flag value, use the `override_flags` context +manager. It restores the original values on exit, even if the test raises: + +```python +from mobius import override_flags + +def test_build_with_warnings(tmp_path): + with override_flags(suppress_dedup_warning=False): + pkg = mobius.build("Qwen/Qwen2.5-0.5B-Instruct") + # suppress_dedup_warning is restored here +``` + +`override_flags` raises `ValueError` for unknown flag names, catching typos early. + +> **Thread safety:** `override_flags` is not thread-safe — concurrent calls +> in different threads may interleave the save/restore cycle (TOCTOU). For +> pytest, this is safe with `pytest -n auto` because xdist workers run in +> separate processes with independent flag singletons. + +## Listing all flags + +```python +import mobius + +print(mobius.list_flags()) +``` + +## Adding new flags + +1. Add a field to `Flags` in `src/mobius/_flags.py`: + + ```python + my_new_flag: bool = dataclasses.field( + default_factory=lambda: _env_bool("MOBIUS_MY_NEW_FLAG", False) + ) + \"\"\"Short description of what my_new_flag controls.\"\"\" + ``` + +2. Wire the flag into the code path it controls. + +3. Regenerate this page: + + ```bash + python scripts/generate_flags_docs.py + ``` + +4. Add tests in `src/mobius/_flags_test.py` following existing patterns. +""" + + +def generate(output: Path = _OUTPUT) -> str: + """Generate the feature-flags markdown and write it to *output*.""" + import dataclasses as dc + + class_def = _parse_class_ast(Flags) + field_docs = _extract_field_docstrings(class_def) + env_info = _extract_env_info(class_def) + + # Build the flags reference table. + table_rows: list[str] = [] + for field in dc.fields(Flags): + name = field.name + env_var, default = env_info.get(name, (f"MOBIUS_{name.upper()}", "?")) + doc = field_docs.get(name, "") + # Collapse multi-line docstrings to a single line for the table cell. + doc_single = " ".join(doc.split()) + default_str = str(default).lower() # "true" / "false" + table_rows.append( + f"| `{name}` | `{env_var}` | `{default_str}` | {doc_single} |" + ) + + table = ( + "## Available flags\n\n" + "| Flag | Environment variable | Default | Description |\n" + "|------|---------------------|---------|-------------|\n" + + "\n".join(table_rows) + + "\n" + ) + + content = _STATIC_PREAMBLE + table + _STATIC_BODY + output.write_text(content, encoding="utf-8") + print(f"Generated {output}") + return content + + +if __name__ == "__main__": + generate() From 6bb549f20f35f4841ef404cbe92553894e68ff06 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:52:58 -0700 Subject: [PATCH 05/11] Remove flags/override_flags/list_flags from public API The flags module is internal-only. Users configure flags via environment variables (MOBIUS_*); only internal code imports from mobius._flags directly. - Remove flags, override_flags from __init__.py __all__ and imports - Update docs to use 'from mobius._flags import ...' instead of 'import mobius' - Regenerate docs/feature-flags.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- docs/feature-flags.md | 19 ++++++++++--------- scripts/generate_flags_docs.py | 24 +++++++++++------------- src/mobius/__init__.py | 3 --- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/docs/feature-flags.md b/docs/feature-flags.md index 86dd1de8c..7949d03e0 100644 --- a/docs/feature-flags.md +++ b/docs/feature-flags.md @@ -5,7 +5,8 @@ mobius exposes runtime feature flags that control experimental or environment-specific behaviour. Flags live in `mobius._flags` and are -accessible through the public API. +for **internal use only** — external callers configure them via environment +variables (see below). ## Available flags @@ -29,15 +30,15 @@ Accepted truthy values: `1`, `true`, `yes` (case-insensitive).\ Accepted falsy values: `0`, `false`, `no` (case-insensitive).\ Any other value falls back to the field default. -## Setting flags programmatically +## Setting flags programmatically (internal code only) -Assign directly to the `flags` singleton at any point after import: +Internal modules can import `flags` directly and assign to it: ```python -import mobius +from mobius._flags import flags -mobius.flags.suppress_dedup_warning = False # disable -mobius.flags.suppress_dedup_warning = True # re-enable +flags.suppress_dedup_warning = False # disable +flags.suppress_dedup_warning = True # re-enable ``` ## Using `override_flags()` in tests @@ -46,7 +47,7 @@ For tests that need a temporary flag value, use the `override_flags` context manager. It restores the original values on exit, even if the test raises: ```python -from mobius import override_flags +from mobius._flags import override_flags def test_build_with_warnings(tmp_path): with override_flags(suppress_dedup_warning=False): @@ -64,9 +65,9 @@ def test_build_with_warnings(tmp_path): ## Listing all flags ```python -import mobius +from mobius._flags import list_flags -print(mobius.list_flags()) +print(list_flags()) ``` ## Adding new flags diff --git a/scripts/generate_flags_docs.py b/scripts/generate_flags_docs.py index 3e2556554..50dc0873c 100644 --- a/scripts/generate_flags_docs.py +++ b/scripts/generate_flags_docs.py @@ -40,7 +40,6 @@ from mobius._flags import Flags # noqa: E402 (after sys.path manipulation) - # --------------------------------------------------------------------------- # AST extraction helpers # --------------------------------------------------------------------------- @@ -113,7 +112,8 @@ def _extract_env_info(class_def: ast.ClassDef) -> dict[str, tuple[str, bool]]: mobius exposes runtime feature flags that control experimental or environment-specific behaviour. Flags live in `mobius._flags` and are -accessible through the public API. +for **internal use only** — external callers configure them via environment +variables (see below). """ @@ -134,15 +134,15 @@ def _extract_env_info(class_def: ast.ClassDef) -> dict[str, tuple[str, bool]]: Accepted falsy values: `0`, `false`, `no` (case-insensitive).\\ Any other value falls back to the field default. -## Setting flags programmatically +## Setting flags programmatically (internal code only) -Assign directly to the `flags` singleton at any point after import: +Internal modules can import `flags` directly and assign to it: ```python -import mobius +from mobius._flags import flags -mobius.flags.suppress_dedup_warning = False # disable -mobius.flags.suppress_dedup_warning = True # re-enable +flags.suppress_dedup_warning = False # disable +flags.suppress_dedup_warning = True # re-enable ``` ## Using `override_flags()` in tests @@ -151,7 +151,7 @@ def _extract_env_info(class_def: ast.ClassDef) -> dict[str, tuple[str, bool]]: manager. It restores the original values on exit, even if the test raises: ```python -from mobius import override_flags +from mobius._flags import override_flags def test_build_with_warnings(tmp_path): with override_flags(suppress_dedup_warning=False): @@ -169,9 +169,9 @@ def test_build_with_warnings(tmp_path): ## Listing all flags ```python -import mobius +from mobius._flags import list_flags -print(mobius.list_flags()) +print(list_flags()) ``` ## Adding new flags @@ -214,9 +214,7 @@ def generate(output: Path = _OUTPUT) -> str: # Collapse multi-line docstrings to a single line for the table cell. doc_single = " ".join(doc.split()) default_str = str(default).lower() # "true" / "false" - table_rows.append( - f"| `{name}` | `{env_var}` | `{default_str}` | {doc_single} |" - ) + table_rows.append(f"| `{name}` | `{env_var}` | `{default_str}` | {doc_single} |") table = ( "## Available flags\n\n" diff --git a/src/mobius/__init__.py b/src/mobius/__init__.py index 247f658ee..8508185b4 100644 --- a/src/mobius/__init__.py +++ b/src/mobius/__init__.py @@ -31,9 +31,7 @@ "build_diffusers_pipeline", "build_from_module", "components", - "flags", "models", - "override_flags", "registry", "tasks", ] @@ -65,7 +63,6 @@ ) from mobius._constants import OPSET_VERSION from mobius._diffusers_builder import build_diffusers_pipeline -from mobius._flags import flags, override_flags from mobius._model_package import ModelPackage from mobius._registry import ( ModelRegistration, From 669606567bb884310eab161bc43b4cb8cb1d3672 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:55:03 -0700 Subject: [PATCH 06/11] Generate feature-flags docs at CI build time, not committed - Remove docs/feature-flags.md from repo (build-time artifact) - Remove check-flags-docs.yml workflow (no longer needed) - Add scripts/generate_flags_docs.py call to pages.yml Build Sphinx docs step - Gitignore docs/feature-flags.md to prevent accidental commits Follows the same pattern as generate_dashboard.py. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- .github/workflows/check-flags-docs.yml | 39 ----------- .github/workflows/pages.yml | 1 + .gitignore | 1 + docs/feature-flags.md | 92 -------------------------- 4 files changed, 2 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/check-flags-docs.yml delete mode 100644 docs/feature-flags.md diff --git a/.github/workflows/check-flags-docs.yml b/.github/workflows/check-flags-docs.yml deleted file mode 100644 index 5b4404bc1..000000000 --- a/.github/workflows/check-flags-docs.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Check flags docs - -on: - pull_request: - paths: - - "src/mobius/_flags.py" - - "docs/feature-flags.md" - - "scripts/generate_flags_docs.py" - -permissions: - contents: read - -jobs: - check: - name: Verify feature-flags docs are up to date - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install dependencies - run: pip install -e '.[testing]' -q - - - name: Regenerate docs - run: python scripts/generate_flags_docs.py - - - name: Check for diff - run: | - if ! git diff --exit-code docs/feature-flags.md; then - echo "" - echo "❌ docs/feature-flags.md is out of date." - echo " Run 'python scripts/generate_flags_docs.py' and commit the result." - exit 1 - fi - echo "✅ docs/feature-flags.md is up to date." diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 2f2bf927c..057f0e60e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -56,6 +56,7 @@ jobs: - name: Build Sphinx docs run: | python docs/_generate_models.py + python scripts/generate_flags_docs.py sphinx-build docs _site/docs - name: Upload Pages artifact diff --git a/.gitignore b/.gitignore index 0382198ef..46fc20d0f 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,4 @@ cache_dir/** .flightdeck/shared/** .worktrees/ dashboard_preview.html +docs/feature-flags.md diff --git a/docs/feature-flags.md b/docs/feature-flags.md deleted file mode 100644 index 7949d03e0..000000000 --- a/docs/feature-flags.md +++ /dev/null @@ -1,92 +0,0 @@ - - -# Feature Flags - -mobius exposes runtime feature flags that control experimental or -environment-specific behaviour. Flags live in `mobius._flags` and are -for **internal use only** — external callers configure them via environment -variables (see below). - -## Available flags - -| Flag | Environment variable | Default | Description | -|------|---------------------|---------|-------------| -| `suppress_dedup_warning` | `MOBIUS_SUPPRESS_DEDUP_WARNING` | `true` | Suppress "has no constant value" warnings from the initializer-deduplication pass. These warnings are expected noise when optimisation passes run before weights are loaded. Set ``MOBIUS_SUPPRESS_DEDUP_WARNING=0`` to see all warnings. | - -## Setting flags via environment variables - -Environment variables are read when the global `flags` singleton is constructed -at import time. Set them before importing mobius (e.g., in a shell or `.env`): - -```bash -# Example: disable warning suppression to see all deduplication warnings -export MOBIUS_SUPPRESS_DEDUP_WARNING=0 - -python -c "import mobius; mobius.build('Qwen/Qwen2.5-0.5B-Instruct')" -``` - -Accepted truthy values: `1`, `true`, `yes` (case-insensitive).\ -Accepted falsy values: `0`, `false`, `no` (case-insensitive).\ -Any other value falls back to the field default. - -## Setting flags programmatically (internal code only) - -Internal modules can import `flags` directly and assign to it: - -```python -from mobius._flags import flags - -flags.suppress_dedup_warning = False # disable -flags.suppress_dedup_warning = True # re-enable -``` - -## Using `override_flags()` in tests - -For tests that need a temporary flag value, use the `override_flags` context -manager. It restores the original values on exit, even if the test raises: - -```python -from mobius._flags import override_flags - -def test_build_with_warnings(tmp_path): - with override_flags(suppress_dedup_warning=False): - pkg = mobius.build("Qwen/Qwen2.5-0.5B-Instruct") - # suppress_dedup_warning is restored here -``` - -`override_flags` raises `ValueError` for unknown flag names, catching typos early. - -> **Thread safety:** `override_flags` is not thread-safe — concurrent calls -> in different threads may interleave the save/restore cycle (TOCTOU). For -> pytest, this is safe with `pytest -n auto` because xdist workers run in -> separate processes with independent flag singletons. - -## Listing all flags - -```python -from mobius._flags import list_flags - -print(list_flags()) -``` - -## Adding new flags - -1. Add a field to `Flags` in `src/mobius/_flags.py`: - - ```python - my_new_flag: bool = dataclasses.field( - default_factory=lambda: _env_bool("MOBIUS_MY_NEW_FLAG", False) - ) - """Short description of what my_new_flag controls.""" - ``` - -2. Wire the flag into the code path it controls. - -3. Regenerate this page: - - ```bash - python scripts/generate_flags_docs.py - ``` - -4. Add tests in `src/mobius/_flags_test.py` following existing patterns. From 5aa6dfba2c3ba9ed4a8ca4d6ea8d3319b00f7b68 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:56:13 -0700 Subject: [PATCH 07/11] Reorganize docs site: Sphinx docs at root, dashboard at /dashboard/ Move Sphinx documentation to the root URL (onnxruntime.github.io/mobius/) and the testing confidence dashboard to a sub-URL (onnxruntime.github.io/mobius/dashboard/). - Swap build order in pages.yml: sphinx-build to _site/, dashboard to _site/dashboard/ - Rename workflow from 'Dashboard' to 'Documentation' - Add dashboard link to docs index page - Add docs link to dashboard header Signed-off-by: Justin Chu --- .github/workflows/pages.yml | 32 +++++++++++++++++++++-------- README.md | 10 ++++----- docs/index.md | 2 ++ scripts/templates/dashboard.html.j2 | 5 ++++- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 2f2bf927c..a77451c8a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,4 +1,4 @@ -name: Dashboard +name: Documentation on: push: @@ -19,7 +19,7 @@ concurrency: jobs: build: - name: Generate Dashboard + name: Build Documentation runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -33,9 +33,9 @@ jobs: uses: actions/cache@v5 with: path: ~/.cache/pip - key: pip-dashboard-${{ hashFiles('pyproject.toml') }} + key: pip-docs-${{ hashFiles('pyproject.toml') }} restore-keys: | - pip-dashboard- + pip-docs- - name: Install PyTorch CPU run: pip install torch --index-url https://download.pytorch.org/whl/cpu @@ -46,17 +46,31 @@ jobs: pip install -r docs/requirements.txt pip install -e '.[testing]' + - name: Generate docs content + run: | + python docs/_generate_models.py + if [ -f scripts/generate_flags_docs.py ]; then + python scripts/generate_flags_docs.py + fi + + - name: Build Sphinx docs + run: sphinx-build docs _site + - name: Generate dashboard run: | - mkdir -p _site + mkdir -p _site/dashboard python scripts/generate_dashboard.py \ - --output _site/index.html \ + --output _site/dashboard/index.html \ --commit $(git rev-parse --short HEAD) - - name: Build Sphinx docs + - name: Add redirect from old /docs/ path run: | - python docs/_generate_models.py - sphinx-build docs _site/docs + mkdir -p _site/docs + cat > _site/docs/index.html << 'EOF' + + Redirecting... + Click here + EOF - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 diff --git a/README.md b/README.md index edce233d0..428ca3e0d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Supports building ONNX models from HuggingFace model IDs with automatic weight downloading, dtype casting (including bfloat16 via `ir.LazyTensor`), and multi-component export for pipelines. -📖 **[Documentation](https://onnxruntime.github.io/mobius/docs/)** · 📦 **[Supported Models](https://onnxruntime.github.io/mobius/docs/models/index.html)** +📖 **[Documentation](https://onnxruntime.github.io/mobius/)** · 📦 **[Supported Models](https://onnxruntime.github.io/mobius/models/index.html)** ## Highlighted Models @@ -34,7 +34,7 @@ multi-component export for pipelines. Supports **130 Transformers model types** and **5 Diffusers component types** across **14 task types** and **56+ reusable components**. -See the [model documentation](https://onnxruntime.github.io/mobius/docs/models/index.html) for the complete list. +See the [model documentation](https://onnxruntime.github.io/mobius/models/index.html) for the complete list. ## Installation @@ -79,7 +79,7 @@ mobius build --model openai/whisper-tiny output_dir/ mobius build --model meta-llama/Llama-3.2-1B output_dir/ --dtype f16 ``` -See the [CLI reference](https://onnxruntime.github.io/mobius/docs/cli.html) for all options. +See the [CLI reference](https://onnxruntime.github.io/mobius/cli.html) for all options. ### Examples @@ -113,7 +113,7 @@ The package is organised into four layers: - **Tasks** — Define the ONNX graph I/O contract (inputs, outputs, KV cache) - **Registry** — Maps HuggingFace `model_type` strings to model classes -See the [design document](https://onnxruntime.github.io/mobius/docs/design.html) for details. +See the [design document](https://onnxruntime.github.io/mobius/design.html) for details. ## Development @@ -133,7 +133,7 @@ lintrunner f --all-files ### Adding a new model -See the [AI-assisted model support strategy](https://onnxruntime.github.io/mobius/docs/ai-model-support-strategy.html) +See the [AI-assisted model support strategy](https://onnxruntime.github.io/mobius/ai-model-support-strategy.html) and the developer skills in `.github/skills/`: | Skill | Use when | diff --git a/docs/index.md b/docs/index.md index 1cd631cca..7e72bee71 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,8 @@ ONNX model definitions for generative AI architectures using the [onnxscript](ht Build ONNX models directly from HuggingFace model IDs with automatic weight downloading, dtype casting, and multi-component export. +📊 [Model Support Dashboard](dashboard/index.html) + ```{toctree} :maxdepth: 2 :caption: Contents diff --git a/scripts/templates/dashboard.html.j2 b/scripts/templates/dashboard.html.j2 index de5fdb9dc..051d9b83f 100644 --- a/scripts/templates/dashboard.html.j2 +++ b/scripts/templates/dashboard.html.j2 @@ -265,7 +265,10 @@ document.documentElement.setAttribute('data-theme',t);}())

🧪 Testing Confidence Dashboard

- +
+ 📖 Docs + +

Generated {{ timestamp }} · Commit {{ commit }} · {{ total_models }} registered model types From 8a5d428687125ceac043ec05ca69f70c127c9857 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 20:59:39 -0700 Subject: [PATCH 08/11] Move generate_flags_docs.py to docs/_generate_flags_docs.py Matches the convention of docs/_generate_models.py. Update pages.yml and self-references in the script. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Justin Chu --- .github/workflows/pages.yml | 2 +- .../_generate_flags_docs.py | 18 +++++++++--------- src/mobius/_flags.py | 12 ++++++------ src/mobius/_flags_test.py | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) rename scripts/generate_flags_docs.py => docs/_generate_flags_docs.py (93%) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 057f0e60e..25def7296 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -56,7 +56,7 @@ jobs: - name: Build Sphinx docs run: | python docs/_generate_models.py - python scripts/generate_flags_docs.py + python docs/_generate_flags_docs.py sphinx-build docs _site/docs - name: Upload Pages artifact diff --git a/scripts/generate_flags_docs.py b/docs/_generate_flags_docs.py similarity index 93% rename from scripts/generate_flags_docs.py rename to docs/_generate_flags_docs.py index 50dc0873c..73072f059 100644 --- a/scripts/generate_flags_docs.py +++ b/docs/_generate_flags_docs.py @@ -2,18 +2,18 @@ # Copyright (c) ONNX Project Contributors # SPDX-License-Identifier: Apache-2.0 -"""Auto-generate docs/feature-flags.md from the Flags dataclass. +"""Auto-generate docs/feature-flags.md from the _Flags dataclass. Run this script whenever a flag is added or modified:: - python scripts/generate_flags_docs.py + python docs/_generate_flags_docs.py The generated file is committed to the repository. A CI check (see ``.github/workflows/check-flags-docs.yml``) verifies it is up to date. Extraction strategy ------------------- -For each field in :class:`mobius._flags.Flags`: +For each field in :class:`mobius._flags._Flags`: * **Name** — from ``dataclasses.fields()``. * **Env var** — parsed from the ``_env_bool("ENV_VAR", default)`` call @@ -38,7 +38,7 @@ # Add src/ to the path so we can import mobius without an editable install. sys.path.insert(0, str(_REPO_ROOT / "src")) -from mobius._flags import Flags # noqa: E402 (after sys.path manipulation) +from mobius._flags import _Flags # noqa: E402 (after sys.path manipulation) # --------------------------------------------------------------------------- # AST extraction helpers @@ -105,7 +105,7 @@ def _extract_env_info(class_def: ast.ClassDef) -> dict[str, tuple[str, bool]]: # --------------------------------------------------------------------------- _STATIC_PREAMBLE = """\ - # Feature Flags @@ -176,7 +176,7 @@ def test_build_with_warnings(tmp_path): ## Adding new flags -1. Add a field to `Flags` in `src/mobius/_flags.py`: +1. Add a field to `_Flags` in `src/mobius/_flags.py`: ```python my_new_flag: bool = dataclasses.field( @@ -190,7 +190,7 @@ def test_build_with_warnings(tmp_path): 3. Regenerate this page: ```bash - python scripts/generate_flags_docs.py + python docs/_generate_flags_docs.py ``` 4. Add tests in `src/mobius/_flags_test.py` following existing patterns. @@ -201,13 +201,13 @@ def generate(output: Path = _OUTPUT) -> str: """Generate the feature-flags markdown and write it to *output*.""" import dataclasses as dc - class_def = _parse_class_ast(Flags) + class_def = _parse_class_ast(_Flags) field_docs = _extract_field_docstrings(class_def) env_info = _extract_env_info(class_def) # Build the flags reference table. table_rows: list[str] = [] - for field in dc.fields(Flags): + for field in dc.fields(_Flags): name = field.name env_var, default = env_info.get(name, (f"MOBIUS_{name.upper()}", "?")) doc = field_docs.get(name, "") diff --git a/src/mobius/_flags.py b/src/mobius/_flags.py index 207115d13..6043139c2 100644 --- a/src/mobius/_flags.py +++ b/src/mobius/_flags.py @@ -7,13 +7,13 @@ set via an environment variable (``MOBIUS_``) or programmatically by assigning to the :data:`flags` singleton. -Environment variable values are read each time a :class:`Flags` instance is +Environment variable values are read each time a :class:`_Flags` instance is constructed. The global :data:`flags` singleton is constructed at import time, so env vars should be set before importing mobius. Valid truthy strings are ``1``, ``true``, ``yes``; falsy are ``0``, ``false``, ``no`` (case-insensitive). Any other value falls back to the field default. -**Adding new flags:** add a field to :class:`Flags` with a +**Adding new flags:** add a field to :class:`_Flags` with a ``dataclasses.field(default_factory=...)`` that calls :func:`_env_bool`, plus a docstring string literal immediately after the field for documentation generation. @@ -56,11 +56,11 @@ def _env_bool(name: str, default: bool) -> bool: @dataclasses.dataclass -class Flags: +class _Flags: """Runtime feature flags singleton. Each flag maps to a ``MOBIUS_`` environment variable read when - a :class:`Flags` instance is constructed. The global :data:`flags` + a :class:`_Flags` instance is constructed. The global :data:`flags` singleton is constructed at import time. Flags can be overridden programmatically at any point or scoped temporarily with :func:`override_flags`. @@ -92,7 +92,7 @@ class Flags: # Global singleton — import and use this directly. -flags = Flags() +flags = _Flags() def list_flags() -> dict[str, object]: @@ -122,7 +122,7 @@ def override_flags(**kwargs: bool) -> Iterator[None]: with override_flags(suppress_dedup_warning=False): build(model_id) """ - valid = {f.name for f in dataclasses.fields(Flags)} + valid = {f.name for f in dataclasses.fields(_Flags)} unknown = sorted(set(kwargs) - valid) if unknown: available = ", ".join(sorted(valid)) diff --git a/src/mobius/_flags_test.py b/src/mobius/_flags_test.py index 7120f6f96..50975f7a5 100644 --- a/src/mobius/_flags_test.py +++ b/src/mobius/_flags_test.py @@ -15,7 +15,7 @@ class TestDefaultValues: def test_suppress_dedup_warning_default_on(self, monkeypatch): monkeypatch.delenv("MOBIUS_SUPPRESS_DEDUP_WARNING", raising=False) - f = _flags.Flags() + f = _flags._Flags() assert f.suppress_dedup_warning is True @@ -25,18 +25,18 @@ class TestEnvVarOverride: @pytest.mark.parametrize("val", ["1", "true", "True", "TRUE", "yes", "YES"]) def test_truthy_values(self, monkeypatch, val): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) - f = _flags.Flags() + f = _flags._Flags() assert f.suppress_dedup_warning is True @pytest.mark.parametrize("val", ["0", "false", "False", "FALSE", "no", "NO"]) def test_falsy_values(self, monkeypatch, val): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", val) - f = _flags.Flags() + f = _flags._Flags() assert f.suppress_dedup_warning is False def test_unknown_value_falls_back_to_default(self, monkeypatch): monkeypatch.setenv("MOBIUS_SUPPRESS_DEDUP_WARNING", "maybe") - f = _flags.Flags() + f = _flags._Flags() assert f.suppress_dedup_warning is True # default From 25653b3d61828b950c5c77b0dbc41fb69dfae36b Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 21:12:46 -0700 Subject: [PATCH 09/11] Replace CI build steps with Sphinx extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move model page generation, feature-flags doc generation, and dashboard generation into Sphinx extensions under docs/_ext/. This consolidates the entire documentation build into a single sphinx-build invocation: - models_gen: builder-inited hook runs _generate_models.py - flags_gen: builder-inited hook runs _generate_flags_docs.py (conditional) - dashboard: build-finished hook runs generate_dashboard.py into /dashboard/ and creates /docs/ → / redirect Simplify pages.yml from 4 separate steps to 1 sphinx-build command. Signed-off-by: Justin Chu --- .github/workflows/pages.yml | 25 +--------- docs/_ext/dashboard.py | 96 +++++++++++++++++++++++++++++++++++++ docs/_ext/flags_gen.py | 56 ++++++++++++++++++++++ docs/_ext/models_gen.py | 52 ++++++++++++++++++++ docs/conf.py | 8 ++++ 5 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 docs/_ext/dashboard.py create mode 100644 docs/_ext/flags_gen.py create mode 100644 docs/_ext/models_gen.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a77451c8a..9090b0f44 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -46,32 +46,9 @@ jobs: pip install -r docs/requirements.txt pip install -e '.[testing]' - - name: Generate docs content - run: | - python docs/_generate_models.py - if [ -f scripts/generate_flags_docs.py ]; then - python scripts/generate_flags_docs.py - fi - - - name: Build Sphinx docs + - name: Build documentation run: sphinx-build docs _site - - name: Generate dashboard - run: | - mkdir -p _site/dashboard - python scripts/generate_dashboard.py \ - --output _site/dashboard/index.html \ - --commit $(git rev-parse --short HEAD) - - - name: Add redirect from old /docs/ path - run: | - mkdir -p _site/docs - cat > _site/docs/index.html << 'EOF' - - Redirecting... - Click here - EOF - - name: Upload Pages artifact uses: actions/upload-pages-artifact@v4 diff --git a/docs/_ext/dashboard.py b/docs/_ext/dashboard.py new file mode 100644 index 000000000..aa0e72432 --- /dev/null +++ b/docs/_ext/dashboard.py @@ -0,0 +1,96 @@ +"""Sphinx extension: generate the confidence dashboard after build. + +Hooks into ``build-finished`` to run ``scripts/generate_dashboard.py`` +and place the output in the build directory under ``/dashboard/``. +Also creates a redirect from the old ``/docs/`` path to the new root. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from typing import Any + +from sphinx.application import Sphinx + +_REDIRECT_HTML = """\ + +\ +\ +Redirecting... +Click here +""" + + +def generate_dashboard(app: Sphinx, exception: Exception | None) -> None: + """Generate dashboard HTML into the build output directory.""" + if exception is not None: + # Don't generate dashboard if Sphinx build failed + return + + repo_root = Path(app.srcdir).parent + script = repo_root / "scripts" / "generate_dashboard.py" + + if not script.exists(): + app.warn(f"Dashboard script not found: {script}") + return + + outdir = Path(app.outdir) + dashboard_dir = outdir / "dashboard" + dashboard_dir.mkdir(parents=True, exist_ok=True) + + # Determine current git commit for display in the dashboard + commit = _git_short_hash(repo_root) + + result = subprocess.run( + [ + sys.executable, + str(script), + "--output", + str(dashboard_dir / "index.html"), + "--commit", + commit, + ], + cwd=str(repo_root), + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + raise RuntimeError( + f"Dashboard generation failed:\n{result.stderr}" + ) + + if result.stdout: + print(result.stdout.strip()) + + # Add redirect from old /docs/ path to root + docs_redirect_dir = outdir / "docs" + docs_redirect_dir.mkdir(parents=True, exist_ok=True) + (docs_redirect_dir / "index.html").write_text(_REDIRECT_HTML) + + +def _git_short_hash(repo_root: Path) -> str: + """Return the short git commit hash, or 'unknown' on failure.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=str(repo_root), + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return "unknown" + + +def setup(app: Sphinx) -> dict[str, Any]: + app.connect("build-finished", generate_dashboard) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_ext/flags_gen.py b/docs/_ext/flags_gen.py new file mode 100644 index 000000000..c0472b6ba --- /dev/null +++ b/docs/_ext/flags_gen.py @@ -0,0 +1,56 @@ +"""Sphinx extension: generate feature flags documentation at build time. + +Hooks into ``builder-inited`` to generate ``docs/feature-flags.md`` from +the ``_Flags`` dataclass. This extension is conditional — it only runs +when the flags module exists (the feature-flags script lives on a +separate PR branch and may not be merged yet). +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from typing import Any + +from sphinx.application import Sphinx + + +def generate_flags_docs(app: Sphinx) -> None: + """Generate feature-flags.md if the generator script exists.""" + docs_dir = Path(app.srcdir) + + # Check both possible locations for the script + script = docs_dir / "_generate_flags_docs.py" + if not script.exists(): + # Fallback to the scripts/ directory (legacy location) + script = docs_dir.parent / "scripts" / "generate_flags_docs.py" + + if not script.exists(): + # Not an error — feature-flags PR may not be merged yet + return + + result = subprocess.run( + [sys.executable, str(script)], + cwd=str(docs_dir.parent), + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + raise RuntimeError( + f"Feature flags doc generation failed:\n{result.stderr}" + ) + + if result.stdout: + app.verbosity and print(result.stdout.strip()) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.connect("builder-inited", generate_flags_docs) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/_ext/models_gen.py b/docs/_ext/models_gen.py new file mode 100644 index 000000000..915ae58e3 --- /dev/null +++ b/docs/_ext/models_gen.py @@ -0,0 +1,52 @@ +"""Sphinx extension: generate model documentation pages at build time. + +Hooks into ``builder-inited`` to run the existing model page generator +(``docs/_generate_models.py``) before Sphinx processes source files. +""" + +from __future__ import annotations + +import importlib +import subprocess +import sys +from pathlib import Path +from typing import Any + +from sphinx.application import Sphinx + + +def generate_model_pages(app: Sphinx) -> None: + """Generate model .md pages from the registry.""" + docs_dir = Path(app.srcdir) + script = docs_dir / "_generate_models.py" + + if not script.exists(): + app.warn(f"Model generation script not found: {script}") + return + + # Run as subprocess to avoid polluting Sphinx's import state. + # The script handles its own sys.path manipulation. + result = subprocess.run( + [sys.executable, str(script)], + cwd=str(docs_dir.parent), + capture_output=True, + text=True, + check=False, + ) + + if result.returncode != 0: + raise RuntimeError( + f"Model page generation failed:\n{result.stderr}" + ) + + if result.stdout: + app.verbosity and print(result.stdout.strip()) + + +def setup(app: Sphinx) -> dict[str, Any]: + app.connect("builder-inited", generate_model_pages) + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/conf.py b/docs/conf.py index 7f07581e3..7409b0365 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,8 +5,12 @@ from __future__ import annotations +import os import sys +# Make local extensions importable +sys.path.insert(0, os.path.abspath("_ext")) + # -- Project information ----------------------------------------------------- project = "mobius" @@ -22,6 +26,10 @@ "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", + # Local extensions that generate content during build + "models_gen", + "flags_gen", + "dashboard", ] myst_enable_extensions = [ From c0b480185a890c88b8b7b8c674c0e00b9dca1b4a Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 21:19:12 -0700 Subject: [PATCH 10/11] Fix Sphinx extension logging for Sphinx 8+ compatibility - Replace removed app.warn() with logging.getLogger(__name__).warning() - Replace 'app.verbosity and print(...)' with logger.info() - Add debug log in flags_gen when generator script is absent Signed-off-by: Justin Chu --- docs/_ext/dashboard.py | 7 +++++-- docs/_ext/flags_gen.py | 6 +++++- docs/_ext/models_gen.py | 8 +++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/_ext/dashboard.py b/docs/_ext/dashboard.py index aa0e72432..3ca394637 100644 --- a/docs/_ext/dashboard.py +++ b/docs/_ext/dashboard.py @@ -7,6 +7,7 @@ from __future__ import annotations +import logging import subprocess import sys from pathlib import Path @@ -14,6 +15,8 @@ from sphinx.application import Sphinx +logger = logging.getLogger(__name__) + _REDIRECT_HTML = """\ \ @@ -33,7 +36,7 @@ def generate_dashboard(app: Sphinx, exception: Exception | None) -> None: script = repo_root / "scripts" / "generate_dashboard.py" if not script.exists(): - app.warn(f"Dashboard script not found: {script}") + logger.warning("Dashboard script not found: %s", script) return outdir = Path(app.outdir) @@ -64,7 +67,7 @@ def generate_dashboard(app: Sphinx, exception: Exception | None) -> None: ) if result.stdout: - print(result.stdout.strip()) + logger.info(result.stdout.strip()) # Add redirect from old /docs/ path to root docs_redirect_dir = outdir / "docs" diff --git a/docs/_ext/flags_gen.py b/docs/_ext/flags_gen.py index c0472b6ba..084bf00de 100644 --- a/docs/_ext/flags_gen.py +++ b/docs/_ext/flags_gen.py @@ -8,6 +8,7 @@ from __future__ import annotations +import logging import subprocess import sys from pathlib import Path @@ -15,6 +16,8 @@ from sphinx.application import Sphinx +logger = logging.getLogger(__name__) + def generate_flags_docs(app: Sphinx) -> None: """Generate feature-flags.md if the generator script exists.""" @@ -28,6 +31,7 @@ def generate_flags_docs(app: Sphinx) -> None: if not script.exists(): # Not an error — feature-flags PR may not be merged yet + logger.debug("Flags docs generator not found; skipping") return result = subprocess.run( @@ -44,7 +48,7 @@ def generate_flags_docs(app: Sphinx) -> None: ) if result.stdout: - app.verbosity and print(result.stdout.strip()) + logger.info(result.stdout.strip()) def setup(app: Sphinx) -> dict[str, Any]: diff --git a/docs/_ext/models_gen.py b/docs/_ext/models_gen.py index 915ae58e3..2b3cca02d 100644 --- a/docs/_ext/models_gen.py +++ b/docs/_ext/models_gen.py @@ -6,7 +6,7 @@ from __future__ import annotations -import importlib +import logging import subprocess import sys from pathlib import Path @@ -14,6 +14,8 @@ from sphinx.application import Sphinx +logger = logging.getLogger(__name__) + def generate_model_pages(app: Sphinx) -> None: """Generate model .md pages from the registry.""" @@ -21,7 +23,7 @@ def generate_model_pages(app: Sphinx) -> None: script = docs_dir / "_generate_models.py" if not script.exists(): - app.warn(f"Model generation script not found: {script}") + logger.warning("Model generation script not found: %s", script) return # Run as subprocess to avoid polluting Sphinx's import state. @@ -40,7 +42,7 @@ def generate_model_pages(app: Sphinx) -> None: ) if result.stdout: - app.verbosity and print(result.stdout.strip()) + logger.info(result.stdout.strip()) def setup(app: Sphinx) -> dict[str, Any]: From 175a5891395d8bbbef5a1054e26027fb8b71aec5 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Fri, 27 Mar 2026 21:23:54 -0700 Subject: [PATCH 11/11] Fix lint: collapse single-line raise statements Signed-off-by: Justin Chu --- docs/_ext/dashboard.py | 4 +--- docs/_ext/flags_gen.py | 4 +--- docs/_ext/models_gen.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/_ext/dashboard.py b/docs/_ext/dashboard.py index 3ca394637..7d496caed 100644 --- a/docs/_ext/dashboard.py +++ b/docs/_ext/dashboard.py @@ -62,9 +62,7 @@ def generate_dashboard(app: Sphinx, exception: Exception | None) -> None: ) if result.returncode != 0: - raise RuntimeError( - f"Dashboard generation failed:\n{result.stderr}" - ) + raise RuntimeError(f"Dashboard generation failed:\n{result.stderr}") if result.stdout: logger.info(result.stdout.strip()) diff --git a/docs/_ext/flags_gen.py b/docs/_ext/flags_gen.py index 084bf00de..c0925e111 100644 --- a/docs/_ext/flags_gen.py +++ b/docs/_ext/flags_gen.py @@ -43,9 +43,7 @@ def generate_flags_docs(app: Sphinx) -> None: ) if result.returncode != 0: - raise RuntimeError( - f"Feature flags doc generation failed:\n{result.stderr}" - ) + raise RuntimeError(f"Feature flags doc generation failed:\n{result.stderr}") if result.stdout: logger.info(result.stdout.strip()) diff --git a/docs/_ext/models_gen.py b/docs/_ext/models_gen.py index 2b3cca02d..61e6d67f3 100644 --- a/docs/_ext/models_gen.py +++ b/docs/_ext/models_gen.py @@ -37,9 +37,7 @@ def generate_model_pages(app: Sphinx) -> None: ) if result.returncode != 0: - raise RuntimeError( - f"Model page generation failed:\n{result.stderr}" - ) + raise RuntimeError(f"Model page generation failed:\n{result.stderr}") if result.stdout: logger.info(result.stdout.strip())