Skip to content

Commit

Permalink
Update custom-components import paths and tests (streamlit#8666)
Browse files Browse the repository at this point in the history
## Describe your changes

Closes streamlit#8644

From [this
comment](streamlit#8644 (comment)):
The usage of `components.html` via `st.components.v1.html` was possible
prior to `1.34` due to [this
import](https://github.com/streamlit/streamlit/pull/8457/files#diff-3f73cf34a1595773b3b79ea85c5efd0c208e23beec4f812fa05419f87cff597eL32):
`from streamlit.components.v1.components import ComponentRegistry`,
which made it transiently available. This means that the usage via
`st.components.v1.html` is only possible when `server.py` is imported at
one point, which is not the case for example when running Streamlit as a
bare-script.

We have discussed this and thought about bringing the behavior back, but
this time explicitly and with tests 🙂
Final decision is still pending.

## Testing Plan

- E2E Tests
- Added new e2e test to test this import behavior. The tests are added
as extra files in order to start with a fresh context and not
accidentally have transient imports that make it appear as if it works.

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
  • Loading branch information
raethlein authored Jun 11, 2024
1 parent aab099c commit db57a24
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/playwright-changed-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ jobs:
uses: tj-actions/changed-files@v44
with:
path: e2e_playwright
files: "**/*_test.py"
files: |
"**/*_test.py"
"!**/custom_components/**/*" # ignore custom_components check in the fast-workflow as it requires additional dependencies
- name: Check changed files
id: check_changed_files
env:
Expand Down
18 changes: 18 additions & 0 deletions e2e_playwright/custom_components/popular_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,27 @@


def use_components_html():
# note that we import streamlit before and so this `components.html` working
# might be coincidental; this is the reason why we have dedicated tests for this kind of imports in the `st_components_v1_*` files
import streamlit.components.v1 as components

components.html("<div>Hello World!</div>")


def use_components_iframe():
# note that we import streamlit before and so this `components.html` working
# might be coincidental; this is the reason why we have dedicated tests for this kind of imports in the `st_components_v1_*` files
import streamlit.components.v1 as components

st.write(str(components.iframe))


def use_components_declare_component():
import streamlit.components.v1 as components

st.write(str(components.declare_component))


# Different custom components:
def use_streamlit_ace():
from streamlit_ace import st_ace
Expand Down Expand Up @@ -179,6 +195,8 @@ def use_url_fragment():

options: dict[str, Callable] = {
"componentsHtml": use_components_html,
"componentsIframe": use_components_iframe,
"componentsDeclareComponent": use_components_declare_component,
"ace": use_streamlit_ace,
"aggrid": use_aggrid,
"antd": use_antd,
Expand Down
26 changes: 25 additions & 1 deletion e2e_playwright/custom_components/popular_components_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import re

import pytest
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import wait_for_app_run
Expand Down Expand Up @@ -42,7 +45,28 @@ def test_components_html(app: Page):
_expect_no_exception(app)
_expect_iframe_attached(app)
iframe = app.frame_locator("iframe")
expect(iframe.locator("div", has_text="Hello World!")).to_be_attached()
div = iframe.locator("div")
expect(div).to_have_text("Hello World!")


@pytest.mark.parametrize(
("name", "expected_text"),
[
("componentsIframe", "bound method IframeMixin._iframe of DeltaGenerator()"),
("componentsDeclareComponent", "function declare_component at"),
],
)
def test_components_import(app: Page, name: str, expected_text: str):
"""Test that components.iframe and components.declare_component can be imported and used.
We only make sure that they are importable but do not call them, so we don't have an iframe element in the DOM.
"""
_select_component(app, name)
_expect_no_exception(app)
div = app.get_by_test_id("stMarkdownContainer").filter(
has_text=re.compile(f"<{expected_text}.*>")
)
expect(div).to_be_attached()


def test_ace(app: Page):
Expand Down
25 changes: 23 additions & 2 deletions e2e_playwright/shared/app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def get_markdown(locator: Locator, text_inside_markdown: str | Pattern[str]) ->


def expect_prefixed_markdown(
locator: Locator,
locator: Locator | Page,
expected_prefix: str,
expected_markdown: str | Pattern[str],
exact_match: bool = False,
Expand Down Expand Up @@ -192,8 +192,29 @@ def expect_prefixed_markdown(
expect(selection_text).to_contain_text(expected_markdown)


def expect_markdown(
locator: Locator | Page,
expected_message: str | Pattern[str],
) -> None:
"""Expect an exception to be displayed in the app.
Parameters
----------
locator : Locator
The locator to search for the exception element.
expected_markdown : str or Pattern[str]
The expected message to be displayed in the exception.
"""
markdown_el = locator.get_by_test_id("stMarkdownContainer").filter(
has_text=expected_message
)
expect(markdown_el).to_be_visible()


def expect_exception(
locator: Locator,
locator: Locator | Page,
expected_message: str | Pattern[str],
) -> None:
"""Expect an exception to be displayed in the app.
Expand Down
23 changes: 23 additions & 0 deletions e2e_playwright/st_components_v1_import_legacy_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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.

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# PLEASE DO NOT ADD MORE IMPORTS HERE OR MOVE THE CODE TO ANOTHER FILE.
# This file relies on a clean import to make sure the functionality is not made available transiently.
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

from streamlit.components.v1 import components

components.declare_component
components.CustomComponent
19 changes: 19 additions & 0 deletions e2e_playwright/st_components_v1_import_legacy_file_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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 playwright.sync_api import Page, expect


def test_imports_dont_throw(app: Page):
expect(app.get_by_test_id("stException")).not_to_be_visible()
24 changes: 24 additions & 0 deletions e2e_playwright/st_components_v1_import_via_st.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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.

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# PLEASE DO NOT ADD MORE IMPORTS HERE OR MOVE THE CODE TO ANOTHER FILE.
# This file relies on a clean import to make sure the functionality is not made available transiently.
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

import streamlit as st

st.components.v1.html("<div>This import and usage worked!</div>")
st.write(str(st.components.v1.iframe))
st.write(str(st.components.v1.declare_component))
29 changes: 29 additions & 0 deletions e2e_playwright/st_components_v1_import_via_st_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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 re

from playwright.sync_api import Page, expect

from e2e_playwright.shared.app_utils import expect_markdown


def test_components_v1_was_imported_successfully(app: Page):
expect(app.locator("iframe")).to_be_attached()
iframe = app.frame_locator("iframe")
div = iframe.locator("div")
expect(div).to_have_text("This import and usage worked!")

expect_markdown(app, "<bound method IframeMixin._iframe of DeltaGenerator()>")
expect_markdown(app, re.compile("<function declare_component at .*>"))
5 changes: 5 additions & 0 deletions lib/streamlit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,8 @@ def _update_logger() -> None:
experimental_connection = _deprecate_func_name(
connection, "experimental_connection", "2024-04-01", name_override="connection"
)

# make it possible to call streamlit.components.v1.html etc. by importing it here
# import in the very end to avoid partially-initialized module import errors, because
# streamlit.components.v1 also uses some streamlit imports
import streamlit.components.v1 # noqa: F401
10 changes: 5 additions & 5 deletions lib/streamlit/components/v1/custom_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import json
from typing import TYPE_CHECKING, Any

from streamlit import _main, type_util
from streamlit.components.types.base_custom_component import BaseCustomComponent
from streamlit.delta_generator import main_dg
from streamlit.elements.form import current_form_id
from streamlit.elements.lib.policies import check_cache_replay_rules
from streamlit.errors import StreamlitAPIException
Expand All @@ -29,7 +29,7 @@
from streamlit.runtime.scriptrunner import get_script_run_ctx
from streamlit.runtime.state import NoValue, register_widget
from streamlit.runtime.state.common import compute_widget_id
from streamlit.type_util import to_bytes
from streamlit.type_util import is_bytes_like, is_dataframe_like, to_bytes

if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
Expand Down Expand Up @@ -122,12 +122,12 @@ def create_instance(
json_args = {}
special_args = []
for arg_name, arg_val in all_args.items():
if type_util.is_bytes_like(arg_val):
if is_bytes_like(arg_val):
bytes_arg = SpecialArg()
bytes_arg.key = arg_name
bytes_arg.bytes = to_bytes(arg_val)
special_args.append(bytes_arg)
elif type_util.is_dataframe_like(arg_val):
elif is_dataframe_like(arg_val):
dataframe_arg = SpecialArg()
dataframe_arg.key = arg_name
component_arrow.marshall(dataframe_arg.arrow_dataframe.data, arg_val)
Expand Down Expand Up @@ -221,7 +221,7 @@ def deserialize_component(ui_value, widget_id=""):

# We currently only support writing to st._main, but this will change
# when we settle on an improved API in a post-layout world.
dg = _main
dg = main_dg

element = Element()
return_value = marshall_component(dg, element)
Expand Down

0 comments on commit db57a24

Please sign in to comment.