Skip to content

Commit a502ff9

Browse files
Fix
1 parent 725bdc6 commit a502ff9

File tree

5 files changed

+95
-128
lines changed

5 files changed

+95
-128
lines changed

packages/python/src/components/DataclassDeclaration.tsx

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { For, Show, childrenArray, computed } from "@alloy-js/core";
2+
import { snakeCase } from "change-case";
23
import { dataclassesModule } from "../builtins/python.js";
34
import { usePythonScope } from "../symbols/scopes.js";
45
import { Atom } from "./Atom.jsx";
@@ -30,13 +31,9 @@ function validateDataclassMemberConflicts(kwargs: DataclassDecoratorKwargs) {
3031
if (!owner) return;
3132

3233
const hasMemberNamed = (name: string): boolean => {
33-
for (const sym of owner.instanceMembers as Iterable<any>) {
34-
if (sym.originalName === name) return true;
35-
}
36-
for (const sym of owner.staticMembers as Iterable<any>) {
37-
if (sym.originalName === name) return true;
38-
}
39-
return false;
34+
const instanceNames: Set<string> | undefined = (owner.instanceMembers as any)?.symbolNames;
35+
const staticNames: Set<string> | undefined = (owner.staticMembers as any)?.symbolNames;
36+
return Boolean(instanceNames?.has(name) || staticNames?.has(name));
4037
};
4138

4239
if (kwargs.order === true) {
@@ -100,6 +97,9 @@ export const dataclassDecoratorKeys = [
10097
"slots",
10198
"weakrefSlot",
10299
] as const;
100+
export const dataclassDecoratorKeySet = new Set<string>(
101+
dataclassDecoratorKeys as unknown as string[],
102+
);
103103
export type DataclassDecoratorKey = (typeof dataclassDecoratorKeys)[number];
104104
export type DataclassDecoratorKwargs = Partial<
105105
Record<DataclassDecoratorKey, boolean>
@@ -110,7 +110,7 @@ export interface DataclassDeclarationProps
110110
DataclassDecoratorKwargs {}
111111

112112
/**
113-
* Renders a Python dataclass. Uses ClassDeclaration component internally.
113+
* Renders a Python dataclass.
114114
*
115115
* Example:
116116
* ```tsx
@@ -138,38 +138,20 @@ export interface DataclassDeclarationProps
138138
* ```
139139
*/
140140
export function DataclassDeclaration(props: DataclassDeclarationProps) {
141-
const decoratorKeys: (keyof DataclassDecoratorKwargs)[] = [
142-
...dataclassDecoratorKeys,
143-
];
144-
const validKeySet = new Set<string>(decoratorKeys as unknown as string[]);
145141
// Collect flags from props in the order they appear (preserves emission order)
146-
const orderedKwargs: Array<[keyof DataclassDecoratorKwargs, any]> = [];
142+
const decoratorEntries: Array<[string, any]> = [];
143+
const kwargs = {} as DataclassDecoratorKwargs;
147144
for (const key of Object.keys(props)) {
148145
// Only include known flags; skip undefined values
149-
if (validKeySet.has(key)) {
146+
if (dataclassDecoratorKeySet.has(key)) {
150147
const value = (props as any)[key];
151-
if (value !== undefined)
152-
orderedKwargs.push([key as keyof DataclassDecoratorKwargs, value]);
148+
if (value !== undefined) {
149+
(kwargs as any)[key] = value;
150+
decoratorEntries.push([snakeCase(key), value]);
151+
}
153152
}
154153
}
155-
// Materialize ordered entries into an object for validation/rendering
156-
const kwargs = orderedKwargs.reduce((acc, [k, v]) => {
157-
(acc as any)[k] = v;
158-
return acc;
159-
}, {} as DataclassDecoratorKwargs);
160-
const hasDecoratorArgs = orderedKwargs.length > 0;
161-
const toSnakeCase = (s: string): string =>
162-
s
163-
.replace(/([a-z\d])([A-Z])/g, "$1_$2")
164-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
165-
.toLowerCase();
166-
const decoratorEntries = orderedKwargs.map(([k, v]) => {
167-
const pyKey = toSnakeCase(k as unknown as string);
168-
return [pyKey, v] as const;
169-
});
170-
const hasBodyChildren = computed(() =>
171-
childrenArray(() => props.children).some(Boolean),
172-
);
154+
const hasDecoratorArgs = decoratorEntries.length > 0;
173155

174156
if (hasDecoratorArgs) {
175157
validateDataclassDecoratorArgs(kwargs);
@@ -180,14 +162,6 @@ export function DataclassDeclaration(props: DataclassDeclarationProps) {
180162
return null;
181163
}
182164

183-
const classBody =
184-
hasBodyChildren.value ?
185-
<>
186-
<StatementList>{props.children}</StatementList>
187-
<RunSymbolValidation />
188-
</>
189-
: undefined;
190-
191165
return (
192166
<>
193167
{"@"}
@@ -205,7 +179,8 @@ export function DataclassDeclaration(props: DataclassDeclarationProps) {
205179
</Show>
206180
<hbr />
207181
<ClassDeclaration name={props.name} bases={props.bases} doc={props.doc}>
208-
{classBody}
182+
<StatementList>{props.children}</StatementList>
183+
<RunSymbolValidation />
209184
</ClassDeclaration>
210185
</>
211186
);

packages/python/src/components/PropertyDeclaration.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ export function PropertyDeclaration(props: PropertyDeclarationProps) {
9494
}
9595
};
9696

97-
const childrenComputed = computed(() => childrenArray(() => props.children));
98-
const children = childrenComputed.value;
97+
const children = childrenArray(() => props.children);
9998
const setterComponent =
10099
findKeyedChild(children, PropertyDeclaration.Setter.tag) ?? undefined;
101100
const deleterComponent =

packages/python/src/components/PyDoc.tsx

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
Prose,
77
Show,
88
childrenArray,
9+
computed,
10+
isKeyedChild,
911
} from "@alloy-js/core";
1012
import { ParameterDescriptor } from "../parameter-descriptor.js";
1113
import { Atom } from "./Atom.jsx";
@@ -244,11 +246,12 @@ export interface PyDocProps {
244246
* linebreaks. This is useful for creating PyDoc comments with multiple paragraphs.
245247
*/
246248
export function PyDoc(props: PyDocProps) {
249+
const children = childrenArray(() => props.children);
247250
return (
248251
<>
249252
{'"""'}
250253
<hbr />
251-
<List doubleHardline>{children(() => props.children)}</List>
254+
<List doubleHardline>{children}</List>
252255
<hbr />
253256
{'"""'}
254257
<hbr />
@@ -264,25 +267,48 @@ export interface PyDocExampleProps {
264267
* Create a PyDoc example, which is prepended by \>\>.
265268
*/
266269
export function PyDocExample(props: PyDocExampleProps) {
267-
const getLines = () => {
268-
const childrenList = childrenArray(() => props.children);
269-
if (childrenList.length === 1 && typeof childrenList[0] === "string") {
270-
// Split, trim each line, and filter out empty lines
271-
return childrenList[0]
270+
const lines = computed(() => {
271+
const kids = childrenArray(() => props.children, { preserveFragments: true });
272+
const out: Children[] = [];
273+
274+
const isBr = (node: any): boolean => {
275+
if (node == null) return false;
276+
const keyed = isKeyedChild(node as any);
277+
const tag = keyed ? (node as any).tag : (node as any)?.tag;
278+
const kind = (node as any)?.kind;
279+
// Consider only <br /> as a line splitter (not rendered as content).
280+
return tag === "br" || kind === "br";
281+
};
282+
283+
// Handles a single string child: split on newlines to produce lines.
284+
const splitAndTrim = (s: string): string[] =>
285+
s
272286
.split(/\r?\n/)
273-
.map((s) => s.trim())
274-
.filter((s) => s.length > 0);
287+
.map((x) => x.trim())
288+
.filter((x) => x.length > 0);
289+
290+
if (kids.length === 1 && typeof kids[0] === "string") {
291+
return splitAndTrim(kids[0] as string);
292+
}
293+
294+
// Handles mixed children: each child becomes a line, string children
295+
// may include embedded newlines, and <br /> acts as a line split.
296+
for (const child of kids) {
297+
if (child == null || isBr(child)) continue;
298+
if (typeof child === "string") {
299+
// String children: split on newlines to produce lines.
300+
for (const seg of splitAndTrim(child)) out.push(seg);
301+
} else {
302+
// Non-string child (component/fragment) are preserved as their own doctest line.
303+
out.push(child);
304+
}
275305
}
276-
// For non-string children, filter out empty/whitespace-only strings
277-
return childrenList
278-
.map((c) => (typeof c === "string" ? c : ""))
279-
.map((s) => s.trim())
280-
.filter((s) => s.length > 0);
281-
};
306+
return out;
307+
});
282308

283309
return (
284310
<>
285-
<For each={getLines}>
311+
<For each={lines.value}>
286312
{(line) => (
287313
<>
288314
{">> "}
@@ -299,12 +325,33 @@ export interface SimpleCommentBlockProps {
299325
}
300326

301327
export function SimpleCommentBlock(props: SimpleCommentBlockProps) {
328+
const children = childrenArray(() => props.children);
329+
let content: Children;
330+
if (children.length === 1 && typeof children[0] === "string") {
331+
const raw = children[0] as string;
332+
if (raw.includes("\n") || raw.includes("\\n")) {
333+
const parts = raw.split(/\r?\n|\\n/);
334+
content = (
335+
<>
336+
{parts.map((p, i) => (
337+
<>
338+
<Prose>{p}</Prose>
339+
{i < parts.length - 1 && <hbr />}
340+
</>
341+
))}
342+
</>
343+
);
344+
} else {
345+
content = <Prose>{props.children}</Prose>;
346+
}
347+
} else {
348+
content = <Prose>{props.children}</Prose>;
349+
}
350+
302351
return (
303352
<>
304353
#{" "}
305-
<align string="# ">
306-
<Prose>{props.children}</Prose>
307-
</align>
354+
<align string="# ">{content}</align>
308355
</>
309356
);
310357
}

packages/python/src/utils.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,7 @@ import {
55
isComponentCreator,
66
splitProps,
77
} from "@alloy-js/core";
8-
import { ClassMethodDeclaration } from "./components/ClassMethodDeclaration.js";
9-
import { DunderMethodDeclaration } from "./components/DunderMethodDeclaration.js";
10-
import { FunctionDeclaration } from "./components/FunctionDeclaration.js";
11-
import {
12-
BaseDeclarationProps,
13-
CallSignatureProps,
14-
} from "./components/index.js";
15-
import { MethodDeclaration } from "./components/MethodDeclaration.js";
16-
import { StaticMethodDeclaration } from "./components/StaticMethodDeclaration.js";
8+
import { CallSignatureProps } from "./components/index.js";
179

1810
/**
1911
* Extract only the call signature props from a props object which extends
@@ -37,35 +29,3 @@ export function getCallSignatureProps(
3729

3830
return defaultProps(callSignatureProps, defaults);
3931
}
40-
41-
/**
42-
* Find the first function-like child declaration whose name matches one of the
43-
* provided method names. Returns the method name when found.
44-
*/
45-
export function findMethodDeclaration(
46-
children: Children[],
47-
methodNames: string[],
48-
): string | undefined {
49-
const creators = [
50-
MethodDeclaration,
51-
FunctionDeclaration,
52-
ClassMethodDeclaration,
53-
StaticMethodDeclaration,
54-
DunderMethodDeclaration,
55-
];
56-
for (const child of children) {
57-
if (creators.some((creator) => isComponentCreator(child, creator))) {
58-
const rawName = (child as ComponentCreator<BaseDeclarationProps>).props
59-
?.name;
60-
const candidateName =
61-
typeof rawName === "string" ? rawName : rawName?.name;
62-
if (
63-
typeof candidateName === "string" &&
64-
methodNames.includes(candidateName)
65-
) {
66-
return candidateName;
67-
}
68-
}
69-
}
70-
return undefined;
71-
}

packages/python/test/pydocs.test.tsx

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,7 @@ describe("PyDocExample", () => {
104104
<py.PyDoc>
105105
<Prose>This is an example of a docstring with a code sample.</Prose>
106106
<py.PyDocExample>
107-
print("Hello world!")
108-
<br />
109-
x = "Hello"
110-
<br />
111-
print(x)
107+
{`print("Hello world!")\nx = "Hello"\nprint(x)`}
112108
</py.PyDocExample>
113109
</py.PyDoc>,
114110
],
@@ -136,12 +132,13 @@ describe("SimpleCommentBlock", () => {
136132
it("renders simple comment block", () => {
137133
const res = toSourceText([
138134
<py.SimpleCommentBlock>
139-
This is a simple comment block that spans multiple lines.
135+
This is a simple comment block that spans multiple lines and should be split automatically.
140136
</py.SimpleCommentBlock>,
141137
]);
142138
expect(res).toRenderTo(
143139
d`
144-
# This is a simple comment block that spans multiple lines.
140+
# This is a simple comment block that spans multiple lines and should be split
141+
# automatically.
145142
146143
`,
147144
);
@@ -150,14 +147,13 @@ describe("SimpleCommentBlock", () => {
150147
it("renders comment block with line breaks", () => {
151148
const res = toSourceText([
152149
<py.SimpleCommentBlock>
153-
First line of comment.
154-
<br />
155-
Second line of comment.
150+
First line of comment.\nSecond line of comment.
156151
</py.SimpleCommentBlock>,
157152
]);
158153
expect(res).toRenderTo(
159154
d`
160-
# First line of comment. Second line of comment.
155+
# First line of comment.
156+
# Second line of comment.
161157
162158
`,
163159
);
@@ -877,9 +873,7 @@ describe("New Documentation Components", () => {
877873
Generators have a Yields section instead of a Returns section.
878874
</Prose>,
879875
<py.PyDocExample>
880-
print([i for i in example_generator(4)])
881-
<br />
882-
[0, 1, 2, 3]
876+
{`print([i for i in example_generator(4)])\n[0, 1, 2, 3]`}
883877
</py.PyDocExample>,
884878
]}
885879
parameters={[
@@ -928,11 +922,7 @@ describe("Full example", () => {
928922
We will also render another paragraph after this one.
929923
</Prose>,
930924
<py.PyDocExample>
931-
print("Hello world!")
932-
<br />
933-
x = "Hello"
934-
<br />
935-
print(x)
925+
{`print("Hello world!")\nx = "Hello"\nprint(x)`}
936926
</py.PyDocExample>,
937927
]}
938928
attributes={[
@@ -1050,11 +1040,7 @@ describe("Full example", () => {
10501040
We will also render another paragraph after this one.
10511041
</Prose>,
10521042
<py.PyDocExample>
1053-
print("Hello world!")
1054-
<br />
1055-
x = "Hello"
1056-
<br />
1057-
print(x)
1043+
{`print("Hello world!")\nx = "Hello"\nprint(x)`}
10581044
</py.PyDocExample>,
10591045
]}
10601046
parameters={[

0 commit comments

Comments
 (0)