Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,10 @@
"typescript-eslint": "catalog:",
"vitest": "catalog:",
"yaml": "catalog:"
},
"pnpm": {
"overrides": {
"@alloy-js/core": "0.23.1"
}
}
}
3 changes: 0 additions & 3 deletions packages/emitter-framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,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
Expand All @@ -34,7 +32,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.
Expand Down
2 changes: 1 addition & 1 deletion packages/emitter-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"devDependencies": {
"@alloy-js/cli": "catalog:",
"@alloy-js/core": "catalog:",
"@alloy-js/python": "catalog:",
"@alloy-js/python": "https://pkg.pr.new/@alloy-js/python@403",
"@alloy-js/rollup-plugin": "catalog:",
"@alloy-js/typescript": "catalog:",
"@typespec/compiler": "workspace:^",
Expand Down
38 changes: 38 additions & 0 deletions packages/emitter-framework/src/python/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,53 @@ export const typingModule = createModule({
name: "typing",
descriptor: {
".": [
"Annotated",
"Any",
"Callable",
"ClassVar",
"Generic",
"Literal",
"Never",
"Optional",
"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"],
},
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { 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";
Expand All @@ -15,11 +15,14 @@ export interface MethodPropsWithType extends Omit<py.MethodDeclarationBaseProps,
doc?: Children;
methodType?: "method" | "class" | "static";
abstract?: boolean;
decorators?: Children[];
/** If true, parameters replaces operation parameters instead of adding to them as keyword-only */
replaceParameters?: boolean;
}

export type MethodProps = MethodPropsWithType | py.MethodDeclarationBaseProps;
export type MethodProps =
| MethodPropsWithType
| (py.MethodDeclarationBaseProps & { decorators?: Children[] });

/**
* Get the method component based on the resolved method type.
Expand Down Expand Up @@ -59,18 +62,35 @@ export function Method(props: Readonly<MethodProps>) {
const explicit = (props as any).abstract as boolean | undefined;
return explicit ?? (!isTypeSpecTyped ? false : undefined);
})();
const decorators = (props as { decorators?: Children[] }).decorators;
const decoratorsBlock = decorators ? (
<For each={decorators} skipFalsy>
{(decorator) => (
<>
{decorator}
<hbr />
</>
)}
</For>
) : 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 <MethodComponent {...props} doc={docElement} abstract={abstractFlag} />;
const { decorators: _decorators, ...methodProps } = props;
return (
<>
{decoratorsBlock}
<MethodComponent {...methodProps} doc={docElement} abstract={abstractFlag} />
</>
);
}

const [efProps, updateProps, forwardProps] = splitProps(
props,
["type"],
["type", "decorators"],
["returnType", "parameters"],
);

Expand All @@ -83,6 +103,7 @@ export function Method(props: Readonly<MethodProps>) {

return (
<>
{decoratorsBlock}
<MethodComponent
{...forwardProps}
{...updateProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./class-declaration.js";
export * from "./class-member.js";
export * from "./class-method.js";
export * from "./primitive-initializer.js";
export * from "./pydantic-class-declaration.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Tester } from "#test/test-host.js";
import { code } from "@alloy-js/core";
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";
import { PydanticClassDeclaration } from "./pydantic-class-declaration.js";

describe("Python PydanticClassDeclaration", () => {
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, [<PydanticClassDeclaration type={User} />])).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, [
<PydanticClassDeclaration
type={User}
modelConfig={{ frozen: true, extra: "forbid", validateAssignment: true }}
/>,
]),
).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, [
<PydanticClassDeclaration name="User">
<Method
type={stripName}
methodType="class"
decorators={[code`@${pydanticModule["."].field_validator}("name", mode="before")`]}
/>
</PydanticClassDeclaration>,
]),
).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, [
<ClassDeclaration name="AppSettings" bases={[pydanticSettingsModule["."].BaseSettings]} />,
]),
).toRenderTo(`
from pydantic_settings import BaseSettings


class AppSettings(BaseSettings):
pass

`);
});
});
Loading