@@ -3,102 +3,126 @@ import {
33 Show ,
44 childrenArray ,
55 computed ,
6- findKeyedChildren ,
6+ namekey ,
77 taggedComponent ,
88} from "@alloy-js/core" ;
99import { dataclassesModule } from "../builtins/python.js" ;
10- import { findMethodDeclaration } from "../utils .js" ;
10+ import { usePythonScope } from "../symbols/scopes .js" ;
1111import { Atom } from "./Atom.jsx" ;
1212import type { ClassDeclarationProps } from "./ClassDeclaration.js" ;
1313import { ClassDeclaration } from "./ClassDeclaration.js" ;
1414import { StatementList } from "./StatementList.js" ;
15+ import { VariableDeclaration } from "./VariableDeclaration.js" ;
1516
1617/**
17- * Validate the keyword arguments for the Python `@dataclass(...)` decorator .
18+ * Validate decorator-only rules that do not depend on class members .
1819 */
19- function validateDataclassDecoratorKwargs (
20- children : any [ ] ,
21- kwargs : DataclassDecoratorKwargs ,
22- ) {
23- if ( kwargs . weakref_slot === true && kwargs . slots !== true ) {
20+ function validateDataclassDecoratorArgs ( kwargs : DataclassDecoratorKwargs ) {
21+ if ( kwargs . weakrefSlot === true && kwargs . slots !== true ) {
2422 throw new Error (
2523 "weakref_slot=True requires slots=True in @dataclass decorator" ,
2624 ) ;
2725 }
28-
2926 if ( kwargs . order === true && kwargs . eq === false ) {
3027 throw new Error ( "order=True requires eq=True in @dataclass decorator" ) ;
3128 }
29+ }
30+
31+ /**
32+ * Validate symbol-level conflicts. Must be called from within a member scope
33+ * (inside the class body) so member symbols are available.
34+ */
35+ function validateDataclassMemberConflicts ( kwargs : DataclassDecoratorKwargs ) {
36+ const scope = usePythonScope ( ) ;
37+ const owner : any = ( scope as any ) . ownerSymbol ;
38+ if ( ! owner ) return ;
39+
40+ const hasMemberNamed = ( name : string ) : boolean => {
41+ for ( const sym of owner . instanceMembers as Iterable < any > ) {
42+ if ( sym . originalName === name ) return true ;
43+ }
44+ for ( const sym of owner . staticMembers as Iterable < any > ) {
45+ if ( sym . originalName === name ) return true ;
46+ }
47+ return false ;
48+ } ;
3249
3350 if ( kwargs . order === true ) {
34- const orderingMethods = [ "__lt__" , "__le__" , "__gt__" , "__ge__" ] ;
35- const conflict = findMethodDeclaration ( children , orderingMethods ) ;
36- if ( conflict ) {
37- throw new TypeError (
38- `Cannot specify order=True when the class already defines ${ conflict } ()` ,
39- ) ;
51+ for ( const m of [ "__lt__" , "__le__" , "__gt__" , "__ge__" ] ) {
52+ if ( hasMemberNamed ( m ) ) {
53+ throw new TypeError (
54+ `Cannot specify order=True when the class already defines ${ m } ()` ,
55+ ) ;
56+ }
4057 }
4158 }
4259
43- if ( kwargs . unsafe_hash === true ) {
44- const conflict = findMethodDeclaration ( children , [ "__hash__" ] ) ;
45- if ( conflict ) {
46- throw new TypeError (
47- `Cannot specify unsafe_hash=True when the class already defines ${ conflict } ()` ,
48- ) ;
49- }
60+ if ( kwargs . unsafeHash === true && hasMemberNamed ( "__hash__" ) ) {
61+ throw new TypeError (
62+ "Cannot specify unsafe_hash=True when the class already defines __hash__()" ,
63+ ) ;
5064 }
5165
5266 if ( kwargs . frozen === true ) {
53- const conflict = findMethodDeclaration ( children , [
54- "__setattr__" ,
55- "__delattr__" ,
56- ] ) ;
57- if ( conflict ) {
67+ if ( hasMemberNamed ( "__setattr__" ) ) {
5868 throw new TypeError (
59- ` Cannot specify frozen=True when the class already defines ${ conflict } ()` ,
69+ " Cannot specify frozen=True when the class already defines __setattr__()" ,
6070 ) ;
6171 }
62- }
63-
64- if ( kwargs . slots === true ) {
65- const conflict = findMethodDeclaration ( children , [ "__slots__" ] ) ;
66- if ( conflict ) {
72+ if ( hasMemberNamed ( "__delattr__" ) ) {
6773 throw new TypeError (
68- ` Cannot specify slots =True when the class already defines ${ conflict } ()` ,
74+ " Cannot specify frozen =True when the class already defines __delattr__()" ,
6975 ) ;
7076 }
7177 }
78+
79+ if ( kwargs . slots === true && hasMemberNamed ( "__slots__" ) ) {
80+ throw new TypeError (
81+ "Cannot specify slots=True when the class already defines __slots__()" ,
82+ ) ;
83+ }
84+
85+ // Enforce at most one KW_ONLY sentinel as a symbol
86+ let kwOnlyCount = 0 ;
87+ for ( const sym of owner . instanceMembers as Iterable < any > ) {
88+ if ( sym . originalName === "_" ) kwOnlyCount ++ ;
89+ }
90+ if ( kwOnlyCount > 1 ) {
91+ throw new Error ( "Only one KW_ONLY sentinel is allowed per dataclass body" ) ;
92+ }
7293}
7394
7495/**
7596 * Allowed keyword arguments for the Python `@dataclass(...)` decorator.
76- * Showcases arguments valid for Python 3.11+ .
97+ * Single source of truth: runtime keys and compile-time type are derived here .
7798 */
78- export interface DataclassDecoratorKwargs {
79- init ?: boolean ;
80- repr ?: boolean ;
81- eq ?: boolean ;
82- order ?: boolean ;
83- unsafe_hash ?: boolean ;
84- frozen ?: boolean ;
85- match_args ?: boolean ;
86- kw_only ?: boolean ;
87- slots ?: boolean ;
88- weakref_slot ?: boolean ;
89- }
99+ export const dataclassDecoratorKeys = [
100+ "init" ,
101+ "repr" ,
102+ "eq" ,
103+ "order" ,
104+ "unsafeHash" ,
105+ "frozen" ,
106+ "matchArgs" ,
107+ "kwOnly" ,
108+ "slots" ,
109+ "weakrefSlot" ,
110+ ] as const ;
111+ export type DataclassDecoratorKey = ( typeof dataclassDecoratorKeys ) [ number ] ;
112+ export type DataclassDecoratorKwargs = Partial <
113+ Record < DataclassDecoratorKey , boolean >
114+ > ;
90115
91- export interface DataclassDeclarationProps extends ClassDeclarationProps {
92- /** Keyword arguments to pass to `@dataclass(...)` (only valid dataclass params). */
93- decoratorKwargs ?: DataclassDecoratorKwargs ;
94- }
116+ export interface DataclassDeclarationProps
117+ extends ClassDeclarationProps ,
118+ DataclassDecoratorKwargs { }
95119
96120/**
97121 * Renders a Python dataclass. Uses ClassDeclaration component internally.
98122 *
99123 * Example:
100124 * ```tsx
101- * <py.DataclassDeclaration name="User" decoratorKwargs={{ kw_only: true }} >
125+ * <py.DataclassDeclaration name="User" kwOnly >
102126 * <py.VariableDeclaration instanceVariable omitNone name="id" type="int" />
103127 * <py.DataclassKWOnly />
104128 * <py.VariableDeclaration
@@ -122,50 +146,74 @@ export interface DataclassDeclarationProps extends ClassDeclarationProps {
122146 * ```
123147 */
124148export function DataclassDeclaration ( props : DataclassDeclarationProps ) {
125- const kwargs = props . decoratorKwargs as DataclassDecoratorKwargs | undefined ;
126- const hasDecoratorArgs =
127- kwargs !== undefined && Object . keys ( kwargs ) . length > 0 ;
128- const childrenComputed = computed ( ( ) => childrenArray ( ( ) => props . children ) ) ;
129- const hasBodyChildren = childrenComputed . value . some ( Boolean ) ;
130- const children = childrenComputed . value ;
131-
132- if ( props . decoratorKwargs ) {
133- validateDataclassDecoratorKwargs ( children , props . decoratorKwargs ) ;
149+ const decoratorKeys : ( keyof DataclassDecoratorKwargs ) [ ] = [
150+ ...dataclassDecoratorKeys ,
151+ ] ;
152+ const validKeySet = new Set < string > ( decoratorKeys as unknown as string [ ] ) ;
153+ // Collect flags from props in the order they appear (preserves emission order)
154+ const orderedKwargs : Array < [ keyof DataclassDecoratorKwargs , any ] > = [ ] ;
155+ for ( const key of Object . keys ( props ) ) {
156+ // Only include known flags; skip undefined values
157+ if ( validKeySet . has ( key ) ) {
158+ const value = ( props as any ) [ key ] ;
159+ if ( value !== undefined )
160+ orderedKwargs . push ( [ key as keyof DataclassDecoratorKwargs , value ] ) ;
161+ }
134162 }
163+ // Materialize ordered entries into an object for validation/rendering
164+ const kwargs = orderedKwargs . reduce ( ( acc , [ k , v ] ) => {
165+ ( acc as any ) [ k ] = v ;
166+ return acc ;
167+ } , { } as DataclassDecoratorKwargs ) ;
168+ const hasDecoratorArgs = orderedKwargs . length > 0 ;
169+ const toSnakeCase = ( s : string ) : string =>
170+ s
171+ . replace ( / ( [ a - z \d ] ) ( [ A - Z ] ) / g, "$1_$2" )
172+ . replace ( / ( [ A - Z ] + ) ( [ A - Z ] [ a - z ] ) / g, "$1_$2" )
173+ . toLowerCase ( ) ;
174+ const decoratorEntries = orderedKwargs . map ( ( [ k , v ] ) => {
175+ const pyKey = toSnakeCase ( k as unknown as string ) ;
176+ return [ pyKey , v ] as const ;
177+ } ) ;
178+ const hasBodyChildren = computed ( ( ) =>
179+ childrenArray ( ( ) => props . children ) . some ( Boolean ) ,
180+ ) ;
135181
136- // Validate at most one KW_ONLY sentinel in children
137- if ( hasBodyChildren ) {
138- if ( findKeyedChildren ( children , DataclassKWOnly . tag ) . length > 1 ) {
139- throw new Error (
140- "Only one KW_ONLY sentinel is allowed per dataclass body" ,
141- ) ;
142- }
182+ if ( hasDecoratorArgs ) {
183+ validateDataclassDecoratorArgs ( kwargs ) ;
184+ }
185+
186+ function RunSymbolValidation ( ) {
187+ validateDataclassMemberConflicts ( kwargs as DataclassDecoratorKwargs ) ;
188+ return null ;
143189 }
144190
191+ const classBody =
192+ hasBodyChildren . value ?
193+ < >
194+ < StatementList > { props . children } </ StatementList >
195+ < RunSymbolValidation />
196+ </ >
197+ : undefined ;
198+
145199 return (
146200 < >
147201 { "@" }
148202 { dataclassesModule [ "." ] . dataclass }
149203 < Show when = { hasDecoratorArgs } >
150- { "(" }
151- < For
152- each = { Object . keys ( kwargs ?? { } ) . map ( ( k ) => [ k , ( kwargs as any ) [ k ] ] ) }
153- comma
154- space
155- >
204+ (
205+ < For each = { decoratorEntries } comma space >
156206 { ( [ k , v ] ) => (
157207 < >
158208 { k } =< Atom jsValue = { v } />
159209 </ >
160210 ) }
161211 </ For >
162- { ")" }
212+ )
163213 </ Show >
164214 < hbr />
165215 < ClassDeclaration name = { props . name } bases = { props . bases } doc = { props . doc } >
166- { hasBodyChildren ?
167- < StatementList > { props . children } </ StatementList >
168- : undefined }
216+ { classBody }
169217 </ ClassDeclaration >
170218 </ >
171219 ) ;
@@ -182,7 +230,12 @@ export const DataclassKWOnly = taggedComponent(
182230 function DataclassKWOnly ( ) {
183231 return (
184232 < >
185- { "_" } : { dataclassesModule [ "." ] . KW_ONLY }
233+ < VariableDeclaration
234+ instanceVariable
235+ name = { namekey ( "_" , { ignoreNamePolicy : true } ) }
236+ type = { dataclassesModule [ "." ] . KW_ONLY }
237+ omitNone
238+ />
186239 </ >
187240 ) ;
188241 } ,
0 commit comments