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 #3618

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open

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

wants to merge 40 commits into from

Conversation

dangotbanned
Copy link
Member

@dangotbanned dangotbanned commented Sep 28, 2024

Need a way to make sense of these and start GH threads:

- 10 total files
- 3 modules named `theme.py`
- 2 modules named `__init__.py`
@dangotbanned dangotbanned added maintenance deprecation Requires a **MINOR** version bump labels Sep 28, 2024
altair/__init__.py Outdated Show resolved Hide resolved
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

"https://altair-viz.github.io/user_guide/customization.html#chart-themes",
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

@@ -93,7 +93,7 @@

class AreaConfigKwds(TypedDict, total=False):
"""
:class:`AreaConfig` ``TypedDict`` wrapper.
:class:`altair.AreaConfig` ``TypedDict`` wrapper.
Copy link
Member Author

@dangotbanned dangotbanned Sep 29, 2024

Choose a reason for hiding this comment

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

Previously these didn't work as links in the API reference.

Now they do

image

Note

I expect this hasn't come up before since everything has already been in altair.___.

The same will apply for any symbols in altair.(theme|typing)

Comment on lines +264 to +266
enable = themes.enable
get = themes.get
names = themes.names
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

@dangotbanned dangotbanned marked this pull request as ready for review September 30, 2024 20:18
I added this while experimenting with a more complex `__getattr__`, but don't need it for anything in this version
Comment on lines +668 to +672
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/api.html#theme\n"
" https://altair-viz.github.io/user_guide/customization.html#chart-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.

alt.themes deprecation warning message

altair/theme.py Outdated
Comment on lines 254 to 263
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

@mattijn
Copy link
Contributor

mattijn commented Oct 11, 2024

First, thanks for all the work and the short and long explanations in your comments!

I found it a bit hard to explain my thoughts in text alone, so let me add some contextual diagrams to it. I aim to distinguish three options:

  1. User experience today
  2. User experience of this PR
  3. Maintaining familiarity while modernizing code

I feel option 3 hasn't been explored enough, though it seems like a valid approach. I apologize, if I've misunderstood your previous comments if you did already discussed this. In that case, could you please direct me to where this option was discussed.

1. User experience today

Given the following code snippet

import altair as alt
from vega_datasets import data

source = data.cars.url
alt.themes.enable('dark')

alt.Chart(source).mark_tick().encode(
    x="Horsepower:Q",
)

image

This generates a dark-themed chart without a DeprecationWarning. The chart is created using existing legacy code, as shown in the diagram below:

graph TD
    subgraph A["alt.themes.enable('dark')"]
        D["Chart"]
    end
    C["Legacy Code"]    
    
    A <--> C
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#b3e0ff,stroke:#333,stroke-width:2px
    style D fill:#ffcc00,stroke:#333,stroke-width:2px
    
    linkStyle 0 stroke:#FF5733, stroke-width:2px; 
Loading

2. User experience of this PR

From my understanding, this PR will lead to the following behavioral changes for users:

import altair as alt
from vega_datasets import data

source = data.cars.url
alt.themes.enable('dark')

alt.Chart(source).mark_tick().encode(
    x="Horsepower:Q",
)
C:\Users\Hoek\AppData\Local\Temp\ipykernel_28532\3399117876.py:5: AltairDeprecationWarning: Deprecated in `altair=5.5.0`. Use altair.theme.themes instead.
Most of the `ThemeRegistry` API is accessible via `altair.theme`.
See the updated User Guide for further details:
    https://altair-viz.github.io/user_guide/api.html#theme
    https://altair-viz.github.io/user_guide/customization.html#chart-themes
  alt.themes.enable('dark')

image

The underlying changes are as follows:

graph TD
    subgraph A["alt.themes.enable('dark')"]
        D["Deprecation Warning + Chart"]
    end
    subgraph E["alt.theme.enable('dark')"]
        F["Chart"]
    end
    B["Modern Code"]
    C["Legacy Code"]
    
    A <--> B
    E <--> B
    B <--> C
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f2f2f2,stroke:#333,stroke-width:2px, stroke-dasharray: 5 5
    style C fill:#b3e0ff,stroke:#333,stroke-width:2px
    style D fill:#ffcc00,stroke:#333,stroke-width:2px
    style F fill:#ffcc00,stroke:#333,stroke-width:2px    
    style E fill:#f9f,stroke:#333,stroke-width:2px
    
    linkStyle 0 stroke:#FF5733, stroke-width:2px; 
    linkStyle 1 stroke:#33FF57, stroke-width:2px; 
    linkStyle 2 stroke:#FF5733, stroke-width:2px; 
Loading

The arrow colors indicate the following: using alt.themes (red arrows) will trigger a DeprecationWarning and interact with legacy code, while using alt.theme (green arrow) does not affect legacy code and resolves everything with the proposed modern code.

3. Maintaining familiarity while modernizing code

I’m curious whether this could be a feasible option as well. It’s more rigorous than consolidating:

import altair as alt
from vega_datasets import data

source = data.cars.url
alt.themes.enable('dark')

alt.Chart(source).mark_tick().encode(
    x="Horsepower:Q",
)

image

The user experience remains the same as today, with only alt.themes available, but the following happens under the hood:

graph TD
    subgraph A["alt.themes.enable('dark')"]
        D["Chart"]
    end
    B["Modern Code"]    
    
    A <--> B
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#f2f2f2,stroke:#333,stroke-width:2px, stroke-dasharray: 5 5    
    style D fill:#ffcc00,stroke:#333,stroke-width:2px
    
    linkStyle 0 stroke:#33FF57, stroke-width:2px; 
Loading

In this case, legacy code is replaced with modern code while maintaining the existing user experience.

Based on the Sourcegraph statistics (great that you brought this up in #3610 (comment)), I see the following current usages:

alt.themes.enable()
alt.themes.register()
alt.themes.names()
alt.themes.active

If these functions continue to work with a modernized codebase, I prefer this option. By "working," I mean that the final output of the visualizations remains the same as before. I completely understand that the implementations of these functions may differ, and that is perfectly fine.

I hope I’ve accurately interpreted your comments to make me think that this option hasn’t been discussed yet. Anyway, thank you again for your efforts on this!

@dangotbanned
Copy link
Member Author

dangotbanned commented Oct 11, 2024

First, thanks for all the work and the short and long explanations in your comments!

Thanks @mattijn

I feel option 3 hasn't been explored enough, though it seems like a valid approach. I apologize, if I've misunderstood your previous comments if you did already discussed this. In that case, could you please direct me to where this option was discussed.

I hope this can serve as an overview for you

Prior Comments

I hope I’ve accurately interpreted your comments to make me think that this option hasn’t been discussed yet.

The central issue is the difference between:

  • alt.theme.register
  • alt.themes.register

I discussed this in:

I also think PEP 318 – Decorators for Functions and Methods might be helpful in understanding what is going on here

@alt.theme.register

Moved in this PR to alt.theme, but the same as

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

Real-world Usage

Looking at the Sourcegraph again, every usage of alt.themes.register follows this pattern:

import altair as alt

# Define a theme function
# - Variant 1
def custom_theme():
    return {"height": 400, "width": 700}

# - Variant 2 (See Note)
custom_theme = lambda: {"height": 400, "width": 700}

# Register and then **immediately** enable theme
alt.themes.register("theme_name", custom_theme)
alt.themes.enable("theme_name")

These can just be replaced, and the result is easier to maintain - since everything is defined in one place:

import altair as alt

@alt.theme.register("theme_name", enable=True)
def custom_theme():
    return {"height": 400, "width": 700}
Variant 2 Note

Technically the sole lambda case is different

https://github.com/vega/dash-vega-components/blob/c3e8cae873580bc7a52bc01daea1f27a7df02b8b/example_app.py#L13-L17

Not too concerned with maintaining compatibility for an example in a related vega project, as we can just update that.

If this had to be a lambda you could do the first option in this test

test: Adds test_theme_remote_lambda

Non-usage

I found no usage of:

alt.themes.register("theme_name", None)

This case was the main focus of my discussion in #3610 (comment).

Note

Unsure if you've seen, but I opened #3619.

My takeaway was that the current behavior may produce unexpected results.

Deprecation

I still stand by my prior conclusion

I really do think it would be simpler to start fresh with a new namespace, without any additional baggage.
...
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

I'd add that situations like this are exactly what the Versioning Policy is there for.

However, by the definition given there alt.themes is not part of the Public API.
This PR adds alt.theme to the Public API, and treats alt.themes as-if it were previously - by providing a deprecation path.

@mattijn
Copy link
Contributor

mattijn commented Oct 11, 2024

Thanks for comment. Than we have discussed all my remarks. Thank you for your patience!

Can we enhance the DeprecationWarning to smoothen the transition?
Something as such:

deprecated_warn(
    "Deprecated in `altair=5.5.0`. Please update your code to use the new `altair.theme` API.\n\n"
    "Update path:\n"
    "Old method (deprecated):\n"
    "    import altair as alt\n"
    "    def custom_theme():\n"
    "        return {'height': 400, 'width': 700}\n\n"
    "    alt.themes.register('theme_name', custom_theme)\n\n"
    "New method (updated):\n"
    "    @alt.theme.register('theme_name', enable=True)\n"
    "    def custom_theme():\n"
    "        return {'height': 400, 'width': 700}\n\n"
    "    alt.theme.enable('theme_name')\n\n"
    "The `altair.theme.themes` method is provided for backward compatibility but should be avoided in new code.\n"
    "For best practices, adopt the new `altair.theme` API.\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\n",
    version="5.5.0",
    alternative="alt.theme",
    stacklevel=3,
)

I was confused that the suggested alternative was alt.theme.themes instead of alt.theme.

@dangotbanned
Copy link
Member Author

Thanks for comment. Than we have discussed all my remarks. Thank you for your patience!

No problem, thanks again @mattijn

Can we enhance the DeprecationWarning to smoothen the transition? Something as such:

Quoted code block

deprecated_warn(
    "Deprecated in `altair=5.5.0`. Please update your code to use the new `altair.theme` API.\n\n"
    "Update path:\n"
    "Old method (deprecated):\n"
    "    import altair as alt\n"
    "    def custom_theme():\n"
    "        return {'height': 400, 'width': 700}\n\n"
    "    alt.themes.register('theme_name', custom_theme)\n\n"
    "New method (updated):\n"
    "    @alt.theme.register('theme_name', enable=True)\n"
    "    def custom_theme():\n"
    "        return {'height': 400, 'width': 700}\n\n"
    "    alt.theme.enable('theme_name')\n\n"
    "The `altair.theme.themes` method is provided for backward compatibility but should be avoided in new code.\n"
    "For best practices, adopt the new `altair.theme` API.\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\n",
    version="5.5.0",
    alternative="alt.theme",
    stacklevel=3,
)

I was confused that the suggested alternative was alt.theme.themes instead of alt.theme.

Ah yeah, I had a related thought in #3610 (comment) but I'd forgotten the observation.

For reference, this is the format string used

def _format_message(
version: LiteralString,
alternative: LiteralString | None,
message: LiteralString | None,
/,
) -> LiteralString:
output = f"Deprecated in `altair={version}`."
if alternative:
output = f"{output} Use {alternative} instead."
return f"{output}\n{message}" if message else output

For the current warning that corresponds to

C:/Users/../ipykernel_17380/2846531205.py:1: AltairDeprecationWarning: """Deprecated in `altair={version='5.5.0'}`. Use {alternative='altair.theme.themes'} instead.
{message='Most of the `ThemeRegistry` API is accessible via `altair.theme`.
See the updated User Guide for further details:
    https://altair-viz.github.io/user_guide/api.html#theme
    https://altair-viz.github.io/user_guide/customization.html#chart-themes'}"""
  alt.themes # stacklevel=3 caller

I'll follow this comment up with some options, just wanted to set the baseline to build on

@dangotbanned dangotbanned changed the title refactor: alt.themes -> alt.theme.themes refactor: alt.themes -> alt.theme Oct 12, 2024
@dangotbanned
Copy link
Member Author

dangotbanned commented Oct 12, 2024

Continuation of #3618 (comment)

Some of these ideas overlap or may remove the need for others.

Ideas

  1. Change alternative="altair.theme.themes" -> alternative="altair.theme" in deprecated_warn
    • I think this makes sense in all cases.
    • My original decision was made prior to supporting alt.theme.(active|options) #3610-comment-6
    • Resolved in (a018165)
  2. Rename (make private) alt.theme.themes -> alt.theme._themes as mentioned in #3610-comment-7
  3. Embed multiple method examples into the message, as proposed by @mattijn in #3618-comment
    • The main issue here is that even at the current length, known usage outputs a lot of content

      Screenshot

      image

  4. Add more granular deprecation warnings on the underlying ThemeRegistry methods
    • Something like
      • the top level alt.themes display links to the new User Guide section
      • themes.register has an example of the decorator transition
        • Or refer to the example in docstring
        • Or refer to some section of the User Guide
      • themes.enable is a trivial case, just specify alternative
    • I think there's more than one way this could be achieved
  5. Display the warning only on the first access to alt.themes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deprecation Requires a **MINOR** version bump maintenance
Projects
None yet
2 participants