[Bug]: `shiny.module` re-exports are stripped from generated type stubs, so Pyright rejects them in Shinylive
bugPriority: Lowneeds-triage
## 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 条评论