ITADN

[Bug]: `shiny.module` re-exports are stripped from generated type stubs, so Pyright rejects them in Shinylive

#2425Openjat255 创建于 12 天前
bugPriority: Lowneeds-triage
J
jat255commented
## Summary In the Shinylive editor, these imports are flagged as errors even though the code runs correctly: ```python from shiny.module import resolve_id # "resolve_id" is unknown import symbol from shiny.module import current_namespace # "current_namespace" is unknown import symbol from shiny.module import ResolvedId # "ResolvedId" is unknown import symbol ``` This came up while working on some doc changes in https://github.com/posit-dev/py-shiny-site/pull/436. For example: <img width="1368" height="1110" alt="Image" src="https://github.com/user-attachments/assets/2b232f39-aa95-4cf5-ad6c-26a36486997b" /> `shiny/module.py` re-exports these names from `shiny._namespaces` but never references them in its own body. `pyright --createstub` drops imports that aren't referenced, so the generated `shiny/module.pyi` still advertises the names in `__all__` while never importing them. Shinylive bundles those stubs for its in-browser Pyright (via `scripts/create_typeshed.py` in the shinylive repo), so the documented import surfaces a spurious error. This is editor-only. Runtime behavior is correct. ## Evidence The stub actually shipped in the shinylive 0.10.14 assets (`shinylive/pyright/typeshed.en.json` → `/src/typings/shiny/module.pyi`): ```python from typing import Callable, TYPE_CHECKING, TypeVar from ._docstring import add_example from ._namespaces import Id # ← only Id survived from ._typing_extensions import Concatenate, ParamSpec from .session import Inputs, Outputs, Session __all__ = ("current_namespace", "resolve_id", "ui", "server", "ResolvedId") ``` `shiny/_namespaces.pyi` in the same bundle defines `resolve_id`, `current_namespace`, and `ResolvedId` correctly — only the re-export hop through `shiny/module.py` is lost. Status of every name in `shiny.module.__all__`: | Name | Stub status | Why | |---|---|---| | `current_namespace` | broken | re-export only | | `resolve_id` | broken | re-export only | | `ResolvedId` | broken | re-export only | | `ui` | OK | defined in `module.py` | | `server` | OK | defined in `module.py` | `Id` survives only because of the existing workaround at `shiny/module.py:23-24`: ```python # Ensure that Id type is not stripped out from .pyi file when generating type stubs _: Id # type: ignore ``` which suggests this was hit once already and patched for a single symbol. ## Reproduction Any Shinylive example using the documented import reproduces it. Minimal standalone repro of the stub generation, with a package mirroring `module.py`'s re-export pattern: ```python # mypkg/module.py __all__ = ("current_namespace", "resolve_id", "ui", "server", "ResolvedId") from ._namespaces import Id, ResolvedId, current_namespace, namespace_context, resolve_id _: Id # type: ignore def ui(fn): ... ``` ```bash pyright --createstub mypkg && cat typings/mypkg/module.pyi ``` Output keeps only `from ._namespaces import Id`; the other three names are dropped while `__all__` still lists them. ## Options tested (pyright 1.1.411) | Approach | Result | |---|---| | Current form (plain import + `__all__`) | Reproduces the bug; only `Id` retained | | PEP 484 redundant alias — `import resolve_id as resolve_id` | **Regresses** — entire import line dropped, `Id` lost too | | Annotation probe — `_r: ResolvedId` | Retains *type* symbols | | Module-level assignment — `_keep = (resolve_id, ...)` | Does **not** retain functions (RHS elided to `...`) | | Defining thin wrappers in `module.py` | Emits real `def resolve_id(...)` / `def current_namespace(...)` | Worth calling out: the redundant-alias form is the idiomatic PEP 484 re-export fix and is the natural first thing to reach for, but it makes this *worse* under `--createstub`. ## Suggested fix Annotations retain type symbols; they can't retain functions, and assignments don't either. So the two categories need different handling in `shiny/module.py`: 1. **`ResolvedId`** — extend the existing annotation workaround: ```python _r: ResolvedId # type: ignore ``` 2. **`resolve_id` / `current_namespace`** — define thin delegating wrappers so `createstub` emits real function signatures: ```python def resolve_id(id: Id) -> ResolvedId: """Resolve an ID, possibly with a module namespace.""" return _namespaces.resolve_id(id) ``` Both were verified to produce a correct stub. A wrapper adds one call of indirection; if that's unwanted, the alternative is fixing it on the shinylive side (post-process the generated stubs, or don't use `--createstub` for `shiny`) — happy to move this issue there if that's the better venue. ## Impact `from shiny.module import resolve_id` is the documented public API for making custom components and hand-written HTML ids module-aware (see the "Custom JavaScript component" docs page and the modules article). Anyone following those docs in Shinylive gets a red squiggle on correct code. ## Environment - shiny `1.7.1.dev6` (py-shiny `f85a444`); the pattern is unchanged in current `main` - shinylive JS assets `0.10.14` (via shinylive PyPI `0.8.11`) - pyright `1.1.411` - Runtime unaffected — apps using these imports run correctly
0 条评论