From 7977554787af4e9e7d52ffc186a2fd25b270f195 Mon Sep 17 00:00:00 2001 From: Snehan Kekre Date: Tue, 4 Jun 2024 14:41:31 +0530 Subject: [PATCH] Make st.write call st.json to display Streamlit secrets object (#8659) ## Describe your changes When you call `st.write(st.secrets)`, the output is displayed as inline code with `st.markdown`. This PR makes `st.write` call `st.json` on objects of type `streamlit.runtime.secrets.Secrets` ## GitHub Issue Link (if applicable) Closes #2905. ## Testing Plan - Added Python unit test --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. --- lib/streamlit/elements/write.py | 3 +++ lib/streamlit/runtime/secrets.py | 14 ++++++++++++++ lib/streamlit/type_util.py | 7 +++++++ lib/tests/streamlit/write_test.py | 11 ++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/streamlit/elements/write.py b/lib/streamlit/elements/write.py index 010ddbf97cd1..d9bc21e8ed40 100644 --- a/lib/streamlit/elements/write.py +++ b/lib/streamlit/elements/write.py @@ -483,6 +483,9 @@ def flush_buffer(): ): # We either explicitly allow HTML or infer it's not HTML self.dg.markdown(repr_html, unsafe_allow_html=unsafe_allow_html) + elif type_util.is_streamlit_secrets_class(arg): + flush_buffer() + self.dg.json(arg.to_dict()) else: stringified_arg = str(arg) diff --git a/lib/streamlit/runtime/secrets.py b/lib/streamlit/runtime/secrets.py index 1f057b29c6cd..403310478cf2 100644 --- a/lib/streamlit/runtime/secrets.py +++ b/lib/streamlit/runtime/secrets.py @@ -25,6 +25,7 @@ KeysView, Mapping, NoReturn, + Union, ValuesView, ) @@ -44,6 +45,14 @@ ] +def _convert_to_dict(obj: Union[Mapping[str, Any], AttrDict]) -> dict[str, Any]: + """Recursively convert Mapping or AttrDict objects to dictionaries.""" + if isinstance(obj, AttrDict): + return obj.to_dict() + elif isinstance(obj, Mapping): + return {k: _convert_to_dict(v) for k, v in obj.items()} + + def _missing_attr_error_message(attr_name: str) -> str: return ( f'st.secrets has no attribute "{attr_name}". ' @@ -227,6 +236,11 @@ def _parse(self, print_exceptions: bool) -> Mapping[str, Any]: return self._secrets + def to_dict(self) -> dict[str, Any]: + """Converts the secrets store into a nested dictionary, where nested AttrDict objects are also converted into dictionaries.""" + secrets = self._parse(True) + return _convert_to_dict(secrets) + @staticmethod def _maybe_set_environment_variable(k: Any, v: Any) -> None: """Add the given key/value pair to os.environ if the value diff --git a/lib/streamlit/type_util.py b/lib/streamlit/type_util.py index cf6b187cb67e..8bba0194a8c0 100644 --- a/lib/streamlit/type_util.py +++ b/lib/streamlit/type_util.py @@ -59,6 +59,8 @@ from plotly.graph_objs import Figure from pydeck import Deck + from streamlit.runtime.secrets import Secrets + # Maximum number of rows to request from an unevaluated (out-of-core) dataframe MAX_UNEVALUATED_DF_ROWS = 10000 @@ -535,6 +537,11 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[Any]]: return True +def is_streamlit_secrets_class(obj: object) -> TypeGuard[Secrets]: + """True if obj is a Streamlit Secrets object.""" + return is_type(obj, "streamlit.runtime.secrets.Secrets") + + def is_sequence(seq: Any) -> bool: """True if input looks like a sequence.""" if isinstance(seq, str): diff --git a/lib/tests/streamlit/write_test.py b/lib/tests/streamlit/write_test.py index cbcd43731829..99e89989bfdf 100644 --- a/lib/tests/streamlit/write_test.py +++ b/lib/tests/streamlit/write_test.py @@ -19,7 +19,7 @@ import unittest from collections import namedtuple from typing import Any -from unittest.mock import MagicMock, Mock, PropertyMock, call, patch +from unittest.mock import MagicMock, Mock, PropertyMock, call, mock_open, patch import numpy as np import pandas as pd @@ -35,6 +35,7 @@ from tests.streamlit.modin_mocks import DataFrame as ModinDataFrame from tests.streamlit.modin_mocks import Series as ModinSeries from tests.streamlit.pyspark_mocks import DataFrame as PysparkDataFrame +from tests.streamlit.runtime.secrets_test import MOCK_TOML from tests.streamlit.snowpandas_mocks import DataFrame as SnowpandasDataFrame from tests.streamlit.snowpandas_mocks import Series as SnowpandasSeries from tests.streamlit.snowpark_mocks import DataFrame as SnowparkDataFrame @@ -250,6 +251,14 @@ def test_query_params(self): p.assert_called_once() + @patch("builtins.open", new_callable=mock_open, read_data=MOCK_TOML) + def test_streamlit_secrets(self, *mocks): + """Test st.write with st.secrets.""" + with patch("streamlit.delta_generator.DeltaGenerator.json") as p: + st.write(st.secrets) + + p.assert_called_once() + @parameterized.expand( [ (pd.DataFrame([[20, 30, 50]], columns=["a", "b", "c"]),),