# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
#
# 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.
import functools
from typing import Any, Callable, List, TypeVar, cast
import streamlit
from streamlit import config
from streamlit.logger import get_logger
_LOGGER = get_logger(__name__)
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
TObj = TypeVar("TObj", bound=object)
def _should_show_deprecation_warning_in_browser() -> bool:
"""True if we should print deprecation warnings to the browser."""
return bool(config.get_option("client.showErrorDetails"))
def show_deprecation_warning(message: str) -> None:
"""Show a deprecation warning message."""
if _should_show_deprecation_warning_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
) -> str:
return (
f"Please replace `st.{old_name}` with `st.{new_name}`.\n\n"
f"`st.{old_name}` will be removed after {removal_date}."
)
def deprecate_func_name(func: TFunc, old_name: str, removal_date: str) -> 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.
"""
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
result = func(*args, **kwargs)
show_deprecation_warning(
make_deprecated_name_warning(old_name, func.__name__, removal_date)
)
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
) -> 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.
"""
return _create_deprecated_obj_wrapper(
obj,
lambda: show_deprecation_warning(
make_deprecated_name_warning(old_name, new_name, removal_date)
),
)
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):
# 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):
# 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(cls) -> 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(cls)
if name not in ignore and name.startswith("__")
]
@staticmethod
def _make_magic_function_proxy(name):
def proxy(self, *args):
maybe_show_warning()
return getattr(obj, name)
return proxy
return cast(TObj, Wrapper())