Repository: vega/altair (language: python)

Issue:
Consolidate: `alt.theme(s)`, `alt.typing.theme`, `alt.vegalite.v5.theme`, `alt.utils.theme`
### What is your suggestion?

The following related PRs have been merged since `v5.4.1` release:
- #3526
- #3536

This issue is the continuation of the discussion below:

<details>
<summary><i>Originally posted by @dangotbanned in <a href="https://github.com/vega/altair/issues/3536#issuecomment-2366736230">post review comment</i></summary>

...
In https://github.com/vega/altair/pull/3536#discussion_r1759328893 I mentioned some related issues, but can now see I forgot to elaborate on **what I would prefer** for `v6`.

I would like to have used `altair.theme` as the namespace instead of `altair.typing.theme`.
However, this would be very easy to mistake for `altair.themes` - which is a variable storing a registry of themes.

For `v6` I would propose the following api changes - and if there is any support I will open a new issue:

- `altair.typing.theme` -> `altair.theme` 
	- Including dropping `altair.typing.ThemeConfig`, which is already included in the above
- `@altair.register_theme` -> `@altair.theme.register`
- New function: `altair.theme.enable`
- Remove `altair.themes` top-level export

This would consolidate all **theme** related functionality behind a single namespace:

```py
from altair import theme

@theme.register("custom 1", enable=False)
def custom_theme() -> theme.ThemeConfig:
    return {
        "autosize": {"contains": "content", "resize": True}, 
        "config": theme.ConfigKwds(
            circle=theme.MarkConfigKwds(fill="purple")
        ),
    }

...

theme.enable("custom 1")
...
theme.enable("default")
```

</details>

<details>
<summary><i>Response by @mattijn in <a href="https://github.com/vega/altair/issues/3536#issuecomment-2366954494">comment</i></summary>

...
I like the suggestion you provide in https://github.com/vega/altair/pull/3536#issuecomment-2366736230 to consolidate everything under a single namespace, including the proposed `ThemeConfig`. 
I’m fine with doing that now, rather than releasing a version first and requiring deprecation immediately after.

However, instead of phasing out the plural `altair.themes` in favour of the singleton `altair.theme` can we consolidate this PR's work into the existing `altair.themes`?
For example, can we have `altair.themes.ThemeConfig` (and `altair.themes.AxisConfigKwds`, etc.) aligning with the existing `altair.themes.register()` and `altair.themes.enable()`.

Aesthetically I like the singleton `altair.theme`, but I think sticking with `altair.themes` is more practical, if that is also an option. Can you reflect on this?

</details>

[Proposing this issue itself](https://github.com/vega/altair/pull/3536#issuecomment-2367591832)
        
---

Also tagging @binste, as our discussion in another issue probably needs consideration
- https://github.com/vega/altair/issues/3586

---

# Notes
- `alt.vegalite.v5.theme`, `alt.utils.theme` were not mentioned in the discussion - stating them here for completeness

# Edit
@mattijn apologies for the delay, but see https://github.com/vega/altair/issues/3610#issuecomment-2374650764 for my thoughts


Discussion / hints:
# `alt.themes`

> Aesthetically I like the singleton `altair.theme`, but I think sticking with `altair.themes` is more practical, if that is also an option. Can you reflect on this?

@mattijn putting aesthetics aside, I see two key issues with using `alt.themes`.

## Instance vs module
As I mentioned in https://github.com/vega/altair/pull/3536#discussion_r1759328893 and https://github.com/vega/altair/pull/3536#issuecomment-2366736230 `alt.themes` is an instance of [`ThemeRegistry`](https://github.com/vega/altair/blob/02e8c3479db0bc4b5adc52bb3453eb5d5b46f06a/altair/utils/theme.py#L25) and [`PluginRegistry`](https://github.com/vega/altair/blob/02e8c3479db0bc4b5adc52bb3453eb5d5b46f06a/altair/utils/plugin_registry.py#L71).
We **could** support usage like below, in a few different ways:

<details>
<summary>How?</summary>

- Adding a `ThemeRegistry.__getattr__`
- Adding the classes as properties of `ThemeRegistry`
- Patching the classes onto the instance `themes`

</details>

```py
from altair import themes

themes.register
themes.ThemeConfig
themes.MarkConfigKwds
...
```
But unless we get particularly creative with `importlib`, we **couldn't**  support:
```py
from altair.themes import ThemeConfig, MarkConfigKwds, ...
```
I imagine if we treated `themes` like it is a module, people would try this to [reduce verbosity](https://github.com/vega/altair/pull/3536#issuecomment-2362016423) and be surprised by the import error.

### Making `alt.themes` a module?
Would solve the import issues.

I think we could recreate **most** of the existing behavior, using a mixture of functions and a module-level `__getattr__`.

The two things I **can't** see a solution for are:
- `__repr__`
- `isinstance()` or other builtins

## `alt.themes.register`
The inherited signature uses `None` to unregister a plugin:

<details>
<summary><code>PluginRegistry.register()</code></summary>

https://github.com/vega/altair/blob/02e8c3479db0bc4b5adc52bb3453eb5d5b46f06a/altair/utils/plugin_registry.py#L135-L147

</details>

Since this is not a default, we *could* probably work around this by adding some sentinel as a default for `@alt.themes.register()`.
The source for [`@dataclasses.dataclass()`](https://github.com/python/cpython/blob/78aeb38f7d880a340295214abc4f7e77ffdad509/Lib/dataclasses.py#L1317-L1345) has an example of dual usage (function and decorator).

I'm fairly sure the issues around this wouldn't be the technical aspect, mostly just a bit of extra complexity for the user and more explanation in the docs.

---

# `alt.theme`
I really do think it would be simpler to start fresh with a new namespace, without any additional baggage.

- Decide what we want to expose under `alt.theme.___`
- Populate `alt.theme`
- Update docs to use `alt.theme`
- Deprecate `alt.themes` and then remove in `v6`
	- We could add a `alt.__init__.__getattr__` to [hide from autocomplete](https://github.com/vega/altair/discussions/3478#discussioncomment-10200941)

We could also be sure that this wouldn't change the meaning of existing user code.
A deprecation warning can always be ignored for those determined to still use `alt.themes`
Thanks for the detailed description! I’m good with your suggestion to start with `alt.theme`. While it may not become or serve as a theme or plugin registry, I’m still hopeful we can achieve a similar user experience to the current `alt.themes` (even if it’s just wishful thinking😄)
> Thanks for the detailed description! I’m good with your suggestion to start with `alt.theme`. While it may not become or serve as a theme or plugin registry, I’m still hopeful we can achieve a similar user experience to the current `alt.themes` (even if it’s just wishful thinking😄)

No problem @mattijn, thanks I'll try to work on this soon.

We can always have `themes` accessible via `alt.theme.themes` - if you'd see a benefit?

Fix patch:
diff --git a/altair/__init__.py b/altair/__init__.py
index d4e20f02f..cbc2c3b52 100644
--- a/altair/__init__.py
+++ b/altair/__init__.py
@@ -617,7 +617,6 @@
     "mixins",
     "param",
     "parse_shorthand",
-    "register_theme",
     "renderers",
     "repeat",
     "sample",
@@ -627,7 +626,6 @@
     "sequence",
     "sphere",
     "theme",
-    "themes",
     "to_csv",
     "to_json",
     "to_values",
@@ -653,10 +651,44 @@ def __dir__():
 from altair.jupyter import JupyterChart
 from altair.expr import expr
 from altair.utils import AltairDeprecationWarning, parse_shorthand, Undefined
-from altair import typing
+from altair import typing, theme
 
 
 def load_ipython_extension(ipython):
     from altair._magics import vegalite
 
     ipython.register_magic_function(vegalite, "cell")
+
+
+def __getattr__(name: str):
+    from altair.utils.deprecation import deprecated_warn
+
+    if name == "themes":
+        deprecated_warn(
+            "Most cases require only the following change:\n\n"
+            "    # Deprecated\n"
+            "    alt.themes.enable('quartz')\n\n"
+            "    # Updated\n"
+            "    alt.theme.enable('quartz')\n\n"
+            "If your code registers a theme, make the following change:\n\n"
+            "    # Deprecated\n"
+            "    def custom_theme():\n"
+            "        return {'height': 400, 'width': 700}\n"
+            "    alt.themes.register('theme_name', custom_theme)\n"
+            "    alt.themes.enable('theme_name')\n\n"
+            "    # Updated\n"
+            "    @alt.theme.register('theme_name', enable=True)\n"
+            "    def custom_theme() -> alt.theme.ThemeConfig:\n"
+            "        return {'height': 400, 'width': 700}\n\n"
+            "See the updated User Guide for further details:\n"
+            "    https://altair-viz.github.io/user_guide/api.html#theme\n"
+            "    https://altair-viz.github.io/user_guide/customization.html#chart-themes",
+            version="5.5.0",
+            alternative="altair.theme",
+            stacklevel=3,
+            action="once",
+        )
+        return theme._themes
+    else:
+        msg = f"module {__name__!r} has no attribute {name!r}"
+        raise AttributeError(msg)
diff --git a/altair/theme.py b/altair/theme.py
new file mode 100644
index 000000000..e77a95eb8
--- /dev/null
+++ b/altair/theme.py
@@ -0,0 +1,321 @@
+"""Customizing chart configuration defaults."""
+
+from __future__ import annotations
+
+from functools import wraps as _wraps
+from typing import TYPE_CHECKING, Any
+from typing import overload as _overload
+
+from altair.vegalite.v5.schema._config import (
+    AreaConfigKwds,
+    AutoSizeParamsKwds,
+    AxisConfigKwds,
+    AxisResolveMapKwds,
+    BarConfigKwds,
+    BindCheckboxKwds,
+    BindDirectKwds,
+    BindInputKwds,
+    BindRadioSelectKwds,
+    BindRangeKwds,
+    BoxPlotConfigKwds,
+    BrushConfigKwds,
+    CompositionConfigKwds,
+    ConfigKwds,
+    DateTimeKwds,
+    DerivedStreamKwds,
+    ErrorBandConfigKwds,
+    ErrorBarConfigKwds,
+    FeatureGeometryGeoJsonPropertiesKwds,
+    FormatConfigKwds,
+    GeoJsonFeatureCollectionKwds,
+    GeoJsonFeatureKwds,
+    GeometryCollectionKwds,
+    GradientStopKwds,
+    HeaderConfigKwds,
+    IntervalSelectionConfigKwds,
+    IntervalSelectionConfigWithoutTypeKwds,
+    LegendConfigKwds,
+    LegendResolveMapKwds,
+    LegendStreamBindingKwds,
+    LinearGradientKwds,
+    LineConfigKwds,
+    LineStringKwds,
+    LocaleKwds,
+    MarkConfigKwds,
+    MergedStreamKwds,
+    MultiLineStringKwds,
+    MultiPointKwds,
+    MultiPolygonKwds,
+    NumberLocaleKwds,
+    OverlayMarkDefKwds,
+    PaddingKwds,
+    PointKwds,
+    PointSelectionConfigKwds,
+    PointSelectionConfigWithoutTypeKwds,
+    PolygonKwds,
+    ProjectionConfigKwds,
+    ProjectionKwds,
+    RadialGradientKwds,
+    RangeConfigKwds,
+    RectConfigKwds,
+    ResolveKwds,
+    RowColKwds,
+    ScaleConfigKwds,
+    ScaleInvalidDataConfigKwds,
+    ScaleResolveMapKwds,
+    SelectionConfigKwds,
+    StepKwds,
+    StyleConfigIndexKwds,
+    ThemeConfig,
+    TickConfigKwds,
+    TimeIntervalStepKwds,
+    TimeLocaleKwds,
+    TitleConfigKwds,
+    TitleParamsKwds,
+    TooltipContentKwds,
+    TopLevelSelectionParameterKwds,
+    VariableParameterKwds,
+    ViewBackgroundKwds,
+    ViewConfigKwds,
+)
+from altair.vegalite.v5.theme import themes as _themes
+
+if TYPE_CHECKING:
+    import sys
+    from typing import Any, Callable, Literal
+
+    if sys.version_info >= (3, 11):
+        from typing import LiteralString
+    else:
+        from typing_extensions import LiteralString
+    if sys.version_info >= (3, 10):
+        from typing import ParamSpec
+    else:
+        from typing_extensions import ParamSpec
+
+    from altair.utils.plugin_registry import Plugin
+
+    P = ParamSpec("P")
+
+__all__ = [
+    "AreaConfigKwds",
+    "AutoSizeParamsKwds",
+    "AxisConfigKwds",
+    "AxisResolveMapKwds",
+    "BarConfigKwds",
+    "BindCheckboxKwds",
+    "BindDirectKwds",
+    "BindInputKwds",
+    "BindRadioSelectKwds",
+    "BindRangeKwds",
+    "BoxPlotConfigKwds",
+    "BrushConfigKwds",
+    "CompositionConfigKwds",
+    "ConfigKwds",
+    "DateTimeKwds",
+    "DerivedStreamKwds",
+    "ErrorBandConfigKwds",
+    "ErrorBarConfigKwds",
+    "FeatureGeometryGeoJsonPropertiesKwds",
+    "FormatConfigKwds",
+    "GeoJsonFeatureCollectionKwds",
+    "GeoJsonFeatureKwds",
+    "GeometryCollectionKwds",
+    "GradientStopKwds",
+    "HeaderConfigKwds",
+    "IntervalSelectionConfigKwds",
+    "IntervalSelectionConfigWithoutTypeKwds",
+    "LegendConfigKwds",
+    "LegendResolveMapKwds",
+    "LegendStreamBindingKwds",
+    "LineConfigKwds",
+    "LineStringKwds",
+    "LinearGradientKwds",
+    "LocaleKwds",
+    "MarkConfigKwds",
+    "MergedStreamKwds",
+    "MultiLineStringKwds",
+    "MultiPointKwds",
+    "MultiPolygonKwds",
+    "NumberLocaleKwds",
+    "OverlayMarkDefKwds",
+    "PaddingKwds",
+    "PointKwds",
+    "PointSelectionConfigKwds",
+    "PointSelectionConfigWithoutTypeKwds",
+    "PolygonKwds",
+    "ProjectionConfigKwds",
+    "ProjectionKwds",
+    "RadialGradientKwds",
+    "RangeConfigKwds",
+    "RectConfigKwds",
+    "ResolveKwds",
+    "RowColKwds",
+    "ScaleConfigKwds",
+    "ScaleInvalidDataConfigKwds",
+    "ScaleResolveMapKwds",
+    "SelectionConfigKwds",
+    "StepKwds",
+    "StyleConfigIndexKwds",
+    "ThemeConfig",
+    "TickConfigKwds",
+    "TimeIntervalStepKwds",
+    "TimeLocaleKwds",
+    "TitleConfigKwds",
+    "TitleParamsKwds",
+    "TooltipContentKwds",
+    "TopLevelSelectionParameterKwds",
+    "VariableParameterKwds",
+    "ViewBackgroundKwds",
+    "ViewConfigKwds",
+    "active",
+    "enable",
+    "get",
+    "names",
+    "options",
+    "register",
+    "unregister",
+]
+
+
+def register(
+    name: LiteralString, *, enable: bool
+) -> Callable[[Plugin[ThemeConfig]], Plugin[ThemeConfig]]:
+    """
+    Decorator for registering a theme function.
+
+    Parameters
+    ----------
+    name
+        Unique name assigned in registry.
+    enable
+        Auto-enable the wrapped theme.
+
+    Examples
+    --------
+    Register and enable a theme::
+
+        import altair as alt
+        from altair import theme
+
+
+        @theme.register("param_font_size", enable=True)
+        def custom_theme() -> theme.ThemeConfig:
+            sizes = 12, 14, 16, 18, 20
+            return {
+                "autosize": {"contains": "content", "resize": True},
+                "background": "#F3F2F1",
+                "config": {
+                    "axisX": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
+                    "axisY": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
+                    "font": "'Lato', 'Segoe UI', Tahoma, Verdana, sans-serif",
+                    "headerColumn": {"labelFontSize": sizes[1]},
+                    "headerFacet": {"labelFontSize": sizes[1]},
+                    "headerRow": {"labelFontSize": sizes[1]},
+                    "legend": {"labelFontSize": sizes[0], "titleFontSize": sizes[1]},
+                    "text": {"fontSize": sizes[0]},
+                    "title": {"fontSize": sizes[-1]},
+                },
+                "height": {"step": 28},
+                "width": 350,
+            }
+
+    We can then see the ``name`` parameter displayed when checking::
+
+        theme.active
+        "param_font_size"
+
+    Until another theme has been enabled, all charts will use defaults set in ``custom_theme()``::
+
+        from vega_datasets import data
+
+        source = data.stocks()
+        lines = (
+            alt.Chart(source, title=alt.Title("Stocks"))
+            .mark_line()
+            .encode(x="date:T", y="price:Q", color="symbol:N")
+        )
+        lines.interactive(bind_y=False)
+
+    """
+
+    # HACK: See for `LiteralString` requirement in `name`
+    # https://github.com/vega/altair/pull/3526#discussion_r1743350127
+    def decorate(func: Plugin[ThemeConfig], /) -> Plugin[ThemeConfig]:
+        _register(name, func)
+        if enable:
+            _themes.enable(name)
+
+        @_wraps(func)
+        def wrapper(*args: P.args, **kwargs: P.kwargs) -> ThemeConfig:
+            return func(*args, **kwargs)
+
+        return wrapper
+
+    return decorate
+
+
+def unregister(name: LiteralString) -> Plugin[ThemeConfig]:
+    """
+    Remove and return a previously registered theme.
+
+    Parameters
+    ----------
+    name
+        Unique name assigned during ``alt.theme.register``.
+
+    Raises
+    ------
+    TypeError
+        When ``name`` has not been registered.
+    """
+    plugin = _register(name, None)
+    if plugin is None:
+        msg = (
+            f"Found no theme named {name!r} in registry.\n"
+            f"Registered themes:\n"
+            f"{names()!r}"
+        )
+        raise TypeError(msg)
+    else:
+        return plugin
+
+
+enable = _themes.enable
+get = _themes.get
+names = _themes.names
+active: str
+"""Return the name of the currently active theme."""
+options: dict[str, Any]
+"""Return the current themes options dictionary."""
+
+
+def __dir__() -> list[str]:
+    return __all__
+
+
+@_overload
+def __getattr__(name: Literal["active"]) -> str: ...  # type: ignore[misc]
+@_overload
+def __getattr__(name: Literal["options"]) -> dict[str, Any]: ...  # type: ignore[misc]
+def __getattr__(name: str) -> Any:
+    if name == "active":
+        return _themes.active
+    elif name == "options":
+        return _themes.options
+    else:
+        msg = f"module {__name__!r} has no attribute {name!r}"
+        raise AttributeError(msg)
+
+
+def _register(
+    name: LiteralString, fn: Plugin[ThemeConfig] | None, /
+) -> Plugin[ThemeConfig] | None:
+    if fn is None:
+        return _themes._plugins.pop(name, None)
+    elif _themes.plugin_type(fn):
+        _themes._plugins[name] = fn
+        return fn
+    else:
+        msg = f"{type(fn).__name__!r} is not a callable theme\n\n{fn!r}"
+        raise TypeError(msg)
diff --git a/altair/typing/__init__.py b/altair/typing/__init__.py
index d80469f35..cd8cb1489 100644
--- a/altair/typing/__init__.py
+++ b/altair/typing/__init__.py
@@ -46,13 +46,9 @@
     "ChartType",
     "EncodeKwds",
     "Optional",
-    "ThemeConfig",
     "is_chart_type",
-    "theme",
 ]
 
-from altair.typing import theme
-from altair.typing.theme import ThemeConfig
 from altair.utils.schemapi import Optional
 from altair.vegalite.v5.api import ChartType, is_chart_type
 from altair.vegalite.v5.schema.channels import (
diff --git a/altair/typing/theme.py b/altair/typing/theme.py
deleted file mode 100644
index 17f9a7fd2..000000000
--- a/altair/typing/theme.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from altair.vegalite.v5.schema._config import *  # noqa: F403
diff --git a/altair/utils/deprecation.py b/altair/utils/deprecation.py
index f11dd1a41..2b07a229e 100644
--- a/altair/utils/deprecation.py
+++ b/altair/utils/deprecation.py
@@ -1,8 +1,9 @@
 from __future__ import annotations
 
 import sys
+import threading
 import warnings
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
 
 if sys.version_info >= (3, 13):
     from warnings import deprecated as _deprecated
@@ -16,6 +17,13 @@
     else:
         from typing_extensions import LiteralString
 
+__all__ = [
+    "AltairDeprecationWarning",
+    "deprecated",
+    "deprecated_static_only",
+    "deprecated_warn",
+]
+
 
 class AltairDeprecationWarning(DeprecationWarning): ...
 
@@ -26,7 +34,7 @@ def _format_message(
     message: LiteralString | None,
     /,
 ) -> LiteralString:
-    output = f"Deprecated in `altair={version}`."
+    output = f"\nDeprecated since `altair={version}`."
     if alternative:
         output = f"{output} Use {alternative} instead."
     return f"{output}\n{message}" if message else output
@@ -81,6 +89,7 @@ def deprecated_warn(
     alternative: LiteralString | None = None,
     category: type[AltairDeprecationWarning] = AltairDeprecationWarning,
     stacklevel: int = 2,
+    action: Literal["once"] | None = None,
 ) -> None:
     """
     Indicate that the current code path is deprecated.
@@ -112,4 +121,76 @@ def deprecated_warn(
     [warnings.warn](https://docs.python.org/3/library/warnings.html#warnings.warn)
     """
     msg = _format_message(version, alternative, message)
-    warnings.warn(msg, category=category, stacklevel=stacklevel)
+    if action is None:
+        warnings.warn(msg, category=category, stacklevel=stacklevel)
+    elif action == "once":
+        _warn_once(msg, category=category, stacklevel=stacklevel)
+    else:
+        raise NotImplementedError(action)
+
+
+deprecated_static_only = _deprecated
+"""
+Using this decorator **exactly as described**, ensures ``message`` is displayed to a static type checker.
+
+**BE CAREFUL USING THIS**.
+
+See screenshots in `comment`_ for motivation.
+
+Every use should look like::
+
+    @deprecated_static_only(
+        "Deprecated since `altair=5.5.0`. Use altair.other instead.",
+        category=None,
+    )
+    def old_function(*args): ...
+
+If a runtime warning is desired, use `@alt.utils.deprecated` instead.
+
+Parameters
+----------
+message : LiteralString
+    - **Not** a variable
+    - **Not** use placeholders
+    - **Not** use concatenation
+    - **Do not use anything that could be considered dynamic**
+
+category : None
+    You **need** to explicitly pass ``None``
+
+.. _comment:
+    https://github.com/vega/altair/pull/3618#issuecomment-2423991968
+---
+"""
+
+
+class _WarningsMonitor:
+    def __init__(self) -> None:
+        self._warned: dict[LiteralString, Literal[True]] = {}
+        self._lock = threading.Lock()
+
+    def __contains__(self, key: LiteralString, /) -> bool:
+        with self._lock:
+            return key in self._warned
+
+    def hit(self, key: LiteralString, /) -> None:
+        with self._lock:
+            self._warned[key] = True
+
+    def clear(self) -> None:
+        with self._lock:
+            self._warned.clear()
+
+
+_warnings_monitor = _WarningsMonitor()
+
+
+def _warn_once(
+    msg: LiteralString, /, *, category: type[AltairDeprecationWarning], stacklevel: int
+) -> None:
+    global _warnings_monitor
+    if msg in _warnings_monitor:
+        return
+    else:
+        _warnings_monitor.hit(msg)
+        warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
diff --git a/altair/utils/plugin_registry.py b/altair/utils/plugin_registry.py
index b2723396a..b4505fd9f 100644
--- a/altair/utils/plugin_registry.py
+++ b/altair/utils/plugin_registry.py
@@ -39,7 +39,7 @@ def __str__(self):
         return f"No {self.name!r} entry point found in group {self.group!r}"
 
 
-class PluginEnabler:
+class PluginEnabler(Generic[PluginT, R]):
     """
     Context manager for enabling plugins.
 
@@ -51,21 +51,23 @@ class PluginEnabler:
         # plugins back to original state
     """
 
-    def __init__(self, registry: PluginRegistry, name: str, **options):
-        self.registry: PluginRegistry = registry
+    def __init__(
+        self, registry: PluginRegistry[PluginT, R], name: str, **options: Any
+    ) -> None:
+        self.registry: PluginRegistry[PluginT, R] = registry
         self.name: str = name
         self.options: dict[str, Any] = options
         self.original_state: dict[str, Any] = registry._get_state()
         self.registry._enable(name, **options)
 
-    def __enter__(self) -> PluginEnabler:
+    def __enter__(self) -> PluginEnabler[PluginT, R]:
         return self
 
     def __exit__(self, typ: type, value: Exception, traceback: TracebackType) -> None:
         self.registry._set_state(self.original_state)
 
     def __repr__(self) -> str:
-        return f"{self.registry.__class__.__name__}.enable({self.name!r})"
+        return f"{type(self.registry).__name__}.enable({self.name!r})"
 
 
 class PluginRegistry(Generic[PluginT, R]):
@@ -211,7 +213,9 @@ def _enable(self, name: str, **options) -> None:
             self._global_settings[key] = options.pop(key)
         self._options = options
 
-    def enable(self, name: str | None = None, **options) -> PluginEnabler:
+    def enable(
+        self, name: str | None = None, **options: Any
+    ) -> PluginEnabler[PluginT, R]:
         """
         Enable a plugin by name.
 
diff --git a/altair/utils/theme.py b/altair/utils/theme.py
deleted file mode 100644
index bbb7247bc..000000000
--- a/altair/utils/theme.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""Utilities for registering and working with themes."""
-
-from __future__ import annotations
-
-import sys
-from typing import TYPE_CHECKING
-
-from altair.utils.plugin_registry import Plugin, PluginRegistry
-from altair.vegalite.v5.schema._config import ThemeConfig
-
-if sys.version_info >= (3, 11):
-    from typing import LiteralString
-else:
-    from typing_extensions import LiteralString
-
-if TYPE_CHECKING:
-    from altair.utils.plugin_registry import PluginEnabler
-    from altair.vegalite.v5.theme import AltairThemes, VegaThemes
-
-ThemeType = Plugin[ThemeConfig]
-
-
-# HACK: See for `LiteralString` requirement in `name`
-# https://github.com/vega/altair/pull/3526#discussion_r1743350127
-class ThemeRegistry(PluginRegistry[ThemeType, ThemeConfig]):
-    def enable(
-        self, name: LiteralString | AltairThemes | VegaThemes | None = None, **options
-    ) -> PluginEnabler:
-        """
-        Enable a theme by name.
-
-        This can be either called directly, or used as a context manager.
-
-        Parameters
-        ----------
-        name : string (optional)
-            The name of the theme to enable. If not specified, then use the
-            current active name.
-        **options :
-            Any additional parameters will be passed to the theme as keyword
-            arguments
-
-        Returns
-        -------
-        PluginEnabler:
-            An object that allows enable() to be used as a context manager
-
-        Notes
-        -----
-        Default `vega` themes can be previewed at https://vega.github.io/vega-themes/
-        """
-        return super().enable(name, **options)
diff --git a/altair/vegalite/v5/__init__.py b/altair/vegalite/v5/__init__.py
index a18be6e11..d8d794b2a 100644
--- a/altair/vegalite/v5/__init__.py
+++ b/altair/vegalite/v5/__init__.py
@@ -1,9 +1,9 @@
-# ruff: noqa: F401, F403
+# ruff: noqa: F401, F403, F405
 from altair.expr.core import datum
-
-from .api import *
-from .compiler import vegalite_compilers
-from .data import (
+from altair.vegalite.v5 import api, compiler, schema
+from altair.vegalite.v5.api import *
+from altair.vegalite.v5.compiler import vegalite_compilers
+from altair.vegalite.v5.data import (
     MaxRowsError,
     data_transformers,
     default_data_transformer,
@@ -13,12 +13,640 @@
     to_json,
     to_values,
 )
-from .display import (
+from altair.vegalite.v5.display import (
     VEGA_VERSION,
     VEGAEMBED_VERSION,
     VEGALITE_VERSION,
     VegaLite,
     renderers,
 )
-from .schema import *
-from .theme import register_theme, themes
+from altair.vegalite.v5.schema import *
+
+# The content of __all__ is automatically written by
+# tools/update_init_file.py. Do not modify directly.
+
+__all__ = [
+    "SCHEMA_URL",
+    "SCHEMA_VERSION",
+    "TOPLEVEL_ONLY_KEYS",
+    "URI",
+    "VEGAEMBED_VERSION",
+    "VEGALITE_VERSION",
+    "VEGA_VERSION",
+    "X2",
+    "Y2",
+    "Aggregate",
+    "AggregateOp",
+    "AggregateTransform",
+    "AggregatedFieldDef",
+    "Align",
+    "AllSortString",
+    "Angle",
+    "AngleDatum",
+    "AngleValue",
+    "AnyMark",
+    "AnyMarkConfig",
+    "AreaConfig",
+    "ArgmaxDef",
+    "ArgminDef",
+    "AutoSizeParams",
+    "AutosizeType",
+    "Axis",
+    "AxisConfig",
+    "AxisOrient",
+    "AxisResolveMap",
+    "BBox",
+    "BarConfig",
+    "BaseTitleNoValueRefs",
+    "Baseline",
+    "Bin",
+    "BinExtent",
+    "BinParams",
+    "BinTransform",
+    "BindCheckbox",
+    "BindDirect",
+    "BindInput",
+    "BindRadioSelect",
+    "BindRange",
+    "Binding",
+    "BinnedTimeUnit",
+    "Blend",
+    "BoxPlot",
+    "BoxPlotConfig",
+    "BoxPlotDef",
+    "BrushConfig",
+    "CalculateTransform",
+    "Categorical",
+    "ChainedWhen",
+    "Chart",
+    "ChartDataType",
+    "Color",
+    "ColorDatum",
+    "ColorDef",
+    "ColorName",
+    "ColorScheme",
+    "ColorValue",
+    "Column",
+    "CompositeMark",
+    "CompositeMarkDef",
+    "CompositionConfig",
+    "ConcatChart",
+    "ConcatSpecGenericSpec",
+    "ConditionalAxisColor",
+    "ConditionalAxisLabelAlign",
+    "ConditionalAxisLabelBaseline",
+    "ConditionalAxisLabelFontStyle",
+    "ConditionalAxisLabelFontWeight",
+    "ConditionalAxisNumber",
+    "ConditionalAxisNumberArray",
+    "ConditionalAxisPropertyAlignnull",
+    "ConditionalAxisPropertyColornull",
+    "ConditionalAxisPropertyFontStylenull",
+    "ConditionalAxisPropertyFontWeightnull",
+    "ConditionalAxisPropertyTextBaselinenull",
+    "ConditionalAxisPropertynumberArraynull",
+    "ConditionalAxisPropertynumbernull",
+    "ConditionalAxisPropertystringnull",
+    "ConditionalAxisString",
+    "ConditionalMarkPropFieldOrDatumDef",
+    "ConditionalMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterMarkPropFieldOrDatumDef",
+    "ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterStringFieldDef",
+    "ConditionalParameterValueDefGradientstringnullExprRef",
+    "ConditionalParameterValueDefTextExprRef",
+    "ConditionalParameterValueDefnumber",
+    "ConditionalParameterValueDefnumberArrayExprRef",
+    "ConditionalParameterValueDefnumberExprRef",
+    "ConditionalParameterValueDefstringExprRef",
+    "ConditionalParameterValueDefstringnullExprRef",
+    "ConditionalPredicateMarkPropFieldOrDatumDef",
+    "ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalPredicateStringFieldDef",
+    "ConditionalPredicateValueDefAlignnullExprRef",
+    "ConditionalPredicateValueDefColornullExprRef",
+    "ConditionalPredicateValueDefFontStylenullExprRef",
+    "ConditionalPredicateValueDefFontWeightnullExprRef",
+    "ConditionalPredicateValueDefGradientstringnullExprRef",
+    "ConditionalPredicateValueDefTextBaselinenullExprRef",
+    "ConditionalPredicateValueDefTextExprRef",
+    "ConditionalPredicateValueDefnumber",
+    "ConditionalPredicateValueDefnumberArrayExprRef",
+    "ConditionalPredicateValueDefnumberArraynullExprRef",
+    "ConditionalPredicateValueDefnumberExprRef",
+    "ConditionalPredicateValueDefnumbernullExprRef",
+    "ConditionalPredicateValueDefstringExprRef",
+    "ConditionalPredicateValueDefstringnullExprRef",
+    "ConditionalStringFieldDef",
+    "ConditionalValueDefGradientstringnullExprRef",
+    "ConditionalValueDefTextExprRef",
+    "ConditionalValueDefnumber",
+    "ConditionalValueDefnumberArrayExprRef",
+    "ConditionalValueDefnumberExprRef",
+    "ConditionalValueDefstringExprRef",
+    "ConditionalValueDefstringnullExprRef",
+    "Config",
+    "CsvDataFormat",
+    "Cursor",
+    "Cyclical",
+    "Data",
+    "DataFormat",
+    "DataSource",
+    "DataType",
+    "Datasets",
+    "DateTime",
+    "DatumChannelMixin",
+    "DatumDef",
+    "Day",
+    "DensityTransform",
+    "DerivedStream",
+    "Description",
+    "DescriptionValue",
+    "Detail",
+    "DictInlineDataset",
+    "DictSelectionInit",
+    "DictSelectionInitInterval",
+    "Diverging",
+    "DomainUnionWith",
+    "DsvDataFormat",
+    "Element",
+    "Encoding",
+    "EncodingSortField",
+    "ErrorBand",
+    "ErrorBandConfig",
+    "ErrorBandDef",
+    "ErrorBar",
+    "ErrorBarConfig",
+    "ErrorBarDef",
+    "ErrorBarExtent",
+    "EventStream",
+    "EventType",
+    "Expr",
+    "ExprRef",
+    "ExtentTransform",
+    "Facet",
+    "FacetChart",
+    "FacetEncodingFieldDef",
+    "FacetFieldDef",
+    "FacetMapping",
+    "FacetSpec",
+    "FacetedEncoding",
+    "FacetedUnitSpec",
+    "Feature",
+    "FeatureCollection",
+    "FeatureGeometryGeoJsonProperties",
+    "Field",
+    "FieldChannelMixin",
+    "FieldDefWithoutScale",
+    "FieldEqualPredicate",
+    "FieldGTEPredicate",
+    "FieldGTPredicate",
+    "FieldLTEPredicate",
+    "FieldLTPredicate",
+    "FieldName",
+    "FieldOneOfPredicate",
+    "FieldOrDatumDefWithConditionDatumDefGradientstringnull",
+    "FieldOrDatumDefWithConditionDatumDefnumber",
+    "FieldOrDatumDefWithConditionDatumDefnumberArray",
+    "FieldOrDatumDefWithConditionDatumDefstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
+    "FieldOrDatumDefWithConditionStringDatumDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefstring",
+    "FieldRange",
+    "FieldRangePredicate",
+    "FieldValidPredicate",
+    "Fill",
+    "FillDatum",
+    "FillOpacity",
+    "FillOpacityDatum",
+    "FillOpacityValue",
+    "FillValue",
+    "FilterTransform",
+    "Fit",
+    "FlattenTransform",
+    "FoldTransform",
+    "FontStyle",
+    "FontWeight",
+    "FormatConfig",
+    "Generator",
+    "GenericUnitSpecEncodingAnyMark",
+    "GeoJsonFeature",
+    "GeoJsonFeatureCollection",
+    "GeoJsonProperties",
+    "Geometry",
+    "GeometryCollection",
+    "Gradient",
+    "GradientStop",
+    "GraticuleGenerator",
+    "GraticuleParams",
+    "HConcatChart",
+    "HConcatSpecGenericSpec",
+    "Header",
+    "HeaderConfig",
+    "HexColor",
+    "Href",
+    "HrefValue",
+    "Impute",
+    "ImputeMethod",
+    "ImputeParams",
+    "ImputeSequence",
+    "ImputeTransform",
+    "InlineData",
+    "InlineDataset",
+    "Interpolate",
+    "IntervalSelectionConfig",
+    "IntervalSelectionConfigWithoutType",
+    "JoinAggregateFieldDef",
+    "JoinAggregateTransform",
+    "JsonDataFormat",
+    "Key",
+    "LabelOverlap",
+    "LatLongDef",
+    "LatLongFieldDef",
+    "Latitude",
+    "Latitude2",
+    "Latitude2Datum",
+    "Latitude2Value",
+    "LatitudeDatum",
+    "LayerChart",
+    "LayerRepeatMapping",
+    "LayerRepeatSpec",
+    "LayerSpec",
+    "LayoutAlign",
+    "Legend",
+    "LegendBinding",
+    "LegendConfig",
+    "LegendOrient",
+    "LegendResolveMap",
+    "LegendStreamBinding",
+    "LineConfig",
+    "LineString",
+    "LinearGradient",
+    "LocalMultiTimeUnit",
+    "LocalSingleTimeUnit",
+    "Locale",
+    "LoessTransform",
+    "LogicalAndPredicate",
+    "LogicalNotPredicate",
+    "LogicalOrPredicate",
+    "Longitude",
+    "Longitude2",
+    "Longitude2Datum",
+    "Longitude2Value",
+    "LongitudeDatum",
+    "LookupData",
+    "LookupSelection",
+    "LookupTransform",
+    "Mark",
+    "MarkConfig",
+    "MarkDef",
+    "MarkInvalidDataMode",
+    "MarkPropDefGradientstringnull",
+    "MarkPropDefnumber",
+    "MarkPropDefnumberArray",
+    "MarkPropDefstringnullTypeForShape",
+    "MarkType",
+    "MaxRowsError",
+    "MergedStream",
+    "Month",
+    "MultiLineString",
+    "MultiPoint",
+    "MultiPolygon",
+    "MultiTimeUnit",
+    "NamedData",
+    "NonArgAggregateOp",
+    "NonLayerRepeatSpec",
+    "NonNormalizedSpec",
+    "NumberLocale",
+    "NumericArrayMarkPropDef",
+    "NumericMarkPropDef",
+    "OffsetDef",
+    "Opacity",
+    "OpacityDatum",
+    "OpacityValue",
+    "Order",
+    "OrderFieldDef",
+    "OrderOnlyDef",
+    "OrderValue",
+    "OrderValueDef",
+    "Orient",
+    "Orientation",
+    "OverlayMarkDef",
+    "Padding",
+    "Parameter",
+    "ParameterExpression",
+    "ParameterExtent",
+    "ParameterName",
+    "ParameterPredicate",
+    "Parse",
+    "ParseValue",
+    "PivotTransform",
+    "Point",
+    "PointSelectionConfig",
+    "PointSelectionConfigWithoutType",
+    "PolarDef",
+    "Polygon",
+    "Position",
+    "Position2Def",
+    "PositionDatumDef",
+    "PositionDatumDefBase",
+    "PositionDef",
+    "PositionFieldDef",
+    "PositionFieldDefBase",
+    "PositionValueDef",
+    "Predicate",
+    "PredicateComposition",
+    "PrimitiveValue",
+    "Projection",
+    "ProjectionConfig",
+    "ProjectionType",
+    "QuantileTransform",
+    "RadialGradient",
+    "Radius",
+    "Radius2",
+    "Radius2Datum",
+    "Radius2Value",
+    "RadiusDatum",
+    "RadiusValue",
+    "RangeConfig",
+    "RangeEnum",
+    "RangeRaw",
+    "RangeRawArray",
+    "RangeScheme",
+    "RectConfig",
+    "RegressionTransform",
+    "RelativeBandSize",
+    "RepeatChart",
+    "RepeatMapping",
+    "RepeatRef",
+    "RepeatSpec",
+    "Resolve",
+    "ResolveMode",
+    "Root",
+    "Row",
+    "RowColLayoutAlign",
+    "RowColboolean",
+    "RowColnumber",
+    "RowColumnEncodingFieldDef",
+    "SampleTransform",
+    "Scale",
+    "ScaleBinParams",
+    "ScaleBins",
+    "ScaleConfig",
+    "ScaleDatumDef",
+    "ScaleFieldDef",
+    "ScaleInterpolateEnum",
+    "ScaleInterpolateParams",
+    "ScaleInvalidDataConfig",
+    "ScaleInvalidDataShowAsValueangle",
+    "ScaleInvalidDataShowAsValuecolor",
+    "ScaleInvalidDataShowAsValuefill",
+    "ScaleInvalidDataShowAsValuefillOpacity",
+    "ScaleInvalidDataShowAsValueopacity",
+    "ScaleInvalidDataShowAsValueradius",
+    "ScaleInvalidDataShowAsValueshape",
+    "ScaleInvalidDataShowAsValuesize",
+    "ScaleInvalidDataShowAsValuestroke",
+    "ScaleInvalidDataShowAsValuestrokeDash",
+    "ScaleInvalidDataShowAsValuestrokeOpacity",
+    "ScaleInvalidDataShowAsValuestrokeWidth",
+    "ScaleInvalidDataShowAsValuetheta",
+    "ScaleInvalidDataShowAsValuex",
+    "ScaleInvalidDataShowAsValuexOffset",
+    "ScaleInvalidDataShowAsValuey",
+    "ScaleInvalidDataShowAsValueyOffset",
+    "ScaleInvalidDataShowAsangle",
+    "ScaleInvalidDataShowAscolor",
+    "ScaleInvalidDataShowAsfill",
+    "ScaleInvalidDataShowAsfillOpacity",
+    "ScaleInvalidDataShowAsopacity",
+    "ScaleInvalidDataShowAsradius",
+    "ScaleInvalidDataShowAsshape",
+    "ScaleInvalidDataShowAssize",
+    "ScaleInvalidDataShowAsstroke",
+    "ScaleInvalidDataShowAsstrokeDash",
+    "ScaleInvalidDataShowAsstrokeOpacity",
+    "ScaleInvalidDataShowAsstrokeWidth",
+    "ScaleInvalidDataShowAstheta",
+    "ScaleInvalidDataShowAsx",
+    "ScaleInvalidDataShowAsxOffset",
+    "ScaleInvalidDataShowAsy",
+    "ScaleInvalidDataShowAsyOffset",
+    "ScaleResolveMap",
+    "ScaleType",
+    "SchemaBase",
+    "SchemeParams",
+    "SecondaryFieldDef",
+    "SelectionConfig",
+    "SelectionExpression",
+    "SelectionInit",
+    "SelectionInitInterval",
+    "SelectionInitIntervalMapping",
+    "SelectionInitMapping",
+    "SelectionParameter",
+    "SelectionPredicateComposition",
+    "SelectionResolution",
+    "SelectionType",
+    "SequenceGenerator",
+    "SequenceParams",
+    "SequentialMultiHue",
+    "SequentialSingleHue",
+    "Shape",
+    "ShapeDatum",
+    "ShapeDef",
+    "ShapeValue",
+    "SharedEncoding",
+    "SingleDefUnitChannel",
+    "SingleTimeUnit",
+    "Size",
+    "SizeDatum",
+    "SizeValue",
+    "Sort",
+    "SortArray",
+    "SortByChannel",
+    "SortByChannelDesc",
+    "SortByEncoding",
+    "SortField",
+    "SortOrder",
+    "Spec",
+    "SphereGenerator",
+    "StackOffset",
+    "StackTransform",
+    "StandardType",
+    "Step",
+    "StepFor",
+    "Stream",
+    "StringFieldDef",
+    "StringFieldDefWithCondition",
+    "StringValueDefWithCondition",
+    "Stroke",
+    "StrokeCap",
+    "StrokeDash",
+    "StrokeDashDatum",
+    "StrokeDashValue",
+    "StrokeDatum",
+    "StrokeJoin",
+    "StrokeOpacity",
+    "StrokeOpacityDatum",
+    "StrokeOpacityValue",
+    "StrokeValue",
+    "StrokeWidth",
+    "StrokeWidthDatum",
+    "StrokeWidthValue",
+    "StyleConfigIndex",
+    "SymbolShape",
+    "Text",
+    "TextBaseline",
+    "TextDatum",
+    "TextDef",
+    "TextDirection",
+    "TextValue",
+    "Then",
+    "Theta",
+    "Theta2",
+    "Theta2Datum",
+    "Theta2Value",
+    "ThetaDatum",
+    "ThetaValue",
+    "TickConfig",
+    "TickCount",
+    "TimeInterval",
+    "TimeIntervalStep",
+    "TimeLocale",
+    "TimeUnit",
+    "TimeUnitParams",
+    "TimeUnitTransform",
+    "TimeUnitTransformParams",
+    "Title",
+    "TitleAnchor",
+    "TitleConfig",
+    "TitleFrame",
+    "TitleOrient",
+    "TitleParams",
+    "Tooltip",
+    "TooltipContent",
+    "TooltipValue",
+    "TopLevelConcatSpec",
+    "TopLevelFacetSpec",
+    "TopLevelHConcatSpec",
+    "TopLevelLayerSpec",
+    "TopLevelMixin",
+    "TopLevelParameter",
+    "TopLevelRepeatSpec",
+    "TopLevelSelectionParameter",
+    "TopLevelSpec",
+    "TopLevelUnitSpec",
+    "TopLevelVConcatSpec",
+    "TopoDataFormat",
+    "Transform",
+    "Type",
+    "TypeForShape",
+    "TypedFieldDef",
+    "UnitSpec",
+    "UnitSpecWithFrame",
+    "Url",
+    "UrlData",
+    "UrlValue",
+    "UtcMultiTimeUnit",
+    "UtcSingleTimeUnit",
+    "VConcatChart",
+    "VConcatSpecGenericSpec",
+    "ValueChannelMixin",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
+    "ValueDefWithConditionStringFieldDefText",
+    "ValueDefnumber",
+    "ValueDefnumberwidthheightExprRef",
+    "VariableParameter",
+    "Vector2DateTime",
+    "Vector2Vector2number",
+    "Vector2boolean",
+    "Vector2number",
+    "Vector2string",
+    "Vector3number",
+    "Vector7string",
+    "Vector10string",
+    "Vector12string",
+    "VegaLite",
+    "VegaLiteSchema",
+    "ViewBackground",
+    "ViewConfig",
+    "When",
+    "WindowEventType",
+    "WindowFieldDef",
+    "WindowOnlyOp",
+    "WindowTransform",
+    "X",
+    "X2Datum",
+    "X2Value",
+    "XDatum",
+    "XError",
+    "XError2",
+    "XError2Value",
+    "XErrorValue",
+    "XOffset",
+    "XOffsetDatum",
+    "XOffsetValue",
+    "XValue",
+    "Y",
+    "Y2Datum",
+    "Y2Value",
+    "YDatum",
+    "YError",
+    "YError2",
+    "YError2Value",
+    "YErrorValue",
+    "YOffset",
+    "YOffsetDatum",
+    "YOffsetValue",
+    "YValue",
+    "api",
+    "binding",
+    "binding_checkbox",
+    "binding_radio",
+    "binding_range",
+    "binding_select",
+    "channels",
+    "check_fields_and_encodings",
+    "compiler",
+    "concat",
+    "condition",
+    "core",
+    "data_transformers",
+    "datum",
+    "default_data_transformer",
+    "graticule",
+    "hconcat",
+    "layer",
+    "limit_rows",
+    "load_schema",
+    "mixins",
+    "param",
+    "renderers",
+    "repeat",
+    "sample",
+    "schema",
+    "selection",
+    "selection_interval",
+    "selection_multi",
+    "selection_point",
+    "selection_single",
+    "sequence",
+    "sphere",
+    "to_csv",
+    "to_json",
+    "to_values",
+    "topo_feature",
+    "value",
+    "vconcat",
+    "vegalite_compilers",
+    "when",
+    "with_property_setters",
+]
diff --git a/altair/vegalite/v5/api.py b/altair/vegalite/v5/api.py
index 14d47064c..c99e556be 100644
--- a/altair/vegalite/v5/api.py
+++ b/altair/vegalite/v5/api.py
@@ -16,7 +16,7 @@
 import jsonschema
 import narwhals.stable.v1 as nw
 
-from altair import utils
+from altair import theme, utils
 from altair.expr import core as _expr_core
 from altair.utils import Optional, SchemaBase, Undefined
 from altair.utils._vegafusion_data import (
@@ -32,7 +32,6 @@
 from .display import VEGA_VERSION, VEGAEMBED_VERSION, VEGALITE_VERSION, renderers
 from .schema import SCHEMA_URL, channels, core, mixins
 from .schema._typing import Map, PrimitiveValue_T, SingleDefUnitChannel_T, Temporal
-from .theme import themes
 
 if sys.version_info >= (3, 14):
     from typing import TypedDict
@@ -2006,9 +2005,8 @@ def to_dict(  # noqa: C901
             if "$schema" not in vegalite_spec:
                 vegalite_spec["$schema"] = SCHEMA_URL
 
-            # apply theme from theme registry
-            if theme := themes.get():
-                vegalite_spec = utils.update_nested(theme(), vegalite_spec, copy=True)
+            if func := theme.get():
+                vegalite_spec = utils.update_nested(func(), vegalite_spec, copy=True)
             else:
                 msg = (
                     f"Expected a theme to be set but got {None!r}.\n"
diff --git a/altair/vegalite/v5/schema/__init__.py b/altair/vegalite/v5/schema/__init__.py
index 1f099eaca..3be8148db 100644
--- a/altair/vegalite/v5/schema/__init__.py
+++ b/altair/vegalite/v5/schema/__init__.py
@@ -1,8 +1,571 @@
-# ruff: noqa
+# ruff: noqa: F403, F405
+# The contents of this file are automatically written by
+# tools/generate_schema_wrapper.py. Do not modify directly.
 
-from .core import *
-from .channels import *
+from altair.vegalite.v5.schema import channels, core
+from altair.vegalite.v5.schema.channels import *
+from altair.vegalite.v5.schema.core import *
 
 SCHEMA_VERSION = "v5.20.1"
 
 SCHEMA_URL = "https://vega.github.io/schema/vega-lite/v5.20.1.json"
+
+__all__ = [
+    "SCHEMA_URL",
+    "SCHEMA_VERSION",
+    "URI",
+    "X2",
+    "Y2",
+    "Aggregate",
+    "AggregateOp",
+    "AggregateTransform",
+    "AggregatedFieldDef",
+    "Align",
+    "AllSortString",
+    "Angle",
+    "AngleDatum",
+    "AngleValue",
+    "AnyMark",
+    "AnyMarkConfig",
+    "AreaConfig",
+    "ArgmaxDef",
+    "ArgminDef",
+    "AutoSizeParams",
+    "AutosizeType",
+    "Axis",
+    "AxisConfig",
+    "AxisOrient",
+    "AxisResolveMap",
+    "BBox",
+    "BarConfig",
+    "BaseTitleNoValueRefs",
+    "Baseline",
+    "BinExtent",
+    "BinParams",
+    "BinTransform",
+    "BindCheckbox",
+    "BindDirect",
+    "BindInput",
+    "BindRadioSelect",
+    "BindRange",
+    "Binding",
+    "BinnedTimeUnit",
+    "Blend",
+    "BoxPlot",
+    "BoxPlotConfig",
+    "BoxPlotDef",
+    "BrushConfig",
+    "CalculateTransform",
+    "Categorical",
+    "Color",
+    "ColorDatum",
+    "ColorDef",
+    "ColorName",
+    "ColorScheme",
+    "ColorValue",
+    "Column",
+    "CompositeMark",
+    "CompositeMarkDef",
+    "CompositionConfig",
+    "ConcatSpecGenericSpec",
+    "ConditionalAxisColor",
+    "ConditionalAxisLabelAlign",
+    "ConditionalAxisLabelBaseline",
+    "ConditionalAxisLabelFontStyle",
+    "ConditionalAxisLabelFontWeight",
+    "ConditionalAxisNumber",
+    "ConditionalAxisNumberArray",
+    "ConditionalAxisPropertyAlignnull",
+    "ConditionalAxisPropertyColornull",
+    "ConditionalAxisPropertyFontStylenull",
+    "ConditionalAxisPropertyFontWeightnull",
+    "ConditionalAxisPropertyTextBaselinenull",
+    "ConditionalAxisPropertynumberArraynull",
+    "ConditionalAxisPropertynumbernull",
+    "ConditionalAxisPropertystringnull",
+    "ConditionalAxisString",
+    "ConditionalMarkPropFieldOrDatumDef",
+    "ConditionalMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterMarkPropFieldOrDatumDef",
+    "ConditionalParameterMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalParameterStringFieldDef",
+    "ConditionalParameterValueDefGradientstringnullExprRef",
+    "ConditionalParameterValueDefTextExprRef",
+    "ConditionalParameterValueDefnumber",
+    "ConditionalParameterValueDefnumberArrayExprRef",
+    "ConditionalParameterValueDefnumberExprRef",
+    "ConditionalParameterValueDefstringExprRef",
+    "ConditionalParameterValueDefstringnullExprRef",
+    "ConditionalPredicateMarkPropFieldOrDatumDef",
+    "ConditionalPredicateMarkPropFieldOrDatumDefTypeForShape",
+    "ConditionalPredicateStringFieldDef",
+    "ConditionalPredicateValueDefAlignnullExprRef",
+    "ConditionalPredicateValueDefColornullExprRef",
+    "ConditionalPredicateValueDefFontStylenullExprRef",
+    "ConditionalPredicateValueDefFontWeightnullExprRef",
+    "ConditionalPredicateValueDefGradientstringnullExprRef",
+    "ConditionalPredicateValueDefTextBaselinenullExprRef",
+    "ConditionalPredicateValueDefTextExprRef",
+    "ConditionalPredicateValueDefnumber",
+    "ConditionalPredicateValueDefnumberArrayExprRef",
+    "ConditionalPredicateValueDefnumberArraynullExprRef",
+    "ConditionalPredicateValueDefnumberExprRef",
+    "ConditionalPredicateValueDefnumbernullExprRef",
+    "ConditionalPredicateValueDefstringExprRef",
+    "ConditionalPredicateValueDefstringnullExprRef",
+    "ConditionalStringFieldDef",
+    "ConditionalValueDefGradientstringnullExprRef",
+    "ConditionalValueDefTextExprRef",
+    "ConditionalValueDefnumber",
+    "ConditionalValueDefnumberArrayExprRef",
+    "ConditionalValueDefnumberExprRef",
+    "ConditionalValueDefstringExprRef",
+    "ConditionalValueDefstringnullExprRef",
+    "Config",
+    "CsvDataFormat",
+    "Cursor",
+    "Cyclical",
+    "Data",
+    "DataFormat",
+    "DataSource",
+    "Datasets",
+    "DateTime",
+    "DatumChannelMixin",
+    "DatumDef",
+    "Day",
+    "DensityTransform",
+    "DerivedStream",
+    "Description",
+    "DescriptionValue",
+    "Detail",
+    "DictInlineDataset",
+    "DictSelectionInit",
+    "DictSelectionInitInterval",
+    "Diverging",
+    "DomainUnionWith",
+    "DsvDataFormat",
+    "Element",
+    "Encoding",
+    "EncodingSortField",
+    "ErrorBand",
+    "ErrorBandConfig",
+    "ErrorBandDef",
+    "ErrorBar",
+    "ErrorBarConfig",
+    "ErrorBarDef",
+    "ErrorBarExtent",
+    "EventStream",
+    "EventType",
+    "Expr",
+    "ExprRef",
+    "ExtentTransform",
+    "Facet",
+    "FacetEncodingFieldDef",
+    "FacetFieldDef",
+    "FacetSpec",
+    "FacetedEncoding",
+    "FacetedUnitSpec",
+    "Feature",
+    "FeatureCollection",
+    "FeatureGeometryGeoJsonProperties",
+    "Field",
+    "FieldChannelMixin",
+    "FieldDefWithoutScale",
+    "FieldEqualPredicate",
+    "FieldGTEPredicate",
+    "FieldGTPredicate",
+    "FieldLTEPredicate",
+    "FieldLTPredicate",
+    "FieldName",
+    "FieldOneOfPredicate",
+    "FieldOrDatumDefWithConditionDatumDefGradientstringnull",
+    "FieldOrDatumDefWithConditionDatumDefnumber",
+    "FieldOrDatumDefWithConditionDatumDefnumberArray",
+    "FieldOrDatumDefWithConditionDatumDefstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefGradientstringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefTypeForShapestringnull",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumber",
+    "FieldOrDatumDefWithConditionMarkPropFieldDefnumberArray",
+    "FieldOrDatumDefWithConditionStringDatumDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefText",
+    "FieldOrDatumDefWithConditionStringFieldDefstring",
+    "FieldRange",
+    "FieldRangePredicate",
+    "FieldValidPredicate",
+    "Fill",
+    "FillDatum",
+    "FillOpacity",
+    "FillOpacityDatum",
+    "FillOpacityValue",
+    "FillValue",
+    "FilterTransform",
+    "Fit",
+    "FlattenTransform",
+    "FoldTransform",
+    "FontStyle",
+    "FontWeight",
+    "FormatConfig",
+    "Generator",
+    "GenericUnitSpecEncodingAnyMark",
+    "GeoJsonFeature",
+    "GeoJsonFeatureCollection",
+    "GeoJsonProperties",
+    "Geometry",
+    "GeometryCollection",
+    "Gradient",
+    "GradientStop",
+    "GraticuleGenerator",
+    "GraticuleParams",
+    "HConcatSpecGenericSpec",
+    "Header",
+    "HeaderConfig",
+    "HexColor",
+    "Href",
+    "HrefValue",
+    "ImputeMethod",
+    "ImputeParams",
+    "ImputeSequence",
+    "ImputeTransform",
+    "InlineData",
+    "InlineDataset",
+    "Interpolate",
+    "IntervalSelectionConfig",
+    "IntervalSelectionConfigWithoutType",
+    "JoinAggregateFieldDef",
+    "JoinAggregateTransform",
+    "JsonDataFormat",
+    "Key",
+    "LabelOverlap",
+    "LatLongDef",
+    "LatLongFieldDef",
+    "Latitude",
+    "Latitude2",
+    "Latitude2Datum",
+    "Latitude2Value",
+    "LatitudeDatum",
+    "LayerRepeatMapping",
+    "LayerRepeatSpec",
+    "LayerSpec",
+    "LayoutAlign",
+    "Legend",
+    "LegendBinding",
+    "LegendConfig",
+    "LegendOrient",
+    "LegendResolveMap",
+    "LegendStreamBinding",
+    "LineConfig",
+    "LineString",
+    "LinearGradient",
+    "LocalMultiTimeUnit",
+    "LocalSingleTimeUnit",
+    "Locale",
+    "LoessTransform",
+    "LogicalAndPredicate",
+    "LogicalNotPredicate",
+    "LogicalOrPredicate",
+    "Longitude",
+    "Longitude2",
+    "Longitude2Datum",
+    "Longitude2Value",
+    "LongitudeDatum",
+    "LookupSelection",
+    "LookupTransform",
+    "Mark",
+    "MarkConfig",
+    "MarkDef",
+    "MarkInvalidDataMode",
+    "MarkPropDefGradientstringnull",
+    "MarkPropDefnumber",
+    "MarkPropDefnumberArray",
+    "MarkPropDefstringnullTypeForShape",
+    "MarkType",
+    "MergedStream",
+    "Month",
+    "MultiLineString",
+    "MultiPoint",
+    "MultiPolygon",
+    "MultiTimeUnit",
+    "NamedData",
+    "NonArgAggregateOp",
+    "NonLayerRepeatSpec",
+    "NonNormalizedSpec",
+    "NumberLocale",
+    "NumericArrayMarkPropDef",
+    "NumericMarkPropDef",
+    "OffsetDef",
+    "Opacity",
+    "OpacityDatum",
+    "OpacityValue",
+    "Order",
+    "OrderFieldDef",
+    "OrderOnlyDef",
+    "OrderValue",
+    "OrderValueDef",
+    "Orient",
+    "Orientation",
+    "OverlayMarkDef",
+    "Padding",
+    "ParameterExtent",
+    "ParameterName",
+    "ParameterPredicate",
+    "Parse",
+    "ParseValue",
+    "PivotTransform",
+    "Point",
+    "PointSelectionConfig",
+    "PointSelectionConfigWithoutType",
+    "PolarDef",
+    "Polygon",
+    "Position",
+    "Position2Def",
+    "PositionDatumDef",
+    "PositionDatumDefBase",
+    "PositionDef",
+    "PositionFieldDef",
+    "PositionFieldDefBase",
+    "PositionValueDef",
+    "Predicate",
+    "PredicateComposition",
+    "PrimitiveValue",
+    "Projection",
+    "ProjectionConfig",
+    "ProjectionType",
+    "QuantileTransform",
+    "RadialGradient",
+    "Radius",
+    "Radius2",
+    "Radius2Datum",
+    "Radius2Value",
+    "RadiusDatum",
+    "RadiusValue",
+    "RangeConfig",
+    "RangeEnum",
+    "RangeRaw",
+    "RangeRawArray",
+    "RangeScheme",
+    "RectConfig",
+    "RegressionTransform",
+    "RelativeBandSize",
+    "RepeatMapping",
+    "RepeatRef",
+    "RepeatSpec",
+    "Resolve",
+    "ResolveMode",
+    "Root",
+    "Row",
+    "RowColLayoutAlign",
+    "RowColboolean",
+    "RowColnumber",
+    "RowColumnEncodingFieldDef",
+    "SampleTransform",
+    "Scale",
+    "ScaleBinParams",
+    "ScaleBins",
+    "ScaleConfig",
+    "ScaleDatumDef",
+    "ScaleFieldDef",
+    "ScaleInterpolateEnum",
+    "ScaleInterpolateParams",
+    "ScaleInvalidDataConfig",
+    "ScaleInvalidDataShowAsValueangle",
+    "ScaleInvalidDataShowAsValuecolor",
+    "ScaleInvalidDataShowAsValuefill",
+    "ScaleInvalidDataShowAsValuefillOpacity",
+    "ScaleInvalidDataShowAsValueopacity",
+    "ScaleInvalidDataShowAsValueradius",
+    "ScaleInvalidDataShowAsValueshape",
+    "ScaleInvalidDataShowAsValuesize",
+    "ScaleInvalidDataShowAsValuestroke",
+    "ScaleInvalidDataShowAsValuestrokeDash",
+    "ScaleInvalidDataShowAsValuestrokeOpacity",
+    "ScaleInvalidDataShowAsValuestrokeWidth",
+    "ScaleInvalidDataShowAsValuetheta",
+    "ScaleInvalidDataShowAsValuex",
+    "ScaleInvalidDataShowAsValuexOffset",
+    "ScaleInvalidDataShowAsValuey",
+    "ScaleInvalidDataShowAsValueyOffset",
+    "ScaleInvalidDataShowAsangle",
+    "ScaleInvalidDataShowAscolor",
+    "ScaleInvalidDataShowAsfill",
+    "ScaleInvalidDataShowAsfillOpacity",
+    "ScaleInvalidDataShowAsopacity",
+    "ScaleInvalidDataShowAsradius",
+    "ScaleInvalidDataShowAsshape",
+    "ScaleInvalidDataShowAssize",
+    "ScaleInvalidDataShowAsstroke",
+    "ScaleInvalidDataShowAsstrokeDash",
+    "ScaleInvalidDataShowAsstrokeOpacity",
+    "ScaleInvalidDataShowAsstrokeWidth",
+    "ScaleInvalidDataShowAstheta",
+    "ScaleInvalidDataShowAsx",
+    "ScaleInvalidDataShowAsxOffset",
+    "ScaleInvalidDataShowAsy",
+    "ScaleInvalidDataShowAsyOffset",
+    "ScaleResolveMap",
+    "ScaleType",
+    "SchemaBase",
+    "SchemeParams",
+    "SecondaryFieldDef",
+    "SelectionConfig",
+    "SelectionInit",
+    "SelectionInitInterval",
+    "SelectionInitIntervalMapping",
+    "SelectionInitMapping",
+    "SelectionParameter",
+    "SelectionResolution",
+    "SelectionType",
+    "SequenceGenerator",
+    "SequenceParams",
+    "SequentialMultiHue",
+    "SequentialSingleHue",
+    "Shape",
+    "ShapeDatum",
+    "ShapeDef",
+    "ShapeValue",
+    "SharedEncoding",
+    "SingleDefUnitChannel",
+    "SingleTimeUnit",
+    "Size",
+    "SizeDatum",
+    "SizeValue",
+    "Sort",
+    "SortArray",
+    "SortByChannel",
+    "SortByChannelDesc",
+    "SortByEncoding",
+    "SortField",
+    "SortOrder",
+    "Spec",
+    "SphereGenerator",
+    "StackOffset",
+    "StackTransform",
+    "StandardType",
+    "Step",
+    "StepFor",
+    "Stream",
+    "StringFieldDef",
+    "StringFieldDefWithCondition",
+    "StringValueDefWithCondition",
+    "Stroke",
+    "StrokeCap",
+    "StrokeDash",
+    "StrokeDashDatum",
+    "StrokeDashValue",
+    "StrokeDatum",
+    "StrokeJoin",
+    "StrokeOpacity",
+    "StrokeOpacityDatum",
+    "StrokeOpacityValue",
+    "StrokeValue",
+    "StrokeWidth",
+    "StrokeWidthDatum",
+    "StrokeWidthValue",
+    "StyleConfigIndex",
+    "SymbolShape",
+    "Text",
+    "TextBaseline",
+    "TextDatum",
+    "TextDef",
+    "TextDirection",
+    "TextValue",
+    "Theta",
+    "Theta2",
+    "Theta2Datum",
+    "Theta2Value",
+    "ThetaDatum",
+    "ThetaValue",
+    "TickConfig",
+    "TickCount",
+    "TimeInterval",
+    "TimeIntervalStep",
+    "TimeLocale",
+    "TimeUnit",
+    "TimeUnitParams",
+    "TimeUnitTransform",
+    "TimeUnitTransformParams",
+    "TitleAnchor",
+    "TitleConfig",
+    "TitleFrame",
+    "TitleOrient",
+    "TitleParams",
+    "Tooltip",
+    "TooltipContent",
+    "TooltipValue",
+    "TopLevelConcatSpec",
+    "TopLevelFacetSpec",
+    "TopLevelHConcatSpec",
+    "TopLevelLayerSpec",
+    "TopLevelParameter",
+    "TopLevelRepeatSpec",
+    "TopLevelSelectionParameter",
+    "TopLevelSpec",
+    "TopLevelUnitSpec",
+    "TopLevelVConcatSpec",
+    "TopoDataFormat",
+    "Transform",
+    "Type",
+    "TypeForShape",
+    "TypedFieldDef",
+    "UnitSpec",
+    "UnitSpecWithFrame",
+    "Url",
+    "UrlData",
+    "UrlValue",
+    "UtcMultiTimeUnit",
+    "UtcSingleTimeUnit",
+    "VConcatSpecGenericSpec",
+    "ValueChannelMixin",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefGradientstringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefTypeForShapestringnull",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumber",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefnumberArray",
+    "ValueDefWithConditionMarkPropFieldOrDatumDefstringnull",
+    "ValueDefWithConditionStringFieldDefText",
+    "ValueDefnumber",
+    "ValueDefnumberwidthheightExprRef",
+    "VariableParameter",
+    "Vector2DateTime",
+    "Vector2Vector2number",
+    "Vector2boolean",
+    "Vector2number",
+    "Vector2string",
+    "Vector3number",
+    "Vector7string",
+    "Vector10string",
+    "Vector12string",
+    "VegaLiteSchema",
+    "ViewBackground",
+    "ViewConfig",
+    "WindowEventType",
+    "WindowFieldDef",
+    "WindowOnlyOp",
+    "WindowTransform",
+    "X",
+    "X2Datum",
+    "X2Value",
+    "XDatum",
+    "XError",
+    "XError2",
+    "XError2Value",
+    "XErrorValue",
+    "XOffset",
+    "XOffsetDatum",
+    "XOffsetValue",
+    "XValue",
+    "Y",
+    "Y2Datum",
+    "Y2Value",
+    "YDatum",
+    "YError",
+    "YError2",
+    "YError2Value",
+    "YErrorValue",
+    "YOffset",
+    "YOffsetDatum",
+    "YOffsetValue",
+    "YValue",
+    "channels",
+    "core",
+    "load_schema",
+    "with_property_setters",
+]
diff --git a/altair/vegalite/v5/schema/_config.py b/altair/vegalite/v5/schema/_config.py
index 96da254c2..b5d4020ff 100644
--- a/altair/vegalite/v5/schema/_config.py
+++ b/altair/vegalite/v5/schema/_config.py
@@ -95,7 +95,7 @@
 
 class AreaConfigKwds(TypedDict, total=False):
     """
-    :class:`AreaConfig` ``TypedDict`` wrapper.
+    :class:`altair.AreaConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -569,7 +569,7 @@ class AreaConfigKwds(TypedDict, total=False):
 
 class AutoSizeParamsKwds(TypedDict, total=False):
     """
-    :class:`AutoSizeParams` ``TypedDict`` wrapper.
+    :class:`altair.AutoSizeParams` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -603,7 +603,7 @@ class AutoSizeParamsKwds(TypedDict, total=False):
 
 class AxisConfigKwds(TypedDict, total=False):
     """
-    :class:`AxisConfig` ``TypedDict`` wrapper.
+    :class:`altair.AxisConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1059,7 +1059,7 @@ class AxisConfigKwds(TypedDict, total=False):
 
 class AxisResolveMapKwds(TypedDict, total=False):
     """
-    :class:`AxisResolveMap` ``TypedDict`` wrapper.
+    :class:`altair.AxisResolveMap` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1075,7 +1075,7 @@ class AxisResolveMapKwds(TypedDict, total=False):
 
 class BarConfigKwds(TypedDict, total=False):
     """
-    :class:`BarConfig` ``TypedDict`` wrapper.
+    :class:`altair.BarConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1546,7 +1546,7 @@ class BarConfigKwds(TypedDict, total=False):
 
 class BindCheckboxKwds(TypedDict, total=False):
     """
-    :class:`BindCheckbox` ``TypedDict`` wrapper.
+    :class:`altair.BindCheckbox` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1572,7 +1572,7 @@ class BindCheckboxKwds(TypedDict, total=False):
 
 class BindDirectKwds(TypedDict, total=False):
     """
-    :class:`BindDirect` ``TypedDict`` wrapper.
+    :class:`altair.BindDirect` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1598,7 +1598,7 @@ class BindDirectKwds(TypedDict, total=False):
 
 class BindInputKwds(TypedDict, total=False):
     """
-    :class:`BindInput` ``TypedDict`` wrapper.
+    :class:`altair.BindInput` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1634,7 +1634,7 @@ class BindInputKwds(TypedDict, total=False):
 
 class BindRadioSelectKwds(TypedDict, total=False):
     """
-    :class:`BindRadioSelect` ``TypedDict`` wrapper.
+    :class:`altair.BindRadioSelect` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1667,7 +1667,7 @@ class BindRadioSelectKwds(TypedDict, total=False):
 
 class BindRangeKwds(TypedDict, total=False):
     """
-    :class:`BindRange` ``TypedDict`` wrapper.
+    :class:`altair.BindRange` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1705,7 +1705,7 @@ class BindRangeKwds(TypedDict, total=False):
 
 class BoxPlotConfigKwds(TypedDict, total=False):
     """
-    :class:`BoxPlotConfig` ``TypedDict`` wrapper.
+    :class:`altair.BoxPlotConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1785,7 +1785,7 @@ class BoxPlotConfigKwds(TypedDict, total=False):
 
 class BrushConfigKwds(TypedDict, total=False):
     """
-    :class:`BrushConfig` ``TypedDict`` wrapper.
+    :class:`altair.BrushConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1827,7 +1827,7 @@ class BrushConfigKwds(TypedDict, total=False):
 
 class CompositionConfigKwds(TypedDict, total=False):
     """
-    :class:`CompositionConfig` ``TypedDict`` wrapper.
+    :class:`altair.CompositionConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -1860,7 +1860,7 @@ class CompositionConfigKwds(TypedDict, total=False):
 
 class ConfigKwds(TypedDict, total=False):
     """
-    :class:`Config` ``TypedDict`` wrapper.
+    :class:`altair.Config` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2209,7 +2209,7 @@ class ConfigKwds(TypedDict, total=False):
 
 class DateTimeKwds(TypedDict, total=False):
     """
-    :class:`DateTime` ``TypedDict`` wrapper.
+    :class:`altair.DateTime` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2257,7 +2257,7 @@ class DateTimeKwds(TypedDict, total=False):
 
 class DerivedStreamKwds(TypedDict, total=False):
     """
-    :class:`DerivedStream` ``TypedDict`` wrapper.
+    :class:`altair.DerivedStream` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2291,7 +2291,7 @@ class DerivedStreamKwds(TypedDict, total=False):
 
 class ErrorBandConfigKwds(TypedDict, total=False):
     """
-    :class:`ErrorBandConfig` ``TypedDict`` wrapper.
+    :class:`altair.ErrorBandConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2361,7 +2361,7 @@ class ErrorBandConfigKwds(TypedDict, total=False):
 
 class ErrorBarConfigKwds(TypedDict, total=False):
     """
-    :class:`ErrorBarConfig` ``TypedDict`` wrapper.
+    :class:`altair.ErrorBarConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2411,7 +2411,7 @@ class ErrorBarConfigKwds(TypedDict, total=False):
 
 class FeatureGeometryGeoJsonPropertiesKwds(TypedDict, total=False):
     """
-    :class:`FeatureGeometryGeoJsonProperties` ``TypedDict`` wrapper.
+    :class:`altair.FeatureGeometryGeoJsonProperties` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2446,7 +2446,7 @@ class FeatureGeometryGeoJsonPropertiesKwds(TypedDict, total=False):
 
 class FormatConfigKwds(TypedDict, total=False):
     """
-    :class:`FormatConfig` ``TypedDict`` wrapper.
+    :class:`altair.FormatConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2515,7 +2515,7 @@ class FormatConfigKwds(TypedDict, total=False):
 
 class GeoJsonFeatureKwds(TypedDict, total=False):
     """
-    :class:`GeoJsonFeature` ``TypedDict`` wrapper.
+    :class:`altair.GeoJsonFeature` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2550,7 +2550,7 @@ class GeoJsonFeatureKwds(TypedDict, total=False):
 
 class GeoJsonFeatureCollectionKwds(TypedDict, total=False):
     """
-    :class:`GeoJsonFeatureCollection` ``TypedDict`` wrapper.
+    :class:`altair.GeoJsonFeatureCollection` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2570,7 +2570,7 @@ class GeoJsonFeatureCollectionKwds(TypedDict, total=False):
 
 class GeometryCollectionKwds(TypedDict, total=False):
     """
-    :class:`GeometryCollection` ``TypedDict`` wrapper.
+    :class:`altair.GeometryCollection` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2598,7 +2598,7 @@ class GeometryCollectionKwds(TypedDict, total=False):
 
 class GradientStopKwds(TypedDict, total=False):
     """
-    :class:`GradientStop` ``TypedDict`` wrapper.
+    :class:`altair.GradientStop` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2614,7 +2614,7 @@ class GradientStopKwds(TypedDict, total=False):
 
 class HeaderConfigKwds(TypedDict, total=False):
     """
-    :class:`HeaderConfig` ``TypedDict`` wrapper.
+    :class:`altair.HeaderConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2789,7 +2789,7 @@ class HeaderConfigKwds(TypedDict, total=False):
 
 class IntervalSelectionConfigKwds(TypedDict, total=False):
     """
-    :class:`IntervalSelectionConfig` ``TypedDict`` wrapper.
+    :class:`altair.IntervalSelectionConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -2899,7 +2899,7 @@ class IntervalSelectionConfigKwds(TypedDict, total=False):
 
 class IntervalSelectionConfigWithoutTypeKwds(TypedDict, total=False):
     """
-    :class:`IntervalSelectionConfigWithoutType` ``TypedDict`` wrapper.
+    :class:`altair.IntervalSelectionConfigWithoutType` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3001,7 +3001,7 @@ class IntervalSelectionConfigWithoutTypeKwds(TypedDict, total=False):
 
 class LegendConfigKwds(TypedDict, total=False):
     """
-    :class:`LegendConfig` ``TypedDict`` wrapper.
+    :class:`altair.LegendConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3356,7 +3356,7 @@ class LegendConfigKwds(TypedDict, total=False):
 
 class LegendResolveMapKwds(TypedDict, total=False):
     """
-    :class:`LegendResolveMap` ``TypedDict`` wrapper.
+    :class:`altair.LegendResolveMap` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3399,7 +3399,7 @@ class LegendResolveMapKwds(TypedDict, total=False):
 
 class LegendStreamBindingKwds(TypedDict, total=False):
     """
-    :class:`LegendStreamBinding` ``TypedDict`` wrapper.
+    :class:`altair.LegendStreamBinding` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3412,7 +3412,7 @@ class LegendStreamBindingKwds(TypedDict, total=False):
 
 class LineConfigKwds(TypedDict, total=False):
     """
-    :class:`LineConfig` ``TypedDict`` wrapper.
+    :class:`altair.LineConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3875,7 +3875,7 @@ class LineConfigKwds(TypedDict, total=False):
 
 class LineStringKwds(TypedDict, total=False):
     """
-    :class:`LineString` ``TypedDict`` wrapper.
+    :class:`altair.LineString` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3895,7 +3895,7 @@ class LineStringKwds(TypedDict, total=False):
 
 class LinearGradientKwds(TypedDict, total=False):
     """
-    :class:`LinearGradient` ``TypedDict`` wrapper.
+    :class:`altair.LinearGradient` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3934,7 +3934,7 @@ class LinearGradientKwds(TypedDict, total=False):
 
 class LocaleKwds(TypedDict, total=False):
     """
-    :class:`Locale` ``TypedDict`` wrapper.
+    :class:`altair.Locale` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -3950,7 +3950,7 @@ class LocaleKwds(TypedDict, total=False):
 
 class MarkConfigKwds(TypedDict, total=False):
     """
-    :class:`MarkConfig` ``TypedDict`` wrapper.
+    :class:`altair.MarkConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4398,7 +4398,7 @@ class MarkConfigKwds(TypedDict, total=False):
 
 class MergedStreamKwds(TypedDict, total=False):
     """
-    :class:`MergedStream` ``TypedDict`` wrapper.
+    :class:`altair.MergedStream` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4432,7 +4432,7 @@ class MergedStreamKwds(TypedDict, total=False):
 
 class MultiLineStringKwds(TypedDict, total=False):
     """
-    :class:`MultiLineString` ``TypedDict`` wrapper.
+    :class:`altair.MultiLineString` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4452,7 +4452,7 @@ class MultiLineStringKwds(TypedDict, total=False):
 
 class MultiPointKwds(TypedDict, total=False):
     """
-    :class:`MultiPoint` ``TypedDict`` wrapper.
+    :class:`altair.MultiPoint` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4472,7 +4472,7 @@ class MultiPointKwds(TypedDict, total=False):
 
 class MultiPolygonKwds(TypedDict, total=False):
     """
-    :class:`MultiPolygon` ``TypedDict`` wrapper.
+    :class:`altair.MultiPolygon` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4492,7 +4492,7 @@ class MultiPolygonKwds(TypedDict, total=False):
 
 class NumberLocaleKwds(TypedDict, total=False):
     """
-    :class:`NumberLocale` ``TypedDict`` wrapper.
+    :class:`altair.NumberLocale` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -4526,7 +4526,7 @@ class NumberLocaleKwds(TypedDict, total=False):
 
 class OverlayMarkDefKwds(TypedDict, total=False):
     """
-    :class:`OverlayMarkDef` ``TypedDict`` wrapper.
+    :class:`altair.OverlayMarkDef` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5016,7 +5016,7 @@ class OverlayMarkDefKwds(TypedDict, total=False):
 
 class PointKwds(TypedDict, total=False):
     """
-    :class:`Point` ``TypedDict`` wrapper.
+    :class:`altair.Point` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5040,7 +5040,7 @@ class PointKwds(TypedDict, total=False):
 
 class PointSelectionConfigKwds(TypedDict, total=False):
     """
-    :class:`PointSelectionConfig` ``TypedDict`` wrapper.
+    :class:`altair.PointSelectionConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5148,7 +5148,7 @@ class PointSelectionConfigKwds(TypedDict, total=False):
 
 class PointSelectionConfigWithoutTypeKwds(TypedDict, total=False):
     """
-    :class:`PointSelectionConfigWithoutType` ``TypedDict`` wrapper.
+    :class:`altair.PointSelectionConfigWithoutType` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5248,7 +5248,7 @@ class PointSelectionConfigWithoutTypeKwds(TypedDict, total=False):
 
 class PolygonKwds(TypedDict, total=False):
     """
-    :class:`Polygon` ``TypedDict`` wrapper.
+    :class:`altair.Polygon` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5268,7 +5268,7 @@ class PolygonKwds(TypedDict, total=False):
 
 class ProjectionKwds(TypedDict, total=False):
     """
-    :class:`Projection` ``TypedDict`` wrapper.
+    :class:`altair.Projection` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5411,7 +5411,7 @@ class ProjectionKwds(TypedDict, total=False):
 
 class ProjectionConfigKwds(TypedDict, total=False):
     """
-    :class:`ProjectionConfig` ``TypedDict`` wrapper.
+    :class:`altair.ProjectionConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5554,7 +5554,7 @@ class ProjectionConfigKwds(TypedDict, total=False):
 
 class RadialGradientKwds(TypedDict, total=False):
     """
-    :class:`RadialGradient` ``TypedDict`` wrapper.
+    :class:`altair.RadialGradient` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5609,7 +5609,7 @@ class RadialGradientKwds(TypedDict, total=False):
 
 class RangeConfigKwds(TypedDict, total=False):
     """
-    :class:`RangeConfig` ``TypedDict`` wrapper.
+    :class:`altair.RangeConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -5663,7 +5663,7 @@ class RangeConfigKwds(TypedDict, total=False):
 
 class RectConfigKwds(TypedDict, total=False):
     """
-    :class:`RectConfig` ``TypedDict`` wrapper.
+    :class:`altair.RectConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6129,7 +6129,7 @@ class RectConfigKwds(TypedDict, total=False):
 
 class ResolveKwds(TypedDict, total=False):
     """
-    :class:`Resolve` ``TypedDict`` wrapper.
+    :class:`altair.Resolve` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6148,7 +6148,7 @@ class ResolveKwds(TypedDict, total=False):
 
 class ScaleConfigKwds(TypedDict, total=False):
     """
-    :class:`ScaleConfig` ``TypedDict`` wrapper.
+    :class:`altair.ScaleConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6331,7 +6331,7 @@ class ScaleConfigKwds(TypedDict, total=False):
 
 class ScaleInvalidDataConfigKwds(TypedDict, total=False):
     """
-    :class:`ScaleInvalidDataConfig` ``TypedDict`` wrapper.
+    :class:`altair.ScaleInvalidDataConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6401,7 +6401,7 @@ class ScaleInvalidDataConfigKwds(TypedDict, total=False):
 
 class ScaleResolveMapKwds(TypedDict, total=False):
     """
-    :class:`ScaleResolveMap` ``TypedDict`` wrapper.
+    :class:`altair.ScaleResolveMap` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6462,7 +6462,7 @@ class ScaleResolveMapKwds(TypedDict, total=False):
 
 class SelectionConfigKwds(TypedDict, total=False):
     """
-    :class:`SelectionConfig` ``TypedDict`` wrapper.
+    :class:`altair.SelectionConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6490,7 +6490,7 @@ class SelectionConfigKwds(TypedDict, total=False):
 
 class StepKwds(TypedDict, closed=True, total=False):  # type: ignore[call-arg]
     """
-    :class:`Step` ``TypedDict`` wrapper.
+    :class:`altair.Step` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6515,7 +6515,7 @@ class StepKwds(TypedDict, closed=True, total=False):  # type: ignore[call-arg]
 
 class StyleConfigIndexKwds(TypedDict, closed=True, total=False):  # type: ignore[call-arg]
     """
-    :class:`StyleConfigIndex` ``TypedDict`` wrapper.
+    :class:`altair.StyleConfigIndex` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -6582,7 +6582,7 @@ class StyleConfigIndexKwds(TypedDict, closed=True, total=False):  # type: ignore
 
 class TickConfigKwds(TypedDict, total=False):
     """
-    :class:`TickConfig` ``TypedDict`` wrapper.
+    :class:`altair.TickConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7041,7 +7041,7 @@ class TickConfigKwds(TypedDict, total=False):
 
 class TimeIntervalStepKwds(TypedDict, total=False):
     """
-    :class:`TimeIntervalStep` ``TypedDict`` wrapper.
+    :class:`altair.TimeIntervalStep` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7057,7 +7057,7 @@ class TimeIntervalStepKwds(TypedDict, total=False):
 
 class TimeLocaleKwds(TypedDict, total=False):
     """
-    :class:`TimeLocale` ``TypedDict`` wrapper.
+    :class:`altair.TimeLocale` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7091,7 +7091,7 @@ class TimeLocaleKwds(TypedDict, total=False):
 
 class TitleConfigKwds(TypedDict, total=False):
     """
-    :class:`TitleConfig` ``TypedDict`` wrapper.
+    :class:`altair.TitleConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7199,7 +7199,7 @@ class TitleConfigKwds(TypedDict, total=False):
 
 class TitleParamsKwds(TypedDict, total=False):
     """
-    :class:`TitleParams` ``TypedDict`` wrapper.
+    :class:`altair.TitleParams` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7330,7 +7330,7 @@ class TitleParamsKwds(TypedDict, total=False):
 
 class TooltipContentKwds(TypedDict, total=False):
     """
-    :class:`TooltipContent` ``TypedDict`` wrapper.
+    :class:`altair.TooltipContent` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7343,7 +7343,7 @@ class TooltipContentKwds(TypedDict, total=False):
 
 class TopLevelSelectionParameterKwds(TypedDict, total=False):
     """
-    :class:`TopLevelSelectionParameter` ``TypedDict`` wrapper.
+    :class:`altair.TopLevelSelectionParameter` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7405,7 +7405,7 @@ class TopLevelSelectionParameterKwds(TypedDict, total=False):
 
 class VariableParameterKwds(TypedDict, total=False):
     """
-    :class:`VariableParameter` ``TypedDict`` wrapper.
+    :class:`altair.VariableParameter` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7450,7 +7450,7 @@ class VariableParameterKwds(TypedDict, total=False):
 
 class ViewBackgroundKwds(TypedDict, total=False):
     """
-    :class:`ViewBackground` ``TypedDict`` wrapper.
+    :class:`altair.ViewBackground` ``TypedDict`` wrapper.
 
     Parameters
     ----------
@@ -7528,7 +7528,7 @@ class ViewBackgroundKwds(TypedDict, total=False):
 
 class ViewConfigKwds(TypedDict, total=False):
     """
-    :class:`ViewConfig` ``TypedDict`` wrapper.
+    :class:`altair.ViewConfig` ``TypedDict`` wrapper.
 
     Parameters
     ----------
diff --git a/altair/vegalite/v5/theme.py b/altair/vegalite/v5/theme.py
index ba5a5ab9a..77f480829 100644
--- a/altair/vegalite/v5/theme.py
+++ b/altair/vegalite/v5/theme.py
@@ -2,22 +2,16 @@
 
 from __future__ import annotations
 
-import sys
-from functools import wraps
-from typing import TYPE_CHECKING, Callable, Final, Literal, get_args
+from typing import TYPE_CHECKING, Any, Final, Literal, get_args
 
-from altair.utils.theme import ThemeRegistry
+from altair.utils.deprecation import deprecated_static_only
+from altair.utils.plugin_registry import Plugin, PluginRegistry
 from altair.vegalite.v5.schema._config import ThemeConfig
 from altair.vegalite.v5.schema._typing import VegaThemes
 
-if sys.version_info >= (3, 10):
-    from typing import ParamSpec
-else:
-    from typing_extensions import ParamSpec
-
-
 if TYPE_CHECKING:
-    from altair.utils.plugin_registry import Plugin
+    import sys
+    from functools import partial
 
     if sys.version_info >= (3, 11):
         from typing import LiteralString
@@ -28,11 +22,64 @@
     else:
         from typing_extensions import TypeAlias
 
-P = ParamSpec("P")
+    from altair.utils.plugin_registry import PluginEnabler
+
+
 AltairThemes: TypeAlias = Literal["default", "opaque"]
 VEGA_THEMES: list[LiteralString] = list(get_args(VegaThemes))
 
 
+# HACK: See for `LiteralString` requirement in `name`
+# https://github.com/vega/altair/pull/3526#discussion_r1743350127
+class ThemeRegistry(PluginRegistry[Plugin[ThemeConfig], ThemeConfig]):
+    def enable(
+        self,
+        name: LiteralString | AltairThemes | VegaThemes | None = None,
+        **options: Any,
+    ) -> PluginEnabler[Plugin[ThemeConfig], ThemeConfig]:
+        """
+        Enable a theme by name.
+
+        This can be either called directly, or used as a context manager.
+
+        Parameters
+        ----------
+        name : string (optional)
+            The name of the theme to enable. If not specified, then use the
+            current active name.
+        **options :
+            Any additional parameters will be passed to the theme as keyword
+            arguments
+
+        Returns
+        -------
+        PluginEnabler:
+            An object that allows enable() to be used as a context manager
+
+        Notes
+        -----
+        Default `vega` themes can be previewed at https://vega.github.io/vega-themes/
+        """
+        return super().enable(name, **options)
+
+    def get(self) -> partial[ThemeConfig] | Plugin[ThemeConfig] | None:
+        """Return the currently active theme."""
+        return super().get()
+
+    def names(self) -> list[str]:
+        """Return the names of the registered and entry points themes."""
+        return super().names()
+
+    @deprecated_static_only(
+        "Deprecated since `altair=5.5.0`. Use @altair.theme.register instead.",
+        category=None,
+    )
+    def register(
+        self, name: str, value: Plugin[ThemeConfig] | None
+    ) -> Plugin[ThemeConfig] | None:
+        return super().register(name, value)
+
+
 class VegaTheme:
     """Implementation of a builtin vega theme."""
 
@@ -53,6 +100,8 @@ def __repr__(self) -> str:
 # themes that will be auto-detected. Explicit registration is also
 # allowed by the PluginRegistry API.
 ENTRY_POINT_GROUP: Final = "altair.vegalite.v5.theme"
+
+# NOTE: `themes` def has an entry point group
 themes = ThemeRegistry(entry_point_group=ENTRY_POINT_GROUP)
 
 themes.register(
@@ -74,75 +123,3 @@ def __repr__(self) -> str:
     themes.register(theme, VegaTheme(theme))
 
 themes.enable("default")
-
-
-# HACK: See for `LiteralString` requirement in `name`
-# https://github.com/vega/altair/pull/3526#discussion_r1743350127
-def register_theme(
-    name: LiteralString, *, enable: bool
-) -> Callable[[Plugin[ThemeConfig]], Plugin[ThemeConfig]]:
-    """
-    Decorator for registering a theme function.
-
-    Parameters
-    ----------
-    name
-        Unique name assigned in ``alt.themes``.
-    enable
-        Auto-enable the wrapped theme.
-
-    Examples
-    --------
-    Register and enable a theme::
-
-        import altair as alt
-        from altair.typing import ThemeConfig
-
-
-        @alt.register_theme("param_font_size", enable=True)
-        def custom_theme() -> ThemeConfig:
-            sizes = 12, 14, 16, 18, 20
-            return {
-                "autosize": {"contains": "content", "resize": True},
-                "background": "#F3F2F1",
-                "config": {
-                    "axisX": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
-                    "axisY": {"labelFontSize": sizes[1], "titleFontSize": sizes[1]},
-                    "font": "'Lato', 'Segoe UI', Tahoma, Verdana, sans-serif",
-                    "headerColumn": {"labelFontSize": sizes[1]},
-                    "headerFacet": {"labelFontSize": sizes[1]},
-                    "headerRow": {"labelFontSize": sizes[1]},
-                    "legend": {"labelFontSize": sizes[0], "titleFontSize": sizes[1]},
-                    "text": {"fontSize": sizes[0]},
-                    "title": {"fontSize": sizes[-1]},
-                },
-                "height": {"step": 28},
-                "width": 350,
-            }
-
-    Until another theme has been enabled, all charts will use defaults set in ``custom_theme``::
-
-        from vega_datasets import data
-
-        source = data.stocks()
-        lines = (
-            alt.Chart(source, title=alt.Title("Stocks"))
-            .mark_line()
-            .encode(x="date:T", y="price:Q", color="symbol:N")
-        )
-        lines.interactive(bind_y=False)
-
-    """
-
-    def decorate(func: Plugin[ThemeConfig], /) -> Plugin[ThemeConfig]:
-        themes.register(name, func)
-        if enable:
-            themes.enable(name)
-
-        @wraps(func)
-        def wrapper(*args: P.args, **kwargs: P.kwargs) -> ThemeConfig:
-            return func(*args, **kwargs)
-
-        return wrapper
-
-    return decorate
diff --git a/doc/user_guide/api.rst b/doc/user_guide/api.rst
index 9000bce99..d3c08f547 100644
--- a/doc/user_guide/api.rst
+++ b/doc/user_guide/api.rst
@@ -9,6 +9,8 @@ Please refer to the `full user guide <http://altair-viz.github.io>`_ for
 further details, as this low-level documentation may not be enough to give
 full guidelines on their use.
 
+.. _api-toplevel:
+
 Top-Level Objects
 -----------------
 .. currentmodule:: altair
@@ -26,6 +28,8 @@ Top-Level Objects
    TopLevelMixin
    VConcatChart
 
+.. _api-channels:
+
 Encoding Channels
 -----------------
 .. currentmodule:: altair
@@ -134,6 +138,8 @@ Encoding Channels
    YOffsetValue
    YValue
 
+.. _api-functions:
+
 API Functions
 -------------
 .. currentmodule:: altair
@@ -164,6 +170,96 @@ API Functions
    vconcat
    when
 
+.. _api-theme:
+
+Theme
+-----
+.. currentmodule:: altair.theme
+
+.. autosummary::
+   :toctree: generated/theme/
+   :nosignatures:
+
+   active
+   enable
+   get
+   names
+   options
+   register
+   unregister
+   ThemeConfig
+   AreaConfigKwds
+   AutoSizeParamsKwds
+   AxisConfigKwds
+   AxisResolveMapKwds
+   BarConfigKwds
+   BindCheckboxKwds
+   BindDirectKwds
+   BindInputKwds
+   BindRadioSelectKwds
+   BindRangeKwds
+   BoxPlotConfigKwds
+   BrushConfigKwds
+   CompositionConfigKwds
+   ConfigKwds
+   DateTimeKwds
+   DerivedStreamKwds
+   ErrorBandConfigKwds
+   ErrorBarConfigKwds
+   FeatureGeometryGeoJsonPropertiesKwds
+   FormatConfigKwds
+   GeoJsonFeatureCollectionKwds
+   GeoJsonFeatureKwds
+   GeometryCollectionKwds
+   GradientStopKwds
+   HeaderConfigKwds
+   IntervalSelectionConfigKwds
+   IntervalSelectionConfigWithoutTypeKwds
+   LegendConfigKwds
+   LegendResolveMapKwds
+   LegendStreamBindingKwds
+   LineConfigKwds
+   LineStringKwds
+   LinearGradientKwds
+   LocaleKwds
+   MarkConfigKwds
+   MergedStreamKwds
+   MultiLineStringKwds
+   MultiPointKwds
+   MultiPolygonKwds
+   NumberLocaleKwds
+   OverlayMarkDefKwds
+   PaddingKwds
+   PointKwds
+   PointSelectionConfigKwds
+   PointSelectionConfigWithoutTypeKwds
+   PolygonKwds
+   ProjectionConfigKwds
+   ProjectionKwds
+   RadialGradientKwds
+   RangeConfigKwds
+   RectConfigKwds
+   ResolveKwds
+   RowColKwds
+   ScaleConfigKwds
+   ScaleInvalidDataConfigKwds
+   ScaleResolveMapKwds
+   SelectionConfigKwds
+   StepKwds
+   StyleConfigIndexKwds
+   TickConfigKwds
+   TimeIntervalStepKwds
+   TimeLocaleKwds
+   TitleConfigKwds
+   TitleParamsKwds
+   TooltipContentKwds
+   TopLevelSelectionParameterKwds
+   VariableParameterKwds
+   ViewBackgroundKwds
+   ViewConfigKwds
+
+.. _api-core:
+
 Low-Level Schema Wrappers
 -------------------------
 .. currentmodule:: altair
@@ -625,6 +721,8 @@ Low-Level Schema Wrappers
    WindowOnlyOp
    WindowTransform
 
+.. _api-cls:
+
 API Utility Classes
 -------------------
 .. currentmodule:: altair
@@ -638,6 +736,8 @@ API Utility Classes
    Then
    ChainedWhen
 
+.. _api-typing:
+
 Typing
 ------
 .. currentmodule:: altair.typing
@@ -689,7 +789,5 @@ Typing
    ChartType
    EncodeKwds
    Optional
-   ThemeConfig
    is_chart_type
-   theme
 
diff --git a/doc/user_guide/customization.rst b/doc/user_guide/customization.rst
index b95ac1c35..8c3781865 100644
--- a/doc/user_guide/customization.rst
+++ b/doc/user_guide/customization.rst
@@ -703,15 +703,20 @@ outside the chart itself; For example, the container may be a ``<div>`` element
 
 Chart Themes
 ------------
+.. note::
+
+   This material was changed considerably with the release of Altair ``5.5.0``.
+
 Altair makes available a theme registry that lets users apply chart configurations
-globally within any Python session. This is done via the ``alt.themes`` object.
+globally within any Python session. 
+The :mod:`altair.theme` module provides :ref:`helper functions <api-theme>` to interact with the registry.
 
-The themes registry consists of functions which define a specification dictionary
+Each theme in the registry is a function which define a specification dictionary
 that will be added to every created chart.
 For example, the default theme configures the default size of a single chart:
 
     >>> import altair as alt
-    >>> default = alt.themes.get()
+    >>> default = alt.theme.get()
     >>> default()
     {'config': {'view': {'continuousWidth': 300, 'continuousHeight': 300}}}
 
@@ -740,14 +745,14 @@ The rendered chart will then reflect these configurations:
 Changing the Theme
 ~~~~~~~~~~~~~~~~~~
 If you would like to enable any other theme for the length of your Python session,
-you can call ``alt.themes.enable(theme_name)``.
+you can call :func:`altair.theme.enable`.
 For example, Altair includes a theme in which the chart background is opaque
 rather than transparent:
 
 .. altair-plot::
     :output: repr
 
-    alt.themes.enable('opaque')
+    alt.theme.enable('opaque')
     chart.to_dict()
 
 .. altair-plot::
@@ -761,7 +766,7 @@ theme named ``'none'``:
 .. altair-plot::
     :output: repr
 
-    alt.themes.enable('none')
+    alt.theme.enable('none')
     chart.to_dict()
 
 .. altair-plot::
@@ -777,9 +782,14 @@ If you would like to use any theme just for a single chart, you can use the
 .. altair-plot::
    :output: none
 
-   with alt.themes.enable('default'):
+   with alt.theme.enable('default'):
        spec = chart.to_json()
 
+.. note::
+    The above requires that a conversion/saving operation occurs during the ``with`` block,
+    such as :meth:`~Chart.to_dict`, :meth:`~Chart.to_json`, :meth:`~Chart.save`.
+    See https://github.com/vega/altair/issues/3586
+
 Built-in Themes
 ~~~~~~~~~~~~~~~
 Currently Altair does not offer many built-in themes, but we plan to add
@@ -793,10 +803,11 @@ You can get a feel for the themes inherited from `Vega Themes`_ via *Vega-Altair
 
 Defining a Custom Theme
 ~~~~~~~~~~~~~~~~~~~~~~~
-The theme registry also allows defining and registering custom themes.
 A theme is simply a function that returns a dictionary of default values
-to be added to the chart specification at rendering time, which is then
-registered and activated.
+to be added to the chart specification at rendering time.
+
+Using :func:`altair.theme.register`, we can both register and enable a theme 
+at the site of the function definition.
 
 For example, here we define a theme in which all marks are drawn with black
 fill unless otherwise specified:
@@ -806,26 +817,17 @@ fill unless otherwise specified:
     import altair as alt
     from vega_datasets import data
 
-    # define the theme by returning the dictionary of configurations
-    def black_marks():
+    # define, register and enable theme
+    
+    @alt.theme.register("black_marks", enable=True)
+    def black_marks() -> alt.theme.ThemeConfig:
         return {
-            'config': {
-                'view': {
-                    'height': 300,
-                    'width': 300,
-                },
-                'mark': {
-                    'color': 'black',
-                    'fill': 'black'
-                }
+            "config": {
+                "view": {"continuousWidth": 300, "continuousHeight": 300},
+                "mark": {"color": "black", "fill": "black"},
             }
         }
 
-    # register the custom theme under a chosen name
-    alt.themes.register('black_marks', black_marks)
-
-    # enable the newly registered theme
-    alt.themes.enable('black_marks')
 
     # draw the chart
     cars = data.cars.url
diff --git a/tools/generate_api_docs.py b/tools/generate_api_docs.py
index 489375786..b9c2a1a2a 100644
--- a/tools/generate_api_docs.py
+++ b/tools/generate_api_docs.py
@@ -26,6 +26,8 @@
 further details, as this low-level documentation may not be enough to give
 full guidelines on their use.
 
+.. _api-toplevel:
+
 Top-Level Objects
 -----------------
 .. currentmodule:: altair
@@ -36,6 +38,8 @@
 
    {toplevel_charts}
 
+.. _api-channels:
+
 Encoding Channels
 -----------------
 .. currentmodule:: altair
@@ -46,6 +50,8 @@
 
    {encoding_wrappers}
 
+.. _api-functions:
+
 API Functions
 -------------
 .. currentmodule:: altair
@@ -56,6 +62,20 @@
 
    {api_functions}
 
+.. _api-theme:
+
+Theme
+-----
+.. currentmodule:: altair.theme
+
+.. autosummary::
+   :toctree: generated/theme/
+   :nosignatures:
+
+   {theme_objects}
+
+.. _api-core:
+
 Low-Level Schema Wrappers
 -------------------------
 .. currentmodule:: altair
@@ -66,6 +86,8 @@
 
    {lowlevel_wrappers}
 
+.. _api-cls:
+
 API Utility Classes
 -------------------
 .. currentmodule:: altair
@@ -76,6 +98,8 @@
 
    {api_classes}
 
+.. _api-typing:
+
 Typing
 ------
 .. currentmodule:: altair.typing
@@ -111,19 +135,19 @@ def iter_objects(
 
 
 def toplevel_charts() -> list[str]:
-    return sorted(iter_objects(alt.api, restrict_to_subclass=alt.TopLevelMixin))  # type: ignore[attr-defined]
+    return sorted(iter_objects(alt.api, restrict_to_subclass=alt.TopLevelMixin))
 
 
 def encoding_wrappers() -> list[str]:
-    return sorted(iter_objects(alt.channels, restrict_to_subclass=alt.SchemaBase))  # type: ignore[attr-defined]
+    return sorted(iter_objects(alt.channels, restrict_to_subclass=alt.SchemaBase))
 
 
 def api_functions() -> list[str]:
     # Exclude `typing` functions/SpecialForm(s)
-    KEEP = set(alt.api.__all__) - set(alt.typing.__all__)  # type: ignore[attr-defined]
+    KEEP = set(alt.api.__all__) - set(alt.typing.__all__)
     return sorted(
         name
-        for name in iter_objects(alt.api, restrict_to_type=types.FunctionType)  # type: ignore[attr-defined]
+        for name in iter_objects(alt.api, restrict_to_type=types.FunctionType)
         if name in KEEP
     )
 
@@ -137,8 +161,16 @@ def type_hints() -> list[str]:
     return sorted(s for s in iter_objects(alt.typing) if s in alt.typing.__all__)
 
 
+def theme() -> list[str]:
+    sort_1 = sorted(s for s in iter_objects(alt.theme) if s in alt.theme.__all__)
+    # Display functions before `TypedDict`, but show `ThemeConfig` before `Kwds`
+    sort_2 = sorted(sort_1, key=lambda s: s.endswith("Kwds"))
+    sort_3 = sorted(sort_2, key=lambda s: not s.islower())
+    return sort_3
+
+
 def lowlevel_wrappers() -> list[str]:
-    objects = sorted(iter_objects(alt.schema.core, restrict_to_subclass=alt.SchemaBase))  # type: ignore[attr-defined]
+    objects = sorted(iter_objects(alt.schema.core, restrict_to_subclass=alt.SchemaBase))
     # The names of these two classes are also used for classes in alt.channels. Due to
     # how imports are set up, these channel classes overwrite the two low-level classes
     # in the top-level Altair namespace. Therefore, they cannot be imported as e.g.
@@ -159,6 +191,7 @@ def write_api_file() -> None:
             lowlevel_wrappers=sep.join(lowlevel_wrappers()),
             api_classes=sep.join(api_classes()),
             typing_objects=sep.join(type_hints()),
+            theme_objects=sep.join(theme()),
         ),
         encoding="utf-8",
     )
diff --git a/tools/generate_schema_wrapper.py b/tools/generate_schema_wrapper.py
index 278606551..e024c2ca1 100644
--- a/tools/generate_schema_wrapper.py
+++ b/tools/generate_schema_wrapper.py
@@ -7,11 +7,12 @@
 import json
 import sys
 import textwrap
+from collections.abc import Iterable, Iterator
 from dataclasses import dataclass
 from itertools import chain
 from operator import attrgetter
 from pathlib import Path
-from typing import TYPE_CHECKING, Any, Final, Generic, Literal, TypedDict, TypeVar
+from typing import TYPE_CHECKING, Any, Final, Generic, Literal, TypeVar
 from urllib import request
 
 if sys.version_info >= (3, 14):
@@ -47,6 +48,7 @@
     from tools.schemapi.codegen import ArgInfo, AttrGetter
     from vl_convert import VegaThemes
 
+T = TypeVar("T", bound="str | Iterable[str]")
 
 SCHEMA_VERSION: Final = "v5.20.1"
 
@@ -530,6 +532,12 @@ class {classname}(DatumChannelMixin, core.{basename}):
     haspropsetters = True
 
 
+class ModuleDef(Generic[T]):
+    def __init__(self, contents: T, all: Iterable[str], /) -> None:
+        self.contents: T = contents
+        self.all: list[str] = list(all)
+
+
 def schema_class(*args, **kwargs) -> str:
     return SchemaGenerator(*args, **kwargs).schema_class()
 
@@ -692,7 +700,7 @@ def visit(nodes):
     return stack
 
 
-def generate_vegalite_schema_wrapper(fp: Path, /) -> str:
+def generate_vegalite_schema_wrapper(fp: Path, /) -> ModuleDef[str]:
     """Generate a schema wrapper at the given path."""
     # TODO: generate simple tests for each wrapper
     basename = "VegaLiteSchema"
@@ -766,7 +774,7 @@ def generate_vegalite_schema_wrapper(fp: Path, /) -> str:
         contents.append(definitions[name].schema_class())
 
     contents.append("")  # end with newline
-    return "\n".join(contents)
+    return ModuleDef("\n".join(contents), all_)
 
 
 @dataclass
@@ -798,7 +806,7 @@ def non_field_names(self) -> Iterator[str]:
                 yield self.value_class_name
 
 
-def generate_vegalite_channel_wrappers(fp: Path, /) -> str:
+def generate_vegalite_channel_wrappers(fp: Path, /) -> ModuleDef[list[str]]:
     schema = load_schema_with_shorthand_properties(fp)
     encoding_def = "FacetedEncoding"
     encoding = SchemaInfo(schema["definitions"][encoding_def], rootschema=schema)
@@ -854,7 +862,7 @@ def generate_vegalite_channel_wrappers(fp: Path, /) -> str:
         "with_property_setters",
     )
     it = chain.from_iterable(info.all_names for info in channel_infos.values())
-    all_ = list(chain(it, COMPAT_EXPORTS))
+    all_ = sorted(chain(it, COMPAT_EXPORTS))
     imports = [
         "import sys",
         "from typing import Any, overload, Sequence, List, Literal, Union, TYPE_CHECKING, TypedDict",
@@ -877,7 +885,7 @@ def generate_vegalite_channel_wrappers(fp: Path, /) -> str:
         ENCODING_SORT_FIELD,
     )
     TYPING_API = INTO_CONDITION, BIN, IMPUTE
-    contents = [
+    contents: list[str] = [
         HEADER,
         CHANNEL_MYPY_IGNORE_STATEMENTS,
         *imports,
@@ -889,14 +897,14 @@ def generate_vegalite_channel_wrappers(fp: Path, /) -> str:
             f"from altair.vegalite.v5.api import {', '.join(TYPING_API)}",
             textwrap.indent(import_typing_extensions((3, 11), "Self"), "    "),
         ),
-        "\n" f"__all__ = {sorted(all_)}\n",
+        "\n" f"__all__ = {all_}\n",
         CHANNEL_MIXINS,
         *class_defs,
         *generate_encoding_artifacts(
             channel_infos, ENCODE_METHOD, facet_encoding=encoding
         ),
     ]
-    return "\n".join(contents)
+    return ModuleDef(contents, all_)
 
 
 def generate_vegalite_mark_mixin(fp: Path, /, markdefs: dict[str, str]) -> str:
@@ -1008,7 +1016,7 @@ def generate_typed_dict(
         name=name,
         metaclass_kwds=metaclass_kwds,
         comment=comment,
-        summary=summary or f"{rst_syntax_for_class(info.title)} ``TypedDict`` wrapper.",
+        summary=(summary or f":class:`altair.{info.title}` ``TypedDict`` wrapper."),
         doc=doc,
         td_args=args,
     )
@@ -1092,6 +1100,72 @@ def generate_vegalite_config_mixin(fp: Path, /) -> str:
     return "\n".join(code)
 
 
+def generate_schema__init__(
+    *modules: str,
+    package: str,
+    expand: dict[Path, ModuleDef[Any]] | None = None,
+) -> Iterator[str]:
+    """
+    Generate schema subpackage init contents.
+
+    Parameters
+    ----------
+    *modules
+        Module names to expose, in addition to their members::
+
+            ...schema.__init__.__all__ = [
+                ...,
+                module_1.__name__,
+                module_1.__all__,
+                module_2.__name__,
+                module_2.__all__,
+                ...,
+            ]
+    package
+        Absolute, dotted path for `schema`, e.g::
+
+            "altair.vegalite.v5.schema"
+    expand
+        Required for 2nd-pass, which explicitly defines the new ``__all__``, using newly generated names.
+
+        .. note::
+            The default `import idiom`_ works at runtime, and for ``pyright`` - but not ``mypy``.
+            See `issue`_.
+
+    .. _import idiom:
+        https://typing.readthedocs.io/en/latest/spec/distributing.html#library-interface-public-and-private-symbols
+    .. _issue:
+        https://github.com/python/mypy/issues/15300
+    """
+    yield f"# ruff: noqa: F403, F405\n{HEADER_COMMENT}"
+    yield f"from {package} import {', '.join(modules)}"
+    yield from (f"from {package}.{mod} import *" for mod in modules)
+    yield f"SCHEMA_VERSION = {SCHEMA_VERSION!r}\n"
+    yield f"SCHEMA_URL = {schema_url()!r}\n"
+    base_all: list[str] = ["SCHEMA_URL", "SCHEMA_VERSION", *modules]
+    if expand:
+        base_all.extend(
+            chain.from_iterable(v.all for k, v in expand.items() if k.stem in modules)
+        )
+        yield f"__all__ = {base_all}"
+    else:
+        yield f"__all__ = {base_all}"
+        yield from (f"__all__ += {mod}.__all__" for mod in modules)
+
+
+def path_to_module_str(
+    fp: Path,
+    /,
+    root: Literal["altair", "doc", "sphinxext", "tests", "tools"] = "altair",
+) -> str:
+    # NOTE: GH runner has 3x altair, local is 2x
+    # - Needs to be the last occurence
+    idx = fp.parts.index(root)
+    start = idx + fp.parts.count(root) - 1 if root == "altair" else idx
+    parents = fp.parts[start:-1]
+    return ".".join(parents if fp.stem == "__init__" else (*parents, fp.stem))
+
+
 def vegalite_main(skip_download: bool = False) -> None:
     version = SCHEMA_VERSION
     vn = version.split(".")[0]
@@ -1109,23 +1183,22 @@ def vegalite_main(skip_download: bool = False) -> None:
 
     # Generate __init__.py file
     outfile = schemapath / "__init__.py"
+    pkg_schema = path_to_module_str(outfile)
     print(f"Writing {outfile!s}")
-    content = [
-        "# ruff: noqa\n",
-        "from .core import *\nfrom .channels import *\n",
-        f"SCHEMA_VERSION = '{version}'\n",
-        f"SCHEMA_URL = {schema_url(version)!r}\n",
-    ]
-    ruff.write_lint_format(outfile, content)
+    ruff.write_lint_format(
+        outfile, generate_schema__init__("channels", "core", package=pkg_schema)
+    )
 
     TypeAliasTracer.update_aliases(("Map", "Mapping[str, Any]"))
 
     files: dict[Path, str | Iterable[str]] = {}
+    modules: dict[Path, ModuleDef[Any]] = {}
 
     # Generate the core schema wrappers
     fp_core = schemapath / "core.py"
     print(f"Generating\n {schemafile!s}\n  ->{fp_core!s}")
-    files[fp_core] = generate_vegalite_schema_wrapper(schemafile)
+    modules[fp_core] = generate_vegalite_schema_wrapper(schemafile)
+    files[fp_core] = modules[fp_core].contents
 
     # Generate the channel wrappers
     fp_channels = schemapath / "channels.py"
@@ -1133,7 +1206,14 @@ def vegalite_main(skip_download: bool = False) -> None:
     with RemapContext(
         {DATETIME: (TEMPORAL, DATETIME), BIN_PARAMS: (BIN,), IMPUTE_PARAMS: (IMPUTE,)}
     ):
-        files[fp_channels] = generate_vegalite_channel_wrappers(schemafile)
+        modules[fp_channels] = generate_vegalite_channel_wrappers(schemafile)
+    files[fp_channels] = modules[fp_channels].contents
+
+    # Expand `schema.__init__.__all__` with new classes
+    ruff.write_lint_format(
+        outfile,
+        generate_schema__init__("channels", "core", package=pkg_schema, expand=modules),
+    )
 
     # generate the mark mixin
     markdefs = {k: f"{k}Def" for k in ["Mark", "BoxPlot", "ErrorBar", "ErrorBand"]}
diff --git a/tools/update_init_file.py b/tools/update_init_file.py
index f795667b7..5ac9c7cb4 100644
--- a/tools/update_init_file.py
+++ b/tools/update_init_file.py
@@ -4,13 +4,18 @@
 
 import typing as t
 import typing_extensions as te
+from importlib import import_module as _import_module
+from importlib.util import find_spec as _find_spec
 from inspect import getattr_static, ismodule
 from pathlib import Path
 from typing import TYPE_CHECKING
 
 from tools.codemod import ruff
 
-_TYPING_CONSTRUCTS = {
+if TYPE_CHECKING:
+    from collections.abc import Iterable, Iterator
+
+_TYPING_CONSTRUCTS: set[t.Any] = {
     te.TypeAlias,
     t.TypeVar,
     t.cast,
@@ -36,6 +41,8 @@
     te.TypeAliasType,
 }
 
+DYNAMIC_ALL: tuple[te.LiteralString, ...] = ("altair.vegalite.v5",)
+
 
 def update__all__variable() -> None:
     """
@@ -47,11 +54,8 @@ def update__all__variable() -> None:
     # Read existing file content
     import altair as alt
 
-    encoding = "utf-8"
-    init_path = Path(alt.__file__)
-    with init_path.open(encoding=encoding) as f:
-        lines = f.readlines()
-    lines = [line.strip("\n") for line in lines]
+    init_path = normalize_source("altair")
+    lines = extract_lines(init_path, strip_chars="\n")
 
     # Find first and last line of the definition of __all__
     first_definition_line = None
@@ -76,6 +80,10 @@ def update__all__variable() -> None:
     # Format file content with ruff
     ruff.write_lint_format(init_path, new_lines)
 
+    for source in DYNAMIC_ALL:
+        print(f"Updating `__all__`\n " f"{source!r}\n  ->{normalize_source(source)!s}")
+        update_dynamic__all__(source)
+
 
 def relevant_attributes(namespace: dict[str, t.Any], /) -> list[str]:
     """
@@ -134,5 +142,104 @@ def _is_relevant(attr: t.Any, name: str, /) -> bool:
         return True
 
 
+def _retrieve_all(name: str, /) -> list[str]:
+    """Import `name` and return a defined ``__all__``."""
+    found = _import_module(name).__all__
+    if not found:
+        msg = (
+            f"Expected to find a populated `__all__` for {name!r},\n"
+            f"but got: {found!r}"
+        )
+        raise AttributeError(msg)
+    return found
+
+
+def normalize_source(src: str | Path, /) -> Path:
+    """
+    Return the ``Path`` representation of a module/package.
+
+    Returned unchanged if already a ``Path``.
+    """
+    if isinstance(src, str):
+        if src == "altair" or src.startswith("altair."):
+            if (spec := _find_spec(src)) and (origin := spec.origin):
+                src = origin
+            else:
+                raise ModuleNotFoundError(src, spec)
+        return Path(src)
+    else:
+        return src
+
+
+def extract_lines(fp: Path, /, strip_chars: str | None = None) -> list[str]:
+    """Return all lines in ``fp`` with whitespace stripped."""
+    with Path(fp).open(encoding="utf-8") as f:
+        lines = f.readlines()
+        if not lines:
+            msg = f"Found no content when reading lines for:\n{lines!r}"
+            raise NotImplementedError(msg)
+    return [line.strip(strip_chars) for line in lines]
+
+
+def _normalize_import_lines(lines: Iterable[str]) -> Iterator[str]:
+    """
+    Collapses file content to contain one line per import source.
+
+    Preserves only lines **before** an existing ``__all__``.
+    """
+    it: Iterator[str] = iter(lines)
+    for line in it:
+        if line.endswith("("):
+            line = line.rstrip("( ")
+            for s_line in it:
+                if s_line.endswith(","):
+                    line = f"{line} {s_line}"
+                elif s_line.endswith(")"):
+                    break
+                else:
+                    NotImplementedError(f"Unexpected line:\n{s_line!r}")
+            yield line.rstrip(",")
+        elif line.startswith("__all__"):
+            break
+        else:
+            yield line
+
+
+def process_lines(lines: Iterable[str], /) -> Iterator[str]:
+    """Normalize imports, follow ``*``(s), reconstruct `__all__``."""
+    _all: set[str] = set()
+    for line in _normalize_import_lines(lines):
+        if line.startswith("#") or line == "":
+            yield line
+        elif "import" in line:
+            origin_stmt, members = line.split(" import ", maxsplit=1)
+            if members == "*":
+                _, origin = origin_stmt.split("from ")
+                targets = _retrieve_all(origin)
+            else:
+                targets = members.split(", ")
+            _all.update(targets)
+            yield line
+        else:
+            msg = f"Unexpected line:\n{line!r}"
+            raise NotImplementedError(msg)
+    yield f"__all__ = {sorted(_all)}"
+
+
+def update_dynamic__all__(source: str | Path, /) -> None:
+    """
+    ## Relies on all `*` imports leading to an `__all__`.
+
+    Acceptable `source`:
+
+        "altair.package.subpackage.etc"
+        Path(...)
+
+    """
+    fp = normalize_source(source)
+    content = process_lines(extract_lines(fp))
+    ruff.write_lint_format(fp, content)
+
+
 if __name__ == "__main__":
     update__all__variable()


Test patch:
diff --git a/tests/utils/test_deprecation.py b/tests/utils/test_deprecation.py
index 3970f4794..3449142aa 100644
--- a/tests/utils/test_deprecation.py
+++ b/tests/utils/test_deprecation.py
@@ -1,9 +1,11 @@
+# ruff: noqa: B018
 import re
 
 import pytest
 
 from altair.utils.deprecation import (
     AltairDeprecationWarning,
+    _warnings_monitor,
     deprecated,
     deprecated_warn,
 )
@@ -38,3 +40,25 @@ def test_deprecation_warn():
         match=re.compile(r"altair=3321.+this code path is a noop", flags=re.DOTALL),
     ):
         deprecated_warn("this code path is a noop", version="3321", stacklevel=1)
+
+
+def test_deprecated_import():
+    import altair as alt
+
+    pattern = re.compile(
+        r"altair=5\.5\.0.+\.theme instead.+user.guide",
+        flags=re.DOTALL | re.IGNORECASE,
+    )
+    with pytest.warns(AltairDeprecationWarning, match=pattern):
+        alt.themes
+
+    # NOTE: Tests that second access does not trigger a warning
+    assert alt.themes
+    # Then reset cache
+    _warnings_monitor.clear()
+
+    with pytest.warns(AltairDeprecationWarning, match=pattern):
+        from altair import themes  # noqa: F401
+
+    assert alt.themes == alt.theme._themes
+    _warnings_monitor.clear()
diff --git a/tests/vegalite/test_common.py b/tests/vegalite/test_common.py
index 3e8760ad7..404328886 100644
--- a/tests/vegalite/test_common.py
+++ b/tests/vegalite/test_common.py
@@ -20,7 +20,10 @@ def basic_spec():
 
 
 def make_final_spec(alt, basic_spec):
-    theme = alt.themes.get()
+    from altair.theme import _themes
+
+    theme = _themes.get()
+    assert theme
     spec = theme()
     spec.update(basic_spec)
     return spec
@@ -67,10 +70,12 @@ def test_basic_chart_from_dict(alt, basic_spec):
 
 @pytest.mark.parametrize("alt", [v5])
 def test_theme_enable(alt, basic_spec):
-    active_theme = alt.themes.active
+    from altair.theme import _themes
+
+    active_theme = _themes.active
 
     try:
-        alt.themes.enable("none")
+        _themes.enable("none")
 
         chart = alt.Chart.from_dict(basic_spec)
         dct = chart.to_dict()
@@ -83,7 +88,7 @@ def test_theme_enable(alt, basic_spec):
         assert dct == basic_spec
     finally:
         # reset the theme to its initial value
-        alt.themes.enable(active_theme)
+        _themes.enable(active_theme)  # pyright: ignore[reportArgumentType]
 
 
 @pytest.mark.parametrize("alt", [v5])
diff --git a/tests/vegalite/v5/test_api.py b/tests/vegalite/v5/test_api.py
index 98b30a9b6..0f60d185a 100644
--- a/tests/vegalite/v5/test_api.py
+++ b/tests/vegalite/v5/test_api.py
@@ -1347,20 +1347,22 @@ def test_LookupData():
 
 
 def test_themes():
+    from altair import theme
+
     chart = alt.Chart("foo.txt").mark_point()
 
-    with alt.themes.enable("default"):
+    with theme.enable("default"):
         assert chart.to_dict()["config"] == {
             "view": {"continuousWidth": 300, "continuousHeight": 300}
         }
 
-    with alt.themes.enable("opaque"):
+    with theme.enable("opaque"):
         assert chart.to_dict()["config"] == {
             "background": "white",
             "view": {"continuousWidth": 300, "continuousHeight": 300},
         }
 
-    with alt.themes.enable("none"):
+    with theme.enable("none"):
         assert "config" not in chart.to_dict()
 
 
diff --git a/tests/vegalite/v5/test_theme.py b/tests/vegalite/v5/test_theme.py
index 97d2fb42e..da7c134ba 100644
--- a/tests/vegalite/v5/test_theme.py
+++ b/tests/vegalite/v5/test_theme.py
@@ -5,10 +5,11 @@
 import pytest
 
 import altair.vegalite.v5 as alt
-from altair.typing import ThemeConfig
-from altair.vegalite.v5.schema._config import ConfigKwds
+from altair import theme
+from altair.theme import ConfigKwds, ThemeConfig
 from altair.vegalite.v5.schema._typing import is_color_hex
-from altair.vegalite.v5.theme import VEGA_THEMES, register_theme, themes
+from altair.vegalite.v5.theme import VEGA_THEMES
+from tests import slow
 
 if TYPE_CHECKING:
     import sys
@@ -25,26 +26,83 @@ def chart() -> alt.Chart:
 
 
 def test_vega_themes(chart) -> None:
-    for theme in VEGA_THEMES:
-        with alt.themes.enable(theme):
+    for theme_name in VEGA_THEMES:
+        with theme.enable(theme_name):
             dct = chart.to_dict()
-        assert dct["usermeta"] == {"embedOptions": {"theme": theme}}
+        assert dct["usermeta"] == {"embedOptions": {"theme": theme_name}}
         assert dct["config"] == {
             "view": {"continuousWidth": 300, "continuousHeight": 300}
         }
 
 
-def test_register_theme_decorator() -> None:
-    @register_theme("unique name", enable=True)
+@slow
+def test_theme_remote_lambda() -> None:
+    """
+    Compatibility test for ``lambda`` usage in `dash-vega-components`_.
+
+    A ``lambda`` here is to fetch the remote resource **once**, wrapping the result in a function.
+
+    .. _dash-vega-components:
+        https://github.com/vega/dash-vega-components/blob/c3e8cae873580bc7a52bc01daea1f27a7df02b8b/example_app.py#L13-L17
+    """
+    import altair as alt  # noqa: I001
+    from urllib.request import urlopen
+    import json
+
+    URL = "https://gist.githubusercontent.com/binste/b4042fa76a89d72d45cbbb9355ec6906/raw/e36f79d722bcd9dd954389b1753a2d4a18113227/altair_theme.json"
+    with urlopen(URL) as response:
+        custom_theme = json.load(response)
+
+    alt.theme.register("remote_binste", enable=True)(lambda: custom_theme)
+    assert alt.theme.active == "remote_binste"
+
+    # NOTE: A decorator-compatible way to define an "anonymous" function
+    @alt.theme.register("remote_binste_2", enable=True)
+    def _():
+        return custom_theme
+
+    assert alt.theme.active == "remote_binste_2"
+
+    decorated_theme = alt.theme.get()
+    alt.theme.enable("remote_binste")
+    assert alt.theme.active == "remote_binste"
+    lambda_theme = alt.theme.get()
+
+    assert decorated_theme
+    assert lambda_theme
+    assert decorated_theme() == lambda_theme()
+
+
+def test_theme_register_decorator() -> None:
+    @theme.register("unique name", enable=True)
     def custom_theme() -> ThemeConfig:
         return {"height": 400, "width": 700}
 
-    assert themes.active == "unique name"
-    registered = themes.get()
+    assert theme._themes.active == "unique name" == theme.active
+    registered = theme._themes.get()
     assert registered is not None
+    assert registered == theme.get()
     assert registered() == {"height": 400, "width": 700} == custom_theme()
 
 
+def test_theme_unregister() -> None:
+    @theme.register("big square", enable=True)
+    def custom_theme() -> ThemeConfig:
+        return {"height": 1000, "width": 1000}
+
+    assert theme.active == "big square"
+    fn = theme.unregister("big square")
+    assert fn() == custom_theme()
+    assert theme.active == theme._themes.active
+    # BUG: https://github.com/vega/altair/issues/3619
+    # assert theme.active != "big square"
+
+    with pytest.raises(
+        TypeError, match=r"Found no theme named 'big square' in registry."
+    ):
+        theme.unregister("big square")
+
+
 @pytest.mark.parametrize(
     ("color_code", "valid"),
     [
@@ -982,5 +1040,6 @@ def test_theme_config(theme_func: Callable[[], ThemeConfig], chart) -> None:
     See ``(test_vega_themes|test_register_theme_decorator)`` for comprehensive suite.
     """
     name = cast("LiteralString", theme_func.__qualname__)
-    register_theme(name, enable=True)
+    theme.register(name, enable=True)(theme_func)
     assert chart.to_dict(validate=True)
+    assert theme.get() == theme_func