Skip to content

Commit 815baa4

Browse files
committed
Merge branch 'main' into machow-feat-narwhals
* main: chore: Fix `langchain_core` test that used a new `Sequence` type (posit-dev#1697) tests(input_slider): add kitchensink tests for input_slider (posit-dev#1691) chore: More minor type changes in prep for py-htmltools updates (posit-dev#1693) chore: Add types and check action for py-htmltools (posit-dev#1692) Anticipate `htmltools.HTML` no longer inheriting from `str` (posit-dev#1690) tests(input_select): Add kitchensink tests for input_select and input_selectize (posit-dev#1686)
2 parents 6273885 + 4f5b666 commit 815baa4

File tree

19 files changed

+671
-116
lines changed

19 files changed

+671
-116
lines changed

.github/py-shiny/check/action.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: 'check py-shiny'
2+
description: 'Action that checks py-shiny in multiple steps so that any of them may fail but not prevent the others from running. Note, this action is used by py-htmltools as a way to consistently check py-shiny. If more checks are needed for py-htmltools to believe py-shiny is working, it should be added here.'
3+
runs:
4+
using: "composite"
5+
steps:
6+
- name: Run unit tests
7+
shell: bash
8+
run: |
9+
# Run unit tests
10+
make check-tests
11+
12+
- name: Type check
13+
shell: bash
14+
run: |
15+
# Type check
16+
make check-types
17+
18+
- name: Lint code
19+
shell: bash
20+
run: |
21+
# Lint code
22+
make check-lint
23+
24+
- name: Verify code formatting
25+
shell: bash
26+
run: |
27+
# Verify code formatting
28+
make check-format
29+
30+
- name: Verify code can run with mypy (not Windows)
31+
if: ${{ runner.os != 'Windows' }}
32+
shell: bash
33+
run: |
34+
# Verify code can run with mypy (not Windows)
35+
make ci-check-mypy-can-run

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525

2626
### Bug fixes
2727

28-
* A few fixes for `ui.Chat()`, including:
28+
* A few fixes for `ui.Chat()`, including:
2929
* Fixed a bug with `Chat()` sometimes silently dropping errors. (#1672)
3030
* Fixed a bug with `Chat()` sometimes not removing it's loading icon (on error or a `None` transform). (#1679)
3131
* `.messages(format="anthropic")` correctly removes non-user starting messages (once again). (#1685)
@@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535

3636
* `ui.Theme()` now works correctly on Windows when the theme requires Sass compilation. (thanks @yuuuxt, #1684)
3737

38+
* Fixed the `InputSlider` controller's `.expect_width()` to check the `width` property within the `style` attribute. (#1691)
39+
3840
## [1.1.0] - 2024-09-03
3941

4042
### New features

shiny/playwright/controller/_input_controls.py

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717
from ..expect._internal import expect_style_to_have_value as _expect_style_to_have_value
1818
from ._base import (
1919
InitLocator,
20+
UiWithContainerP,
2021
UiWithLabel,
2122
WidthContainerM,
22-
WidthLocM,
23-
_expect_multiple,
2423
all_missing,
2524
not_is_missing,
2625
)
@@ -30,7 +29,7 @@
3029
)
3130

3231

33-
class _InputSliderBase(WidthLocM, UiWithLabel):
32+
class _InputSliderBase(UiWithLabel):
3433

3534
loc_irs: Locator
3635
"""
@@ -203,6 +202,19 @@ def expect_max(self, value: AttrValue, *, timeout: Timeout = None) -> None:
203202
self.loc, "data-max", value=value, timeout=timeout
204203
)
205204

205+
def expect_width(self, value: str, *, timeout: Timeout = None) -> None:
206+
"""
207+
Expects the slider to have the specified width.
208+
209+
Parameters
210+
----------
211+
value
212+
The expected width.
213+
timeout
214+
The maximum time to wait for the width to be visible and interactable. Defaults to `None`.
215+
"""
216+
_expect_style_to_have_value(self.loc_container, "width", value, timeout=timeout)
217+
206218
def expect_step(self, value: AttrValue, *, timeout: Timeout = None) -> None:
207219
"""
208220
Expect the input element to have the expected `step` attribute value.
@@ -923,30 +935,40 @@ def __init__(
923935
)
924936

925937

926-
class _InputSelectBase(
927-
WidthLocM,
928-
UiWithLabel,
929-
):
930-
loc_selected: Locator
931-
"""
932-
Playwright `Locator` for the selected option of the input select.
933-
"""
934-
loc_choices: Locator
935-
"""
936-
Playwright `Locator` for the choices of the input select.
938+
class InputSelectWidthM:
937939
"""
938-
loc_choice_groups: Locator
939-
"""
940-
Playwright `Locator` for the choice groups of the input select.
940+
A base class representing the input `select` and `selectize` widths.
941+
942+
This class provides methods to expect the width attribute of a DOM element.
941943
"""
942944

943-
def __init__(
944-
self,
945-
page: Page,
946-
id: str,
945+
def expect_width(
946+
self: UiWithContainerP,
947+
value: AttrValue,
947948
*,
948-
select_class: str = "",
949+
timeout: Timeout = None,
949950
) -> None:
951+
"""
952+
Expect the input select to have a specific width.
953+
954+
Parameters
955+
----------
956+
value
957+
The expected width.
958+
timeout
959+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
960+
"""
961+
_expect_style_to_have_value(self.loc_container, "width", value, timeout=timeout)
962+
963+
964+
class InputSelect(InputSelectWidthM, UiWithLabel):
965+
"""
966+
Controller for :func:`shiny.ui.input_select`.
967+
968+
If you have defined your app's select input (`ui.input_select()`) with `selectize=TRUE`, use `InputSelectize` to test your app's UI.
969+
"""
970+
971+
def __init__(self, page: Page, id: str) -> None:
950972
"""
951973
Initializes the input select.
952974
@@ -956,13 +978,11 @@ def __init__(
956978
The page where the input select is located.
957979
id
958980
The id of the input select.
959-
select_class
960-
The class of the select element. Defaults to "".
961981
"""
962982
super().__init__(
963983
page,
964984
id=id,
965-
loc=f"select#{id}.shiny-bound-input{select_class}",
985+
loc=f"select#{id}.shiny-bound-input.form-select",
966986
)
967987
self.loc_selected = self.loc.locator("option:checked")
968988
self.loc_choices = self.loc.locator("option")
@@ -988,9 +1008,29 @@ def set(
9881008
selected = [selected]
9891009
self.loc.select_option(value=selected, timeout=timeout)
9901010

1011+
# If `selectize=` parameter does not become deprecated, uncomment this
1012+
# # selectize: bool = False,
1013+
# def expect_selectize(self, value: bool, *, timeout: Timeout = None) -> None:
1014+
# """
1015+
# Expect the input select to be selectize.
1016+
1017+
# Parameters
1018+
# ----------
1019+
# value
1020+
# Whether the input select is selectize.
1021+
# timeout
1022+
# The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1023+
# """
1024+
# # class_=None if selectize else "form-select",
1025+
# _expect_class_to_have_value(
1026+
# self.loc,
1027+
# "form-select",
1028+
# has_class=not value,
1029+
# timeout=timeout,
1030+
# )
1031+
9911032
def expect_choices(
9921033
self,
993-
# TODO-future; support patterns?
9941034
choices: ListPatternOrStr,
9951035
*,
9961036
timeout: Timeout = None,
@@ -1111,10 +1151,9 @@ def expect_choice_labels(
11111151
return
11121152
playwright_expect(self.loc_choices).to_have_text(value, timeout=timeout)
11131153

1114-
# multiple: bool = False,
11151154
def expect_multiple(self, value: bool, *, timeout: Timeout = None) -> None:
11161155
"""
1117-
Expect the input select to allow multiple selections.
1156+
Expect the input selectize to allow multiple selections.
11181157
11191158
Parameters
11201159
----------
@@ -1123,7 +1162,12 @@ def expect_multiple(self, value: bool, *, timeout: Timeout = None) -> None:
11231162
timeout
11241163
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
11251164
"""
1126-
_expect_multiple(self.loc, value, timeout=timeout)
1165+
_expect_attribute_to_have_value(
1166+
self.loc,
1167+
"multiple",
1168+
value="" if value else None,
1169+
timeout=timeout,
1170+
)
11271171

11281172
def expect_size(self, value: AttrValue, *, timeout: Timeout = None) -> None:
11291173
"""
@@ -1144,50 +1188,7 @@ def expect_size(self, value: AttrValue, *, timeout: Timeout = None) -> None:
11441188
)
11451189

11461190

1147-
class InputSelect(_InputSelectBase):
1148-
"""Controller for :func:`shiny.ui.input_select`."""
1149-
1150-
def __init__(self, page: Page, id: str) -> None:
1151-
"""
1152-
Initializes the input select.
1153-
1154-
Parameters
1155-
----------
1156-
page
1157-
The page where the input select is located.
1158-
id
1159-
The id of the input select.
1160-
"""
1161-
super().__init__(
1162-
page,
1163-
id=id,
1164-
select_class=".form-select",
1165-
)
1166-
1167-
# selectize: bool = False,
1168-
def expect_selectize(self, value: bool, *, timeout: Timeout = None) -> None:
1169-
"""
1170-
Expect the input select to be selectize.
1171-
1172-
Parameters
1173-
----------
1174-
value
1175-
Whether the input select is selectize.
1176-
timeout
1177-
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1178-
"""
1179-
# class_=None if selectize else "form-select",
1180-
_expect_class_to_have_value(
1181-
self.loc,
1182-
"form-select",
1183-
has_class=not value,
1184-
timeout=timeout,
1185-
)
1186-
1187-
1188-
class InputSelectize(
1189-
UiWithLabel,
1190-
):
1191+
class InputSelectize(InputSelectWidthM, UiWithLabel):
11911192
"""Controller for :func:`shiny.ui.input_selectize`."""
11921193

11931194
def __init__(self, page: Page, id: str) -> None:

shiny/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def resolve(
209209
"""
210210
...
211211

212-
def get_value(self) -> Optional[str]:
212+
def get_value(self) -> str | None:
213213
"""
214214
Get the value of this navigation item (if any).
215215

shiny/ui/_chat.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ def messages(
489489
transform_user == "last" and i == len(messages) - 1
490490
)
491491
content_key = m["transform_key" if transform else "pre_transform_key"]
492-
chat_msg = ChatMessage(content=m[content_key], role=m["role"])
492+
chat_msg = ChatMessage(content=str(m[content_key]), role=m["role"])
493493
if not isinstance(format, MISSING_TYPE):
494494
chat_msg = as_provider_message(chat_msg, format)
495495
res.append(chat_msg)
@@ -635,7 +635,7 @@ async def _send_append_message(
635635
content_type = "html" if isinstance(content, HTML) else "markdown"
636636

637637
msg = ClientMessage(
638-
content=content,
638+
content=str(content),
639639
role=message["role"],
640640
content_type=content_type,
641641
chunk_type=chunk_type,
@@ -790,7 +790,7 @@ async def _transform_message(
790790
if content is None:
791791
return None
792792

793-
res[key] = content
793+
res[key] = content # type: ignore
794794

795795
return res
796796

@@ -950,7 +950,7 @@ def user_input(self, transform: bool = False) -> str | None:
950950
if msg is None:
951951
return None
952952
key = "content_server" if transform else "content_client"
953-
return msg[key]
953+
return str(msg[key])
954954

955955
def _user_input(self) -> str:
956956
id = self.user_input_id

shiny/ui/_chat_normalize.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from abc import ABC, abstractmethod
33
from typing import TYPE_CHECKING, Any, Optional, cast
44

5+
from htmltools import HTML
6+
57
from ._chat_types import ChatMessage
68

79
if TYPE_CHECKING:
@@ -49,10 +51,10 @@ def normalize_chunk(self, chunk: Any) -> ChatMessage:
4951
return ChatMessage(content=x or "", role="assistant")
5052

5153
def can_normalize(self, message: Any) -> bool:
52-
return isinstance(message, str) or message is None
54+
return isinstance(message, (str, HTML)) or message is None
5355

5456
def can_normalize_chunk(self, chunk: Any) -> bool:
55-
return isinstance(chunk, str) or chunk is None
57+
return isinstance(chunk, (str, HTML)) or chunk is None
5658

5759

5860
class DictNormalizer(BaseMessageNormalizer):

shiny/ui/_chat_types.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import Literal, TypedDict
44

5+
from htmltools import HTML
6+
57
Role = Literal["assistant", "user", "system"]
68

79

@@ -14,7 +16,7 @@ class ChatMessage(TypedDict):
1416

1517
# A message once transformed have been applied
1618
class TransformedMessage(TypedDict):
17-
content_client: str
19+
content_client: str | HTML
1820
content_server: str
1921
role: Role
2022
transform_key: Literal["content_client", "content_server"]

0 commit comments

Comments
 (0)