From b584d87d724c88ed0000bc354f47e3c2cb5fd557 Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Wed, 6 May 2026 21:27:18 +0000 Subject: [PATCH 01/11] Create PydanticClassDeclaration --- package.json | 5 + packages/emitter-framework/package.json | 4 +- .../emitter-framework/src/python/builtins.ts | 38 +++++ .../class-declaration/class-method.tsx | 27 +++- .../components/class-declaration/index.ts | 1 + .../pydantic-class-declaration.test.tsx | 101 ++++++++++++ .../pydantic-class-declaration.tsx | 153 ++++++++++++++++++ .../src/python/test-utils.tsx | 11 +- 8 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx create mode 100644 packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx diff --git a/package.json b/package.json index ac253cc5d7e..7f32fd7f25e 100644 --- a/package.json +++ b/package.json @@ -67,5 +67,10 @@ "typescript": "catalog:", "vitest": "catalog:", "yaml": "catalog:" + }, + "pnpm": { + "overrides": { + "@alloy-js/core": "file:../alloy/packages/core/alloy-js-core-0.22.0.tgz" + } } } diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json index 83c5dd5ea91..73c05e407fe 100644 --- a/packages/emitter-framework/package.json +++ b/packages/emitter-framework/package.json @@ -75,8 +75,8 @@ }, "devDependencies": { "@alloy-js/cli": "catalog:", - "@alloy-js/core": "catalog:", - "@alloy-js/python": "catalog:", + "@alloy-js/core": "file:../../../alloy/packages/core/alloy-js-core-0.22.0.tgz", + "@alloy-js/python": "file:../../../alloy/packages/python/alloy-js-python-0.3.0.tgz", "@alloy-js/rollup-plugin": "catalog:", "@alloy-js/typescript": "catalog:", "@typespec/compiler": "workspace:^", diff --git a/packages/emitter-framework/src/python/builtins.ts b/packages/emitter-framework/src/python/builtins.ts index 7fc20b4d539..f4c95c527db 100644 --- a/packages/emitter-framework/src/python/builtins.ts +++ b/packages/emitter-framework/src/python/builtins.ts @@ -29,8 +29,10 @@ export const typingModule = createModule({ name: "typing", descriptor: { ".": [ + "Annotated", "Any", "Callable", + "ClassVar", "Generic", "Literal", "Never", @@ -38,6 +40,42 @@ export const typingModule = createModule({ "Protocol", "TypeAlias", "TypeVar", + "Union", ], }, }); + +export const pydanticModule = createModule({ + name: "pydantic", + descriptor: { + ".": [ + "AfterValidator", + "BaseModel", + "BeforeValidator", + "ConfigDict", + "EmailStr", + "Field", + "HttpUrl", + "PlainSerializer", + "RootModel", + "SecretStr", + "TypeAdapter", + "ValidationError", + "WrapValidator", + "computed_field", + "field_serializer", + "field_validator", + "model_serializer", + "model_validator", + ], + alias_generators: ["to_camel", "to_pascal", "to_snake"], + types: ["PositiveFloat", "PositiveInt"], + }, +}); + +export const pydanticSettingsModule = createModule({ + name: "pydantic_settings", + descriptor: { + ".": ["BaseSettings", "SettingsConfigDict"], + }, +}); diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx index da717d79694..3e3ecefcaad 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx @@ -1,4 +1,4 @@ -import { type Children, createContext, splitProps, useContext } from "@alloy-js/core"; +import { For, type Children, createContext, splitProps, useContext } from "@alloy-js/core"; import * as py from "@alloy-js/python"; import type { Operation } from "@typespec/compiler"; import { useTsp } from "../../../core/index.js"; @@ -15,11 +15,12 @@ export interface MethodPropsWithType extends Omit) { const explicit = (props as any).abstract as boolean | undefined; return explicit ?? (!isTypeSpecTyped ? false : undefined); })(); + const decorators = (props as { decorators?: Children[] }).decorators; + const decoratorsBlock = decorators ? ( + + {(decorator) => ( + <> + {decorator} + + + )} + + ) : undefined; /** * If the method does not come from the Typespec class declaration, return a standard Python method declaration. * Have in mind that, with that, we lose some of the TypeSpec class declaration overrides. */ if (!isTypeSpecTyped) { - return ; + const { decorators: _decorators, ...methodProps } = props; + return ( + <> + {decoratorsBlock} + + + ); } const [efProps, updateProps, forwardProps] = splitProps( props, - ["type"], + ["type", "decorators"], ["returnType", "parameters"], ); @@ -83,6 +101,7 @@ export function Method(props: Readonly) { return ( <> + {decoratorsBlock} { + it("creates a pydantic class from a model", async () => { + const { program, User } = await Tester.compile(t.code` + model ${t.model("User")} { + id: string; + } + `); + + expect(getOutput(program, [])).toRenderTo(` + from pydantic import BaseModel + + + class User(BaseModel): + id: str + + `); + }); + + it("emits model_config from structured modelConfig", async () => { + const { program, User } = await Tester.compile(t.code` + model ${t.model("User")} { + id: string; + } + `); + + expect( + getOutput(program, [ + , + ]), + ).toRenderTo(` + from pydantic import BaseModel + from pydantic import ConfigDict + + + class User(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid", validate_assignment=True) + id: str + + `); + }); + + it("places pydantic validator decorators above @classmethod", async () => { + const { program, stripName } = await Tester.compile( + t.code`@test op ${t.op("stripName")}(value: string): string;`, + ); + + expect( + getOutput(program, [ + + + , + ]), + ).toRenderTo(` + from pydantic import BaseModel + from pydantic import field_validator + + + class User(BaseModel): + @field_validator("name", mode="before") + @classmethod + def strip_name(cls, value: str) -> str: + pass + + + `); + }); + + it("supports BaseSettings via pydantic_settings module", async () => { + const { program } = await Tester.compile(``); + + expect( + getOutput(program, [ + , + ]), + ).toRenderTo(` + from pydantic_settings import BaseSettings + + + class AppSettings(BaseSettings): + pass + + `); + }); +}); diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx new file mode 100644 index 00000000000..47cd9e62a2d --- /dev/null +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx @@ -0,0 +1,153 @@ +import { For, type Children } from "@alloy-js/core"; +import * as py from "@alloy-js/python"; +import type { Model } from "@typespec/compiler"; +import { useTsp } from "../../../core/context/tsp-context.js"; +import { reportDiagnostic } from "../../../lib.js"; +import { pydanticModule } from "../../builtins.js"; +import { declarationRefkeys } from "../../utils/refkey.js"; +import { ClassMember } from "./class-member.js"; +import { DocElement } from "../doc-element/doc-element.js"; +import { ClassDeclaration } from "./class-declaration.js"; +import { ClassBases } from "./class-bases.js"; +import { MethodProvider } from "./class-method.js"; + +export interface PydanticModelConfigDictProps { + arbitraryTypesAllowed?: boolean; + extra?: "allow" | "forbid" | "ignore"; + fromAttributes?: boolean; + frozen?: boolean; + populateByName?: boolean; + strStripWhitespace?: boolean; + strict?: boolean; + validateAssignment?: boolean; + validateDefault?: boolean; +} + +export interface PydanticClassDeclarationBaseProps extends Omit { + name: string; + modelConfig?: PydanticModelConfigDictProps; + modelConfigExpression?: Children; +} + +export interface PydanticClassDeclarationPropsWithType + extends Omit { + type: Model; + name?: string; + methodType?: "method" | "class" | "static"; +} + +export type PydanticClassDeclarationProps = + | PydanticClassDeclarationPropsWithType + | PydanticClassDeclarationBaseProps; + +function isTypedPydanticClassDeclarationProps( + props: PydanticClassDeclarationProps, +): props is PydanticClassDeclarationPropsWithType { + return "type" in props; +} + +/** + * Converts TypeSpec Models to Pydantic classes. + */ +export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { + const { $ } = useTsp(); + const configEntries: Array<[string, unknown]> = []; + if (props.modelConfig && props.modelConfigExpression === undefined) { + for (const key of Object.keys(props.modelConfig)) { + const value = (props.modelConfig as Record)[key]; + if (value !== undefined) { + configEntries.push([toSnakeCase(key), value]); + } + } + } + const hasStructuredModelConfig = props.modelConfigExpression === undefined && configEntries.length > 0; + const hasExpressionModelConfig = props.modelConfigExpression !== undefined; + const configLine = hasExpressionModelConfig ? ( + <> + {"model_config = "} + {props.modelConfigExpression} + + + ) : hasStructuredModelConfig ? ( + <> + {"model_config = "} + {pydanticModule["."].ConfigDict} + {"("} + + {([k, v]) => ( + <> + {k}= + + )} + + {")"} + + + ) : undefined; + + if (!isTypedPydanticClassDeclarationProps(props)) { + return ( + + {configLine} + {props.children} + + ); + } + + const docSource = props.doc ?? $.type.getDoc(props.type); + const docElement = docSource ? : undefined; + const namePolicy = py.usePythonNamePolicy(); + + let name = props.name ?? props.type.name; + if (!name) { + reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type }); + } + name = namePolicy.getName(name, "class"); + + // Pydantic models currently don't support anonymous additional properties. + const additionalPropsRecord = $.model.getAdditionalPropertiesRecord(props.type); + if (additionalPropsRecord) { + throw new Error("Models with additional properties (Record[…]) are not supported"); + } + const modelMembers = Array.from($.model.getProperties(props.type).values()); + + const refkeys = declarationRefkeys(props.refkey, props.type); + + const bases = ClassBases({ + type: props.type, + bases: props.bases, + }); + const resolvedBases = + props.bases === undefined && bases.length === 0 + ? [pydanticModule["."].BaseModel] + : bases; + + return ( + + + {configLine} + + {(typeMember) => ( + + )} + + {/* Allow callers to append custom class content after generated members. */} + <> + {props.children} + + + + ); +} + +function toSnakeCase(value: string): string { + return value.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(); +} diff --git a/packages/emitter-framework/src/python/test-utils.tsx b/packages/emitter-framework/src/python/test-utils.tsx index c94e9f66dfd..05942e21082 100644 --- a/packages/emitter-framework/src/python/test-utils.tsx +++ b/packages/emitter-framework/src/python/test-utils.tsx @@ -2,7 +2,14 @@ import { Output } from "#core/components/index.js"; import { type Children } from "@alloy-js/core"; import * as py from "@alloy-js/python"; import type { Program } from "@typespec/compiler"; -import { abcModule, datetimeModule, decimalModule, typingModule } from "./builtins.js"; +import { + abcModule, + datetimeModule, + decimalModule, + pydanticModule, + pydanticSettingsModule, + typingModule, +} from "./builtins.js"; export const renderOptions = { printWidth: 80, @@ -18,6 +25,8 @@ export function getOutput(program: Program, children: Children[]): Children { abcModule, datetimeModule, decimalModule, + pydanticModule, + pydanticSettingsModule, typingModule, py.abcModule, py.dataclassesModule, From e622eeac3a342a36d7daa69021f0e35d7ced30a3 Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Thu, 7 May 2026 17:37:36 +0000 Subject: [PATCH 02/11] Fix to point alloy at branch --- package.json | 2 +- packages/emitter-framework/CHANGELOG.md | 3 -- packages/emitter-framework/package.json | 4 +-- .../class-declaration/class-method.tsx | 6 ++-- .../components/class-declaration/index.ts | 2 +- .../pydantic-class-declaration.test.tsx | 4 +-- .../pydantic-class-declaration.tsx | 30 ++++++++----------- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 7f32fd7f25e..ce26dd17ed0 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "pnpm": { "overrides": { - "@alloy-js/core": "file:../alloy/packages/core/alloy-js-core-0.22.0.tgz" + "@alloy-js/core": "0.23.1" } } } diff --git a/packages/emitter-framework/CHANGELOG.md b/packages/emitter-framework/CHANGELOG.md index f187f678389..b58b3d81982 100644 --- a/packages/emitter-framework/CHANGELOG.md +++ b/packages/emitter-framework/CHANGELOG.md @@ -22,14 +22,12 @@ - [#9879](https://github.com/microsoft/typespec/pull/9879) Add the missing export in index for extensible-enum in csharp - ## 0.16.0 ### Bump dependencies - [#9446](https://github.com/microsoft/typespec/pull/9446) Upgrade dependencies - ## 0.15.0 ### Features @@ -42,7 +40,6 @@ - [#9202](https://github.com/microsoft/typespec/pull/9202) Update to alloy 0.22 - [#9223](https://github.com/microsoft/typespec/pull/9223) Upgrade dependencies - ## 0.14.0 No changes, version bump only. diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json index 73c05e407fe..0d09d14d946 100644 --- a/packages/emitter-framework/package.json +++ b/packages/emitter-framework/package.json @@ -75,8 +75,8 @@ }, "devDependencies": { "@alloy-js/cli": "catalog:", - "@alloy-js/core": "file:../../../alloy/packages/core/alloy-js-core-0.22.0.tgz", - "@alloy-js/python": "file:../../../alloy/packages/python/alloy-js-python-0.3.0.tgz", + "@alloy-js/core": "catalog:", + "@alloy-js/python": "https://pkg.pr.new/@alloy-js/python@403", "@alloy-js/rollup-plugin": "catalog:", "@alloy-js/typescript": "catalog:", "@typespec/compiler": "workspace:^", diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx index 3e3ecefcaad..233ba5a9899 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx @@ -1,4 +1,4 @@ -import { For, type Children, createContext, splitProps, useContext } from "@alloy-js/core"; +import { createContext, For, splitProps, useContext, type Children } from "@alloy-js/core"; import * as py from "@alloy-js/python"; import type { Operation } from "@typespec/compiler"; import { useTsp } from "../../../core/index.js"; @@ -20,7 +20,9 @@ export interface MethodPropsWithType extends Omit { diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx index 47cd9e62a2d..20c3a9f539a 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx @@ -5,10 +5,10 @@ import { useTsp } from "../../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../../lib.js"; import { pydanticModule } from "../../builtins.js"; import { declarationRefkeys } from "../../utils/refkey.js"; -import { ClassMember } from "./class-member.js"; import { DocElement } from "../doc-element/doc-element.js"; -import { ClassDeclaration } from "./class-declaration.js"; import { ClassBases } from "./class-bases.js"; +import { ClassDeclaration } from "./class-declaration.js"; +import { ClassMember } from "./class-member.js"; import { MethodProvider } from "./class-method.js"; export interface PydanticModelConfigDictProps { @@ -29,8 +29,10 @@ export interface PydanticClassDeclarationBaseProps extends Omit { +export interface PydanticClassDeclarationPropsWithType extends Omit< + PydanticClassDeclarationBaseProps, + "name" +> { type: Model; name?: string; methodType?: "method" | "class" | "static"; @@ -60,7 +62,8 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { } } } - const hasStructuredModelConfig = props.modelConfigExpression === undefined && configEntries.length > 0; + const hasStructuredModelConfig = + props.modelConfigExpression === undefined && configEntries.length > 0; const hasExpressionModelConfig = props.modelConfigExpression !== undefined; const configLine = hasExpressionModelConfig ? ( <> @@ -87,10 +90,7 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { if (!isTypedPydanticClassDeclarationProps(props)) { return ( - + {configLine} {props.children} @@ -121,9 +121,7 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { bases: props.bases, }); const resolvedBases = - props.bases === undefined && bases.length === 0 - ? [pydanticModule["."].BaseModel] - : bases; + props.bases === undefined && bases.length === 0 ? [pydanticModule["."].BaseModel] : bases; return ( @@ -135,14 +133,10 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { > {configLine} - {(typeMember) => ( - - )} + {(typeMember) => } {/* Allow callers to append custom class content after generated members. */} - <> - {props.children} - + <>{props.children} ); From f030b41f89b09d10042b5b4019abbcc20c60b12e Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Wed, 10 Jun 2026 17:05:39 +0000 Subject: [PATCH 03/11] feat(emitter-framework): add Python Pydantic class/decorator helpers and docs Adds first-class Pydantic support helpers to the Python emitter framework, including convenience class declarations for BaseModel, BaseSettings, and RootModel, plus typed decorator builders for common validator/serializer patterns. Also expands tests and adds package-level documentation with practical usage examples. Co-authored-by: Cursor --- packages/emitter-framework/readme.md | 75 ++++++++++++ .../components/class-declaration/index.ts | 1 + .../pydantic-class-declaration.test.tsx | 45 ++++++- .../pydantic-class-declaration.tsx | 95 ++++++++++++++- .../pydantic-decorators.test.tsx | 112 ++++++++++++++++++ .../class-declaration/pydantic-decorators.tsx | 66 +++++++++++ 6 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 packages/emitter-framework/readme.md create mode 100644 packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx create mode 100644 packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx diff --git a/packages/emitter-framework/readme.md b/packages/emitter-framework/readme.md new file mode 100644 index 00000000000..a016fe3c3ec --- /dev/null +++ b/packages/emitter-framework/readme.md @@ -0,0 +1,75 @@ +# Emitter Framework + +**WARNING: THIS PACKAGE IS EXPERIMENTAL AND WILL CHANGE** + +`@typespec/emitter-framework` provides JSX components and helpers for turning a +TypeSpec program into source files. + +## Python Pydantic Helpers + +The Python surface includes convenience components for common Pydantic patterns: + +- `PydanticClassDeclaration` for `BaseModel` classes generated from TypeSpec models +- `PydanticSettingsClassDeclaration` for `BaseSettings` classes +- `PydanticRootModelDeclaration` for `RootModel[T]` classes + +It also includes decorator builders for common validators/serializers: + +- `fieldValidatorDecorator()` +- `modelValidatorDecorator()` +- `fieldSerializerDecorator()` +- `computedFieldDecorator()` + +### Minimal Example + +```tsx +import { code } from "@alloy-js/core"; +import { + Method, + PydanticClassDeclaration, + PydanticRootModelDeclaration, + PydanticSettingsClassDeclaration, + computedFieldDecorator, + fieldSerializerDecorator, + fieldValidatorDecorator, +} from "@typespec/emitter-framework/python"; + +export function Models() { + return ( + <> + + + + + + + + + + + ); +} +``` + +### Notes + +- Pass custom decorators to `Method` through the `decorators` prop. +- Decorators are emitted before `@classmethod`/`@staticmethod` when both are present. +- If you need direct symbol access, use `pydanticModule` and + `pydanticSettingsModule` from `@typespec/emitter-framework/python`. diff --git a/packages/emitter-framework/src/python/components/class-declaration/index.ts b/packages/emitter-framework/src/python/components/class-declaration/index.ts index d43cc2bbd8f..9a307aee4c1 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/index.ts +++ b/packages/emitter-framework/src/python/components/class-declaration/index.ts @@ -5,3 +5,4 @@ export * from "./class-member.js"; export * from "./class-method.js"; export * from "./primitive-initializer.js"; export * from "./pydantic-class-declaration.js"; +export * from "./pydantic-decorators.js"; diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx index ffc0f8f79a6..ada958d3f3b 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx @@ -6,7 +6,11 @@ import { pydanticModule, pydanticSettingsModule } from "../../builtins.js"; import { getOutput } from "../../test-utils.js"; import { ClassDeclaration } from "./class-declaration.js"; import { Method } from "./class-method.js"; -import { PydanticClassDeclaration } from "./pydantic-class-declaration.js"; +import { + PydanticClassDeclaration, + PydanticRootModelDeclaration, + PydanticSettingsClassDeclaration, +} from "./pydantic-class-declaration.js"; describe("Python PydanticClassDeclaration", () => { it("creates a pydantic class from a model", async () => { @@ -98,4 +102,43 @@ describe("Python PydanticClassDeclaration", () => { `); }); + + it("creates a settings class with SettingsConfigDict", async () => { + const { program } = await Tester.compile(``); + + expect( + getOutput(program, [ + , + ]), + ).toRenderTo(` + from pydantic_settings import BaseSettings + from pydantic_settings import SettingsConfigDict + + + class AppSettings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env") + + + `); + }); + + it("creates a RootModel declaration from a root type", async () => { + const { program } = await Tester.compile(``); + + expect( + getOutput(program, [ + , + ]), + ).toRenderTo(` + from pydantic import RootModel + + + class TagList(RootModel[list[str]]): + pass + + `); + }); }); diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx index 20c3a9f539a..cff8e2f2e2c 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx @@ -1,9 +1,9 @@ -import { For, type Children } from "@alloy-js/core"; +import { code, For, type Children } from "@alloy-js/core"; import * as py from "@alloy-js/python"; import type { Model } from "@typespec/compiler"; import { useTsp } from "../../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../../lib.js"; -import { pydanticModule } from "../../builtins.js"; +import { pydanticModule, pydanticSettingsModule } from "../../builtins.js"; import { declarationRefkeys } from "../../utils/refkey.js"; import { DocElement } from "../doc-element/doc-element.js"; import { ClassBases } from "./class-bases.js"; @@ -23,6 +23,15 @@ export interface PydanticModelConfigDictProps { validateDefault?: boolean; } +export interface PydanticSettingsConfigDictProps { + caseSensitive?: boolean; + envFile?: string; + envFileEncoding?: string; + envIgnoreEmpty?: boolean; + envNestedDelimiter?: string; + envPrefix?: string; +} + export interface PydanticClassDeclarationBaseProps extends Omit { name: string; modelConfig?: PydanticModelConfigDictProps; @@ -142,6 +151,88 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { ); } +type PydanticSettingsClassDeclarationBaseProps = Omit< + PydanticClassDeclarationBaseProps, + "bases" | "modelConfig" | "modelConfigExpression" +>; + +type PydanticSettingsClassDeclarationPropsWithType = Omit< + PydanticClassDeclarationPropsWithType, + "bases" | "modelConfig" | "modelConfigExpression" +>; + +export type PydanticSettingsClassDeclarationProps = + | (PydanticSettingsClassDeclarationBaseProps & { + settingsConfig?: PydanticSettingsConfigDictProps; + settingsConfigExpression?: Children; + }) + | (PydanticSettingsClassDeclarationPropsWithType & { + settingsConfig?: PydanticSettingsConfigDictProps; + settingsConfigExpression?: Children; + }); + +/** + * Convenience wrapper for classes that inherit from pydantic-settings BaseSettings. + */ +export function PydanticSettingsClassDeclaration(props: PydanticSettingsClassDeclarationProps) { + const { settingsConfig, settingsConfigExpression, ...classProps } = props; + const configEntries: Array<[string, unknown]> = []; + if (settingsConfig && settingsConfigExpression === undefined) { + for (const key of Object.keys(settingsConfig)) { + const value = (settingsConfig as Record)[key]; + if (value !== undefined) { + configEntries.push([toSnakeCase(key), value]); + } + } + } + + const hasStructuredConfig = + settingsConfigExpression === undefined && configEntries.length > 0; + const hasExpressionConfig = settingsConfigExpression !== undefined; + + const modelConfigExpression = hasExpressionConfig ? ( + settingsConfigExpression + ) : hasStructuredConfig ? ( + <> + {pydanticSettingsModule["."].SettingsConfigDict} + {"("} + + {([k, v]) => ( + <> + {k}= + + )} + + {")"} + + ) : undefined; + + return ( + + ); +} + +export interface PydanticRootModelDeclarationProps extends Omit { + rootType: Children; +} + +/** + * Convenience wrapper for pydantic RootModel[T] declarations. + */ +export function PydanticRootModelDeclaration(props: PydanticRootModelDeclarationProps) { + const { rootType, ...classProps } = props; + return ( + + ); +} + function toSnakeCase(value: string): string { return value.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(); } diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx new file mode 100644 index 00000000000..92ae750ab1a --- /dev/null +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx @@ -0,0 +1,112 @@ +import { Tester } from "#test/test-host.js"; +import { t } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; +import { getOutput } from "../../test-utils.js"; +import { Method } from "./class-method.js"; +import { PydanticClassDeclaration } from "./pydantic-class-declaration.js"; +import { + computedFieldDecorator, + fieldSerializerDecorator, + fieldValidatorDecorator, + modelValidatorDecorator, +} from "./pydantic-decorators.js"; + +describe("Python pydantic decorator helpers", () => { + it("builds @field_validator decorator with mode", async () => { + const { program, normalizeName } = await Tester.compile( + t.code`@test op ${t.op("normalizeName")}(value: string): string;`, + ); + + expect( + getOutput(program, [ + + + , + ]), + ).toRenderTo(` + from pydantic import BaseModel + from pydantic import field_validator + + + class User(BaseModel): + @field_validator("name", mode="before") + @classmethod + def normalize_name(cls, value: str) -> str: + pass + + + `); + }); + + it("builds @model_validator decorator for class methods", async () => { + const { program, checkModel } = await Tester.compile( + t.code`@test op ${t.op("checkModel")}(value: string): string;`, + ); + + expect( + getOutput(program, [ + + + , + ]), + ).toRenderTo(` + from pydantic import BaseModel + from pydantic import model_validator + + + class User(BaseModel): + @model_validator(mode="after") + @classmethod + def check_model(cls, value: str) -> str: + pass + + + `); + }); + + it("builds serializer and computed field decorators", async () => { + const { program } = await Tester.compile(``); + + expect( + getOutput(program, [ + + + + , + ]), + ).toRenderTo(` + from pydantic import BaseModel + from pydantic import computed_field + from pydantic import field_serializer + + + class User(BaseModel): + @field_serializer("name", when_used="json") + def serialize_name(self) -> str: + pass + @computed_field + def display_name(self) -> str: + pass + + + `); + }); +}); diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx new file mode 100644 index 00000000000..adfffe2097b --- /dev/null +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx @@ -0,0 +1,66 @@ +import { code, type Children } from "@alloy-js/core"; +import { pydanticModule } from "../../builtins.js"; + +export type PydanticValidationMode = "before" | "after" | "plain" | "wrap"; + +export interface PydanticFieldValidatorOptions { + mode?: PydanticValidationMode; + checkFields?: boolean; +} + +export interface PydanticModelValidatorOptions { + mode?: Exclude; +} + +export interface PydanticFieldSerializerOptions { + mode?: "plain" | "wrap"; + returnType?: string; + whenUsed?: "always" | "unless-none" | "json" | "json-unless-none"; +} + +/** + * Build a `@field_validator(...)` decorator for method `decorators`. + */ +export function fieldValidatorDecorator( + fields: string | string[], + options: PydanticFieldValidatorOptions = {}, +): Children { + const values = Array.isArray(fields) ? fields : [fields]; + const args = values.map((x) => JSON.stringify(x)); + if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); + if (options.checkFields !== undefined) args.push(`check_fields=${options.checkFields}`); + + return code`@${pydanticModule["."].field_validator}(${args.join(", ")})`; +} + +/** + * Build a `@model_validator(...)` decorator for method `decorators`. + */ +export function modelValidatorDecorator(options: PydanticModelValidatorOptions): Children { + const args: string[] = []; + if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); + return code`@${pydanticModule["."].model_validator}(${args.join(", ")})`; +} + +/** + * Build a `@field_serializer(...)` decorator for method `decorators`. + */ +export function fieldSerializerDecorator( + fields: string | string[], + options: PydanticFieldSerializerOptions = {}, +): Children { + const values = Array.isArray(fields) ? fields : [fields]; + const args = values.map((x) => JSON.stringify(x)); + if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); + if (options.returnType !== undefined) args.push(`return_type=${options.returnType}`); + if (options.whenUsed !== undefined) args.push(`when_used=${JSON.stringify(options.whenUsed)}`); + + return code`@${pydanticModule["."].field_serializer}(${args.join(", ")})`; +} + +/** + * Build a `@computed_field` decorator for method `decorators`. + */ +export function computedFieldDecorator(): Children { + return code`@${pydanticModule["."].computed_field}`; +} From c785a7c391348a6d73125627fbc362fd27c4c9cc Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Wed, 10 Jun 2026 17:31:41 +0000 Subject: [PATCH 04/11] Remove redundant pydantic module --- packages/emitter-framework/readme.md | 2 +- .../emitter-framework/src/python/builtins.ts | 35 ------------------- .../pydantic-class-declaration.test.tsx | 6 ++-- .../pydantic-class-declaration.tsx | 13 ++++--- .../class-declaration/pydantic-decorators.tsx | 10 +++--- .../src/python/test-utils.tsx | 6 ++-- 6 files changed, 17 insertions(+), 55 deletions(-) diff --git a/packages/emitter-framework/readme.md b/packages/emitter-framework/readme.md index a016fe3c3ec..d953a1fef27 100644 --- a/packages/emitter-framework/readme.md +++ b/packages/emitter-framework/readme.md @@ -72,4 +72,4 @@ export function Models() { - Pass custom decorators to `Method` through the `decorators` prop. - Decorators are emitted before `@classmethod`/`@staticmethod` when both are present. - If you need direct symbol access, use `pydanticModule` and - `pydanticSettingsModule` from `@typespec/emitter-framework/python`. + `pydanticSettingsModule` from `@alloy-js/python`. diff --git a/packages/emitter-framework/src/python/builtins.ts b/packages/emitter-framework/src/python/builtins.ts index f4c95c527db..b04fe804782 100644 --- a/packages/emitter-framework/src/python/builtins.ts +++ b/packages/emitter-framework/src/python/builtins.ts @@ -44,38 +44,3 @@ export const typingModule = createModule({ ], }, }); - -export const pydanticModule = createModule({ - name: "pydantic", - descriptor: { - ".": [ - "AfterValidator", - "BaseModel", - "BeforeValidator", - "ConfigDict", - "EmailStr", - "Field", - "HttpUrl", - "PlainSerializer", - "RootModel", - "SecretStr", - "TypeAdapter", - "ValidationError", - "WrapValidator", - "computed_field", - "field_serializer", - "field_validator", - "model_serializer", - "model_validator", - ], - alias_generators: ["to_camel", "to_pascal", "to_snake"], - types: ["PositiveFloat", "PositiveInt"], - }, -}); - -export const pydanticSettingsModule = createModule({ - name: "pydantic_settings", - descriptor: { - ".": ["BaseSettings", "SettingsConfigDict"], - }, -}); diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx index ada958d3f3b..fdb742d6bdd 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx @@ -1,8 +1,8 @@ import { Tester } from "#test/test-host.js"; import { code } from "@alloy-js/core"; +import * as py from "@alloy-js/python"; import { t } from "@typespec/compiler/testing"; import { describe, expect, it } from "vitest"; -import { pydanticModule, pydanticSettingsModule } from "../../builtins.js"; import { getOutput } from "../../test-utils.js"; import { ClassDeclaration } from "./class-declaration.js"; import { Method } from "./class-method.js"; @@ -67,7 +67,7 @@ describe("Python PydanticClassDeclaration", () => { , ]), @@ -91,7 +91,7 @@ describe("Python PydanticClassDeclaration", () => { expect( getOutput(program, [ - , + , ]), ).toRenderTo(` from pydantic_settings import BaseSettings diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx index cff8e2f2e2c..3449414696e 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx @@ -3,7 +3,6 @@ import * as py from "@alloy-js/python"; import type { Model } from "@typespec/compiler"; import { useTsp } from "../../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../../lib.js"; -import { pydanticModule, pydanticSettingsModule } from "../../builtins.js"; import { declarationRefkeys } from "../../utils/refkey.js"; import { DocElement } from "../doc-element/doc-element.js"; import { ClassBases } from "./class-bases.js"; @@ -83,7 +82,7 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { ) : hasStructuredModelConfig ? ( <> {"model_config = "} - {pydanticModule["."].ConfigDict} + {py.pydanticModule["."].ConfigDict} {"("} {([k, v]) => ( @@ -99,7 +98,7 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { if (!isTypedPydanticClassDeclarationProps(props)) { return ( - + {configLine} {props.children} @@ -130,7 +129,7 @@ export function PydanticClassDeclaration(props: PydanticClassDeclarationProps) { bases: props.bases, }); const resolvedBases = - props.bases === undefined && bases.length === 0 ? [pydanticModule["."].BaseModel] : bases; + props.bases === undefined && bases.length === 0 ? [py.pydanticModule["."].BaseModel] : bases; return ( @@ -194,7 +193,7 @@ export function PydanticSettingsClassDeclaration(props: PydanticSettingsClassDec settingsConfigExpression ) : hasStructuredConfig ? ( <> - {pydanticSettingsModule["."].SettingsConfigDict} + {py.pydanticSettingsModule["."].SettingsConfigDict} {"("} {([k, v]) => ( @@ -210,7 +209,7 @@ export function PydanticSettingsClassDeclaration(props: PydanticSettingsClassDec return ( ); @@ -228,7 +227,7 @@ export function PydanticRootModelDeclaration(props: PydanticRootModelDeclaration return ( ); } diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx index adfffe2097b..a5f68727466 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx @@ -1,5 +1,5 @@ import { code, type Children } from "@alloy-js/core"; -import { pydanticModule } from "../../builtins.js"; +import * as py from "@alloy-js/python"; export type PydanticValidationMode = "before" | "after" | "plain" | "wrap"; @@ -30,7 +30,7 @@ export function fieldValidatorDecorator( if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); if (options.checkFields !== undefined) args.push(`check_fields=${options.checkFields}`); - return code`@${pydanticModule["."].field_validator}(${args.join(", ")})`; + return code`@${py.pydanticModule["."].field_validator}(${args.join(", ")})`; } /** @@ -39,7 +39,7 @@ export function fieldValidatorDecorator( export function modelValidatorDecorator(options: PydanticModelValidatorOptions): Children { const args: string[] = []; if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); - return code`@${pydanticModule["."].model_validator}(${args.join(", ")})`; + return code`@${py.pydanticModule["."].model_validator}(${args.join(", ")})`; } /** @@ -55,12 +55,12 @@ export function fieldSerializerDecorator( if (options.returnType !== undefined) args.push(`return_type=${options.returnType}`); if (options.whenUsed !== undefined) args.push(`when_used=${JSON.stringify(options.whenUsed)}`); - return code`@${pydanticModule["."].field_serializer}(${args.join(", ")})`; + return code`@${py.pydanticModule["."].field_serializer}(${args.join(", ")})`; } /** * Build a `@computed_field` decorator for method `decorators`. */ export function computedFieldDecorator(): Children { - return code`@${pydanticModule["."].computed_field}`; + return code`@${py.pydanticModule["."].computed_field}`; } diff --git a/packages/emitter-framework/src/python/test-utils.tsx b/packages/emitter-framework/src/python/test-utils.tsx index 05942e21082..4da2d0fd00b 100644 --- a/packages/emitter-framework/src/python/test-utils.tsx +++ b/packages/emitter-framework/src/python/test-utils.tsx @@ -6,8 +6,6 @@ import { abcModule, datetimeModule, decimalModule, - pydanticModule, - pydanticSettingsModule, typingModule, } from "./builtins.js"; @@ -25,8 +23,8 @@ export function getOutput(program: Program, children: Children[]): Children { abcModule, datetimeModule, decimalModule, - pydanticModule, - pydanticSettingsModule, + py.pydanticModule, + py.pydanticSettingsModule, typingModule, py.abcModule, py.dataclassesModule, From e20dadfdf6f3eb984cf88c0aae5210a4cefa4cdb Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Wed, 10 Jun 2026 17:52:47 +0000 Subject: [PATCH 05/11] chore: refresh pnpm lockfile Regenerate pnpm-lock.yaml after rebasing the pydantic-support branch onto upstream main so dependency resolution is consistent in CI. Co-authored-by: Cursor --- pnpm-lock.yaml | 101 +++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 923a402e4c6..560798af964 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,9 +9,6 @@ catalogs: '@alloy-js/cli': specifier: ^0.24.0 version: 0.24.0 - '@alloy-js/core': - specifier: ^0.24.1 - version: 0.24.1 '@alloy-js/csharp': specifier: ^0.24.0 version: 0.24.0 @@ -21,9 +18,6 @@ catalogs: '@alloy-js/msbuild': specifier: ^0.24.0 version: 0.24.0 - '@alloy-js/python': - specifier: ^0.5.0 - version: 0.5.0 '@alloy-js/rollup-plugin': specifier: ^0.1.2 version: 0.1.2 @@ -497,10 +491,7 @@ catalogs: version: 18.0.0 overrides: - cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5' - diff@>=6.0.0 <8.0.3: '>=8.0.3' - dompurify: ^3.3.3 - yaml@>=2.0.0 <2.8.3: '>=2.8.3' + '@alloy-js/core': 0.23.1 importers: @@ -868,11 +859,11 @@ importers: specifier: 'catalog:' version: 0.24.0 '@alloy-js/core': - specifier: 'catalog:' - version: 0.24.1 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/python': - specifier: 'catalog:' - version: 0.5.0 + specifier: https://pkg.pr.new/@alloy-js/python@403 + version: https://pkg.pr.new/@alloy-js/python@403 '@alloy-js/rollup-plugin': specifier: 'catalog:' version: 0.1.2(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2) @@ -1092,8 +1083,8 @@ importers: specifier: 'catalog:' version: 0.24.0 '@alloy-js/core': - specifier: 'catalog:' - version: 0.24.1 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/rollup-plugin': specifier: 'catalog:' version: 0.1.2(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2) @@ -1128,8 +1119,8 @@ importers: packages/http-client-js: dependencies: '@alloy-js/core': - specifier: 'catalog:' - version: 0.24.1 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/typescript': specifier: 'catalog:' version: 0.24.0 @@ -1219,8 +1210,8 @@ importers: packages/http-server-csharp: dependencies: '@alloy-js/core': - specifier: 'catalog:' - version: 0.24.1 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/csharp': specifier: 'catalog:' version: 0.24.0 @@ -2637,8 +2628,8 @@ importers: packages/tspd: dependencies: '@alloy-js/core': - specifier: 'catalog:' - version: 0.24.1 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/markdown': specifier: 'catalog:' version: 0.24.0 @@ -3070,8 +3061,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@alloy-js/core@0.24.1': - resolution: {integrity: sha512-cye97/HGo0/G8XIc8UnS0PaSoSOj9DDPEiuKsXsn7xQgR1sbL+Nf0oTMZnL2fFb7mRnmULO84x5wK8R/YtReTg==} + '@alloy-js/core@0.23.1': + resolution: {integrity: sha512-zbPXbiWjF3BvgO+a6dyDiA9uI19x5cHKDy5iC3U2/DIimIB0/j1/ObT9BNIzcM69h5pYt7ADDGXx5eNyb8j7bw==} '@alloy-js/csharp@0.24.0': resolution: {integrity: sha512-TR+VAAnwbFV7YWnGSlyqL2X2/lrS3UBZ8JBKxszn7z+yk4Y5Sgs6dag3xPZMR5CsQXbxkN3gY/c4G3vqgKZf9A==} @@ -3082,8 +3073,9 @@ packages: '@alloy-js/msbuild@0.24.0': resolution: {integrity: sha512-Y4bDr7uYEafm40GP92TdBYV4xYZA4O4AoFmES50wovTVSHp904JPU6Ct5GEsV7OG7RhrzT00oeCpJsx4bIYuFQ==} - '@alloy-js/python@0.5.0': - resolution: {integrity: sha512-p1O5eUFtfW5MLg4ngywnjMz43adco3+pdSLxfRFPYc452S7fStToP0jy7i9fyuLlR0o//1K9SDmfuP/VMK/Rtw==} + '@alloy-js/python@https://pkg.pr.new/@alloy-js/python@403': + resolution: {tarball: https://pkg.pr.new/@alloy-js/python@403} + version: 0.5.0-pr.403.1498 '@alloy-js/rollup-plugin@0.1.2': resolution: {integrity: sha512-7jDdnePI+GA8UemsQEStdJ6XYNhs1rvv+yO5O/2jJqaoc0mYDchC+vwylljy2wUqGKDbFUXFX4xJktqtBZtyBg==} @@ -8688,6 +8680,10 @@ packages: engines: {node: '>=20'} hasBin: true + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -9080,10 +9076,6 @@ packages: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} - diff@9.0.0: - resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -9116,6 +9108,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dompurify@3.4.11: resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} @@ -13365,7 +13360,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: '>=2.8.3' + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true @@ -13767,6 +13762,11 @@ packages: resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==} hasBin: true + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} + hasBin: true + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -13939,7 +13939,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@alloy-js/core@0.24.1': + '@alloy-js/core@0.23.1': dependencies: '@types/ws': 8.18.1 '@vue/reactivity': 3.5.38 @@ -13947,6 +13947,7 @@ snapshots: devalue: 5.8.1 pathe: 2.0.3 picocolors: 1.1.1 + prettier: 3.8.4 ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -13954,7 +13955,7 @@ snapshots: '@alloy-js/csharp@0.24.0': dependencies: - '@alloy-js/core': 0.24.1 + '@alloy-js/core': 0.23.1 '@alloy-js/msbuild': 0.24.0 change-case: 5.4.4 marked: 18.0.5 @@ -13965,7 +13966,7 @@ snapshots: '@alloy-js/markdown@0.24.0': dependencies: - '@alloy-js/core': 0.24.1 + '@alloy-js/core': 0.23.1 yaml: 2.9.0 transitivePeerDependencies: - bufferutil @@ -13973,7 +13974,7 @@ snapshots: '@alloy-js/msbuild@0.24.0': dependencies: - '@alloy-js/core': 0.24.1 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 marked: 18.0.5 pathe: 2.0.3 @@ -13981,9 +13982,9 @@ snapshots: - bufferutil - utf-8-validate - '@alloy-js/python@0.5.0': + '@alloy-js/python@https://pkg.pr.new/@alloy-js/python@403': dependencies: - '@alloy-js/core': 0.24.1 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: @@ -14003,7 +14004,7 @@ snapshots: '@alloy-js/typescript@0.24.0': dependencies: - '@alloy-js/core': 0.24.1 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: @@ -16950,7 +16951,7 @@ snapshots: '@rushstack/rig-package': 0.7.3 '@rushstack/terminal': 0.24.0(@types/node@26.0.0) '@rushstack/ts-command-line': 5.3.10(@types/node@26.0.0) - diff: 9.0.0 + diff: 8.0.4 minimatch: 10.2.3 resolve: 1.22.12 semver: 7.7.4 @@ -20305,7 +20306,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.6 + cross-spawn: 7.0.3 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -21239,6 +21240,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -21668,8 +21675,6 @@ snapshots: diff@8.0.4: {} - diff@9.0.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -21700,6 +21705,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -24220,12 +24229,12 @@ snapshots: monaco-editor-core@0.55.1: dependencies: - dompurify: 3.4.11 + dompurify: 3.2.7 marked: 14.0.0 monaco-editor@0.55.1: dependencies: - dompurify: 3.4.11 + dompurify: 3.2.7 marked: 14.0.0 morgan@1.11.0: @@ -27203,7 +27212,9 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.18.0 vscode-uri: 3.1.0 - yaml: 2.9.0 + yaml: 2.7.1 + + yaml@2.7.1: {} yaml@2.9.0: {} From 538fb340cf4c2d27cee6e8d47582634040c81491 Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Wed, 10 Jun 2026 17:55:55 +0000 Subject: [PATCH 06/11] chore(emitter-framework): remove temporary package readme Remove the temporary emitter-framework readme added during pydantic helper iteration to keep this PR focused on code and tests only. Co-authored-by: Cursor --- packages/emitter-framework/readme.md | 75 ---------------------------- 1 file changed, 75 deletions(-) delete mode 100644 packages/emitter-framework/readme.md diff --git a/packages/emitter-framework/readme.md b/packages/emitter-framework/readme.md deleted file mode 100644 index d953a1fef27..00000000000 --- a/packages/emitter-framework/readme.md +++ /dev/null @@ -1,75 +0,0 @@ -# Emitter Framework - -**WARNING: THIS PACKAGE IS EXPERIMENTAL AND WILL CHANGE** - -`@typespec/emitter-framework` provides JSX components and helpers for turning a -TypeSpec program into source files. - -## Python Pydantic Helpers - -The Python surface includes convenience components for common Pydantic patterns: - -- `PydanticClassDeclaration` for `BaseModel` classes generated from TypeSpec models -- `PydanticSettingsClassDeclaration` for `BaseSettings` classes -- `PydanticRootModelDeclaration` for `RootModel[T]` classes - -It also includes decorator builders for common validators/serializers: - -- `fieldValidatorDecorator()` -- `modelValidatorDecorator()` -- `fieldSerializerDecorator()` -- `computedFieldDecorator()` - -### Minimal Example - -```tsx -import { code } from "@alloy-js/core"; -import { - Method, - PydanticClassDeclaration, - PydanticRootModelDeclaration, - PydanticSettingsClassDeclaration, - computedFieldDecorator, - fieldSerializerDecorator, - fieldValidatorDecorator, -} from "@typespec/emitter-framework/python"; - -export function Models() { - return ( - <> - - - - - - - - - - - ); -} -``` - -### Notes - -- Pass custom decorators to `Method` through the `decorators` prop. -- Decorators are emitted before `@classmethod`/`@staticmethod` when both are present. -- If you need direct symbol access, use `pydanticModule` and - `pydanticSettingsModule` from `@alloy-js/python`. From 3524b0a2dcb3b6aaa6eb34f1389bf2e6e2fbf23b Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Mon, 13 Jul 2026 20:02:46 +0000 Subject: [PATCH 07/11] Minor fixes --- package.json | 5 -- .../class-declaration/pydantic-decorators.tsx | 4 +- pnpm-lock.yaml | 79 +++++++------------ 3 files changed, 33 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index ce26dd17ed0..ac253cc5d7e 100644 --- a/package.json +++ b/package.json @@ -67,10 +67,5 @@ "typescript": "catalog:", "vitest": "catalog:", "yaml": "catalog:" - }, - "pnpm": { - "overrides": { - "@alloy-js/core": "0.23.1" - } } } diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx index a5f68727466..bd08ad19f99 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx @@ -36,7 +36,9 @@ export function fieldValidatorDecorator( /** * Build a `@model_validator(...)` decorator for method `decorators`. */ -export function modelValidatorDecorator(options: PydanticModelValidatorOptions): Children { +export function modelValidatorDecorator( + options: PydanticModelValidatorOptions = {}, +): Children { const args: string[] = []; if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); return code`@${py.pydanticModule["."].model_validator}(${args.join(", ")})`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 560798af964..a0aeeaadd0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@alloy-js/cli': specifier: ^0.24.0 version: 0.24.0 + '@alloy-js/core': + specifier: ^0.24.1 + version: 0.24.1 '@alloy-js/csharp': specifier: ^0.24.0 version: 0.24.0 @@ -491,7 +494,10 @@ catalogs: version: 18.0.0 overrides: - '@alloy-js/core': 0.23.1 + cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5' + diff@>=6.0.0 <8.0.3: '>=8.0.3' + dompurify: ^3.3.3 + yaml@>=2.0.0 <2.8.3: '>=2.8.3' importers: @@ -859,8 +865,8 @@ importers: specifier: 'catalog:' version: 0.24.0 '@alloy-js/core': - specifier: 0.23.1 - version: 0.23.1 + specifier: 'catalog:' + version: 0.24.1 '@alloy-js/python': specifier: https://pkg.pr.new/@alloy-js/python@403 version: https://pkg.pr.new/@alloy-js/python@403 @@ -1083,8 +1089,8 @@ importers: specifier: 'catalog:' version: 0.24.0 '@alloy-js/core': - specifier: 0.23.1 - version: 0.23.1 + specifier: 'catalog:' + version: 0.24.1 '@alloy-js/rollup-plugin': specifier: 'catalog:' version: 0.1.2(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2) @@ -1119,8 +1125,8 @@ importers: packages/http-client-js: dependencies: '@alloy-js/core': - specifier: 0.23.1 - version: 0.23.1 + specifier: 'catalog:' + version: 0.24.1 '@alloy-js/typescript': specifier: 'catalog:' version: 0.24.0 @@ -1210,8 +1216,8 @@ importers: packages/http-server-csharp: dependencies: '@alloy-js/core': - specifier: 0.23.1 - version: 0.23.1 + specifier: 'catalog:' + version: 0.24.1 '@alloy-js/csharp': specifier: 'catalog:' version: 0.24.0 @@ -2628,8 +2634,8 @@ importers: packages/tspd: dependencies: '@alloy-js/core': - specifier: 0.23.1 - version: 0.23.1 + specifier: 'catalog:' + version: 0.24.1 '@alloy-js/markdown': specifier: 'catalog:' version: 0.24.0 @@ -3061,8 +3067,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@alloy-js/core@0.23.1': - resolution: {integrity: sha512-zbPXbiWjF3BvgO+a6dyDiA9uI19x5cHKDy5iC3U2/DIimIB0/j1/ObT9BNIzcM69h5pYt7ADDGXx5eNyb8j7bw==} + '@alloy-js/core@0.24.1': + resolution: {integrity: sha512-cye97/HGo0/G8XIc8UnS0PaSoSOj9DDPEiuKsXsn7xQgR1sbL+Nf0oTMZnL2fFb7mRnmULO84x5wK8R/YtReTg==} '@alloy-js/csharp@0.24.0': resolution: {integrity: sha512-TR+VAAnwbFV7YWnGSlyqL2X2/lrS3UBZ8JBKxszn7z+yk4Y5Sgs6dag3xPZMR5CsQXbxkN3gY/c4G3vqgKZf9A==} @@ -8680,10 +8686,6 @@ packages: engines: {node: '>=20'} hasBin: true - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -9108,9 +9110,6 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} - dompurify@3.4.11: resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} @@ -13360,7 +13359,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: ^2.4.2 + yaml: '>=2.8.3' peerDependenciesMeta: '@types/node': optional: true @@ -13762,11 +13761,6 @@ packages: resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==} hasBin: true - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} - hasBin: true - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -13939,7 +13933,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@alloy-js/core@0.23.1': + '@alloy-js/core@0.24.1': dependencies: '@types/ws': 8.18.1 '@vue/reactivity': 3.5.38 @@ -13947,7 +13941,6 @@ snapshots: devalue: 5.8.1 pathe: 2.0.3 picocolors: 1.1.1 - prettier: 3.8.4 ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -13955,7 +13948,7 @@ snapshots: '@alloy-js/csharp@0.24.0': dependencies: - '@alloy-js/core': 0.23.1 + '@alloy-js/core': 0.24.1 '@alloy-js/msbuild': 0.24.0 change-case: 5.4.4 marked: 18.0.5 @@ -13966,7 +13959,7 @@ snapshots: '@alloy-js/markdown@0.24.0': dependencies: - '@alloy-js/core': 0.23.1 + '@alloy-js/core': 0.24.1 yaml: 2.9.0 transitivePeerDependencies: - bufferutil @@ -13974,7 +13967,7 @@ snapshots: '@alloy-js/msbuild@0.24.0': dependencies: - '@alloy-js/core': 0.23.1 + '@alloy-js/core': 0.24.1 change-case: 5.4.4 marked: 18.0.5 pathe: 2.0.3 @@ -13984,7 +13977,7 @@ snapshots: '@alloy-js/python@https://pkg.pr.new/@alloy-js/python@403': dependencies: - '@alloy-js/core': 0.23.1 + '@alloy-js/core': 0.24.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: @@ -14004,7 +13997,7 @@ snapshots: '@alloy-js/typescript@0.24.0': dependencies: - '@alloy-js/core': 0.23.1 + '@alloy-js/core': 0.24.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: @@ -20306,7 +20299,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -21240,12 +21233,6 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -21705,10 +21692,6 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.7: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -24229,12 +24212,12 @@ snapshots: monaco-editor-core@0.55.1: dependencies: - dompurify: 3.2.7 + dompurify: 3.4.11 marked: 14.0.0 monaco-editor@0.55.1: dependencies: - dompurify: 3.2.7 + dompurify: 3.4.11 marked: 14.0.0 morgan@1.11.0: @@ -27212,9 +27195,7 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.18.0 vscode-uri: 3.1.0 - yaml: 2.7.1 - - yaml@2.7.1: {} + yaml: 2.9.0 yaml@2.9.0: {} From cb4258549ffd07fb1f2879eadd727d459e66f8a2 Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Tue, 14 Jul 2026 15:18:37 +0000 Subject: [PATCH 08/11] Fix alloy-js python version --- packages/emitter-framework/package.json | 2 +- pnpm-lock.yaml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json index 0d09d14d946..83c5dd5ea91 100644 --- a/packages/emitter-framework/package.json +++ b/packages/emitter-framework/package.json @@ -76,7 +76,7 @@ "devDependencies": { "@alloy-js/cli": "catalog:", "@alloy-js/core": "catalog:", - "@alloy-js/python": "https://pkg.pr.new/@alloy-js/python@403", + "@alloy-js/python": "catalog:", "@alloy-js/rollup-plugin": "catalog:", "@alloy-js/typescript": "catalog:", "@typespec/compiler": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0aeeaadd0a..f5c3765a928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ catalogs: '@alloy-js/msbuild': specifier: ^0.24.0 version: 0.24.0 + '@alloy-js/python': + specifier: ^0.5.0 + version: 0.5.0 '@alloy-js/rollup-plugin': specifier: ^0.1.2 version: 0.1.2 @@ -868,8 +871,8 @@ importers: specifier: 'catalog:' version: 0.24.1 '@alloy-js/python': - specifier: https://pkg.pr.new/@alloy-js/python@403 - version: https://pkg.pr.new/@alloy-js/python@403 + specifier: 'catalog:' + version: 0.5.0 '@alloy-js/rollup-plugin': specifier: 'catalog:' version: 0.1.2(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2) @@ -3079,9 +3082,8 @@ packages: '@alloy-js/msbuild@0.24.0': resolution: {integrity: sha512-Y4bDr7uYEafm40GP92TdBYV4xYZA4O4AoFmES50wovTVSHp904JPU6Ct5GEsV7OG7RhrzT00oeCpJsx4bIYuFQ==} - '@alloy-js/python@https://pkg.pr.new/@alloy-js/python@403': - resolution: {tarball: https://pkg.pr.new/@alloy-js/python@403} - version: 0.5.0-pr.403.1498 + '@alloy-js/python@0.5.0': + resolution: {integrity: sha512-p1O5eUFtfW5MLg4ngywnjMz43adco3+pdSLxfRFPYc452S7fStToP0jy7i9fyuLlR0o//1K9SDmfuP/VMK/Rtw==} '@alloy-js/rollup-plugin@0.1.2': resolution: {integrity: sha512-7jDdnePI+GA8UemsQEStdJ6XYNhs1rvv+yO5O/2jJqaoc0mYDchC+vwylljy2wUqGKDbFUXFX4xJktqtBZtyBg==} @@ -13975,7 +13977,7 @@ snapshots: - bufferutil - utf-8-validate - '@alloy-js/python@https://pkg.pr.new/@alloy-js/python@403': + '@alloy-js/python@0.5.0': dependencies: '@alloy-js/core': 0.24.1 change-case: 5.4.4 From 8503db657935c773ea8400bca93c8955bb84cb24 Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Tue, 14 Jul 2026 16:08:12 +0000 Subject: [PATCH 09/11] Fix tests --- .../pydantic-class-declaration.test.tsx | 20 ++++++------- .../pydantic-decorators.test.tsx | 28 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx index fdb742d6bdd..3d9c5c68570 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx @@ -25,7 +25,7 @@ describe("Python PydanticClassDeclaration", () => { class User(BaseModel): - id: str + id: str `); }); @@ -50,8 +50,8 @@ describe("Python PydanticClassDeclaration", () => { class User(BaseModel): - model_config = ConfigDict(frozen=True, extra="forbid", validate_assignment=True) - id: str + model_config = ConfigDict(frozen=True, extra="forbid", validate_assignment=True) + id: str `); }); @@ -77,10 +77,10 @@ describe("Python PydanticClassDeclaration", () => { class User(BaseModel): - @field_validator("name", mode="before") - @classmethod - def strip_name(cls, value: str) -> str: - pass + @field_validator("name", mode="before") + @classmethod + def strip_name(cls, value: str) -> str: + pass `); @@ -98,7 +98,7 @@ describe("Python PydanticClassDeclaration", () => { class AppSettings(BaseSettings): - pass + pass `); }); @@ -119,7 +119,7 @@ describe("Python PydanticClassDeclaration", () => { class AppSettings(BaseSettings): - model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env") + model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env") `); @@ -137,7 +137,7 @@ describe("Python PydanticClassDeclaration", () => { class TagList(RootModel[list[str]]): - pass + pass `); }); diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx index 92ae750ab1a..0f52e60dada 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.test.tsx @@ -33,10 +33,10 @@ describe("Python pydantic decorator helpers", () => { class User(BaseModel): - @field_validator("name", mode="before") - @classmethod - def normalize_name(cls, value: str) -> str: - pass + @field_validator("name", mode="before") + @classmethod + def normalize_name(cls, value: str) -> str: + pass `); @@ -63,10 +63,10 @@ describe("Python pydantic decorator helpers", () => { class User(BaseModel): - @model_validator(mode="after") - @classmethod - def check_model(cls, value: str) -> str: - pass + @model_validator(mode="after") + @classmethod + def check_model(cls, value: str) -> str: + pass `); @@ -99,12 +99,12 @@ describe("Python pydantic decorator helpers", () => { class User(BaseModel): - @field_serializer("name", when_used="json") - def serialize_name(self) -> str: - pass - @computed_field - def display_name(self) -> str: - pass + @field_serializer("name", when_used="json") + def serialize_name(self) -> str: + pass + @computed_field + def display_name(self) -> str: + pass `); From 9d825e88f9b821d6c2fbf4d99159ab122192446a Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Tue, 14 Jul 2026 16:23:30 +0000 Subject: [PATCH 10/11] Add changelog --- .../add-python-pydantic-helpers-2026-7-14-16-15-0.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/add-python-pydantic-helpers-2026-7-14-16-15-0.md diff --git a/.chronus/changes/add-python-pydantic-helpers-2026-7-14-16-15-0.md b/.chronus/changes/add-python-pydantic-helpers-2026-7-14-16-15-0.md new file mode 100644 index 00000000000..504a8e957cb --- /dev/null +++ b/.chronus/changes/add-python-pydantic-helpers-2026-7-14-16-15-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/emitter-framework" +--- + +Add Python Pydantic helpers to the emitter framework: `PydanticClassDeclaration`, `PydanticSettingsClassDeclaration`, and `PydanticRootModelDeclaration` components, along with `field_validator`, `model_validator`, `field_serializer`, and `computed_field` decorator helpers. From fba5aa3bd2bbf4234a8d6a5b0a102eb5a012c4ae Mon Sep 17 00:00:00 2001 From: Mauricio Gardini Date: Tue, 14 Jul 2026 16:34:11 +0000 Subject: [PATCH 11/11] format and cspell --- .claude/settings.local.json | 16 ++++++++++++++++ cspell.yaml | 1 + .../pydantic-class-declaration.test.tsx | 5 ++++- .../pydantic-class-declaration.tsx | 3 +-- .../class-declaration/pydantic-decorators.tsx | 4 +--- .../emitter-framework/src/python/test-utils.tsx | 7 +------ 6 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..968b2b43b42 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(npx vitest *)", + "Bash(node -e \"const p=require\\('./packages/compiler/package.json'\\); console.log\\(JSON.stringify\\(p.exports['./testing'],null,2\\)\\); console.log\\('---main---'\\); console.log\\(JSON.stringify\\(p.exports['.'],null,2\\)\\)\")", + "Bash(node -e \"const p=require\\('./packages/compiler/package.json'\\); console.log\\('declared:', p.dependencies['vscode-languageserver']\\)\")", + "Bash(node -e \"const p=require\\('./node_modules/.pnpm/vscode-languageserver@9.0.1/node_modules/vscode-languageserver/package.json'\\); console.log\\(JSON.stringify\\(p.exports,null,2\\)\\)\")", + "Bash(npm run *)", + "Bash(node -e \"const p=require\\('./node_modules/.pnpm/lock'\\);\")", + "Bash(pnpm chronus *)", + "Bash(xargs cat)", + "Bash(npx prettier *)", + "Bash(pnpm run *)" + ] + } +} diff --git a/cspell.yaml b/cspell.yaml index e3477f52836..ff43c71be03 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -218,6 +218,7 @@ words: - ptvsd - pwsh - pycache + - pydantic - pyexpat - pygen - pygments diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx index 3d9c5c68570..7f3687b6436 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.test.tsx @@ -91,7 +91,10 @@ describe("Python PydanticClassDeclaration", () => { expect( getOutput(program, [ - , + , ]), ).toRenderTo(` from pydantic_settings import BaseSettings diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx index 3449414696e..85bf3aa1d5a 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-class-declaration.tsx @@ -185,8 +185,7 @@ export function PydanticSettingsClassDeclaration(props: PydanticSettingsClassDec } } - const hasStructuredConfig = - settingsConfigExpression === undefined && configEntries.length > 0; + const hasStructuredConfig = settingsConfigExpression === undefined && configEntries.length > 0; const hasExpressionConfig = settingsConfigExpression !== undefined; const modelConfigExpression = hasExpressionConfig ? ( diff --git a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx index bd08ad19f99..3663203debc 100644 --- a/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx +++ b/packages/emitter-framework/src/python/components/class-declaration/pydantic-decorators.tsx @@ -36,9 +36,7 @@ export function fieldValidatorDecorator( /** * Build a `@model_validator(...)` decorator for method `decorators`. */ -export function modelValidatorDecorator( - options: PydanticModelValidatorOptions = {}, -): Children { +export function modelValidatorDecorator(options: PydanticModelValidatorOptions = {}): Children { const args: string[] = []; if (options.mode !== undefined) args.push(`mode=${JSON.stringify(options.mode)}`); return code`@${py.pydanticModule["."].model_validator}(${args.join(", ")})`; diff --git a/packages/emitter-framework/src/python/test-utils.tsx b/packages/emitter-framework/src/python/test-utils.tsx index 4da2d0fd00b..21ba85af158 100644 --- a/packages/emitter-framework/src/python/test-utils.tsx +++ b/packages/emitter-framework/src/python/test-utils.tsx @@ -2,12 +2,7 @@ import { Output } from "#core/components/index.js"; import { type Children } from "@alloy-js/core"; import * as py from "@alloy-js/python"; import type { Program } from "@typespec/compiler"; -import { - abcModule, - datetimeModule, - decimalModule, - typingModule, -} from "./builtins.js"; +import { abcModule, datetimeModule, decimalModule, typingModule } from "./builtins.js"; export const renderOptions = { printWidth: 80,