# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import functools from collections.abc import Callable from typing import Any, Final, TypeVar, cast import streamlit from streamlit import config from streamlit.logger import get_logger _LOGGER: Final = get_logger(__name__) TFunc = TypeVar("TFunc", bound=Callable[..., Any]) TObj = TypeVar("TObj", bound=object) def _error_details_in_browser_enabled() -> bool: """True if we should print deprecation warnings to the browser. Deprecation warnings are only shown when showErrorDetails is set to "full" or the legacy True value. All other values ("stacktrace", "type", "none", False) hide deprecation warnings in the browser. """ show_error_details = config.get_option("client.showErrorDetails") return ( show_error_details == config.ShowErrorDetailsConfigOptions.FULL or config.ShowErrorDetailsConfigOptions.is_true_variation(show_error_details) ) def show_deprecation_warning(message: str, show_in_browser: bool = True) -> None: """Show a deprecation warning message. Parameters ---------- message : str The deprecation warning message. show_in_browser : bool, default=True Whether to show the deprecation warning in the browser. When this is True, we will show the deprecation warning in the browser only if the user has set `client.showErrorDetails` to "full" or the legacy True value. All other values ("stacktrace", "type", "none", False) will hide deprecation warnings in the browser (but still log them to the console). """ if _error_details_in_browser_enabled() and show_in_browser: streamlit.warning(message) # We always log deprecation warnings _LOGGER.warning(message) def make_deprecated_name_warning( old_name: str, new_name: str, removal_date: str, extra_message: str | None = None, include_st_prefix: bool = True, ) -> str: if include_st_prefix: old_name = f"st.{old_name}" new_name = f"st.{new_name}" return ( f"Please replace `{old_name}` with `{new_name}`.\n\n" f"`{old_name}` will be removed after {removal_date}." + (f"\n\n{extra_message}" if extra_message else "") ) def deprecate_func_name( func: TFunc, old_name: str, removal_date: str, extra_message: str | None = None, name_override: str | None = None, ) -> TFunc: """Wrap an `st` function whose name has changed. Wrapped functions will run as normal, but will also show an st.warning saying that the old name will be removed after removal_date. (We generally set `removal_date` to 3 months from the deprecation date.) Parameters ---------- func The `st.` function whose name has changed. old_name The function's deprecated name within __init__.py. removal_date A date like "2020-01-01", indicating the last day we'll guarantee support for the deprecated name. extra_message An optional extra message to show in the deprecation warning. name_override An optional name to use in place of func.__name__. """ @functools.wraps(func) def wrapped_func(*args: Any, **kwargs: Any) -> Any: result = func(*args, **kwargs) show_deprecation_warning( make_deprecated_name_warning( old_name, name_override or (str(func.__name__) if hasattr(func, "__name__") else "unknown"), removal_date, extra_message, ) ) return result # Update the wrapped func's name & docstring so st.help does the right thing wrapped_func.__name__ = old_name wrapped_func.__doc__ = func.__doc__ return cast("TFunc", wrapped_func) def deprecate_obj_name( obj: TObj, old_name: str, new_name: str, removal_date: str, include_st_prefix: bool = True, ) -> TObj: """Wrap an `st` object whose name has changed. Wrapped objects will behave as normal, but will also show an st.warning saying that the old name will be removed after `removal_date`. (We generally set `removal_date` to 3 months from the deprecation date.) Parameters ---------- obj The `st.` object whose name has changed. old_name The object's deprecated name within __init__.py. new_name The object's new name within __init__.py. removal_date A date like "2020-01-01", indicating the last day we'll guarantee support for the deprecated name. include_st_prefix If False, does not prefix each of the object names in the deprecation message with `st.*`. Defaults to True. """ return _create_deprecated_obj_wrapper( obj, lambda: show_deprecation_warning( make_deprecated_name_warning( old_name, new_name, removal_date, include_st_prefix=include_st_prefix ) ), ) def _create_deprecated_obj_wrapper(obj: TObj, show_warning: Callable[[], Any]) -> TObj: """Create a wrapper for an object that has been deprecated. The first time one of the object's properties or functions is accessed, the given `show_warning` callback will be called. """ has_shown_warning = False def maybe_show_warning() -> None: # Call `show_warning` if it hasn't already been called once. nonlocal has_shown_warning if not has_shown_warning: has_shown_warning = True show_warning() class Wrapper: def __init__(self) -> None: # Override all the Wrapped object's magic functions for name in Wrapper._get_magic_functions(obj.__class__): setattr( self.__class__, name, property(self._make_magic_function_proxy(name)), ) def __getattr__(self, attr: str) -> Any: # We handle __getattr__ separately from our other magic # functions. The wrapped class may not actually implement it, # but we still need to implement it to call all its normal # functions. if attr in self.__dict__: return getattr(self, attr) maybe_show_warning() return getattr(obj, attr) @staticmethod def _get_magic_functions(self_cls: type[object]) -> list[str]: # ignore the handful of magic functions we cannot override without # breaking the Wrapper. ignore = ("__class__", "__dict__", "__getattribute__", "__getattr__") return [ name for name in dir(self_cls) if name not in ignore and name.startswith("__") ] @staticmethod def _make_magic_function_proxy(name: str) -> Callable[[Any], Any]: def proxy(_self: Any, *args: Any) -> Any: maybe_show_warning() return getattr(obj, name) return proxy return cast("TObj", Wrapper())