fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3448 from hey-api/feat/py-dsl-kwarg

feat: add kwarg nodes

authored by

Lubos and committed by
GitHub
68551a8e 6d9c89ce

+1542 -390
+5
.changeset/ready-boxes-lie.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **output**: fix: avoid double sanitizing leading character
+6
.changeset/thick-cases-wonder.md
··· 1 + --- 2 + "@hey-api/codegen-core": patch 3 + "@hey-api/openapi-ts": patch 4 + --- 5 + 6 + **internal**: log symbol meta if name is falsy
+1 -1
packages/codegen-core/src/symbols/symbol.ts
··· 305 305 if (canonical._finalName && canonical._finalName !== canonical._name) { 306 306 return `[Symbol ${canonical._name} → ${canonical._finalName}#${canonical.id}]`; 307 307 } 308 - return `[Symbol ${canonical._name}#${canonical.id}]`; 308 + return `[Symbol ${canonical._name || canonical._meta !== undefined ? JSON.stringify(canonical._meta) : '<unknown>'}#${canonical.id}]`; 309 309 } 310 310 311 311 /**
+1
packages/openapi-python/src/plugins/pydantic/config.ts
··· 7 7 config: { 8 8 case: 'PascalCase', 9 9 comments: true, 10 + enums: 'enum', 10 11 includeInEntry: false, 11 12 strict: false, 12 13 },
+109 -25
packages/openapi-python/src/plugins/pydantic/shared/export.ts
··· 1 + import type { Symbol } from '@hey-api/codegen-core'; 1 2 import { applyNaming, pathToName } from '@hey-api/shared'; 2 3 3 - // import { createSchemaComment } from '../../../plugins/shared/utils/schema'; 4 4 import { $ } from '../../../py-dsl'; 5 + import type { PydanticPlugin } from '../types'; 6 + import { identifiers } from '../v2/constants'; 7 + import { createFieldCall } from './field'; 5 8 import type { ProcessorContext } from './processor'; 6 - // import { identifiers } from '../v2/constants'; 7 - // import { pipesToNode } from './pipes'; 8 - import type { PydanticFinal } from './types'; 9 + import type { PydanticField, PydanticFinal } from './types'; 9 10 10 11 export function exportAst({ 11 12 final, ··· 29 30 }, 30 31 }); 31 32 32 - if (final.fields) { 33 - const baseModel = plugin.external('pydantic.BaseModel'); 34 - const classDef = $.class(symbol).extends(baseModel); 35 - // .export() 36 - // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 37 - // .$if(state.hasLazyExpression['~ref'], (c) => 38 - // c.type($.type(v).attr(ast.typeName || identifiers.types.GenericSchema)), 39 - // ) 40 - // .assign(pipesToNode(ast.pipes, plugin)); 33 + if (final.enumMembers) { 34 + exportEnumClass({ final, plugin, symbol }); 35 + } else if (final.fields) { 36 + exportClass({ final, plugin, symbol }); 37 + } else { 38 + exportTypeAlias({ final, plugin, symbol }); 39 + } 40 + } 41 41 42 - for (const field of final.fields) { 43 - // TODO: Field(...) constraints in next pass 44 - classDef.do($.var(field.name).assign($.literal('hey'))); 45 - // classDef.do($.var(field.name).annotate(field.typeAnnotation)); 46 - } 42 + function exportClass({ 43 + final, 44 + plugin, 45 + symbol, 46 + }: { 47 + final: PydanticFinal; 48 + plugin: PydanticPlugin['Instance']; 49 + symbol: Symbol; 50 + }): void { 51 + const baseModel = plugin.external('pydantic.BaseModel'); 52 + const classDef = $.class(symbol).extends(baseModel); 47 53 48 - plugin.node(classDef); 49 - } else { 50 - const statement = $.var(symbol) 51 - // .export() 52 - // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) 53 - .assign(final.typeAnnotation); 54 + if (plugin.config.strict) { 55 + const configDict = plugin.external('pydantic.ConfigDict'); 56 + classDef.do( 57 + $.var(identifiers.model_config).assign($(configDict).call($.kwarg('extra', 'forbid'))), 58 + ); 59 + } 54 60 55 - plugin.node(statement); 61 + for (const field of final.fields!) { 62 + const fieldStatement = createFieldStatement(field, plugin); 63 + classDef.do(fieldStatement); 56 64 } 65 + 66 + plugin.node(classDef); 67 + } 68 + 69 + function exportEnumClass({ 70 + final, 71 + plugin, 72 + symbol, 73 + }: { 74 + final: PydanticFinal; 75 + plugin: PydanticPlugin['Instance']; 76 + symbol: Symbol; 77 + }): void { 78 + const members = final.enumMembers ?? []; 79 + const hasStrings = members.some((m) => typeof m.value === 'string'); 80 + const hasNumbers = members.some((m) => typeof m.value === 'number'); 81 + 82 + const enumSymbol = plugin.external('enum.Enum'); 83 + const classDef = $.class(symbol).extends(enumSymbol); 84 + 85 + if (hasStrings && !hasNumbers) { 86 + classDef.extends('str'); 87 + } else if (!hasStrings && hasNumbers) { 88 + classDef.extends('int'); 89 + } 90 + 91 + for (const member of final.enumMembers ?? []) { 92 + classDef.do($.var(member.name).assign($.literal(member.value))); 93 + } 94 + 95 + plugin.node(classDef); 96 + } 97 + 98 + function createFieldStatement( 99 + field: PydanticField, 100 + plugin: PydanticPlugin['Instance'], 101 + ): ReturnType<typeof $.var> { 102 + const fieldSymbol = field.name; 103 + const varStatement = $.var(fieldSymbol).$if(field.typeAnnotation, (v, a) => v.annotate(a)); 104 + 105 + const originalName = field.originalName ?? fieldSymbol.name; 106 + const needsAlias = field.originalName !== undefined && fieldSymbol.name !== originalName; 107 + 108 + const constraints = { 109 + ...field.fieldConstraints, 110 + ...(needsAlias && !field.fieldConstraints?.alias && { alias: originalName }), 111 + }; 112 + 113 + if (Object.keys(constraints).length > 0) { 114 + const fieldCall = createFieldCall(constraints, plugin, { 115 + required: !field.isOptional, 116 + }); 117 + return varStatement.assign(fieldCall); 118 + } 119 + 120 + if (field.isOptional) { 121 + return varStatement.assign('None'); 122 + } 123 + 124 + return varStatement; 125 + } 126 + 127 + function exportTypeAlias({ 128 + final, 129 + plugin, 130 + symbol, 131 + }: { 132 + final: PydanticFinal; 133 + plugin: PydanticPlugin['Instance']; 134 + symbol: Symbol; 135 + }): void { 136 + const typeAlias = plugin.external('typing.TypeAlias'); 137 + const statement = $.var(symbol) 138 + .annotate(typeAlias) 139 + .assign(final.typeAnnotation ?? plugin.external('typing.Any')); 140 + plugin.node(statement); 57 141 }
+70
packages/openapi-python/src/plugins/pydantic/shared/field.ts
··· 1 + import { $ } from '../../../py-dsl'; 2 + import type { PydanticPlugin } from '../types'; 3 + import type { FieldConstraints } from '../v2/constants'; 4 + 5 + type FieldArg = ReturnType<typeof $.expr | typeof $.kwarg | typeof $.literal>; 6 + 7 + export function createFieldCall( 8 + constraints: FieldConstraints, 9 + plugin: PydanticPlugin['Instance'], 10 + options?: { 11 + /** If true, the field is required. */ 12 + required?: boolean; 13 + }, 14 + ): ReturnType<typeof $.call> { 15 + const field = plugin.external('pydantic.Field'); 16 + const args: Array<FieldArg> = []; 17 + 18 + const isRequired = options?.required !== false && constraints.default === undefined; 19 + 20 + // For required fields with no default, use ... as first arg 21 + if (isRequired && constraints.default === undefined) { 22 + args.push($('...')); 23 + } 24 + 25 + // TODO: move to DSL 26 + // Add constraint arguments in a consistent order 27 + const orderedKeys: Array<keyof FieldConstraints> = [ 28 + 'default', 29 + 'default_factory', 30 + 'alias', 31 + 'title', 32 + 'description', 33 + 'gt', 34 + 'ge', 35 + 'lt', 36 + 'le', 37 + 'multiple_of', 38 + 'min_length', 39 + 'max_length', 40 + 'pattern', 41 + ]; 42 + 43 + for (const key of orderedKeys) { 44 + const value = constraints[key]; 45 + if (value === undefined) continue; 46 + 47 + // Skip default if we already added ... for required fields 48 + if (key === 'default' && isRequired) continue; 49 + 50 + args.push($.kwarg(key, toKwargValue(value))); 51 + } 52 + 53 + return $(field).call(...(args as Array<Parameters<typeof $.call>[1]>)); 54 + } 55 + 56 + /** 57 + * Converts a constraint value to a kwarg-compatible value. 58 + */ 59 + function toKwargValue(value: unknown): string | number | boolean | null { 60 + if ( 61 + value === null || 62 + typeof value === 'string' || 63 + typeof value === 'number' || 64 + typeof value === 'boolean' 65 + ) { 66 + return value; 67 + } 68 + // For complex types, stringify 69 + return String(value); 70 + }
+4 -6
packages/openapi-python/src/plugins/pydantic/shared/meta.ts
··· 8 8 export function defaultMeta(schema: IR.SchemaObject): PydanticMeta { 9 9 return { 10 10 default: schema.default, 11 - format: schema.format, 12 - hasLazy: false, 11 + hasForwardReference: false, 13 12 nullable: false, 14 13 readonly: schema.accessScope === 'read', 15 14 }; ··· 18 17 /** 19 18 * Composes metadata from child results. 20 19 * 21 - * Automatically propagates hasLazy, nullable, readonly from children. 20 + * Automatically propagates hasForwardReference, nullable, readonly from children. 22 21 * 23 22 * @param children - Results from walking child schemas 24 23 * @param overrides - Explicit overrides (e.g., from parent schema) ··· 29 28 ): PydanticMeta { 30 29 return { 31 30 default: overrides?.default, 32 - format: overrides?.format, 33 - hasLazy: overrides?.hasLazy ?? children.some((c) => c.meta.hasLazy), 31 + hasForwardReference: 32 + overrides?.hasForwardReference ?? children.some((c) => c.meta.hasForwardReference), 34 33 nullable: overrides?.nullable ?? children.some((c) => c.meta.nullable), 35 34 readonly: overrides?.readonly ?? children.some((c) => c.meta.readonly), 36 35 }; ··· 48 47 ): PydanticMeta { 49 48 return composeMeta(children, { 50 49 default: parent.default, 51 - format: parent.format, 52 50 nullable: false, 53 51 readonly: parent.accessScope === 'read', 54 52 });
+7 -6
packages/openapi-python/src/plugins/pydantic/shared/processor.ts
··· 5 5 SchemaProcessorResult, 6 6 } from '@hey-api/shared'; 7 7 8 - import type { IrSchemaToAstOptions } from './types'; 8 + import type { PydanticPlugin } from '../types'; 9 9 10 - export type ProcessorContext = Pick<IrSchemaToAstOptions, 'plugin'> & 11 - SchemaProcessorContext & { 12 - naming: NamingConfig; 13 - schema: IR.SchemaObject; 14 - }; 10 + export type ProcessorContext = SchemaProcessorContext & { 11 + naming: NamingConfig; 12 + /** The plugin instance. */ 13 + plugin: PydanticPlugin['Instance']; 14 + schema: IR.SchemaObject; 15 + }; 15 16 16 17 export type ProcessorResult = SchemaProcessorResult<ProcessorContext>;
+17 -91
packages/openapi-python/src/plugins/pydantic/shared/types.ts
··· 1 - import type { Refs, Symbol, SymbolMeta } from '@hey-api/codegen-core'; 2 - import type { IR, SchemaExtractor } from '@hey-api/shared'; 3 - 4 - import type { $, MaybePyDsl } from '../../../py-dsl'; 5 - import type { py } from '../../../ts-python'; 6 - import type { PydanticPlugin } from '../types'; 7 - import type { ProcessorContext } from './processor'; 8 - 9 - export type Ast = { 10 - /** 11 - * Field constraints for pydantic.Field() 12 - */ 13 - fieldConstraints?: Record<string, unknown>; 14 - /** 15 - * Whether this AST node has a lazy expression (forward reference) 16 - */ 17 - hasLazyExpression?: boolean; 18 - models: Array<{ 19 - baseName: string; 20 - expression: ReturnType<typeof $.class>; 21 - symbol: Symbol; 22 - }>; 23 - /** 24 - * Type annotation for the field 25 - */ 26 - typeAnnotation: string; 27 - /** 28 - * Type name for the model class 29 - */ 30 - typeName?: string; 31 - }; 1 + import type { Symbol } from '@hey-api/codegen-core'; 32 2 33 - export type IrSchemaToAstOptions = { 34 - /** The plugin instance. */ 35 - plugin: PydanticPlugin['Instance']; 36 - /** Optional schema extractor function. */ 37 - schemaExtractor?: SchemaExtractor<ProcessorContext>; 38 - /** The plugin state references. */ 39 - state: Refs<PluginState>; 40 - }; 41 - 42 - export type PluginState = Pick<Required<SymbolMeta>, 'path'> & 43 - Pick<Partial<SymbolMeta>, 'tags'> & { 44 - hasLazyExpression: boolean; 45 - }; 3 + import type { AnnotationExpr } from '../../../py-dsl'; 4 + import type { FieldConstraints } from '../v2/constants'; 46 5 47 6 /** 48 - * Pipe system for building field constraints (similar to Valibot pattern) 49 - */ 50 - export type Pipes = Array<unknown>; 51 - 52 - /** 53 - * Context for type resolver functions 7 + * Return type for toType converters. 54 8 */ 55 - export interface ResolverContext { 56 - /** 57 - * Field constraints being built 58 - */ 59 - constraints: Record<string, unknown>; 60 - /** 61 - * The plugin instance 62 - */ 63 - plugin: PydanticPlugin['Instance']; 64 - /** 65 - * IR schema being processed 66 - */ 67 - schema: IR.SchemaObject; 9 + export interface PydanticType { 10 + fieldConstraints?: FieldConstraints; 11 + typeAnnotation?: AnnotationExpr; 68 12 } 69 - 70 - // ..... ^^^^^^ OLD 71 13 72 14 /** 73 15 * Metadata that flows through schema walking. ··· 75 17 export interface PydanticMeta { 76 18 /** Default value from schema. */ 77 19 default?: unknown; 78 - /** Original format (for BigInt coercion). */ 79 - format?: string; 80 - /** Whether this or any child contains a lazy reference. */ 81 - hasLazy: boolean; 20 + /** Whether this or any child contains a forward reference. */ 21 + hasForwardReference: boolean; 82 22 /** Does this schema explicitly allow null? */ 83 23 nullable: boolean; 84 24 /** Is this schema read-only? */ ··· 88 28 /** 89 29 * Result from walking a schema node. 90 30 */ 91 - export interface PydanticResult { 92 - fieldConstraints: Record<string, unknown>; 93 - fields?: Array<PydanticField>; 31 + export interface PydanticResult extends PydanticType { 32 + enumMembers?: Array<{ name: Symbol; value: string | number }>; 33 + fields?: Array<PydanticField>; // present = emit class, absent = emit type alias 94 34 meta: PydanticMeta; 95 - typeAnnotation: string | MaybePyDsl<py.Expression>; 96 35 } 97 36 98 - export interface PydanticField { 99 - fieldConstraints: Record<string, unknown>; 37 + export interface PydanticField extends PydanticType { 100 38 isOptional: boolean; 101 - name: string; 102 - typeAnnotation: string | MaybePyDsl<py.Expression>; 39 + name: Symbol; 40 + originalName?: string; 103 41 } 104 42 105 43 /** 106 44 * Finalized result after applyModifiers. 107 45 */ 108 - export interface PydanticFinal { 109 - fieldConstraints: Record<string, unknown>; 110 - fields?: Array<PydanticField>; // present = emit class, absent = emit type alias 111 - typeAnnotation: string | MaybePyDsl<py.Expression>; 112 - } 113 - 114 - /** 115 - * Result from composite handlers that walk children. 116 - */ 117 - export interface PydanticCompositeHandlerResult { 118 - childResults: Array<PydanticResult>; 119 - fieldConstraints: Record<string, unknown>; 120 - typeAnnotation: string | MaybePyDsl<py.Expression>; 121 - } 46 + export interface PydanticFinal 47 + extends PydanticType, Pick<PydanticResult, 'enumMembers' | 'fields'> {}
+11
packages/openapi-python/src/plugins/pydantic/types.ts
··· 54 54 name?: NameTransformer; 55 55 }; 56 56 /** 57 + * How to generate enum types. 58 + * 59 + * - `'enum'`: Generate Python Enum classes (e.g., `class Status(str, Enum): ...`) 60 + * - `'literal'`: Generate Literal type hints (e.g., `Literal["pending", "active"]`) 61 + * 62 + * @default 'enum' 63 + */ 64 + enums?: 'enum' | 'literal'; 65 + /** 57 66 * Configuration for request-specific Pydantic models. 58 67 * 59 68 * Controls generation of Pydantic models for request bodies, ··· 181 190 case: Casing; 182 191 /** Configuration for reusable schema definitions. */ 183 192 definitions: NamingOptions & FeatureToggle; 193 + /** How to generate enum types. */ 194 + enums: 'enum' | 'literal'; 184 195 /** Configuration for request-specific Pydantic models. */ 185 196 requests: NamingOptions & FeatureToggle; 186 197 /** Configuration for response-specific Pydantic models. */
+28 -40
packages/openapi-python/src/plugins/pydantic/v2/constants.ts
··· 1 1 export const identifiers = { 2 - Annotated: 'Annotated', 3 - Any: 'Any', 4 - BaseModel: 'BaseModel', 5 - ConfigDict: 'ConfigDict', 6 - Dict: 'Dict', 7 - Field: 'Field', 8 - List: 'List', 9 - Literal: 'Literal', 10 - Optional: 'Optional', 11 - Union: 'Union', 12 - alias: 'alias', 13 - default: 'default', 14 - description: 'description', 15 - ge: 'ge', 16 - gt: 'gt', 17 - le: 'le', 18 - lt: 'lt', 19 - max_length: 'max_length', 20 - min_length: 'min_length', 21 2 model_config: 'model_config', 22 - multiple_of: 'multiple_of', 23 - pattern: 'pattern', 24 - } as const; 25 - 26 - export const typeMappings: Record<string, string> = { 27 - array: 'list', 28 - boolean: 'bool', 29 - integer: 'int', 30 - null: 'None', 31 - number: 'float', 32 - object: 'dict', 33 - string: 'str', 34 3 }; 35 4 36 - export const pydanticTypes = { 37 - array: 'list', 38 - boolean: 'bool', 39 - integer: 'int', 40 - null: 'None', 41 - number: 'float', 42 - object: 'dict', 43 - string: 'str', 44 - } as const; 5 + export interface FieldConstraints { 6 + /** Alias for the field name in serialization. */ 7 + alias?: string; 8 + /** Default value for the field. */ 9 + default?: unknown; 10 + /** Default factory function (for mutable defaults). */ 11 + default_factory?: string; 12 + /** Description of the field. */ 13 + description?: string; 14 + /** Greater than or equal constraint for numbers. */ 15 + ge?: number; 16 + /** Greater than constraint for numbers. */ 17 + gt?: number; 18 + /** Less than or equal constraint for numbers. */ 19 + le?: number; 20 + /** Less than constraint for numbers. */ 21 + lt?: number; 22 + /** Maximum length constraint for strings/arrays. */ 23 + max_length?: number; 24 + /** Minimum length constraint for strings/arrays. */ 25 + min_length?: number; 26 + /** Multiple of constraint for numbers. */ 27 + multiple_of?: number; 28 + /** Regex pattern constraint for strings. */ 29 + pattern?: string; 30 + /** Title for the field. */ 31 + title?: string; 32 + }
+53 -20
packages/openapi-python/src/plugins/pydantic/v2/plugin.ts
··· 1 1 import { pathToJsonPointer } from '@hey-api/shared'; 2 2 3 - // import { $ } from '../../../py-dsl'; 4 3 import type { PydanticPlugin } from '../types'; 5 4 import { createProcessor } from './processor'; 6 5 7 6 export const handlerV2: PydanticPlugin['Handler'] = ({ plugin }) => { 7 + // enum 8 + plugin.symbol('Enum', { 9 + external: 'enum', 10 + meta: { 11 + category: 'external', 12 + resource: 'enum.Enum', 13 + }, 14 + }); 15 + 16 + // typing 8 17 plugin.symbol('Any', { 9 18 external: 'typing', 10 - importKind: 'named', 11 19 meta: { 12 20 category: 'external', 13 21 resource: 'typing.Any', 14 22 }, 15 23 }); 16 - plugin.symbol('BaseModel', { 17 - external: 'pydantic', 18 - importKind: 'named', 24 + plugin.symbol('List', { 25 + external: 'typing', 26 + meta: { 27 + category: 'external', 28 + resource: 'typing.List', 29 + }, 30 + }); 31 + plugin.symbol('Literal', { 32 + external: 'typing', 19 33 meta: { 20 34 category: 'external', 21 - resource: 'pydantic.BaseModel', 35 + resource: 'typing.Literal', 22 36 }, 23 37 }); 24 - plugin.symbol('ConfigDict', { 25 - external: 'pydantic', 26 - importKind: 'named', 38 + plugin.symbol('NoReturn', { 39 + external: 'typing', 27 40 meta: { 28 41 category: 'external', 29 - resource: 'pydantic.ConfigDict', 42 + resource: 'typing.NoReturn', 30 43 }, 31 44 }); 32 - plugin.symbol('Field', { 33 - external: 'pydantic', 34 - importKind: 'named', 45 + plugin.symbol('Optional', { 46 + external: 'typing', 35 47 meta: { 36 48 category: 'external', 37 - resource: 'pydantic.Field', 49 + resource: 'typing.Optional', 38 50 }, 39 51 }); 40 - plugin.symbol('Literal', { 52 + plugin.symbol('TypeAlias', { 41 53 external: 'typing', 42 - importKind: 'named', 43 54 meta: { 44 55 category: 'external', 45 - resource: 'typing.Literal', 56 + resource: 'typing.TypeAlias', 46 57 }, 47 58 }); 48 - plugin.symbol('Optional', { 59 + plugin.symbol('Union', { 49 60 external: 'typing', 50 - importKind: 'named', 51 61 meta: { 52 62 category: 'external', 53 - resource: 'typing.Optional', 63 + resource: 'typing.Union', 64 + }, 65 + }); 66 + 67 + // Pydantic 68 + plugin.symbol('BaseModel', { 69 + external: 'pydantic', 70 + meta: { 71 + category: 'external', 72 + resource: 'pydantic.BaseModel', 73 + }, 74 + }); 75 + plugin.symbol('ConfigDict', { 76 + external: 'pydantic', 77 + meta: { 78 + category: 'external', 79 + resource: 'pydantic.ConfigDict', 80 + }, 81 + }); 82 + plugin.symbol('Field', { 83 + external: 'pydantic', 84 + meta: { 85 + category: 'external', 86 + resource: 'pydantic.Field', 54 87 }, 55 88 }); 56 89
+13 -4
packages/openapi-python/src/plugins/pydantic/v2/processor.ts
··· 1 1 import { ref } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { Hooks, IR } from '@hey-api/shared'; 3 3 import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; ··· 11 11 export function createProcessor(plugin: PydanticPlugin['Instance']): ProcessorResult { 12 12 const processor = createSchemaProcessor(); 13 13 14 - const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 14 + const extractorHooks: ReadonlyArray<NonNullable<Hooks['schemas']>['shouldExtract']> = [ 15 + (ctx) => 16 + ctx.schema.type === 'object' && 17 + ctx.schema.properties !== undefined && 18 + Object.keys(ctx.schema.properties).length > 0, 19 + (ctx) => 20 + ctx.schema.type === 'enum' && ctx.schema.items !== undefined && ctx.schema.items.length > 0, 21 + plugin.config['~hooks']?.schemas?.shouldExtract, 22 + plugin.context.config.parser.hooks.schemas?.shouldExtract, 23 + ]; 15 24 16 25 function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 26 if (processor.hasEmitted(ctx.path)) { 18 27 return ctx.schema; 19 28 } 20 29 21 - for (const hook of hooks) { 22 - const result = hook?.shouldExtract?.(ctx); 30 + for (const hook of extractorHooks) { 31 + const result = hook?.(ctx); 23 32 if (result) { 24 33 process({ 25 34 namingAnchor: processor.context.anchor,
+82
packages/openapi-python/src/plugins/pydantic/v2/toAst/array.ts
··· 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 + import { childContext, deduplicateSchema } from '@hey-api/shared'; 3 + 4 + import { $ } from '../../../../py-dsl'; 5 + import type { PydanticFinal, PydanticResult, PydanticType } from '../../shared/types'; 6 + import type { PydanticPlugin } from '../../types'; 7 + import type { FieldConstraints } from '../constants'; 8 + 9 + interface ArrayToTypeContext { 10 + applyModifiers: (result: PydanticResult, options?: { optional?: boolean }) => PydanticFinal; 11 + plugin: PydanticPlugin['Instance']; 12 + schema: SchemaWithType<'array'>; 13 + walk: Walker<PydanticResult, PydanticPlugin['Instance']>; 14 + walkerCtx: SchemaVisitorContext<PydanticPlugin['Instance']>; 15 + } 16 + 17 + export interface ArrayToTypeResult extends PydanticType { 18 + childResults: Array<PydanticResult>; 19 + } 20 + 21 + export function arrayToType(ctx: ArrayToTypeContext): ArrayToTypeResult { 22 + const { plugin, walk, walkerCtx } = ctx; 23 + let { schema } = ctx; 24 + 25 + const childResults: Array<PydanticResult> = []; 26 + const constraints: FieldConstraints = {}; 27 + const list = plugin.external('typing.List'); 28 + const any = plugin.external('typing.Any'); 29 + 30 + if (schema.minItems !== undefined) { 31 + constraints.min_length = schema.minItems; 32 + } 33 + 34 + if (schema.maxItems !== undefined) { 35 + constraints.max_length = schema.maxItems; 36 + } 37 + 38 + if (schema.description !== undefined) { 39 + constraints.description = schema.description; 40 + } 41 + 42 + if (!schema.items) { 43 + return { 44 + childResults, 45 + fieldConstraints: constraints, 46 + typeAnnotation: $(list).slice(any), 47 + }; 48 + } 49 + 50 + schema = deduplicateSchema({ schema }); 51 + 52 + for (let i = 0; i < schema.items!.length; i++) { 53 + const item = schema.items![i]!; 54 + const result = walk(item, childContext(walkerCtx, 'items', i)); 55 + childResults.push(result); 56 + } 57 + 58 + if (childResults.length === 1) { 59 + const itemResult = ctx.applyModifiers(childResults[0]!); 60 + return { 61 + childResults, 62 + fieldConstraints: constraints, 63 + typeAnnotation: $(list).slice(itemResult.typeAnnotation ?? any), 64 + }; 65 + } 66 + 67 + if (childResults.length > 1) { 68 + const union = plugin.external('typing.Union'); 69 + const itemTypes = childResults.map((r) => ctx.applyModifiers(r).typeAnnotation ?? any); 70 + return { 71 + childResults, 72 + fieldConstraints: constraints, 73 + typeAnnotation: $(list).slice($(union).slice(...itemTypes)), 74 + }; 75 + } 76 + 77 + return { 78 + childResults, 79 + fieldConstraints: constraints, 80 + typeAnnotation: $(list).slice(any), 81 + }; 82 + }
+4 -8
packages/openapi-python/src/plugins/pydantic/v2/toAst/boolean.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - import { defaultMeta } from '../../shared/meta'; 4 - import type { PydanticResult } from '../../shared/types'; 3 + import { $ } from '../../../../py-dsl'; 4 + import type { PydanticType } from '../../shared/types'; 5 5 import type { PydanticPlugin } from '../../types'; 6 6 7 7 export function booleanToType({ ··· 10 10 }: { 11 11 plugin: PydanticPlugin['Instance']; 12 12 schema: SchemaWithType<'boolean'>; 13 - }): PydanticResult { 13 + }): PydanticType { 14 14 if (typeof schema.const === 'boolean') { 15 15 const literal = plugin.external('typing.Literal'); 16 16 return { 17 - fieldConstraints: {}, 18 - meta: defaultMeta(schema), 19 - typeAnnotation: `${literal}[${schema.const ? 'True' : 'False'}]`, 17 + typeAnnotation: $(literal).slice($.literal(schema.const)), 20 18 }; 21 19 } 22 20 23 21 return { 24 - fieldConstraints: {}, 25 - meta: defaultMeta(schema), 26 22 typeAnnotation: 'bool', 27 23 }; 28 24 }
+102
packages/openapi-python/src/plugins/pydantic/v2/toAst/enum.ts
··· 1 + import type { Symbol } from '@hey-api/codegen-core'; 2 + import type { SchemaWithType } from '@hey-api/shared'; 3 + import { toCase } from '@hey-api/shared'; 4 + 5 + import { $ } from '../../../../py-dsl'; 6 + import type { PydanticFinal, PydanticType } from '../../shared/types'; 7 + import type { PydanticPlugin } from '../../types'; 8 + 9 + export interface EnumToTypeResult extends PydanticType { 10 + enumMembers: Required<PydanticFinal>['enumMembers']; 11 + isNullable: boolean; 12 + } 13 + 14 + // TODO: replace with casing utils 15 + function toEnumMemberName(value: string | number): string { 16 + if (typeof value === 'number') { 17 + // For numbers, prefix with underscore if starts with digit 18 + return `VALUE_${value}`.replace(/-/g, '_NEG_').replace(/\./g, '_DOT_'); 19 + } 20 + 21 + return toCase(value, 'SCREAMING_SNAKE_CASE'); 22 + } 23 + 24 + function extractEnumMembers( 25 + schema: SchemaWithType<'enum'>, 26 + plugin: PydanticPlugin['Instance'], 27 + ): { 28 + enumMembers: Required<PydanticFinal>['enumMembers']; 29 + isNullable: boolean; 30 + } { 31 + const enumMembers: Required<PydanticFinal>['enumMembers'] = []; 32 + let isNullable = false; 33 + 34 + for (const item of schema.items ?? []) { 35 + if (item.type === 'null' || item.const === null) { 36 + isNullable = true; 37 + continue; 38 + } 39 + 40 + if ( 41 + (item.type === 'string' && typeof item.const === 'string') || 42 + ((item.type === 'integer' || item.type === 'number') && typeof item.const === 'number') 43 + ) { 44 + enumMembers.push({ 45 + name: plugin.symbol(toEnumMemberName(item.const)), 46 + value: item.const, 47 + }); 48 + } 49 + } 50 + 51 + return { enumMembers, isNullable }; 52 + } 53 + 54 + function toLiteralType( 55 + enumMembers: Required<PydanticFinal>['enumMembers'], 56 + plugin: PydanticPlugin['Instance'], 57 + ): string | Symbol | ReturnType<typeof $.subscript> { 58 + if (enumMembers.length === 0) { 59 + return plugin.external('typing.Any'); 60 + } 61 + 62 + const literal = plugin.external('typing.Literal'); 63 + const values = enumMembers.map((m) => 64 + // TODO: replace 65 + typeof m.value === 'string' ? `"<<<<${m.value}"` : `<<<${m.value}`, 66 + ); 67 + 68 + return $(literal).slice(...values); 69 + } 70 + 71 + export function enumToType({ 72 + mode = 'enum', 73 + plugin, 74 + schema, 75 + }: { 76 + mode?: 'enum' | 'literal'; 77 + plugin: PydanticPlugin['Instance']; 78 + schema: SchemaWithType<'enum'>; 79 + }): EnumToTypeResult { 80 + const { enumMembers, isNullable } = extractEnumMembers(schema, plugin); 81 + 82 + if (enumMembers.length === 0) { 83 + return { 84 + enumMembers, 85 + isNullable, 86 + typeAnnotation: plugin.external('typing.Any'), 87 + }; 88 + } 89 + 90 + if (mode === 'literal') { 91 + return { 92 + enumMembers, 93 + isNullable, 94 + typeAnnotation: toLiteralType(enumMembers, plugin), 95 + }; 96 + } 97 + 98 + return { 99 + enumMembers, 100 + isNullable, 101 + }; 102 + }
+105
packages/openapi-python/src/plugins/pydantic/v2/toAst/intersection.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import type { AnnotationExpr } from '../../../../py-dsl'; 4 + import type { 5 + PydanticField, 6 + PydanticFinal, 7 + PydanticResult, 8 + PydanticType, 9 + } from '../../shared/types'; 10 + import type { PydanticPlugin } from '../../types'; 11 + import type { FieldConstraints } from '../constants'; 12 + 13 + export interface IntersectionToTypeResult extends PydanticType { 14 + baseClasses?: Array<string>; 15 + childResults: Array<PydanticResult>; 16 + mergedFields?: Array<PydanticField>; 17 + } 18 + 19 + export function intersectionToType({ 20 + applyModifiers, 21 + childResults, 22 + parentSchema, 23 + plugin, 24 + }: { 25 + applyModifiers: (result: PydanticResult, options?: { optional?: boolean }) => PydanticFinal; 26 + childResults: Array<PydanticResult>; 27 + parentSchema: IR.SchemaObject; 28 + plugin: PydanticPlugin['Instance']; 29 + }): IntersectionToTypeResult { 30 + const constraints: FieldConstraints = {}; 31 + 32 + if (parentSchema.description !== undefined) { 33 + constraints.description = parentSchema.description; 34 + } 35 + 36 + if (childResults.length === 0) { 37 + return { 38 + childResults, 39 + fieldConstraints: constraints, 40 + typeAnnotation: plugin.external('typing.Any'), 41 + }; 42 + } 43 + 44 + if (childResults.length === 1) { 45 + const finalResult = applyModifiers(childResults[0]!); 46 + return { 47 + childResults, 48 + fieldConstraints: { ...constraints, ...finalResult.fieldConstraints }, 49 + mergedFields: finalResult.fields, 50 + typeAnnotation: finalResult.typeAnnotation, 51 + }; 52 + } 53 + 54 + const baseClasses: Array<string> = []; 55 + const mergedFields: Array<PydanticField> = []; 56 + const seenFieldIds = new Set<number>(); 57 + 58 + for (const result of childResults) { 59 + const finalResult = applyModifiers(result); 60 + 61 + // TODO: replace 62 + const typeStr = String(finalResult.typeAnnotation); 63 + const isReference = 64 + !finalResult.fields && 65 + typeStr !== '' && 66 + !typeStr.startsWith('dict[') && 67 + !typeStr.startsWith('Dict[') && 68 + typeStr !== String(plugin.external('typing.Any')); 69 + 70 + if (isReference) { 71 + const baseName = typeStr.replace(/^'|'$/g, ''); 72 + if (baseName && !baseClasses.includes(baseName)) { 73 + baseClasses.push(baseName); 74 + } 75 + } 76 + 77 + if (finalResult.fields) { 78 + for (const field of finalResult.fields) { 79 + if (!seenFieldIds.has(field.name.id)) { 80 + seenFieldIds.add(field.name.id); 81 + mergedFields.push(field); 82 + } 83 + } 84 + } 85 + } 86 + 87 + let typeAnnotation: AnnotationExpr; 88 + 89 + if (baseClasses.length > 0 && mergedFields.length === 0) { 90 + typeAnnotation = baseClasses[0]!; 91 + } else if (mergedFields.length > 0) { 92 + // TODO: replace 93 + typeAnnotation = '__INTERSECTION_PLACEHOLDER__'; 94 + } else { 95 + typeAnnotation = plugin.external('typing.Any'); 96 + } 97 + 98 + return { 99 + baseClasses: baseClasses.length > 0 ? baseClasses : undefined, 100 + childResults, 101 + fieldConstraints: constraints, 102 + mergedFields: mergedFields.length > 0 ? mergedFields : undefined, 103 + typeAnnotation, 104 + }; 105 + }
+15
packages/openapi-python/src/plugins/pydantic/v2/toAst/never.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import type { PydanticType } from '../../shared/types'; 4 + import type { PydanticPlugin } from '../../types'; 5 + 6 + export function neverToType({ 7 + plugin, 8 + }: { 9 + plugin: PydanticPlugin['Instance']; 10 + schema: SchemaWithType<'never'>; 11 + }): PydanticType { 12 + return { 13 + typeAnnotation: plugin.external('typing.NoReturn'), 14 + }; 15 + }
+14
packages/openapi-python/src/plugins/pydantic/v2/toAst/null.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import type { PydanticType } from '../../shared/types'; 4 + import type { PydanticPlugin } from '../../types'; 5 + 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function nullToType(args: { 8 + plugin: PydanticPlugin['Instance']; 9 + schema: SchemaWithType<'null'>; 10 + }): PydanticType { 11 + return { 12 + typeAnnotation: 'None', 13 + }; 14 + }
+48
packages/openapi-python/src/plugins/pydantic/v2/toAst/number.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import { $ } from '../../../../py-dsl'; 4 + import type { PydanticType } from '../../shared/types'; 5 + import type { PydanticPlugin } from '../../types'; 6 + import type { FieldConstraints } from '../constants'; 7 + 8 + export function numberToType({ 9 + plugin, 10 + schema, 11 + }: { 12 + plugin: PydanticPlugin['Instance']; 13 + schema: SchemaWithType<'integer' | 'number'>; 14 + }): PydanticType { 15 + const constraints: FieldConstraints = {}; 16 + 17 + if (typeof schema.const === 'number') { 18 + const literal = plugin.external('typing.Literal'); 19 + return { 20 + typeAnnotation: $(literal).slice($.literal(schema.const)), 21 + }; 22 + } 23 + 24 + if (schema.minimum !== undefined) { 25 + constraints.ge = schema.minimum; 26 + } 27 + 28 + if (schema.exclusiveMinimum !== undefined) { 29 + constraints.gt = schema.exclusiveMinimum; 30 + } 31 + 32 + if (schema.maximum !== undefined) { 33 + constraints.le = schema.maximum; 34 + } 35 + 36 + if (schema.exclusiveMaximum !== undefined) { 37 + constraints.lt = schema.exclusiveMaximum; 38 + } 39 + 40 + if (schema.description !== undefined) { 41 + constraints.description = schema.description; 42 + } 43 + 44 + return { 45 + fieldConstraints: constraints, 46 + typeAnnotation: schema.type === 'integer' ? 'int' : 'float', 47 + }; 48 + }
+12 -9
packages/openapi-python/src/plugins/pydantic/v2/toAst/object.ts
··· 1 1 import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 - import { childContext } from '@hey-api/shared'; 2 + import { childContext, toCase } from '@hey-api/shared'; 3 3 4 - import { $, type MaybePyDsl } from '../../../../py-dsl'; 5 - import type { py } from '../../../../ts-python'; 4 + import { $, type AnnotationExpr } from '../../../../py-dsl'; 5 + import { safeRuntimeName } from '../../../../py-dsl/utils/name'; 6 6 import type { PydanticField, PydanticFinal, PydanticResult } from '../../shared/types'; 7 7 import type { PydanticPlugin } from '../../types'; 8 8 ··· 15 15 walkerCtx: SchemaVisitorContext<PydanticPlugin['Instance']>; 16 16 } 17 17 18 - export interface ObjectToFieldsResult { 18 + export interface ObjectToFieldsResult extends Pick<PydanticResult, 'fields' | 'typeAnnotation'> { 19 19 childResults: Array<PydanticResult>; 20 - fields?: Array<PydanticField>; // present = emit class 21 - typeAnnotation?: string | MaybePyDsl<py.Expression>; // present = emit type alias (dict case) 22 20 } 23 21 24 22 function resolveAdditionalProperties( 25 23 ctx: ObjectResolverContext, 26 - ): string | MaybePyDsl<py.Expression> | null | undefined { 24 + ): AnnotationExpr | null | undefined { 27 25 const { schema } = ctx; 28 26 29 27 if (!schema.additionalProperties || !schema.additionalProperties.type) return undefined; ··· 50 48 ctx._childResults.push(propertyResult); 51 49 52 50 const final = ctx.applyModifiers(propertyResult, { optional: isOptional }); 51 + const snakeCaseName = safeRuntimeName(toCase(name, 'snake_case')); 53 52 fields.push({ 54 53 fieldConstraints: final.fieldConstraints, 55 54 isOptional, 56 - name, 55 + name: ctx.plugin.symbol(snakeCaseName), 56 + originalName: name, 57 57 typeAnnotation: final.typeAnnotation, 58 58 }); 59 59 } ··· 71 71 return { fields }; 72 72 } 73 73 74 - if (additional && !ctx.schema.properties) { 74 + if (additional) { 75 75 const any = ctx.plugin.external('typing.Any'); 76 + if (!ctx.schema.properties) { 77 + return { typeAnnotation: $('dict').slice('str', any) }; 78 + } 76 79 return { typeAnnotation: $('dict').slice('str', any) }; 77 80 } 78 81
+17 -21
packages/openapi-python/src/plugins/pydantic/v2/toAst/string.ts
··· 1 1 import type { SchemaWithType } from '@hey-api/shared'; 2 2 3 - // import { $ } from '../../../../py-dsl'; 4 - import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 3 + import { $ } from '../../../../py-dsl'; 4 + import type { PydanticType } from '../../shared/types'; 5 + import type { PydanticPlugin } from '../../types'; 6 + import type { FieldConstraints } from '../constants'; 5 7 6 - export function stringToNode({ 8 + export function stringToType({ 9 + plugin, 7 10 schema, 8 - }: IrSchemaToAstOptions & { 11 + }: { 12 + plugin: PydanticPlugin['Instance']; 9 13 schema: SchemaWithType<'string'>; 10 - }): Ast { 11 - const constraints: Record<string, unknown> = {}; 14 + }): PydanticType { 15 + const constraints: FieldConstraints = {}; 16 + 17 + if (typeof schema.const === 'string') { 18 + const literal = plugin.external('typing.Literal'); 19 + return { 20 + typeAnnotation: $(literal).slice($.literal(schema.const)), 21 + }; 22 + } 12 23 13 24 if (schema.minLength !== undefined) { 14 25 constraints.min_length = schema.minLength; ··· 26 37 constraints.description = schema.description; 27 38 } 28 39 29 - if (typeof schema.const === 'string') { 30 - return { 31 - // expression: $.expr(`Literal["${schema.const}"]`), 32 - fieldConstraints: constraints, 33 - hasLazyExpression: false, 34 - models: [], 35 - // pipes: [], 36 - typeAnnotation: `Literal["${schema.const}"]`, 37 - }; 38 - } 39 - 40 40 return { 41 - // expression: $.expr('str'), 42 41 fieldConstraints: constraints, 43 - hasLazyExpression: false, 44 - models: [], 45 - // pipes: [], 46 42 typeAnnotation: 'str', 47 43 }; 48 44 }
+67
packages/openapi-python/src/plugins/pydantic/v2/toAst/tuple.ts
··· 1 + import type { SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; 2 + import { childContext } from '@hey-api/shared'; 3 + 4 + import { $, type AnnotationExpr } from '../../../../py-dsl'; 5 + import type { PydanticFinal, PydanticResult, PydanticType } from '../../shared/types'; 6 + import type { PydanticPlugin } from '../../types'; 7 + import type { FieldConstraints } from '../constants'; 8 + 9 + interface TupleToTypeContext { 10 + applyModifiers: (result: PydanticResult, options?: { optional?: boolean }) => PydanticFinal; 11 + plugin: PydanticPlugin['Instance']; 12 + schema: SchemaWithType<'tuple'>; 13 + walk: Walker<PydanticResult, PydanticPlugin['Instance']>; 14 + walkerCtx: SchemaVisitorContext<PydanticPlugin['Instance']>; 15 + } 16 + 17 + export interface TupleToTypeResult extends PydanticType { 18 + childResults: Array<PydanticResult>; 19 + } 20 + 21 + export function tupleToType(ctx: TupleToTypeContext): TupleToTypeResult { 22 + const { applyModifiers, plugin, schema, walk, walkerCtx } = ctx; 23 + 24 + const childResults: Array<PydanticResult> = []; 25 + const constraints: FieldConstraints = {}; 26 + const tuple = plugin.external('typing.Tuple'); 27 + const any = plugin.external('typing.Any'); 28 + 29 + if (schema.description !== undefined) { 30 + constraints.description = schema.description; 31 + } 32 + 33 + if (!schema.items || schema.items.length === 0) { 34 + return { 35 + childResults, 36 + fieldConstraints: constraints, 37 + typeAnnotation: $(tuple).slice(), 38 + }; 39 + } 40 + 41 + const itemTypes: Array<AnnotationExpr> = []; 42 + 43 + for (let i = 0; i < schema.items.length; i++) { 44 + const item = schema.items[i]!; 45 + const result = walk(item, childContext(walkerCtx, 'items', i)); 46 + childResults.push(result); 47 + 48 + const finalResult = applyModifiers(result); 49 + if (finalResult.typeAnnotation !== undefined) { 50 + itemTypes.push(finalResult.typeAnnotation); 51 + } 52 + } 53 + 54 + if (itemTypes.length === 0) { 55 + return { 56 + childResults, 57 + fieldConstraints: constraints, 58 + typeAnnotation: $(tuple).slice(any, '...'), 59 + }; 60 + } 61 + 62 + return { 63 + childResults, 64 + fieldConstraints: constraints, 65 + typeAnnotation: $(tuple).slice(...itemTypes), 66 + }; 67 + }
+14
packages/openapi-python/src/plugins/pydantic/v2/toAst/undefined.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import type { PydanticType } from '../../shared/types'; 4 + import type { PydanticPlugin } from '../../types'; 5 + 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function undefinedToType(args: { 8 + plugin: PydanticPlugin['Instance']; 9 + schema: SchemaWithType<'undefined'>; 10 + }): PydanticType { 11 + return { 12 + typeAnnotation: 'None', 13 + }; 14 + }
+77
packages/openapi-python/src/plugins/pydantic/v2/toAst/union.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + 3 + import { $ } from '../../../../py-dsl'; 4 + import type { PydanticFinal, PydanticResult, PydanticType } from '../../shared/types'; 5 + import type { PydanticPlugin } from '../../types'; 6 + import type { FieldConstraints } from '../constants'; 7 + 8 + export interface UnionToTypeResult extends PydanticType { 9 + childResults: Array<PydanticResult>; 10 + isNullable: boolean; 11 + } 12 + 13 + export function unionToType({ 14 + applyModifiers, 15 + childResults, 16 + parentSchema, 17 + plugin, 18 + }: { 19 + applyModifiers: (result: PydanticResult, options?: { optional?: boolean }) => PydanticFinal; 20 + childResults: Array<PydanticResult>; 21 + parentSchema: IR.SchemaObject; 22 + plugin: PydanticPlugin['Instance']; 23 + }): UnionToTypeResult { 24 + const constraints: FieldConstraints = {}; 25 + 26 + if (parentSchema.description !== undefined) { 27 + constraints.description = parentSchema.description; 28 + } 29 + 30 + const nonNullResults: Array<PydanticResult> = []; 31 + let isNullable = false; 32 + 33 + for (const result of childResults) { 34 + if (result.typeAnnotation === 'None') { 35 + isNullable = true; 36 + } else { 37 + nonNullResults.push(result); 38 + } 39 + } 40 + 41 + isNullable = isNullable || childResults.some((r) => r.meta.nullable); 42 + 43 + if (nonNullResults.length === 0) { 44 + return { 45 + childResults, 46 + fieldConstraints: constraints, 47 + isNullable: true, 48 + typeAnnotation: 'None', 49 + }; 50 + } 51 + 52 + if (nonNullResults.length === 1) { 53 + const finalResult = applyModifiers(nonNullResults[0]!); 54 + return { 55 + childResults, 56 + fieldConstraints: { ...constraints, ...finalResult.fieldConstraints }, 57 + isNullable, 58 + typeAnnotation: finalResult.typeAnnotation, 59 + }; 60 + } 61 + 62 + const union = plugin.external('typing.Union'); 63 + const itemTypes = nonNullResults.map( 64 + (r) => applyModifiers(r).typeAnnotation ?? plugin.external('typing.Any'), 65 + ); 66 + 67 + if (isNullable) { 68 + itemTypes.push('None'); 69 + } 70 + 71 + return { 72 + childResults, 73 + fieldConstraints: constraints, 74 + isNullable, 75 + typeAnnotation: $(union).slice(...itemTypes), 76 + }; 77 + }
+15
packages/openapi-python/src/plugins/pydantic/v2/toAst/unknown.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import type { PydanticType } from '../../shared/types'; 4 + import type { PydanticPlugin } from '../../types'; 5 + 6 + export function unknownToType({ 7 + plugin, 8 + }: { 9 + plugin: PydanticPlugin['Instance']; 10 + schema: SchemaWithType<'unknown'>; 11 + }): PydanticType { 12 + return { 13 + typeAnnotation: plugin.external('typing.Any'), 14 + }; 15 + }
+14
packages/openapi-python/src/plugins/pydantic/v2/toAst/void.ts
··· 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 3 + import type { PydanticType } from '../../shared/types'; 4 + import type { PydanticPlugin } from '../../types'; 5 + 6 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 + export function voidToType(args: { 8 + plugin: PydanticPlugin['Instance']; 9 + schema: SchemaWithType<'void'>; 10 + }): PydanticType { 11 + return { 12 + typeAnnotation: 'None', 13 + }; 14 + }
+145 -57
packages/openapi-python/src/plugins/pydantic/v2/walker.ts
··· 3 3 import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared'; 4 4 import { pathToJsonPointer } from '@hey-api/shared'; 5 5 6 + import { $ } from '../../../py-dsl'; 6 7 import { composeMeta, defaultMeta, inheritMeta } from '../shared/meta'; 7 8 import type { ProcessorContext } from '../shared/processor'; 8 9 import type { PydanticFinal, PydanticResult } from '../shared/types'; 9 10 import type { PydanticPlugin } from '../types'; 11 + import { arrayToType } from './toAst/array'; 10 12 import { booleanToType } from './toAst/boolean'; 13 + import { enumToType } from './toAst/enum'; 14 + import { intersectionToType } from './toAst/intersection'; 15 + import { neverToType } from './toAst/never'; 16 + import { nullToType } from './toAst/null'; 17 + import { numberToType } from './toAst/number'; 11 18 import { objectToFields } from './toAst/object'; 19 + import { stringToType } from './toAst/string'; 20 + import { tupleToType } from './toAst/tuple'; 21 + import { undefinedToType } from './toAst/undefined'; 22 + import { unionToType } from './toAst/union'; 23 + import { unknownToType } from './toAst/unknown'; 24 + import { voidToType } from './toAst/void'; 12 25 13 26 export interface VisitorConfig { 27 + /** Optional schema extractor function. */ 14 28 schemaExtractor?: SchemaExtractor<ProcessorContext>; 15 29 } 16 30 ··· 27 41 const needsOptional = optional || hasDefault; 28 42 const needsNullable = result.meta.nullable; 29 43 30 - let { typeAnnotation } = result; 31 - const fieldConstraints: Record<string, unknown> = { ...result.fieldConstraints }; 44 + let typeAnnotation = result.typeAnnotation; 45 + const fieldConstraints = { ...result.fieldConstraints }; 32 46 33 47 if (needsOptional || needsNullable) { 34 - const optional = ctx.plugin.external('typing.Optional'); 35 - typeAnnotation = `${optional}[${typeAnnotation}]`; 48 + const optionalType = ctx.plugin.external('typing.Optional'); 49 + typeAnnotation = $(optionalType).slice(typeAnnotation ?? ctx.plugin.external('typing.Any')); 36 50 if (needsOptional) { 37 51 fieldConstraints.default = hasDefault ? result.meta.default : null; 38 52 } 39 53 } 40 54 41 - return { fieldConstraints, fields: result.fields, typeAnnotation }; 55 + return { 56 + enumMembers: result.enumMembers, 57 + fieldConstraints, 58 + fields: result.fields, 59 + typeAnnotation, 60 + }; 42 61 }, 43 - // @ts-expect-error 44 - array(schema, ctx) { 62 + array(schema, ctx, walk) { 63 + const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => 64 + this.applyModifiers(result, ctx, opts) as PydanticFinal; 65 + 66 + const { childResults, fieldConstraints, typeAnnotation } = arrayToType({ 67 + applyModifiers, 68 + plugin: ctx.plugin, 69 + schema, 70 + walk, 71 + walkerCtx: ctx, 72 + }); 73 + 45 74 return { 46 - fieldConstraints: {}, 47 - meta: defaultMeta(schema), 48 - typeAnnotation: ctx.plugin.external('typing.Any'), 75 + fieldConstraints, 76 + meta: composeMeta(childResults, { ...defaultMeta(schema) }), 77 + typeAnnotation, 49 78 }; 50 79 }, 51 80 boolean(schema, ctx) { 52 - return booleanToType({ plugin: ctx.plugin, schema }); 81 + const result = booleanToType({ plugin: ctx.plugin, schema }); 82 + return { 83 + ...result, 84 + meta: defaultMeta(schema), 85 + }; 53 86 }, 54 - // @ts-expect-error 55 87 enum(schema, ctx) { 88 + const mode = ctx.plugin.config.enums ?? 'enum'; 89 + const result = enumToType({ mode, plugin: ctx.plugin, schema }); 56 90 return { 57 - fieldConstraints: {}, 91 + ...result, 58 92 meta: defaultMeta(schema), 59 - typeAnnotation: ctx.plugin.external('typing.Any'), 60 93 }; 61 94 }, 62 - integer(schema) { 63 - return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'int' }; 95 + integer(schema, ctx) { 96 + const result = numberToType({ plugin: ctx.plugin, schema }); 97 + return { 98 + ...result, 99 + meta: defaultMeta(schema), 100 + }; 64 101 }, 65 102 intercept(schema, ctx, walk) { 66 103 if (schemaExtractor && !schema.$ref) { ··· 74 111 if (extracted !== schema) return walk(extracted, ctx); 75 112 } 76 113 }, 77 - // @ts-expect-error 78 114 intersection(items, schemas, parentSchema, ctx) { 115 + const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => 116 + this.applyModifiers(result, ctx, opts) as PydanticFinal; 117 + 118 + const result = intersectionToType({ 119 + applyModifiers, 120 + childResults: items, 121 + parentSchema, 122 + plugin: ctx.plugin, 123 + }); 124 + 79 125 return { 80 - fieldConstraints: {}, 126 + ...result, 81 127 meta: composeMeta(items, { default: parentSchema.default }), 82 - typeAnnotation: ctx.plugin.external('typing.Any'), 83 128 }; 84 129 }, 85 - // @ts-expect-error 86 130 never(schema, ctx) { 131 + const result = neverToType({ plugin: ctx.plugin, schema }); 87 132 return { 88 - fieldConstraints: {}, 89 - meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 90 - typeAnnotation: ctx.plugin.external('typing.Any'), 133 + ...result, 134 + meta: { 135 + ...defaultMeta(schema), 136 + nullable: false, 137 + readonly: false, 138 + }, 91 139 }; 92 140 }, 93 - null(schema) { 141 + null(schema, ctx) { 142 + const result = nullToType({ plugin: ctx.plugin, schema }); 94 143 return { 95 - fieldConstraints: {}, 96 - meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 97 - typeAnnotation: 'None', 144 + ...result, 145 + meta: { 146 + ...defaultMeta(schema), 147 + nullable: true, 148 + readonly: false, 149 + }, 98 150 }; 99 151 }, 100 - number(schema) { 101 - return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'float' }; 152 + number(schema, ctx) { 153 + const result = numberToType({ plugin: ctx.plugin, schema }); 154 + return { 155 + ...result, 156 + meta: defaultMeta(schema), 157 + }; 102 158 }, 103 159 object(schema, ctx, walk) { 104 160 const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => ··· 113 169 }); 114 170 115 171 return { 116 - fieldConstraints: {}, 117 172 fields, 118 173 meta: inheritMeta(schema, childResults), 119 174 typeAnnotation: typeAnnotation ?? '', ··· 133 188 const refSymbol = ctx.plugin.referenceSymbol(query); 134 189 const isRegistered = ctx.plugin.isSymbolRegistered(query); 135 190 136 - // TODO: replace string with symbol 137 - const refName = typeof refSymbol === 'string' ? refSymbol : refSymbol.name; 138 191 return { 139 - fieldConstraints: {}, 140 - meta: { ...defaultMeta(schema), hasLazy: !isRegistered }, 141 - typeAnnotation: isRegistered ? refName : `'${refName}'`, 192 + meta: { 193 + ...defaultMeta(schema), 194 + hasForwardReference: !isRegistered, 195 + }, 196 + typeAnnotation: refSymbol, 142 197 }; 143 198 }, 144 - string(schema) { 145 - return { fieldConstraints: {}, meta: defaultMeta(schema), typeAnnotation: 'str' }; 199 + string(schema, ctx) { 200 + const result = stringToType({ plugin: ctx.plugin, schema }); 201 + return { 202 + ...result, 203 + meta: defaultMeta(schema), 204 + }; 146 205 }, 147 - // @ts-expect-error 148 - tuple(schema, ctx) { 206 + tuple(schema, ctx, walk) { 207 + const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => 208 + this.applyModifiers(result, ctx, opts) as PydanticFinal; 209 + 210 + const { childResults, fieldConstraints, typeAnnotation } = tupleToType({ 211 + applyModifiers, 212 + plugin: ctx.plugin, 213 + schema, 214 + walk, 215 + walkerCtx: ctx, 216 + }); 217 + 149 218 return { 150 - fieldConstraints: {}, 151 - meta: defaultMeta(schema), 152 - typeAnnotation: ctx.plugin.external('typing.Any'), 219 + fieldConstraints, 220 + meta: composeMeta(childResults, { ...defaultMeta(schema) }), 221 + typeAnnotation, 153 222 }; 154 223 }, 155 - undefined(schema) { 224 + undefined(schema, ctx) { 225 + const result = undefinedToType({ plugin: ctx.plugin, schema }); 156 226 return { 157 - fieldConstraints: {}, 158 - meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 159 - typeAnnotation: 'None', 227 + ...result, 228 + meta: { 229 + ...defaultMeta(schema), 230 + nullable: false, 231 + readonly: false, 232 + }, 160 233 }; 161 234 }, 162 - // @ts-expect-error 163 235 union(items, schemas, parentSchema, ctx) { 236 + const applyModifiers = (result: PydanticResult, opts?: { optional?: boolean }) => 237 + this.applyModifiers(result, ctx, opts) as PydanticFinal; 238 + 239 + const result = unionToType({ 240 + applyModifiers, 241 + childResults: items, 242 + parentSchema, 243 + plugin: ctx.plugin, 244 + }); 245 + 164 246 return { 165 - fieldConstraints: {}, 247 + ...result, 166 248 meta: composeMeta(items, { default: parentSchema.default }), 167 - typeAnnotation: ctx.plugin.external('typing.Any'), 168 249 }; 169 250 }, 170 - // @ts-expect-error 171 251 unknown(schema, ctx) { 252 + const result = unknownToType({ plugin: ctx.plugin, schema }); 172 253 return { 173 - fieldConstraints: {}, 174 - meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 175 - typeAnnotation: ctx.plugin.external('typing.Any'), 254 + ...result, 255 + meta: { 256 + ...defaultMeta(schema), 257 + nullable: false, 258 + readonly: false, 259 + }, 176 260 }; 177 261 }, 178 - void(schema) { 262 + void(schema, ctx) { 263 + const result = voidToType({ plugin: ctx.plugin, schema }); 179 264 return { 180 - fieldConstraints: {}, 181 - meta: { ...defaultMeta(schema), nullable: false, readonly: false }, 182 - typeAnnotation: 'None', 265 + ...result, 266 + meta: { 267 + ...defaultMeta(schema), 268 + nullable: false, 269 + readonly: false, 270 + }, 183 271 }; 184 272 }, 185 273 };
+1 -1
packages/openapi-python/src/py-dsl/decl/class.ts
··· 72 72 return this; 73 73 } 74 74 75 - override toAst(): py.ClassDeclaration { 75 + override toAst() { 76 76 this.$validate(); 77 77 // const uniqueClasses: Array<py.Expression> = []; 78 78
+1 -1
packages/openapi-python/src/py-dsl/decl/func.ts
··· 62 62 return this; 63 63 } 64 64 65 - override toAst(): py.FunctionDeclaration { 65 + override toAst() { 66 66 this.$validate(); 67 67 return py.factory.createFunctionDeclaration( 68 68 this.name.toString(),
+1 -1
packages/openapi-python/src/py-dsl/expr/attr.ts
··· 34 34 return this.missingRequiredCalls().length === 0; 35 35 } 36 36 37 - override toAst(): py.MemberExpression { 37 + override toAst() { 38 38 this.$validate(); 39 39 40 40 const leftNode = this.$node(this.left);
+1 -1
packages/openapi-python/src/py-dsl/expr/binary.ts
··· 132 132 return this.opAndExpr('*', right); 133 133 } 134 134 135 - override toAst(): py.BinaryExpression { 135 + override toAst() { 136 136 this.$validate(); 137 137 138 138 return py.factory.createBinaryExpression(
+1 -1
packages/openapi-python/src/py-dsl/expr/call.ts
··· 35 35 return this.missingRequiredCalls().length === 0; 36 36 } 37 37 38 - override toAst(): py.CallExpression { 38 + override toAst() { 39 39 this.$validate(); 40 40 41 41 return py.factory.createCallExpression(this.$node(this._callee!), this.$args());
+1 -1
packages/openapi-python/src/py-dsl/expr/dict.ts
··· 42 42 return this; 43 43 } 44 44 45 - override toAst(): py.DictExpression { 45 + override toAst() { 46 46 const astEntries = this._entries.map((entry) => ({ 47 47 key: this.$node(entry.key), 48 48 value: this.$node(entry.value),
+1 -1
packages/openapi-python/src/py-dsl/expr/identifier.ts
··· 18 18 ctx.analyze(this.name); 19 19 } 20 20 21 - override toAst(): py.Identifier { 21 + override toAst() { 22 22 return py.factory.createIdentifier(this.name.toString()); 23 23 } 24 24 }
+30
packages/openapi-python/src/py-dsl/expr/kwarg.ts
··· 1 + import { py } from '../../ts-python'; 2 + import type { MaybePyDsl } from '../base'; 3 + import { PyDsl } from '../base'; 4 + 5 + export type KwargValue = string | number | boolean | null | MaybePyDsl<py.Expression>; 6 + 7 + export class KwargPyDsl extends PyDsl<py.KeywordArgument> { 8 + readonly '~dsl' = 'KwargPyDsl'; 9 + 10 + constructor( 11 + private readonly argName: string, 12 + private readonly argValue: KwargValue, 13 + ) { 14 + super(); 15 + } 16 + 17 + override toAst() { 18 + return py.factory.createKeywordArgument(this.argName, this.$valueToNode(this.argValue)); 19 + } 20 + 21 + private $valueToNode(value: KwargValue) { 22 + if (value === null) { 23 + return py.factory.createIdentifier('None'); 24 + } 25 + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { 26 + return py.factory.createLiteral(value); 27 + } 28 + return this.$node(value); 29 + } 30 + }
+1 -1
packages/openapi-python/src/py-dsl/expr/list.ts
··· 34 34 return this; 35 35 } 36 36 37 - override toAst(): py.ListExpression { 37 + override toAst() { 38 38 const astElements = this._elements.map((el) => this.$node(el)); 39 39 return py.factory.createListExpression(astElements); 40 40 }
+1 -1
packages/openapi-python/src/py-dsl/expr/literal.ts
··· 21 21 super.analyze(_ctx); 22 22 } 23 23 24 - override toAst(): py.Literal { 24 + override toAst() { 25 25 return py.factory.createLiteral(this.value); 26 26 } 27 27 }
+1 -1
packages/openapi-python/src/py-dsl/expr/set.ts
··· 34 34 return this; 35 35 } 36 36 37 - override toAst(): py.SetExpression { 37 + override toAst() { 38 38 const astElements = this._elements.map((el) => this.$node(el)); 39 39 return py.factory.createSetExpression(astElements); 40 40 }
+1 -1
packages/openapi-python/src/py-dsl/expr/subscript.ts
··· 34 34 } 35 35 } 36 36 37 - override toAst(): py.SubscriptExpression { 37 + override toAst() { 38 38 const slice = 39 39 this._slices.length === 1 40 40 ? this.$node(this._slices[0]!)
+1 -1
packages/openapi-python/src/py-dsl/expr/tuple.ts
··· 36 36 return this; 37 37 } 38 38 39 - override toAst(): py.TupleExpression { 39 + override toAst() { 40 40 const astElements = this._elements.map((el) => this.$node(el)); 41 41 return py.factory.createTupleExpression(astElements); 42 42 }
+5
packages/openapi-python/src/py-dsl/index.ts
··· 23 23 import { ExprPyDsl } from './expr/expr'; 24 24 // import { fromValue as exprValue } from './expr/fromValue'; 25 25 import { IdPyDsl } from './expr/identifier'; 26 + import { KwargPyDsl } from './expr/kwarg'; 26 27 import { ListPyDsl } from './expr/list'; 27 28 import { LiteralPyDsl } from './expr/literal'; 28 29 // import { NewPyDsl } from './expr/new'; ··· 145 146 146 147 /** Creates an import statement. */ 147 148 import: (...args: ConstructorParameters<typeof ImportPyDsl>) => new ImportPyDsl(...args), 149 + 150 + /** Creates a keyword argument expression (e.g. `name=value`). */ 151 + kwarg: (...args: ConstructorParameters<typeof KwargPyDsl>) => new KwargPyDsl(...args), 148 152 149 153 /** Creates an initialization block or statement. */ 150 154 // init: (...args: ConstructorParameters<typeof InitTsDsl>) => new InitTsDsl(...args), ··· 316 320 // export type { MaybePyDsl, TypePyDsl } from './base'; 317 321 export { PyDsl } from './base'; 318 322 export type { CallArgs } from './expr/call'; 323 + export type { AnnotationExpr } from './stmt/var'; 319 324 export type { ExampleOptions } from './utils/context'; 320 325 export { ctx, PyDslContext } from './utils/context'; 321 326 export { keywords } from './utils/keywords';
+1 -1
packages/openapi-python/src/py-dsl/layout/doc.ts
··· 42 42 return lines.join('\n'); 43 43 } 44 44 45 - override toAst(): py.Comment { 45 + override toAst() { 46 46 // Return a dummy comment node for compliance. 47 47 return py.factory.createComment(this.resolve() ?? ''); 48 48 // return this.$node(new IdTsDsl(''));
+1 -1
packages/openapi-python/src/py-dsl/layout/hint.ts
··· 40 40 return node; 41 41 } 42 42 43 - override toAst(): py.Comment { 43 + override toAst() { 44 44 // Return a dummy comment node for compliance. 45 45 const lines = this._resolveLines(); 46 46 return py.factory.createComment(lines.join('\n'));
+1 -1
packages/openapi-python/src/py-dsl/layout/newline.ts
··· 10 10 super.analyze(ctx); 11 11 } 12 12 13 - override toAst(): py.EmptyStatement { 13 + override toAst() { 14 14 return py.factory.createEmptyStatement(); 15 15 } 16 16 }
+2 -2
packages/openapi-python/src/py-dsl/mixins/value.ts
··· 1 - import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 1 + import type { AnalysisContext, Node, NodeName } from '@hey-api/codegen-core'; 2 2 3 3 import type { py } from '../../ts-python'; 4 4 import type { MaybePyDsl } from '../base'; 5 5 import type { BaseCtor, MixinCtor } from './types'; 6 6 7 - export type ValueExpr = string | MaybePyDsl<py.Expression>; 7 + export type ValueExpr = NodeName | MaybePyDsl<py.Expression>; 8 8 9 9 export interface ValueMethods extends Node { 10 10 $value(): py.Expression | undefined;
+1 -1
packages/openapi-python/src/py-dsl/stmt/break.ts
··· 12 12 super.analyze(_ctx); 13 13 } 14 14 15 - override toAst(): py.BreakStatement { 15 + override toAst() { 16 16 return py.factory.createBreakStatement(); 17 17 } 18 18 }
+1 -1
packages/openapi-python/src/py-dsl/stmt/continue.ts
··· 12 12 super.analyze(_ctx); 13 13 } 14 14 15 - override toAst(): py.ContinueStatement { 15 + override toAst() { 16 16 return py.factory.createContinueStatement(); 17 17 } 18 18 }
+1 -1
packages/openapi-python/src/py-dsl/stmt/for.ts
··· 67 67 return this; 68 68 } 69 69 70 - override toAst(): py.ForStatement { 70 + override toAst() { 71 71 this.$validate(); 72 72 73 73 const body = new BlockPyDsl(...this._body!).$do();
+2 -7
packages/openapi-python/src/py-dsl/stmt/import.ts
··· 46 46 super.analyze(_ctx); 47 47 } 48 48 49 - override toAst(): py.ImportStatement { 50 - return { 51 - isFrom: this.isFrom, 52 - kind: py.PyNodeKind.ImportStatement, 53 - module: this.module, 54 - names: this.names, 55 - }; 49 + override toAst() { 50 + return py.factory.createImportStatement(this.module, this.names, this.isFrom); 56 51 } 57 52 }
+24 -18
packages/openapi-python/src/py-dsl/stmt/var.ts
··· 2 2 import { isSymbol } from '@hey-api/codegen-core'; 3 3 4 4 import { py } from '../../ts-python'; 5 - // import { TypePyDsl } from '../base'; 5 + import type { MaybePyDsl } from '../base'; 6 6 import { PyDsl } from '../base'; 7 - // import { DocMixin } from '../mixins/doc'; 8 - // import { HintMixin } from '../mixins/hint'; 9 - // import { DefaultMixin, ExportMixin } from '../mixins/modifiers'; 10 - // import { PatternMixin } from '../mixins/pattern'; 11 7 import { ValueMixin } from '../mixins/value'; 12 - // import { TypeExprPyDsl } from '../type/expr'; 13 8 import { safeRuntimeName } from '../utils/name'; 14 9 15 - // const Mixed = DefaultMixin( 16 - // DocMixin(ExportMixin(HintMixin(PatternMixin(ValueMixin(PyDsl<py.Assignment>))))), 17 - // ); 18 10 const Mixed = ValueMixin(PyDsl<py.Assignment>); 19 11 12 + export type AnnotationExpr = NodeName | MaybePyDsl<py.Expression>; 13 + 20 14 export class VarPyDsl extends Mixed { 21 15 readonly '~dsl' = 'VarPyDsl'; 22 16 override readonly nameSanitizer = safeRuntimeName; 23 17 24 - // protected _type?: TypePyDsl; 18 + protected _annotation?: AnnotationExpr; 25 19 26 20 constructor(name?: NodeName) { 27 21 super(); ··· 34 28 override analyze(ctx: AnalysisContext): void { 35 29 super.analyze(ctx); 36 30 ctx.analyze(this.name); 37 - // ctx.analyze(this._type); 31 + ctx.analyze(this._annotation); 38 32 } 39 33 40 34 /** Returns true when all required builder calls are present. */ ··· 42 36 return this.missingRequiredCalls().length === 0; 43 37 } 44 38 45 - // /** Sets the variable type annotation. */ 46 - // type(type: string | TypePyDsl): this { 47 - // this._type = type instanceof TypePyDsl ? type : new TypeExprPyDsl(type); 48 - // return this; 49 - // } 39 + /** Sets the type annotation for the variable. */ 40 + annotate(annotation: AnnotationExpr): this { 41 + this._annotation = annotation; 42 + return this; 43 + } 50 44 51 45 override toAst() { 52 46 this.$validate(); 53 - return py.factory.createAssignment(this.$node(this.name)!, this.$value()!); 47 + const target = this.$node(this.name)!; 48 + const annotation = this.$annotation(); 49 + const value = this.$value(); 50 + 51 + return py.factory.createAssignment(target, annotation, value); 54 52 } 55 53 56 54 $validate(): asserts this { ··· 59 57 throw new Error(`Variable assignment missing ${missing.join(' and ')}`); 60 58 } 61 59 60 + protected $annotation(): py.Expression | undefined { 61 + return this.$node(this._annotation); 62 + } 63 + 62 64 private missingRequiredCalls(): ReadonlyArray<string> { 63 65 const missing: Array<string> = []; 64 66 if (!this.$node(this.name)) missing.push('name'); 65 - if (!this.$value()) missing.push('.value()'); 67 + const hasAnnotation = this.$annotation(); 68 + const hasValue = this.$value(); 69 + if (!hasAnnotation && !hasValue) { 70 + missing.push('.annotate() or .assign()'); 71 + } 66 72 return missing; 67 73 } 68 74 }
+1 -1
packages/openapi-python/src/py-dsl/stmt/while.ts
··· 60 60 return this; 61 61 } 62 62 63 - override toAst(): py.WhileStatement { 63 + override toAst() { 64 64 this.$validate(); 65 65 66 66 const body = new BlockPyDsl(...this._body!).$do();
+1 -1
packages/openapi-python/src/py-dsl/stmt/with.ts
··· 72 72 return this; 73 73 } 74 74 75 - override toAst(): py.WithStatement { 75 + override toAst() { 76 76 this.$validate(); 77 77 78 78 const astItems = this._items.map((item) => {
+30
packages/openapi-python/src/py-dsl/utils/__tests__/name.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { safeRuntimeName } from '../name'; 4 + 5 + describe('safeRuntimeName', () => { 6 + const scenarios = [ 7 + // Digits: valid as regular char, can reprocess → leading underscore 8 + { name: '3foo', output: '_3foo' }, 9 + { name: '123', output: '_123' }, 10 + 11 + // $ sign: invalid in Python as regular char → single underscore, skip reprocess 12 + { name: '$schema', output: '_schema' }, 13 + { name: '$foo', output: '_foo' }, 14 + 15 + // Hyphen: first char is valid (a, f), hyphen becomes underscore in loop 16 + { name: 'api-version', output: 'api_version' }, 17 + { name: 'foo-bar', output: 'foo_bar' }, 18 + 19 + // Normal cases 20 + { name: 'foo', output: 'foo' }, 21 + { name: '_private', output: '_private' }, 22 + 23 + // Reserved words 24 + { name: 'class', output: 'class_' }, 25 + ] as const; 26 + 27 + it.each(scenarios)('transforms $name -> $output', ({ name, output }) => { 28 + expect(safeRuntimeName(name)).toEqual(output); 29 + }); 30 + });
+1 -1
packages/openapi-python/src/py-dsl/utils/lazy.ts
··· 26 26 return this._thunk(ctx); 27 27 } 28 28 29 - override toAst(): T { 29 + override toAst() { 30 30 return this.toResult().toAst(); 31 31 } 32 32 }
+11 -3
packages/openapi-python/src/py-dsl/utils/name.ts
··· 15 15 return `'${name}'`; 16 16 }; 17 17 18 + const validPythonChar = /^[a-zA-Z0-9_]$/; 19 + 18 20 const safeName = (name: string, reserved: ReservedList): string => { 19 21 let sanitized = ''; 20 22 let index: number; ··· 22 24 const first = name[0] ?? ''; 23 25 regexp.illegalStartCharacters.lastIndex = 0; 24 26 if (regexp.illegalStartCharacters.test(first)) { 25 - sanitized += '_'; 26 - index = 0; 27 + // Check if character becomes valid when not in leading position (e.g., digits) 28 + if (validPythonChar.test(first)) { 29 + sanitized += '_'; 30 + index = 0; 31 + } else { 32 + sanitized += '_'; 33 + index = 1; 34 + } 27 35 } else { 28 36 sanitized += first; 29 37 index = 1; ··· 31 39 32 40 while (index < name.length) { 33 41 const char = name[index] ?? ''; 34 - sanitized += /^[a-zA-Z0-9_]$/.test(char) ? char : '_'; 42 + sanitized += validPythonChar.test(char) ? char : '_'; 35 43 index += 1; 36 44 } 37 45
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/kwarg/__init__.py

This is a binary file and will not be displayed.

+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/kwarg/multiple.py
··· 1 + result = Field(..., min_length=1, max_length=100, description="A field")
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/kwarg/number.py
··· 1 + result = func(count=42)
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/expressions/kwarg/string.py
··· 1 + result = func(name="test")
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/statements/assignment/annotation-only.py
··· 1 + name: str
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/statements/assignment/annotation-with-value.py
··· 1 + name: str = "default"
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/statements/assignment/complex-annotation.py
··· 1 + items: List[str] = Field(..., min_length=1)
+1
packages/openapi-python/src/ts-python/__snapshots__/nodes/statements/assignment/optional-annotation.py
··· 1 + name: Optional[str] = None
+22 -4
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/binary.test.ts
··· 4 4 describe('binary expression', () => { 5 5 it('add', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(42)), 8 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(84)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('a'), 9 + undefined, 10 + py.factory.createLiteral(42), 11 + ), 12 + py.factory.createAssignment( 13 + py.factory.createIdentifier('b'), 14 + undefined, 15 + py.factory.createLiteral(84), 16 + ), 9 17 py.factory.createAssignment( 10 18 py.factory.createIdentifier('z'), 19 + undefined, 11 20 py.factory.createBinaryExpression( 12 21 py.factory.createIdentifier('a'), 13 22 '+', ··· 20 29 21 30 it('subtract', async () => { 22 31 const file = py.factory.createSourceFile([ 23 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(42)), 24 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(84)), 32 + py.factory.createAssignment( 33 + py.factory.createIdentifier('a'), 34 + undefined, 35 + py.factory.createLiteral(42), 36 + ), 37 + py.factory.createAssignment( 38 + py.factory.createIdentifier('b'), 39 + undefined, 40 + py.factory.createLiteral(84), 41 + ), 25 42 py.factory.createAssignment( 26 43 py.factory.createIdentifier('z'), 44 + undefined, 27 45 py.factory.createBinaryExpression( 28 46 py.factory.createIdentifier('a'), 29 47 '-',
+1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/dict.test.ts
··· 11 11 [ 12 12 py.factory.createAssignment( 13 13 py.factory.createIdentifier('items'), 14 + undefined, 14 15 py.factory.createDictExpression([ 15 16 { 16 17 key: py.factory.createLiteral('key1'),
+2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/list.test.ts
··· 11 11 [ 12 12 py.factory.createAssignment( 13 13 py.factory.createIdentifier('items'), 14 + undefined, 14 15 py.factory.createListExpression([ 15 16 py.factory.createLiteral(1), 16 17 py.factory.createLiteral(2), ··· 19 20 ), 20 21 py.factory.createAssignment( 21 22 py.factory.createIdentifier('evens'), 23 + undefined, 22 24 py.factory.createListComprehension( 23 25 py.factory.createIdentifier('x'), 24 26 py.factory.createIdentifier('x'),
+1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/nested.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('data'), 9 + undefined, 9 10 py.factory.createDictExpression([ 10 11 { 11 12 key: py.factory.createLiteral('numbers'),
+2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/set.test.ts
··· 11 11 [ 12 12 py.factory.createAssignment( 13 13 py.factory.createIdentifier('items'), 14 + undefined, 14 15 py.factory.createListExpression([ 15 16 py.factory.createLiteral(1), 16 17 py.factory.createLiteral(2), ··· 19 20 ), 20 21 py.factory.createAssignment( 21 22 py.factory.createIdentifier('unique_evens'), 23 + undefined, 22 24 py.factory.createSetComprehension( 23 25 py.factory.createIdentifier('x'), 24 26 py.factory.createIdentifier('x'),
+1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/dict.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('person'), 9 + undefined, 9 10 py.factory.createDictExpression([ 10 11 { 11 12 key: py.factory.createLiteral('name'),
+11 -2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/fString.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('name'), 9 + undefined, 9 10 py.factory.createLiteral('Joe'), 10 11 ), 11 12 py.factory.createExpressionStatement( ··· 19 20 20 21 it('with multiple expressions', async () => { 21 22 const file = py.factory.createSourceFile([ 22 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(1)), 23 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(2)), 23 + py.factory.createAssignment( 24 + py.factory.createIdentifier('a'), 25 + undefined, 26 + py.factory.createLiteral(1), 27 + ), 28 + py.factory.createAssignment( 29 + py.factory.createIdentifier('b'), 30 + undefined, 31 + py.factory.createLiteral(2), 32 + ), 24 33 py.factory.createExpressionStatement( 25 34 py.factory.createCallExpression(py.factory.createIdentifier('print'), [ 26 35 py.factory.createFStringExpression([
+3
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/generator.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('x_iter'), 9 + undefined, 9 10 py.factory.createListExpression([ 10 11 py.factory.createLiteral(1), 11 12 py.factory.createLiteral(2), ··· 27 28 const file = py.factory.createSourceFile([ 28 29 py.factory.createAssignment( 29 30 py.factory.createIdentifier('x_iter'), 31 + undefined, 30 32 py.factory.createListExpression([ 31 33 py.factory.createLiteral(1), 32 34 py.factory.createLiteral(2), ··· 55 57 const file = py.factory.createSourceFile([ 56 58 py.factory.createAssignment( 57 59 py.factory.createIdentifier('x_iter'), 60 + undefined, 58 61 py.factory.createListExpression([ 59 62 py.factory.createLiteral(1), 60 63 py.factory.createLiteral(2),
+6 -1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/identifier.test.ts
··· 4 4 describe('identifier expression', () => { 5 5 it('assignment', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('y'), py.factory.createLiteral(42)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('y'), 9 + undefined, 10 + py.factory.createLiteral(42), 11 + ), 8 12 py.factory.createAssignment( 9 13 py.factory.createIdentifier('x'), 14 + undefined, 10 15 py.factory.createIdentifier('y'), 11 16 ), 12 17 ]);
+46
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/kwarg.test.ts
··· 1 + import { py } from '../../../index'; 2 + import { assertPrintedMatchesSnapshot } from '../utils'; 3 + 4 + describe('keyword argument expression', () => { 5 + it('string value', async () => { 6 + const file = py.factory.createSourceFile([ 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('result'), 9 + undefined, 10 + py.factory.createCallExpression(py.factory.createIdentifier('func'), [ 11 + py.factory.createKeywordArgument('name', py.factory.createLiteral('test')), 12 + ]), 13 + ), 14 + ]); 15 + await assertPrintedMatchesSnapshot(file, 'string.py'); 16 + }); 17 + 18 + it('number value', async () => { 19 + const file = py.factory.createSourceFile([ 20 + py.factory.createAssignment( 21 + py.factory.createIdentifier('result'), 22 + undefined, 23 + py.factory.createCallExpression(py.factory.createIdentifier('func'), [ 24 + py.factory.createKeywordArgument('count', py.factory.createLiteral(42)), 25 + ]), 26 + ), 27 + ]); 28 + await assertPrintedMatchesSnapshot(file, 'number.py'); 29 + }); 30 + 31 + it('multiple keyword arguments', async () => { 32 + const file = py.factory.createSourceFile([ 33 + py.factory.createAssignment( 34 + py.factory.createIdentifier('result'), 35 + undefined, 36 + py.factory.createCallExpression(py.factory.createIdentifier('Field'), [ 37 + py.factory.createIdentifier('...'), 38 + py.factory.createKeywordArgument('min_length', py.factory.createLiteral(1)), 39 + py.factory.createKeywordArgument('max_length', py.factory.createLiteral(100)), 40 + py.factory.createKeywordArgument('description', py.factory.createLiteral('A field')), 41 + ]), 42 + ), 43 + ]); 44 + await assertPrintedMatchesSnapshot(file, 'multiple.py'); 45 + }); 46 + });
+5 -1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/lambda.test.ts
··· 4 4 describe('lambda expression', () => { 5 5 it('simple', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('x'), py.factory.createLiteral(5)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('x'), 9 + undefined, 10 + py.factory.createLiteral(5), 11 + ), 8 12 py.factory.createExpressionStatement( 9 13 py.factory.createLambdaExpression( 10 14 [],
+1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/list.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('nums'), 9 + undefined, 9 10 py.factory.createListExpression([ 10 11 py.factory.createLiteral(1), 11 12 py.factory.createLiteral(2),
+12 -2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/literal.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('s'), 9 + undefined, 9 10 py.factory.createLiteral('hello'), 10 11 ), 11 - py.factory.createAssignment(py.factory.createIdentifier('n'), py.factory.createLiteral(123)), 12 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(true)), 12 + py.factory.createAssignment( 13 + py.factory.createIdentifier('n'), 14 + undefined, 15 + py.factory.createLiteral(123), 16 + ), 17 + py.factory.createAssignment( 18 + py.factory.createIdentifier('b'), 19 + undefined, 20 + py.factory.createLiteral(true), 21 + ), 13 22 py.factory.createAssignment( 14 23 py.factory.createIdentifier('none'), 24 + undefined, 15 25 py.factory.createLiteral(null), 16 26 ), 17 27 ]);
+4
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/set.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('foo'), 9 + undefined, 9 10 py.factory.createLiteral('bar'), 10 11 ), 11 12 py.factory.createAssignment( 12 13 py.factory.createIdentifier('emptySet'), 14 + undefined, 13 15 py.factory.createSetExpression([]), 14 16 ), 15 17 py.factory.createAssignment( 16 18 py.factory.createIdentifier('numberSet'), 19 + undefined, 17 20 py.factory.createSetExpression([ 18 21 py.factory.createLiteral(1), 19 22 py.factory.createLiteral(2), ··· 22 25 ), 23 26 py.factory.createAssignment( 24 27 py.factory.createIdentifier('mixedSet'), 28 + undefined, 25 29 py.factory.createSetExpression([ 26 30 py.factory.createLiteral('a'), 27 31 py.factory.createLiteral(true),
+5
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/subscript.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('numbers'), 9 + undefined, 9 10 py.factory.createSubscriptExpression( 10 11 py.factory.createIdentifier('list'), 11 12 py.factory.createIdentifier('int'), ··· 19 20 const file = py.factory.createSourceFile([ 20 21 py.factory.createAssignment( 21 22 py.factory.createIdentifier('data'), 23 + undefined, 22 24 py.factory.createSubscriptExpression( 23 25 py.factory.createIdentifier('dict'), 24 26 py.factory.createSubscriptSlice([ ··· 35 37 const file = py.factory.createSourceFile([ 36 38 py.factory.createAssignment( 37 39 py.factory.createIdentifier('items'), 40 + undefined, 38 41 py.factory.createTupleExpression([ 39 42 py.factory.createLiteral(1), 40 43 py.factory.createLiteral(2), ··· 43 46 ), 44 47 py.factory.createAssignment( 45 48 py.factory.createIdentifier('first'), 49 + undefined, 46 50 py.factory.createSubscriptExpression( 47 51 py.factory.createIdentifier('items'), 48 52 py.factory.createLiteral(0), ··· 56 60 const file = py.factory.createSourceFile([ 57 61 py.factory.createAssignment( 58 62 py.factory.createIdentifier('matrix'), 63 + undefined, 59 64 py.factory.createSubscriptExpression( 60 65 py.factory.createIdentifier('list'), 61 66 py.factory.createSubscriptExpression(
+2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/tuple.test.ts
··· 6 6 const file = py.factory.createSourceFile([ 7 7 py.factory.createAssignment( 8 8 py.factory.createIdentifier('t'), 9 + undefined, 9 10 py.factory.createTupleExpression([ 10 11 py.factory.createLiteral(1), 11 12 py.factory.createLiteral(2), ··· 14 15 ), 15 16 py.factory.createAssignment( 16 17 py.factory.createIdentifier('single'), 18 + undefined, 17 19 py.factory.createTupleExpression([py.factory.createLiteral(42)]), 18 20 ), 19 21 ]);
+1
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/yield.test.ts
··· 26 26 const file = py.factory.createSourceFile([ 27 27 py.factory.createAssignment( 28 28 py.factory.createIdentifier('iterable'), 29 + undefined, 29 30 py.factory.createListExpression([ 30 31 py.factory.createLiteral(1), 31 32 py.factory.createLiteral(2),
+57 -1
packages/openapi-python/src/ts-python/__tests__/nodes/statements/assignment.test.ts
··· 4 4 describe('assignment statement', () => { 5 5 it('primitive variables', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('foo'), py.factory.createLiteral(42)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('foo'), 9 + undefined, 10 + py.factory.createLiteral(42), 11 + ), 8 12 ]); 9 13 await assertPrintedMatchesSnapshot(file, 'primitive.py'); 14 + }); 15 + 16 + it('annotation only', async () => { 17 + const file = py.factory.createSourceFile([ 18 + py.factory.createAssignment( 19 + py.factory.createIdentifier('name'), 20 + py.factory.createIdentifier('str'), 21 + ), 22 + ]); 23 + await assertPrintedMatchesSnapshot(file, 'annotation-only.py'); 24 + }); 25 + 26 + it('annotation with value', async () => { 27 + const file = py.factory.createSourceFile([ 28 + py.factory.createAssignment( 29 + py.factory.createIdentifier('name'), 30 + py.factory.createIdentifier('str'), 31 + py.factory.createLiteral('default'), 32 + ), 33 + ]); 34 + await assertPrintedMatchesSnapshot(file, 'annotation-with-value.py'); 35 + }); 36 + 37 + it('optional annotation', async () => { 38 + const file = py.factory.createSourceFile([ 39 + py.factory.createAssignment( 40 + py.factory.createIdentifier('name'), 41 + py.factory.createSubscriptExpression( 42 + py.factory.createIdentifier('Optional'), 43 + py.factory.createIdentifier('str'), 44 + ), 45 + py.factory.createIdentifier('None'), 46 + ), 47 + ]); 48 + await assertPrintedMatchesSnapshot(file, 'optional-annotation.py'); 49 + }); 50 + 51 + it('complex type annotation', async () => { 52 + const file = py.factory.createSourceFile([ 53 + py.factory.createAssignment( 54 + py.factory.createIdentifier('items'), 55 + py.factory.createSubscriptExpression( 56 + py.factory.createIdentifier('List'), 57 + py.factory.createIdentifier('str'), 58 + ), 59 + py.factory.createCallExpression(py.factory.createIdentifier('Field'), [ 60 + py.factory.createIdentifier('...'), 61 + py.factory.createKeywordArgument('min_length', py.factory.createLiteral(1)), 62 + ]), 63 + ), 64 + ]); 65 + await assertPrintedMatchesSnapshot(file, 'complex-annotation.py'); 10 66 }); 11 67 });
+60 -12
packages/openapi-python/src/ts-python/__tests__/nodes/statements/augmentedAssignment.test.ts
··· 4 4 describe('augmented assignment statement', () => { 5 5 it('arithmetic operators', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('x'), py.factory.createLiteral(0)), 8 - py.factory.createAssignment(py.factory.createIdentifier('y'), py.factory.createLiteral(0)), 9 - py.factory.createAssignment(py.factory.createIdentifier('z'), py.factory.createLiteral(0)), 10 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(0.0)), 11 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(0)), 12 - py.factory.createAssignment(py.factory.createIdentifier('c'), py.factory.createLiteral(0)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('x'), 9 + undefined, 10 + py.factory.createLiteral(0), 11 + ), 12 + py.factory.createAssignment( 13 + py.factory.createIdentifier('y'), 14 + undefined, 15 + py.factory.createLiteral(0), 16 + ), 17 + py.factory.createAssignment( 18 + py.factory.createIdentifier('z'), 19 + undefined, 20 + py.factory.createLiteral(0), 21 + ), 22 + py.factory.createAssignment( 23 + py.factory.createIdentifier('a'), 24 + undefined, 25 + py.factory.createLiteral(0.0), 26 + ), 27 + py.factory.createAssignment( 28 + py.factory.createIdentifier('b'), 29 + undefined, 30 + py.factory.createLiteral(0), 31 + ), 32 + py.factory.createAssignment( 33 + py.factory.createIdentifier('c'), 34 + undefined, 35 + py.factory.createLiteral(0), 36 + ), 13 37 14 38 py.factory.createAugmentedAssignment( 15 39 py.factory.createIdentifier('x'), ··· 47 71 48 72 it('power and bitwise operators', async () => { 49 73 const file = py.factory.createSourceFile([ 50 - py.factory.createAssignment(py.factory.createIdentifier('x'), py.factory.createLiteral(1)), 51 - py.factory.createAssignment(py.factory.createIdentifier('y'), py.factory.createLiteral(1)), 52 - py.factory.createAssignment(py.factory.createIdentifier('z'), py.factory.createLiteral(1)), 53 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(1)), 54 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(1)), 55 - py.factory.createAssignment(py.factory.createIdentifier('c'), py.factory.createLiteral(1)), 74 + py.factory.createAssignment( 75 + py.factory.createIdentifier('x'), 76 + undefined, 77 + py.factory.createLiteral(1), 78 + ), 79 + py.factory.createAssignment( 80 + py.factory.createIdentifier('y'), 81 + undefined, 82 + py.factory.createLiteral(1), 83 + ), 84 + py.factory.createAssignment( 85 + py.factory.createIdentifier('z'), 86 + undefined, 87 + py.factory.createLiteral(1), 88 + ), 89 + py.factory.createAssignment( 90 + py.factory.createIdentifier('a'), 91 + undefined, 92 + py.factory.createLiteral(1), 93 + ), 94 + py.factory.createAssignment( 95 + py.factory.createIdentifier('b'), 96 + undefined, 97 + py.factory.createLiteral(1), 98 + ), 99 + py.factory.createAssignment( 100 + py.factory.createIdentifier('c'), 101 + undefined, 102 + py.factory.createLiteral(1), 103 + ), 56 104 57 105 py.factory.createAugmentedAssignment( 58 106 py.factory.createIdentifier('x'),
+1
packages/openapi-python/src/ts-python/__tests__/nodes/statements/for.test.ts
··· 25 25 const file = py.factory.createSourceFile([ 26 26 py.factory.createAssignment( 27 27 py.factory.createIdentifier('items'), 28 + undefined, 28 29 py.factory.createListExpression([ 29 30 py.factory.createLiteral(1), 30 31 py.factory.createLiteral(2),
+5 -1
packages/openapi-python/src/ts-python/__tests__/nodes/statements/if.test.ts
··· 17 17 18 18 it('with else', async () => { 19 19 const file = py.factory.createSourceFile([ 20 - py.factory.createAssignment(py.factory.createIdentifier('x'), py.factory.createLiteral(0)), 20 + py.factory.createAssignment( 21 + py.factory.createIdentifier('x'), 22 + undefined, 23 + py.factory.createLiteral(0), 24 + ), 21 25 py.factory.createIfStatement( 22 26 py.factory.createBinaryExpression( 23 27 py.factory.createIdentifier('x'),
+6 -1
packages/openapi-python/src/ts-python/__tests__/nodes/statements/while.test.ts
··· 4 4 describe('while statement', () => { 5 5 it('simple', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('x'), py.factory.createLiteral(3)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('x'), 9 + undefined, 10 + py.factory.createLiteral(3), 11 + ), 8 12 py.factory.createWhileStatement( 9 13 py.factory.createBinaryExpression( 10 14 py.factory.createIdentifier('x'), ··· 19 23 ), 20 24 py.factory.createAssignment( 21 25 py.factory.createIdentifier('x'), 26 + undefined, 22 27 py.factory.createBinaryExpression( 23 28 py.factory.createIdentifier('x'), 24 29 '-',
+11 -2
packages/openapi-python/src/ts-python/__tests__/nodes/structure/sourceFile.test.ts
··· 4 4 describe('source file', () => { 5 5 it('simple', async () => { 6 6 const file = py.factory.createSourceFile([ 7 - py.factory.createAssignment(py.factory.createIdentifier('a'), py.factory.createLiteral(1)), 8 - py.factory.createAssignment(py.factory.createIdentifier('b'), py.factory.createLiteral(2)), 7 + py.factory.createAssignment( 8 + py.factory.createIdentifier('a'), 9 + undefined, 10 + py.factory.createLiteral(1), 11 + ), 12 + py.factory.createAssignment( 13 + py.factory.createIdentifier('b'), 14 + undefined, 15 + py.factory.createLiteral(2), 16 + ), 9 17 ]); 10 18 await assertPrintedMatchesSnapshot(file, 'simple.py'); 11 19 }); ··· 15 23 [ 16 24 py.factory.createAssignment( 17 25 py.factory.createIdentifier('foo'), 26 + undefined, 18 27 py.factory.createLiteral(1), 19 28 ), 20 29 ],
+2
packages/openapi-python/src/ts-python/index.ts
··· 21 21 import type { PyFStringExpression as _PyFStringExpression } from './nodes/expressions/fString'; 22 22 import type { PyGeneratorExpression as _PyGeneratorExpression } from './nodes/expressions/generator'; 23 23 import type { PyIdentifier as _PyIdentifier } from './nodes/expressions/identifier'; 24 + import type { PyKeywordArgument as _PyKeywordArgument } from './nodes/expressions/keywordArg'; 24 25 import type { PyLambdaExpression as _PyLambdaExpression } from './nodes/expressions/lambda'; 25 26 import type { PyListExpression as _PyListExpression } from './nodes/expressions/list'; 26 27 import type { PyLiteral as _PyLiteral } from './nodes/expressions/literal'; ··· 107 108 export type FStringExpression = _PyFStringExpression; 108 109 export type GeneratorExpression = _PyGeneratorExpression; 109 110 export type Identifier = _PyIdentifier; 111 + export type KeywordArgument = _PyKeywordArgument; 110 112 export type LambdaExpression = _PyLambdaExpression; 111 113 export type ListExpression = _PyListExpression; 112 114 export type Literal = _PyLiteral;
+2
packages/openapi-python/src/ts-python/nodes/expression.ts
··· 7 7 import type { PyFStringExpression } from './expressions/fString'; 8 8 import type { PyGeneratorExpression } from './expressions/generator'; 9 9 import type { PyIdentifier } from './expressions/identifier'; 10 + import type { PyKeywordArgument } from './expressions/keywordArg'; 10 11 import type { PyLambdaExpression } from './expressions/lambda'; 11 12 import type { PyListExpression } from './expressions/list'; 12 13 import type { PyLiteral } from './expressions/literal'; ··· 28 29 | PyFStringExpression 29 30 | PyGeneratorExpression 30 31 | PyIdentifier 32 + | PyKeywordArgument 31 33 | PyLambdaExpression 32 34 | PyListExpression 33 35 | PyLiteral
+24
packages/openapi-python/src/ts-python/nodes/expressions/keywordArg.ts
··· 1 + import type { PyNodeBase } from '../base'; 2 + import type { PyExpression } from '../expression'; 3 + import { PyNodeKind } from '../kinds'; 4 + 5 + export interface PyKeywordArgument extends PyNodeBase { 6 + kind: PyNodeKind.KeywordArgument; 7 + name: string; 8 + value: PyExpression; 9 + } 10 + 11 + export function createKeywordArgument( 12 + name: string, 13 + value: PyExpression, 14 + leadingComments?: ReadonlyArray<string>, 15 + trailingComments?: ReadonlyArray<string>, 16 + ): PyKeywordArgument { 17 + return { 18 + kind: PyNodeKind.KeywordArgument, 19 + leadingComments, 20 + name, 21 + trailingComments, 22 + value, 23 + }; 24 + }
+2
packages/openapi-python/src/ts-python/nodes/factory.ts
··· 12 12 import { createFStringExpression } from './expressions/fString'; 13 13 import { createGeneratorExpression } from './expressions/generator'; 14 14 import { createIdentifier } from './expressions/identifier'; 15 + import { createKeywordArgument } from './expressions/keywordArg'; 15 16 import { createLambdaExpression } from './expressions/lambda'; 16 17 import { createListExpression } from './expressions/list'; 17 18 import { createLiteral } from './expressions/literal'; ··· 67 68 createIdentifier, 68 69 createIfStatement, 69 70 createImportStatement, 71 + createKeywordArgument, 70 72 createLambdaExpression, 71 73 createListComprehension, 72 74 createListExpression,
+1
packages/openapi-python/src/ts-python/nodes/kinds.ts
··· 23 23 Identifier = 'Identifier', 24 24 IfStatement = 'IfStatement', 25 25 ImportStatement = 'ImportStatement', 26 + KeywordArgument = 'KeywordArgument', 26 27 LambdaExpression = 'LambdaExpression', 27 28 ListComprehension = 'ListComprehension', 28 29 ListExpression = 'ListExpression',
+9 -2
packages/openapi-python/src/ts-python/nodes/statements/assignment.ts
··· 3 3 import { PyNodeKind } from '../kinds'; 4 4 5 5 export interface PyAssignment extends PyNodeBase { 6 + annotation?: PyExpression; 6 7 kind: PyNodeKind.Assignment; 7 8 target: PyExpression; 8 - value: PyExpression; 9 + value?: PyExpression; 9 10 } 10 11 11 12 export function createAssignment( 12 13 target: PyExpression, 13 - value: PyExpression, 14 + annotation?: PyExpression, 15 + value?: PyExpression, 14 16 leadingComments?: ReadonlyArray<string>, 15 17 trailingComments?: ReadonlyArray<string>, 16 18 ): PyAssignment { 19 + if (!annotation && !value) { 20 + throw new Error('Assignment requires at least annotation or value'); 21 + } 22 + 17 23 return { 24 + annotation, 18 25 kind: PyNodeKind.Assignment, 19 26 leadingComments, 20 27 target,
+18 -4
packages/openapi-python/src/ts-python/printer.ts
··· 48 48 let indentTrailingComments = false; 49 49 50 50 switch (node.kind) { 51 - case PyNodeKind.Assignment: 52 - parts.push(printLine(`${printNode(node.target)} = ${printNode(node.value)}`)); 51 + case PyNodeKind.Assignment: { 52 + const target = printNode(node.target); 53 + if (node.annotation) { 54 + const annotation = printNode(node.annotation); 55 + if (node.value) { 56 + parts.push(printLine(`${target}: ${annotation} = ${printNode(node.value)}`)); 57 + } else { 58 + parts.push(printLine(`${target}: ${annotation}`)); 59 + } 60 + } else { 61 + parts.push(printLine(`${target} = ${printNode(node.value!)}`)); 62 + } 53 63 break; 64 + } 54 65 55 66 case PyNodeKind.AsyncExpression: 56 67 parts.push(`async ${printNode(node.expression)}`); ··· 211 222 parts.push(`${printLine('else:')}`); 212 223 parts.push(`${printNode(node.elseBlock)}`); 213 224 } 225 + break; 226 + 227 + case PyNodeKind.KeywordArgument: 228 + parts.push(`${node.name}=${printNode(node.value)}`); 214 229 break; 215 230 216 231 case PyNodeKind.ImportStatement: { ··· 399 414 break; 400 415 401 416 default: 402 - // @ts-expect-error 403 - throw new Error(`Unsupported node kind: ${node.kind}`); 417 + throw new Error(`Unsupported node kind: ${(node as { kind: string }).kind}`); 404 418 } 405 419 406 420 if (node.trailingComments) {
+7 -4
packages/openapi-ts/src/plugins/valibot/v1/processor.ts
··· 1 1 import { ref } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { Hooks, IR } from '@hey-api/shared'; 3 3 import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; 4 4 5 5 import { exportAst } from '../shared/export'; ··· 11 11 export function createProcessor(plugin: ValibotPlugin['Instance']): ProcessorResult { 12 12 const processor = createSchemaProcessor(); 13 13 14 - const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; 14 + const extractorHooks: ReadonlyArray<NonNullable<Hooks['schemas']>['shouldExtract']> = [ 15 + plugin.config['~hooks']?.schemas?.shouldExtract, 16 + plugin.context.config.parser.hooks.schemas?.shouldExtract, 17 + ]; 15 18 16 19 function extractor(ctx: ProcessorContext): IR.SchemaObject { 17 20 if (processor.hasEmitted(ctx.path)) { 18 21 return ctx.schema; 19 22 } 20 23 21 - for (const hook of hooks) { 22 - const result = hook?.shouldExtract?.(ctx); 24 + for (const hook of extractorHooks) { 25 + const result = hook?.(ctx); 23 26 if (result) { 24 27 process({ 25 28 namingAnchor: processor.context.anchor,
+8 -6
packages/openapi-ts/src/plugins/zod/shared/processor.ts
··· 5 5 SchemaProcessorResult, 6 6 } from '@hey-api/shared'; 7 7 8 - import type { IrSchemaToAstOptions, TypeOptions } from './types'; 8 + import type { ZodPlugin } from '../types'; 9 + import type { TypeOptions } from './types'; 9 10 10 - export type ProcessorContext = Pick<IrSchemaToAstOptions, 'plugin'> & 11 - SchemaProcessorContext & { 12 - naming: NamingConfig & TypeOptions; 13 - schema: IR.SchemaObject; 14 - }; 11 + export type ProcessorContext = SchemaProcessorContext & { 12 + naming: NamingConfig & TypeOptions; 13 + /** The plugin instance. */ 14 + plugin: ZodPlugin['Instance']; 15 + schema: IR.SchemaObject; 16 + }; 15 17 16 18 export type ProcessorResult = SchemaProcessorResult<ProcessorContext>;
+1 -1
packages/openapi-ts/src/ts-dsl/layout/doc.ts
··· 60 60 return node; 61 61 } 62 62 63 - override toAst(): ts.Node { 63 + override toAst() { 64 64 // this class does not build a standalone node; 65 65 // it modifies other nodes via `apply()`. 66 66 // Return a dummy comment node for compliance.
+1 -1
packages/openapi-ts/src/ts-dsl/layout/hint.ts
··· 48 48 return node; 49 49 } 50 50 51 - override toAst(): ts.Node { 51 + override toAst() { 52 52 // this class does not build a standalone node; 53 53 // it modifies other nodes via `apply()`. 54 54 // Return a dummy comment node for compliance.
+1 -1
packages/openapi-ts/src/ts-dsl/layout/newline.ts
··· 11 11 super.analyze(ctx); 12 12 } 13 13 14 - override toAst(): ts.Identifier { 14 + override toAst() { 15 15 return this.$node(new IdTsDsl('\n')); 16 16 } 17 17 }
+1 -1
packages/openapi-ts/src/ts-dsl/layout/note.ts
··· 51 51 return node; 52 52 } 53 53 54 - override toAst(): ts.Node { 54 + override toAst() { 55 55 // this class does not build a standalone node; 56 56 // it modifies other nodes via `apply()`. 57 57 // Return a dummy comment node for compliance.
+1 -1
packages/openapi-ts/src/ts-dsl/utils/lazy.ts
··· 26 26 return this._thunk(ctx); 27 27 } 28 28 29 - override toAst(): T { 29 + override toAst() { 30 30 return this.toResult().toAst(); 31 31 } 32 32 }
+11 -3
packages/openapi-ts/src/ts-dsl/utils/name.ts
··· 46 46 return new LiteralTsDsl(name) as TsDsl<ts.StringLiteral>; 47 47 }; 48 48 49 + const validTypeScriptChar = /^[\u200c\u200d\p{ID_Continue}]$/u; 50 + 49 51 const safeName = (name: string, reserved: ReservedList): string => { 50 52 let sanitized = ''; 51 53 let index: number; ··· 53 55 const first = name[0] ?? ''; 54 56 regexp.illegalStartCharacters.lastIndex = 0; 55 57 if (regexp.illegalStartCharacters.test(first)) { 56 - sanitized += '_'; 57 - index = 0; 58 + // Check if character becomes valid when not in leading position (e.g., digits) 59 + if (validTypeScriptChar.test(first)) { 60 + sanitized += '_'; 61 + index = 0; 62 + } else { 63 + sanitized += '_'; 64 + index = 1; 65 + } 58 66 } else { 59 67 sanitized += first; 60 68 index = 1; ··· 62 70 63 71 while (index < name.length) { 64 72 const char = name[index] ?? ''; 65 - sanitized += /^[\u200c\u200d\p{ID_Continue}]$/u.test(char) ? char : '_'; 73 + sanitized += validTypeScriptChar.test(char) ? char : '_'; 66 74 index += 1; 67 75 } 68 76