Skip to content

Commit 7e9133e

Browse files
knutwannhedenbmuschkotimtebeeksteve-aom-elliottsambsnyd
authored
JavaScript: More key APIs are now non-async (#6504)
* UUID generation fallback for Node version pre-14.17.0 (#6495) * UUID generation fallback for Node version pre-14.17.0 * Apply suggestions from code review * Use module-load-time selection for performance * Polish --------- Co-authored-by: Tim te Beek <tim@moderne.io> Co-authored-by: Knut Wannheden <knut@moderne.io> * Changing `AddDependencyVisitor` so it is able to broaden the scope of a direct dependency if needed (#6434) * Allowing `AddDependency` to broaden the scope of existing dependency, provided you're requesting an equal or higher version number. If requesting same scope but higher version number, the version number will be upgraded. * Dropping `import` scope validity in favour of `compile`, given `import` is only applicable for dependencies in `dependencyManagement` Co-authored-by: Tim te Beek <tim@moderne.io> --------- Co-authored-by: Tim te Beek <tim@moderne.io> * Add style parameter to OrderImports. (#6496) * Drop Lombok hint from RemoveUnusedImports class (#6500) * Drop Lombok hint from RemoveUnusedImports class Since we've supported Lombok for quite some time now. * Also update recipes.csv * In yaml `CopyValue`, invoke `UnfoldProperties` after `MergeYaml` (#6499) * unit test with poc for UnfoldProperties * use imperative CopyValue in unit test * Invoke UnfoldProperties after MergeYaml * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Slight polish --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <tim@moderne.io> * Allow adding comments to Maven plugins too Fixes #6502 * JavaScript: Make visitor base clases sync and add async alternative * Polish parser APIs * More async cleanups * JavaScript: Notify about parsed source files from `parseProject()` * Polish object property shorthand * Fix Prettier integration when calling it using sync API * Altering check for `AddDependency` to compare versions differently when direct vs indirect dependency. (#6505) * If transitive, actually just allow regardless of version, as per old behaviour. * Changing the parsing of the `relativePath` for `parent` in the `MavenPomDownloader` to prevent the situation where `/` was producing `/\pom.xml` on Windows rather than `\pom.xml` (#6506) * Claude/catalogue async apis u67 hy (#6507) * Convert package-manager and related APIs from async to sync - Convert runInstallInTempDir and runWorkspaceInstallInTempDir to use sync fs APIs (fs.mkdtempSync, fs.writeFileSync, fs.readFileSync, fs.rmSync) - Convert runInstallIfNeeded callback from async to sync - Convert updateNodeResolutionMarker to sync (was marked async with no awaits) - Convert createLockFileEditor to use sync JsonVisitor and TreeVisitor - Update add-dependency, upgrade-dependency-version, and upgrade-transitive-dependency-version recipes to use sync visitors - Convert Result.diff() to sync (createTwoFilesPatch is already sync) The package manager operations were unnecessarily async since the underlying process spawning (spawnSync) was already synchronous. Only the file I/O was async, which is negligible compared to the npm/yarn/pnpm install time. * Remove unnecessary async from IsSourceFile.preVisit() * Add async property to TreeVisitor and AsyncTreeVisitor Add a readonly `async` property to both visitor base classes: - TreeVisitor: async = false - AsyncTreeVisitor: async = true This allows runtime discrimination of sync vs async visitors via the RecipeVisitor union type. Useful for sync-to-sync recipe composition where a sync visitor wants to call another sync visitor without async overhead. * Lift async I/O out of visitors into editorWithData() Refactor package-manager recipes to do async I/O (npm install) in editorWithData() before returning the visitor, keeping visitors pure. Changes: - Add async runInstallInTempDirAsync() using spawn() and fs.promises - Add async runWorkspaceInstallInTempDirAsync() for workspace support - Refactor AddDependency, UpgradeDependencyVersion, and UpgradeTransitiveDependencyVersion to run package manager installs in editorWithData() before returning visitors - Remove unused runInstallIfNeeded() helper function - Visitors are now pure tree transformations with no I/O This provides a cleaner separation of concerns: - Async I/O happens in the recipe's async editor() method - Visitors are pure, synchronous tree transformations using pre-computed data * More sync visitors * Simplify receivers by extending visitors --------- Co-authored-by: Claude <noreply@anthropic.com> * Bugfix * Add back runInstallIfNeeded() * Also parse `jrxml` as XML Fixes #6512 * Update description for onlyIfUsing option Clarified description for 'onlyIfUsing' option to specify its importance in multi-module projects. Fixes #5795 * Retain nested class imports in `ChangePackage` (#6515) Fixes #6513 * Add Python language support (#6508) * Compact array RpcObjectData only for JS * Less async test code --------- Co-authored-by: Benjamin Muschko <benjamin.muschko@gmail.com> Co-authored-by: Tim te Beek <tim@moderne.io> Co-authored-by: Steve Elliott <steve@moderne.io> Co-authored-by: Sam Snyder <sam@moderne.io> Co-authored-by: David Grieve <david@moderne.io> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jonathan Schnéider <jkschneider@gmail.com>
1 parent 4b01270 commit 7e9133e

361 files changed

Lines changed: 50481 additions & 5048 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)