Skip to content

Commit 06c9e75

Browse files
committed
added some example scripts
1 parent 2d94015 commit 06c9e75

2 files changed

Lines changed: 634 additions & 0 deletions

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# /// script
2+
# requires-python = ">=3.12"
3+
# dependencies = [
4+
# "lsp-types>=0.12.2",
5+
# "rich>=14.2.0",
6+
# ]
7+
# ///
8+
"""
9+
Minimal hover-only repro for cross-package circular types between external
10+
packages pkgb and pkgc added via search_path/extraPaths.
11+
12+
Layout (both pkgb and pkgc live outside the workspace root and are added via
13+
search paths):
14+
- pkgb/b.py: defines PkgB and holds a PkgC instance
15+
- pkgc/c.py: defines PkgC and holds a PkgB instance
16+
17+
The active document instantiates PkgB and PkgC, binds a few variables, and logs
18+
hover contents to check whether types flow across the boundary.
19+
20+
This script runs for both Pyrefly and Pyright backends automatically.
21+
22+
Run with:
23+
uv run python examples/pyrefly_circular_imports.py
24+
"""
25+
26+
from __future__ import annotations
27+
28+
import asyncio
29+
from pathlib import Path
30+
from tempfile import TemporaryDirectory
31+
from textwrap import dedent
32+
33+
import lsp_types
34+
from rich.console import Console
35+
from rich.markdown import Markdown
36+
from lsp_types.pyrefly.backend import PyreflyBackend
37+
from lsp_types.pyright.backend import PyrightBackend
38+
39+
console = Console()
40+
41+
# Simple structured logging helpers
42+
def log_step(title: str) -> None:
43+
console.print(f"\n=== {title} ===")
44+
45+
46+
def log_result(label: str, value) -> None:
47+
if label.endswith(".md"):
48+
console.print(f"{label}:\n")
49+
console.print(Markdown(value))
50+
else:
51+
console.print(f"{label}: {value}")
52+
53+
PKGB_B = dedent(
54+
"""\
55+
from __future__ import annotations
56+
57+
from pkgc.c import PkgC
58+
59+
class PkgB:
60+
def __init__(self) -> None:
61+
self.c: PkgC = PkgC(self)
62+
63+
"""
64+
)
65+
66+
PKGC_C = dedent(
67+
"""\
68+
from __future__ import annotations
69+
70+
from typing import TYPE_CHECKING
71+
72+
if TYPE_CHECKING:
73+
from pkgb.b import PkgB
74+
75+
76+
class PkgC:
77+
def __init__(self, b: PkgB) -> None:
78+
self.b: PkgB = b
79+
80+
"""
81+
)
82+
83+
ACTIVE_CODE = dedent(
84+
"""\
85+
from pkgb.b import PkgB
86+
from pkgc.c import PkgC
87+
88+
b = PkgB()
89+
c = PkgC(b)
90+
91+
c_from_b = b.c
92+
b_from_c = c.b
93+
"""
94+
)
95+
96+
97+
def prepare_workspace(pkgb_dir: Path, pkgc_dir: Path) -> None:
98+
"""Create pkgb and pkgc packages in external paths used via search_path/extraPaths."""
99+
pkgb = pkgb_dir / "pkgb"
100+
pkgc = pkgc_dir / "pkgc"
101+
102+
pkgb.mkdir(parents=True, exist_ok=True)
103+
pkgc.mkdir(parents=True, exist_ok=True)
104+
105+
pkgb.joinpath("__init__.py").write_text("")
106+
pkgc.joinpath("__init__.py").write_text("")
107+
pkgb.joinpath("b.py").write_text(PKGB_B)
108+
pkgc.joinpath("c.py").write_text(PKGC_C)
109+
110+
111+
async def run_backend(backend_name: str, backend, options_key: str, root: Path, external_pkgb: Path, external_pkgc: Path) -> None:
112+
"""Run the circular imports test for a specific backend."""
113+
console.rule(f"[bold cyan]{backend_name.upper()} Backend[/bold cyan]", characters="=")
114+
115+
session = await lsp_types.Session.create(
116+
backend,
117+
base_path=root,
118+
initial_code=ACTIVE_CODE,
119+
options={options_key: [str(external_pkgb), str(external_pkgc)]},
120+
)
121+
122+
try:
123+
log_step("Diagnostics for active document")
124+
diagnostics = await session.get_diagnostics()
125+
log_result("Diagnostics count", len(diagnostics))
126+
if diagnostics:
127+
log_result("Diagnostics", diagnostics)
128+
129+
hover_targets = {
130+
"b (PkgB)": lsp_types.Position(line=3, character=0),
131+
"c (PkgC)": lsp_types.Position(line=4, character=0),
132+
"c_from_b (PkgC)": lsp_types.Position(line=6, character=0),
133+
"b_from_c (PkgB)": lsp_types.Position(line=7, character=0),
134+
}
135+
136+
for label, position in hover_targets.items():
137+
log_step(f"Hover: {label}")
138+
hover = await session.get_hover_info(position)
139+
if hover:
140+
match hover["contents"]:
141+
case {"kind": "markdown", "value": value}:
142+
log_result("Hover contents.md", value)
143+
case contents:
144+
log_result("Hover contents", contents)
145+
finally:
146+
await session.shutdown()
147+
148+
149+
async def main() -> None:
150+
with TemporaryDirectory(prefix="circular-root-") as tmp_root, TemporaryDirectory(
151+
prefix="circular-pkgb-"
152+
) as tmp_pkgb, TemporaryDirectory(prefix="circular-pkgc-") as tmp_pkgc:
153+
root = Path(tmp_root)
154+
external_pkgb = Path(tmp_pkgb)
155+
external_pkgc = Path(tmp_pkgc)
156+
prepare_workspace(external_pkgb, external_pkgc)
157+
158+
console.print()
159+
console.print("[bold]Circular Imports Test - Running for both backends[/bold]")
160+
console.print()
161+
162+
# Run Pyrefly backend
163+
await run_backend("pyrefly", PyreflyBackend(), "search_path", root, external_pkgb, external_pkgc)
164+
165+
console.print("\n")
166+
167+
# Run Pyright backend
168+
await run_backend("pyright", PyrightBackend(), "extraPaths", root, external_pkgb, external_pkgc)
169+
170+
171+
if __name__ == "__main__":
172+
asyncio.run(main())

0 commit comments

Comments
 (0)