Skip to content

Commit 1412f20

Browse files
gadenbuiecpsievert
andauthored
fix(pkg-py): Avoid circular import error when shinychat is loaded before shiny (#142)
* fix(pkg-py): Avoid circular import issues with shiny * chore(pkg-py): Bring MISSING to shinychat * chore: Directly import from shiny.reactive * chore: fix type annotation formatting Co-authored-by: Carson Sievert <[email protected]>
1 parent b80b8ba commit 1412f20

File tree

4 files changed

+78
-40
lines changed

4 files changed

+78
-40
lines changed

pkg-py/src/shinychat/_chat.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,6 @@
2121
from weakref import WeakValueDictionary
2222

2323
from htmltools import HTML, Tag, TagAttrValue, TagChild, TagList, css
24-
from shiny import reactive
25-
from shiny._deprecated import warn_deprecated
26-
from shiny.bookmark import BookmarkState, RestoreState
27-
from shiny.bookmark._types import BookmarkStore
28-
from shiny.module import ResolvedId, resolve_id
29-
from shiny.reactive._reactives import Effect_
30-
from shiny.session import (
31-
get_current_session,
32-
require_active_session,
33-
session_context,
34-
)
35-
from shiny.types import MISSING, MISSING_TYPE, Jsonifiable, NotifyException
36-
from shiny.ui.css import CssUnit, as_css_unit
37-
from shiny.ui.fill import as_fill_item, as_fillable_container
3824

3925
from . import _utils
4026
from ._chat_bookmark import (
@@ -69,9 +55,16 @@
6955
)
7056
from ._html_deps_py_shiny import chat_deps
7157
from ._typing_extensions import TypeGuard
58+
from ._utils_types import MISSING, MISSING_TYPE
7259

7360
if TYPE_CHECKING:
7461
import chatlas
62+
from shiny.bookmark import BookmarkState, RestoreState
63+
from shiny.bookmark._types import BookmarkStore
64+
from shiny.reactive import ExtendedTask
65+
from shiny.reactive._reactives import Effect_
66+
from shiny.types import Jsonifiable
67+
from shiny.ui.css import CssUnit
7568

7669
else:
7770
chatlas = object
@@ -200,6 +193,10 @@ def __init__(
200193
on_error: Literal["auto", "actual", "sanitize", "unhandled"] = "auto",
201194
tokenizer: TokenEncoding | None = None,
202195
):
196+
from shiny._deprecated import warn_deprecated
197+
from shiny.module import ResolvedId, resolve_id
198+
from shiny.session import require_active_session
199+
203200
if not isinstance(id, str):
204201
raise TypeError("`id` must be a string.")
205202

@@ -248,10 +245,13 @@ def __init__(
248245
self._suspend_input_handler: bool = False
249246

250247
# Keep track of effects so we can destroy them when the chat is destroyed
251-
self._effects: list[Effect_] = []
248+
self._effects: list["Effect_"] = []
252249
self._cancel_bookmarking_callbacks: CancelCallback | None = None
253250

254251
# Initialize chat state and user input effect
252+
from shiny import reactive
253+
from shiny.session import session_context
254+
255255
with session_context(self._session):
256256
# Initialize message state
257257
self._messages: reactive.Value[tuple[TransformedMessage, ...]] = (
@@ -354,6 +354,8 @@ def on_user_submit(
354354
"""
355355

356356
def create_effect(fn: UserSubmitFunction):
357+
from shiny import reactive
358+
357359
fn_params = inspect.signature(fn).parameters
358360

359361
@reactive.effect
@@ -397,6 +399,8 @@ async def _raise_exception(
397399
self,
398400
e: BaseException,
399401
) -> None:
402+
from shiny.types import NotifyException
403+
400404
if self.on_error == "unhandled":
401405
raise e
402406
else:
@@ -468,7 +472,7 @@ def messages(
468472
def messages(
469473
self,
470474
*,
471-
format: MISSING_TYPE | ProviderMessageFormat = MISSING,
475+
format: "MISSING_TYPE | ProviderMessageFormat" = MISSING,
472476
token_limits: tuple[int, int] | None = None,
473477
transform_user: Literal["all", "last", "none"] = "all",
474478
transform_assistant: bool = False,
@@ -504,6 +508,7 @@ def messages(
504508
tuple[ChatMessage, ...]
505509
A tuple of chat messages.
506510
"""
511+
from shiny._deprecated import warn_deprecated
507512

508513
if not isinstance(format, MISSING_TYPE):
509514
warn_deprecated(
@@ -866,6 +871,7 @@ async def append_message_stream(
866871
of the task can be called in a reactive context to get the final state of the
867872
stream.
868873
"""
874+
from shiny import reactive
869875

870876
message = _utils.wrap_async_iterable(message)
871877

@@ -890,7 +896,7 @@ async def _handle_error():
890896
return _stream_task
891897

892898
@property
893-
def latest_message_stream(self) -> reactive.ExtendedTask[[], str]:
899+
def latest_message_stream(self) -> ExtendedTask[[], str]:
894900
"""
895901
React to changes in the latest message stream.
896902
@@ -1022,6 +1028,7 @@ def transform_user_input(
10221028
"""
10231029
Deprecated. User input transformation features will be removed in a future version.
10241030
"""
1031+
from shiny._deprecated import warn_deprecated
10251032

10261033
warn_deprecated(
10271034
"The `.transform_user_input` decorator is deprecated. "
@@ -1054,6 +1061,7 @@ def transform_assistant_response(
10541061
"""
10551062
Deprecated. Assistant response transformation features will be removed in a future version.
10561063
"""
1064+
from shiny._deprecated import warn_deprecated
10571065

10581066
warn_deprecated(
10591067
"The `.transform_assistant_response` decorator is deprecated. "
@@ -1145,6 +1153,8 @@ def _store_message(
11451153
message: TransformedMessage | ChatMessage,
11461154
index: int | None = None,
11471155
) -> None:
1156+
from shiny import reactive
1157+
11481158
if not isinstance(message, TransformedMessage):
11491159
message = TransformedMessage.from_chat_message(message)
11501160

@@ -1277,6 +1287,7 @@ def user_input(self, transform: bool = False) -> str | None:
12771287
2. Maintaining message state separately from `.messages()`.
12781288
12791289
"""
1290+
from shiny._deprecated import warn_deprecated
12801291

12811292
if transform:
12821293
warn_deprecated(
@@ -1345,6 +1356,7 @@ def set_user_message(self, value: str):
13451356
"""
13461357
Deprecated. Use `update_user_input(value=value)` instead.
13471358
"""
1359+
from shiny._deprecated import warn_deprecated
13481360

13491361
warn_deprecated(
13501362
"set_user_message() is deprecated. Use update_user_input(value=value) instead."
@@ -1397,7 +1409,7 @@ async def _send_custom_message(
13971409

13981410
def enable_bookmarking(
13991411
self,
1400-
client: ClientWithState | chatlas.Chat[Any, Any],
1412+
client: "ClientWithState | chatlas.Chat[Any, Any]",
14011413
/,
14021414
*,
14031415
bookmark_on: Optional[Literal["response"]] = "response",
@@ -1435,6 +1447,8 @@ def enable_bookmarking(
14351447
:
14361448
A callback to cancel the bookmarking hooks.
14371449
"""
1450+
from shiny import reactive
1451+
from shiny.session import get_current_session
14381452

14391453
session = get_current_session()
14401454
if session is None or session.is_stub_session():
@@ -1589,8 +1603,8 @@ def ui(
15891603
Iterable[str | TagChild | ChatMessageDict | ChatMessage | Any]
15901604
] = None,
15911605
placeholder: str = "Enter a message...",
1592-
width: CssUnit = "min(680px, 100%)",
1593-
height: CssUnit = "auto",
1606+
width: "CssUnit" = "min(680px, 100%)",
1607+
height: "CssUnit" = "auto",
15941608
fill: bool = True,
15951609
icon_assistant: HTML | Tag | TagList | None = None,
15961610
**kwargs: TagAttrValue,
@@ -1635,10 +1649,10 @@ def ui(
16351649

16361650
def enable_bookmarking(
16371651
self,
1638-
client: ClientWithState | chatlas.Chat[Any, Any],
1652+
client: "ClientWithState | chatlas.Chat[Any, Any]",
16391653
/,
16401654
*,
1641-
bookmark_store: Optional[BookmarkStore] = None,
1655+
bookmark_store: "Optional[BookmarkStore]" = None,
16421656
bookmark_on: Optional[Literal["response"]] = "response",
16431657
) -> CancelCallback:
16441658
"""
@@ -1693,8 +1707,8 @@ def chat_ui(
16931707
Iterable[str | TagChild | ChatMessageDict | ChatMessage | Any]
16941708
] = None,
16951709
placeholder: str = "Enter a message...",
1696-
width: CssUnit = "min(680px, 100%)",
1697-
height: CssUnit = "auto",
1710+
width: "CssUnit" = "min(680px, 100%)",
1711+
height: "CssUnit" = "auto",
16981712
fill: bool = True,
16991713
icon_assistant: Optional[HTML | Tag | TagList] = None,
17001714
**kwargs: TagAttrValue,
@@ -1742,6 +1756,9 @@ def chat_ui(
17421756
kwargs
17431757
Additional attributes for the chat container element.
17441758
"""
1759+
from shiny.module import resolve_id
1760+
from shiny.ui.css import as_css_unit
1761+
from shiny.ui.fill import as_fill_item, as_fillable_container
17451762

17461763
id = resolve_id(id)
17471764

pkg-py/src/shinychat/_markdown_stream.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
from contextlib import asynccontextmanager
2-
from typing import AsyncIterable, Iterable, Literal, Union
2+
from typing import TYPE_CHECKING, AsyncIterable, Iterable, Literal, Union
33

44
from htmltools import RenderedHTML, Tag, TagChild, TagList, css
5-
from shiny import _utils, reactive
6-
from shiny._deprecated import warn_deprecated
7-
from shiny.module import resolve_id
8-
from shiny.session import require_active_session, session_context
9-
from shiny.session._utils import RenderedDeps
10-
from shiny.types import NotifyException
11-
from shiny.ui.css import CssUnit, as_css_unit
125

136
from ._html_deps_py_shiny import markdown_stream_dependency
147
from ._typing_extensions import TypedDict
158

9+
if TYPE_CHECKING:
10+
from shiny import reactive
11+
from shiny.ui.css import CssUnit
12+
1613
__all__ = (
1714
"output_markdown_stream",
1815
"MarkdownStream",
@@ -72,6 +69,9 @@ def __init__(
7269
*,
7370
on_error: Literal["auto", "actual", "sanitize", "unhandled"] = "auto",
7471
):
72+
from shiny.module import resolve_id
73+
from shiny.session import require_active_session
74+
7575
self.id = resolve_id(id)
7676
# TODO: remove the `None` when this PR lands:
7777
# https://github.com/posit-dev/py-shiny/pull/793/files
@@ -86,15 +86,18 @@ def __init__(
8686

8787
self.on_error = on_error
8888

89+
from shiny import reactive
90+
from shiny.session import session_context
91+
8992
with session_context(self._session):
9093

9194
@reactive.extended_task
9295
async def _mock_task() -> str:
9396
return ""
9497

95-
self._latest_stream: reactive.Value[
96-
reactive.ExtendedTask[[], str]
97-
] = reactive.Value(_mock_task)
98+
self._latest_stream: "reactive.Value[reactive.ExtendedTask[[], str]]" = reactive.Value(
99+
_mock_task
100+
)
98101

99102
async def stream(
100103
self,
@@ -127,6 +130,8 @@ async def stream(
127130
of the task can be called in a reactive context to get the final state of the
128131
stream.
129132
"""
133+
from shiny import _utils, reactive
134+
from shiny.session._utils import RenderedDeps
130135

131136
content = _utils.wrap_async_iterable(content)
132137

@@ -205,6 +210,8 @@ def get_latest_stream_result(self) -> Union[str, None]:
205210
206211
Deprecated. Use `latest_stream.result()` instead.
207212
"""
213+
from shiny._deprecated import warn_deprecated
214+
208215
warn_deprecated(
209216
"The `.get_latest_stream_result()` method is deprecated and will be removed "
210217
"in a future release. Use `.latest_stream.result()` instead. "
@@ -256,6 +263,8 @@ async def _send_custom_message(
256263
)
257264

258265
async def _raise_exception(self, e: BaseException):
266+
from shiny.types import NotifyException
267+
259268
if self.on_error == "unhandled":
260269
raise e
261270
else:
@@ -271,8 +280,8 @@ def ui(
271280
content: TagChild = "",
272281
content_type: StreamingContentType = "markdown",
273282
auto_scroll: bool = True,
274-
width: CssUnit = "min(680px, 100%)",
275-
height: CssUnit = "auto",
283+
width: "CssUnit" = "min(680px, 100%)",
284+
height: "CssUnit" = "auto",
276285
) -> Tag:
277286
"""
278287
Create a UI element for this `MarkdownStream`.
@@ -319,8 +328,8 @@ def output_markdown_stream(
319328
content: TagChild = "",
320329
content_type: StreamingContentType = "markdown",
321330
auto_scroll: bool = True,
322-
width: CssUnit = "min(680px, 100%)",
323-
height: CssUnit = "auto",
331+
width: "CssUnit" = "min(680px, 100%)",
332+
height: "CssUnit" = "auto",
324333
) -> Tag:
325334
"""
326335
Create a UI element for a :class:`~shiny.ui.MarkdownStream`.
@@ -352,6 +361,8 @@ def output_markdown_stream(
352361
height
353362
The height of the UI element.
354363
"""
364+
from shiny.module import resolve_id
365+
from shiny.ui.css import as_css_unit
355366

356367
# `content` is most likely a string, so avoid overhead in that case
357368
# (it's also important that we *don't escape HTML* here).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__all__ = ["MISSING", "MISSING_TYPE"]
2+
3+
4+
class MISSING_TYPE:
5+
"""Sentinel value for missing function parameters."""
6+
7+
pass
8+
9+
10+
MISSING: MISSING_TYPE = MISSING_TYPE()

pkg-py/tests/pytest/test_chat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from shiny import Session
99
from shiny.module import ResolvedId
1010
from shiny.session import session_context
11-
from shiny.types import MISSING
1211
from shinychat import Chat
1312
from shinychat._chat_normalize import message_content, message_content_chunk
1413
from shinychat._chat_types import (
@@ -17,6 +16,7 @@
1716
Role,
1817
TransformedMessage,
1918
)
19+
from shinychat._utils_types import MISSING
2020

2121
# ----------------------------------------------------------------------
2222
# Helpers

0 commit comments

Comments
 (0)