Skip to content

Commit 5011e98

Browse files
committed
Add Python language support (#6508)
1 parent 1890ce1 commit 5011e98

173 files changed

Lines changed: 40899 additions & 13 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""
2+
Exploring compile-time metaprogramming options for typed replace() methods.
3+
4+
Goal: Keep LST source clean and readable, but provide IDE autocomplete for replace().
5+
"""
6+
7+
from dataclasses import dataclass, fields, replace
8+
from typing import Optional, Self, dataclass_transform, TypeVar, get_type_hints
9+
from uuid import UUID
10+
import functools
11+
12+
13+
# =============================================================================
14+
# OPTION 1: Custom decorator that adds a typed replace() method at import time
15+
# =============================================================================
16+
# The decorator inspects fields and dynamically creates a replace() method
17+
# with the correct signature. BUT - IDEs won't see the generated signature.
18+
19+
def add_replace_method(cls):
20+
"""Decorator that adds a replace() method to a frozen dataclass."""
21+
def _replace(self, **changes):
22+
return replace(self, **changes)
23+
cls.replace = _replace
24+
return cls
25+
26+
27+
# =============================================================================
28+
# OPTION 2: .pyi stub files (RECOMMENDED)
29+
# =============================================================================
30+
# Generate .pyi stub files that declare the typed replace() signature.
31+
# The runtime code uses plain replace(), but IDEs read the .pyi for types.
32+
#
33+
# tree.py (runtime - clean and simple):
34+
#
35+
# @dataclass(frozen=True, slots=True)
36+
# class Await(Py, Expression):
37+
# id: UUID
38+
# prefix: Space
39+
# markers: Markers
40+
# expression: Expression
41+
# type: Optional[JavaType] = None
42+
#
43+
# tree.pyi (stub file - provides IDE autocomplete):
44+
#
45+
# class Await(Py, Expression):
46+
# id: UUID
47+
# prefix: Space
48+
# markers: Markers
49+
# expression: Expression
50+
# type: Optional[JavaType]
51+
#
52+
# def replace(
53+
# self,
54+
# *,
55+
# id: UUID = ...,
56+
# prefix: Space = ...,
57+
# markers: Markers = ...,
58+
# expression: Expression = ...,
59+
# type: Optional[JavaType] = ...,
60+
# ) -> Self: ...
61+
#
62+
# The stub generator script reads tree.py and generates tree.pyi automatically.
63+
64+
65+
# =============================================================================
66+
# OPTION 3: __init_subclass__ with dataclass_transform
67+
# =============================================================================
68+
# Use PEP 681 dataclass_transform to tell type checkers about our pattern.
69+
# This is how attrs, pydantic, and other libraries get IDE support.
70+
71+
@dataclass_transform(frozen_default=True)
72+
class LstBase:
73+
"""Base class for all LST types that adds replace() method."""
74+
75+
def __init_subclass__(cls, **kwargs):
76+
super().__init_subclass__(**kwargs)
77+
# Apply dataclass decorator
78+
dataclass(frozen=True, slots=True, eq=False)(cls)
79+
80+
def replace(self, **changes) -> Self:
81+
"""Create a copy with the specified fields replaced."""
82+
return replace(self, **changes)
83+
84+
85+
# Usage would be:
86+
# class Await(LstBase, Py, Expression):
87+
# id: UUID
88+
# prefix: Space
89+
# ...
90+
#
91+
# BUT: dataclass_transform alone doesn't give us typed kwargs on replace()
92+
93+
94+
# =============================================================================
95+
# OPTION 4: Protocol + stub file hybrid
96+
# =============================================================================
97+
# Define a Protocol that declares replace() exists, generate stubs for specifics
98+
99+
from typing import Protocol
100+
101+
class Replaceable(Protocol):
102+
def replace(self, **kwargs) -> Self: ...
103+
104+
105+
# =============================================================================
106+
# OPTION 5: Use typing.overload in stub files
107+
# =============================================================================
108+
# In .pyi files, we can use @overload to provide multiple typed signatures.
109+
# This is the most IDE-friendly approach.
110+
#
111+
# tree.pyi:
112+
#
113+
# from typing import overload
114+
#
115+
# class Await(Py, Expression):
116+
# @overload
117+
# def replace(self) -> Await: ...
118+
# @overload
119+
# def replace(self, *, id: UUID) -> Await: ...
120+
# @overload
121+
# def replace(self, *, prefix: Space) -> Await: ...
122+
# @overload
123+
# def replace(self, *, id: UUID, prefix: Space) -> Await: ...
124+
# # ... combinatorial explosion, not practical
125+
#
126+
# Better: Just use keyword-only args with defaults in stub:
127+
#
128+
# def replace(
129+
# self,
130+
# *,
131+
# id: UUID = ...,
132+
# prefix: Space = ...,
133+
# ) -> Await: ...
134+
135+
136+
# =============================================================================
137+
# STUB GENERATOR SCRIPT (for Option 2)
138+
# =============================================================================
139+
import ast
140+
import inspect
141+
from pathlib import Path
142+
143+
144+
def generate_stubs_for_module(source_path: Path) -> str:
145+
"""Generate .pyi stub content for a dataclass module with replace() methods."""
146+
with open(source_path) as f:
147+
source = f.read()
148+
149+
tree = ast.parse(source)
150+
stub_lines = []
151+
152+
# Add imports
153+
stub_lines.append("from typing import Optional, Self")
154+
stub_lines.append("from uuid import UUID")
155+
stub_lines.append("# ... other imports")
156+
stub_lines.append("")
157+
158+
for node in ast.walk(tree):
159+
if isinstance(node, ast.ClassDef):
160+
# Check if it's a dataclass (has @dataclass decorator)
161+
is_dataclass = any(
162+
(isinstance(d, ast.Name) and d.id == 'dataclass') or
163+
(isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == 'dataclass')
164+
for d in node.decorator_list
165+
)
166+
167+
if is_dataclass:
168+
# Extract fields (annotated assignments)
169+
fields_info = []
170+
for item in node.body:
171+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
172+
field_name = item.target.id
173+
# Get the annotation as string
174+
field_type = ast.unparse(item.annotation)
175+
has_default = item.value is not None
176+
fields_info.append((field_name, field_type, has_default))
177+
178+
# Generate stub class
179+
bases = ", ".join(ast.unparse(b) for b in node.bases)
180+
stub_lines.append(f"class {node.name}({bases}):")
181+
182+
# Add field declarations
183+
for name, type_str, _ in fields_info:
184+
stub_lines.append(f" {name}: {type_str}")
185+
186+
stub_lines.append("")
187+
188+
# Add typed replace() method
189+
stub_lines.append(" def replace(")
190+
stub_lines.append(" self,")
191+
stub_lines.append(" *,")
192+
for name, type_str, _ in fields_info:
193+
stub_lines.append(f" {name}: {type_str} = ...,")
194+
stub_lines.append(" ) -> Self: ...")
195+
stub_lines.append("")
196+
197+
return "\n".join(stub_lines)
198+
199+
200+
# =============================================================================
201+
# DEMONSTRATION
202+
# =============================================================================
203+
204+
if __name__ == "__main__":
205+
# Example LST class (clean source)
206+
@dataclass(frozen=True, slots=True, eq=False)
207+
class Space:
208+
whitespace: str = ""
209+
comments: list = None
210+
211+
@dataclass(frozen=True, slots=True, eq=False)
212+
class Markers:
213+
id: UUID
214+
markers: list = None
215+
216+
@dataclass(frozen=True, slots=True, eq=False)
217+
class Await:
218+
id: UUID
219+
prefix: Space
220+
markers: Markers
221+
expression: object # Would be Expression
222+
type: Optional[str] = None
223+
224+
# Simple replace method - runtime works, but IDE doesn't know the kwargs
225+
def replace(self, **changes) -> "Await":
226+
return replace(self, **changes)
227+
228+
# Usage
229+
a = Await(
230+
id=UUID("12345678-1234-5678-1234-567812345678"),
231+
prefix=Space(),
232+
markers=Markers(id=UUID("12345678-1234-5678-1234-567812345678")),
233+
expression=None
234+
)
235+
236+
# This works at runtime, but IDE won't autocomplete 'expression'
237+
a2 = a.replace(expression="new_expr")
238+
print(f"Original: {a}")
239+
print(f"Replaced: {a2}")
240+
241+
# Generate stub for this file
242+
print("\n--- Generated Stub ---")
243+
print(generate_stubs_for_module(Path(__file__)))

.context/notes.md

Whitespace-only changes.

0 commit comments

Comments
 (0)