Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: alt.themes -> alt.theme.themes #3618

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
54e7e48
chore: Add notes for change targets
dangotbanned Sep 28, 2024
66c51c3
chore: Add TODO in `update_init_file.py`
dangotbanned Sep 28, 2024
754f665
refactor: Remove `altair.vegalite.v5.theme` from `alt.__all__`
dangotbanned Sep 28, 2024
3dd9100
refactor: Add `alt.theme.py`
dangotbanned Sep 28, 2024
b1be633
build(typing): Generate `altair.vegalite.v5.schema.__init__.__all__`
dangotbanned Sep 29, 2024
0bd8f75
build: Generate `altair.vegalite.v5.__init__.__all__`
dangotbanned Sep 29, 2024
c996c95
refactor: `utils.theme` -> `vegalite.v5.theme`
dangotbanned Sep 29, 2024
c286c38
refactor: Remove `alt.typing.theme`
dangotbanned Sep 29, 2024
ef9b846
refactor: Adds `alt.theme` implementation
dangotbanned Sep 29, 2024
e8d3062
test: Fix `test_common`
dangotbanned Sep 29, 2024
f7fe38b
chore(typing): Remove now fix ignore comments
dangotbanned Sep 29, 2024
0a1c8d3
fix(typing): Use a bounded `TypeVar` to support `list[str]`
dangotbanned Sep 29, 2024
05eca89
refactor: Add deprecation handling `alt.__init__.__getattr__`
dangotbanned Sep 29, 2024
217d7fb
docs: Adds `alt.theme` to API Reference
dangotbanned Sep 29, 2024
5078f88
docs: Add missing `altair` qualifier for `TypedDict`(s)
dangotbanned Sep 29, 2024
2339116
fix(typing): Preserve generics in `PluginEnabler`
dangotbanned Sep 29, 2024
48e44c0
refactor: Refine and fully document `generate_schema__init__`
dangotbanned Sep 30, 2024
a7dbaca
refactor: Use new functions for part of `update__all__variable`
dangotbanned Sep 30, 2024
a0947f8
fix: Don't consider suffix a part in `path_to_module_str`
dangotbanned Sep 30, 2024
1e9f639
fix: Account for GH runner path
dangotbanned Sep 30, 2024
d895447
docs: Add link targets for API Reference sections
dangotbanned Sep 30, 2024
a9538bc
docs: Customize theme toctree order
dangotbanned Sep 30, 2024
e114493
docs: Update User Guide section
dangotbanned Sep 30, 2024
c8033b2
docs: Provide API ref link in warning
dangotbanned Sep 30, 2024
ef0f263
refactor: Remove unused `__future__` import
dangotbanned Sep 30, 2024
58da996
feat: Support `alt.theme.(active|options)`
dangotbanned Oct 1, 2024
bc507a9
fix(typing): Partial resolve `mypy` `__getattr__`
dangotbanned Oct 1, 2024
e0f4721
test: Rename test to `test_theme_register_decorator`
dangotbanned Oct 1, 2024
4e8def0
test: Add some more `(theme|themes)` equal checks
dangotbanned Oct 1, 2024
9bd0316
test: Add `test_theme_unregister`
dangotbanned Oct 1, 2024
0d95b44
feat: Raise instead of returning `None` in `unregister`
dangotbanned Oct 1, 2024
3e6bde8
docs: Update remaining `ThemeRegistry` methods
dangotbanned Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions altair/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

# ruff: noqa
__version__ = "5.5.0dev"

Expand Down Expand Up @@ -617,7 +619,6 @@
"mixins",
"param",
"parse_shorthand",
"register_theme",
"renderers",
"repeat",
"sample",
Expand All @@ -627,7 +628,6 @@
"sequence",
"sphere",
"theme",
"themes",
"to_csv",
"to_json",
"to_values",
Expand All @@ -653,10 +653,29 @@ 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
from typing import Any as _Any


def load_ipython_extension(ipython):
from altair._magics import vegalite

ipython.register_magic_function(vegalite, "cell")


def __getattr__(name: str) -> _Any:
from altair.utils.deprecation import deprecated_warn

if name == "themes":
Copy link
Member Author

Choose a reason for hiding this comment

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

Solution is fine currently, but as soon as we start deprecating other imports it would make sense to generalize

deprecated_warn(
"Most of the `ThemeRegistry` API is accessible via `altair.theme`.\n"
"See the updated User Guide for further details:\n"
"https://altair-viz.github.io/user_guide/customization.html#chart-themes",
dangotbanned marked this conversation as resolved.
Show resolved Hide resolved
version="5.5.0",
alternative="altair.theme.themes",
stacklevel=3,
Copy link
Member Author

Choose a reason for hiding this comment

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

While I was testing, the default I'd set of 2 didn't report any warnings.

Not sure if that is related to __getattr__ itself, but noting it anyway

)
return theme.themes
else:
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
266 changes: 266 additions & 0 deletions altair/theme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"""Customizing chart configuration defaults."""

from __future__ import annotations

from functools import wraps as _wraps
from typing import TYPE_CHECKING

from altair.vegalite.v5.schema._config import (
AreaConfigKwds,
AutoSizeParamsKwds,
AxisConfigKwds,
AxisResolveMapKwds,
BarConfigKwds,
BindCheckboxKwds,
BindDirectKwds,
BindInputKwds,
BindRadioSelectKwds,
dangotbanned marked this conversation as resolved.
Show resolved Hide resolved
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

if TYPE_CHECKING:
import sys
from typing import Callable

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",
"ThemeConfig",
"TickConfigKwds",
"TimeIntervalStepKwds",
"TimeLocaleKwds",
"TitleConfigKwds",
"TitleParamsKwds",
"TooltipContentKwds",
"TopLevelSelectionParameterKwds",
"VariableParameterKwds",
"ViewBackgroundKwds",
"ViewConfigKwds",
"enable",
"get",
"names",
"register",
"themes",
"unregister",
]


def register(
name: LiteralString, *, enable: bool
) -> Callable[[Plugin[ThemeConfig]], Plugin[ThemeConfig]]:
"""
Decorator for registering a theme function.

Parameters
----------
name
Unique name assigned in ``alt.theme.themes``.
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,
}

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]:
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


def unregister(name: LiteralString) -> Plugin[ThemeConfig] | None:
"""
Remove and return a previously registered theme.

Parameters
----------
name
Unique name assigned in ``alt.theme.themes``.
"""
return themes.register(name, None)
Copy link
Member Author

Choose a reason for hiding this comment

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

Note

  • More explicit than being part of themes.register
  • This behavior is not part of @alt.theme.register
  • It wouldn't make sense decorating a function with it



enable = themes.enable
get = themes.get
names = themes.names
Comment on lines +280 to +282
Copy link
Member Author

Choose a reason for hiding this comment

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

Note

Originally planned for this to be a temporary solution.

Quite surpised at how well aliasing worked:

Docstring

image

API Reference

image

Indistinguishable from an actual function

image

4 changes: 0 additions & 4 deletions altair/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 0 additions & 1 deletion altair/typing/theme.py

This file was deleted.

16 changes: 10 additions & 6 deletions altair/utils/plugin_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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]):
Expand Down Expand Up @@ -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.

Expand Down
Loading