diff --git a/.chronus/changes/feature-ef-python-2025-11-19-1-26-8.md b/.chronus/changes/feature-ef-python-2025-11-19-1-26-8.md
new file mode 100644
index 00000000000..31ae4beca01
--- /dev/null
+++ b/.chronus/changes/feature-ef-python-2025-11-19-1-26-8.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+ - "@typespec/emitter-framework"
+---
+
+Add Python Emitter Framework implementation
\ No newline at end of file
diff --git a/packages/emitter-framework/CHANGELOG.md b/packages/emitter-framework/CHANGELOG.md
index 63d99036145..83f6111a2a8 100644
--- a/packages/emitter-framework/CHANGELOG.md
+++ b/packages/emitter-framework/CHANGELOG.md
@@ -14,7 +14,6 @@ No changes, version bump only.
- [#8823](https://github.com/microsoft/typespec/pull/8823) Upgrade dependencies
-
## 0.12.0
### Features
@@ -29,7 +28,6 @@ No changes, version bump only.
- [#8474](https://github.com/microsoft/typespec/pull/8474) Remove development exports from published package
-
## 0.11.0
### Features
@@ -53,7 +51,6 @@ No changes, version bump only.
- [#8302](https://github.com/microsoft/typespec/pull/8302) [c#] Avoid generating double '?' after property name when the property is nullable union and it's prop.optional is true in the meantime
- [#8362](https://github.com/microsoft/typespec/pull/8362) Upgrade alloy to 0.20
-
## 0.10.0
### Bump dependencies
@@ -61,7 +58,6 @@ No changes, version bump only.
- [#8050](https://github.com/microsoft/typespec/pull/8050) Upgrade alloy 0.19
- [#7978](https://github.com/microsoft/typespec/pull/7978) Upgrade dependencies
-
## 0.9.0
### Features
@@ -79,7 +75,6 @@ No changes, version bump only.
- [#7650](https://github.com/microsoft/typespec/pull/7650) Adds subpath export for csharp emitter-framework components
-
## 0.8.0
### Features
@@ -94,7 +89,6 @@ No changes, version bump only.
- [#7369](https://github.com/microsoft/typespec/pull/7369) Render discriminated unions correctly
-
## 0.7.1
### Bump dependencies
@@ -105,21 +99,19 @@ No changes, version bump only.
- [#7321](https://github.com/microsoft/typespec/pull/7321) Use wasm version of tree sitter for snippet extractor
-
## 0.7.0
### Bump dependencies
- [#7186](https://github.com/microsoft/typespec/pull/7186) Upgrade to alloy 15
-
## 0.6.0
### Features
- [#7017](https://github.com/microsoft/typespec/pull/7017) [TypeScript] Add various function-related components - FunctionType, FunctionExpression, ArrowFunction, and InterfaceMethod.
- [#6972](https://github.com/microsoft/typespec/pull/6972) Add support for rendering a Value Expression
-- [#7018](https://github.com/microsoft/typespec/pull/7018) Adds the `TspContextProvider` and `useTsp()` hook for providing and accessing TypeSpec context and the Typekit APIs (e.g. `# Changelog - @typespec/emitter-framework). Adds a new `Output` component that accepts a TypeSpec `Program` and automatically wraps children components with the `TspContextProvider`.
+- [#7018](https://github.com/microsoft/typespec/pull/7018) Adds the `TspContextProvider` and `useTsp()` hook for providing and accessing TypeSpec context and the Typekit APIs (e.g. `# Changelog - @typespec/emitter-framework). Adds a new `Output`component that accepts a TypeSpec`Program`and automatically wraps children components with the`TspContextProvider`.
### Bump dependencies
@@ -129,21 +121,18 @@ No changes, version bump only.
- [#6951](https://github.com/microsoft/typespec/pull/6951) InterfaceMember should use Alloy
-
## 0.5.0
### Features
- [#6875](https://github.com/microsoft/typespec/pull/6875) Upgrade to alloy 0.10.0
-
## 0.4.0
### Bump dependencies
- [#6595](https://github.com/microsoft/typespec/pull/6595) Upgrade dependencies
-
## 0.3.0
### Bump dependencies
@@ -155,12 +144,8 @@ No changes, version bump only.
- [#6178](https://github.com/microsoft/typespec/pull/6178) Improvements on the TestHarness
- [#6460](https://github.com/microsoft/typespec/pull/6460) Update dependency structure for EmitterFramework, HttpClient and JS Emitter
-
-
-
## 0.2.0
### Features
- [#5996](https://github.com/microsoft/typespec/pull/5996) Adding Emitter Framework and Http Client packages
-
diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json
index cae4ea31d97..03e16d69534 100644
--- a/packages/emitter-framework/package.json
+++ b/packages/emitter-framework/package.json
@@ -31,6 +31,9 @@
"./typescript": {
"import": "./dist/src/typescript/index.js"
},
+ "./python": {
+ "import": "./dist/src/python/index.js"
+ },
"./testing": {
"import": "./dist/src/testing/index.js"
}
@@ -48,6 +51,10 @@
"#typescript/*": {
"development": "./src/typescript/*",
"default": "./dist/src/typescript/*"
+ },
+ "#python/*": {
+ "development": "./src/python/*",
+ "default": "./dist/src/python/*"
}
},
"keywords": [],
@@ -57,12 +64,14 @@
"peerDependencies": {
"@alloy-js/core": "^0.22.0",
"@alloy-js/csharp": "^0.22.0",
+ "@alloy-js/python": "^0.3.0",
"@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^"
},
"devDependencies": {
"@alloy-js/cli": "^0.22.0",
"@alloy-js/core": "^0.22.0",
+ "@alloy-js/python": "^0.3.0",
"@alloy-js/rollup-plugin": "^0.1.0",
"@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^",
diff --git a/packages/emitter-framework/src/python/builtins.ts b/packages/emitter-framework/src/python/builtins.ts
new file mode 100644
index 00000000000..7fc20b4d539
--- /dev/null
+++ b/packages/emitter-framework/src/python/builtins.ts
@@ -0,0 +1,43 @@
+import type { SymbolCreator } from "@alloy-js/core";
+import { createModule } from "@alloy-js/python";
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+type dummy = SymbolCreator;
+
+export const abcModule = createModule({
+ name: "abc",
+ descriptor: {
+ ".": ["ABC"],
+ },
+});
+
+export const datetimeModule = createModule({
+ name: "datetime",
+ descriptor: {
+ ".": ["datetime", "date", "time", "timedelta", "timezone"],
+ },
+});
+
+export const decimalModule = createModule({
+ name: "decimal",
+ descriptor: {
+ ".": ["Decimal"],
+ },
+});
+
+export const typingModule = createModule({
+ name: "typing",
+ descriptor: {
+ ".": [
+ "Any",
+ "Callable",
+ "Generic",
+ "Literal",
+ "Never",
+ "Optional",
+ "Protocol",
+ "TypeAlias",
+ "TypeVar",
+ ],
+ },
+});
diff --git a/packages/emitter-framework/src/python/components/array-expression/array-expression.test.tsx b/packages/emitter-framework/src/python/components/array-expression/array-expression.test.tsx
new file mode 100644
index 00000000000..d91493a8f0a
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/array-expression/array-expression.test.tsx
@@ -0,0 +1,14 @@
+import { Tester } from "#test/test-host.js";
+import { d } from "@alloy-js/core/testing";
+import { t } from "@typespec/compiler/testing";
+import { expect, it } from "vitest";
+import { getOutput } from "../../test-utils.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+it("maps array expression to Python list", async () => {
+ const { program, TestArray } = await Tester.compile(t.code`
+ alias ${t.type("TestArray")} = string[];
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`list[str]`);
+});
diff --git a/packages/emitter-framework/src/python/components/array-expression/array-expression.tsx b/packages/emitter-framework/src/python/components/array-expression/array-expression.tsx
new file mode 100644
index 00000000000..68173b0a10f
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/array-expression/array-expression.tsx
@@ -0,0 +1,11 @@
+import { code } from "@alloy-js/core";
+import type { Type } from "@typespec/compiler";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export interface ArrayExpressionProps {
+ elementType: Type;
+}
+
+export function ArrayExpression({ elementType }: ArrayExpressionProps) {
+ return code`list[${()}]`;
+}
diff --git a/packages/emitter-framework/src/python/components/array-expression/index.ts b/packages/emitter-framework/src/python/components/array-expression/index.ts
new file mode 100644
index 00000000000..6dc81620baf
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/array-expression/index.ts
@@ -0,0 +1 @@
+export * from "./array-expression.js";
diff --git a/packages/emitter-framework/src/python/components/atom/atom.test.tsx b/packages/emitter-framework/src/python/components/atom/atom.test.tsx
new file mode 100644
index 00000000000..82ed8930a4c
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/atom/atom.test.tsx
@@ -0,0 +1,244 @@
+import { getOutput } from "#python/test-utils.js";
+import { Tester } from "#test/test-host.js";
+import { type Model, type Program, type Value } from "@typespec/compiler";
+import { t } from "@typespec/compiler/testing";
+import { $ } from "@typespec/compiler/typekit";
+import { assert, beforeAll, describe, expect, it } from "vitest";
+import { Atom } from "../../index.js";
+
+let program: Program;
+
+beforeAll(async () => {
+ const result = await Tester.compile("");
+ program = result.program;
+});
+
+describe("NullValue", () => {
+ it("null value", async () => {
+ const value = { entityKind: "Value", valueKind: "NullValue", value: null } as Value;
+
+ await testValueExpression(value, `None`);
+ });
+});
+
+describe("StringValue", () => {
+ it("normal string", async () => {
+ const value = $(program).value.createString("test");
+
+ await testValueExpression(value, `"test"`);
+ });
+
+ it("empty string", async () => {
+ const value = $(program).value.createString("");
+
+ await testValueExpression(value, `""`);
+ });
+});
+
+describe("BooleanValue", () => {
+ it("True", async () => {
+ const value = $(program).value.createBoolean(true);
+
+ await testValueExpression(value, `True`);
+ });
+
+ it("False", async () => {
+ const value = $(program).value.createBoolean(false);
+
+ await testValueExpression(value, `False`);
+ });
+});
+
+describe("NumericValue", () => {
+ it("integers", async () => {
+ const value = $(program).value.createNumeric(42);
+
+ await testValueExpression(value, `42`);
+ });
+
+ it("decimals", async () => {
+ const fractional = $(program).value.createNumeric(42.5);
+ await testValueExpression(fractional, `42.5`);
+ });
+
+ it("decimals with .0", async () => {
+ // Generic Atom (no hint) normalizes 42.0 to 42 because it uses asNumber()
+ const value = $(program).value.createNumeric(42.0);
+ await testValueExpression(value, `42`);
+ });
+
+ it("decimals with .0 when float", async () => {
+ const value = $(program).value.createNumeric(42.0);
+ expect(getOutput(program, [])).toRenderTo(`42.0`);
+ });
+
+ it("negative integers", async () => {
+ const value = $(program).value.createNumeric(-42);
+ await testValueExpression(value, `-42`);
+ });
+
+ it("negative decimals", async () => {
+ const value = $(program).value.createNumeric(-42.5);
+ await testValueExpression(value, `-42.5`);
+ });
+
+ it("zero", async () => {
+ const value = $(program).value.createNumeric(0);
+ await testValueExpression(value, `0`);
+ });
+
+ it("zero with float", async () => {
+ const value = $(program).value.createNumeric(0);
+ expect(getOutput(program, [])).toRenderTo(`0.0`);
+ });
+
+ it("exponent that resolves to integer", async () => {
+ const value = $(program).value.createNumeric(1e3);
+ await testValueExpression(value, `1000`);
+ });
+
+ it("exponent that resolves to integer with float", async () => {
+ const value = $(program).value.createNumeric(1e3);
+ expect(getOutput(program, [])).toRenderTo(`1000.0`);
+ });
+
+ it("small decimal via exponent", async () => {
+ const value = $(program).value.createNumeric(1e-3);
+ await testValueExpression(value, `0.001`);
+ });
+});
+
+describe("ArrayValue", () => {
+ it("empty", async () => {
+ // Can be replaced with TypeKit once #6976 is implemented
+ const value = {
+ entityKind: "Value",
+ valueKind: "ArrayValue",
+ values: [],
+ } as unknown as Value;
+ await testValueExpression(value, `[]`);
+ });
+
+ it("with mixed values", async () => {
+ // Can be replaced with TypeKit once #6976 is implemented
+ const value = {
+ entityKind: "Value",
+ valueKind: "ArrayValue",
+ values: [
+ $(program).value.createString("some_text"),
+ $(program).value.createNumeric(42),
+ $(program).value.createBoolean(true),
+ {
+ entityKind: "Value",
+ valueKind: "ArrayValue",
+ values: [
+ $(program).value.createNumeric(1),
+ $(program).value.createNumeric(2),
+ $(program).value.createNumeric(3),
+ ],
+ } as Value,
+ ],
+ } as Value;
+ await testValueExpression(value, `["some_text", 42, True, [1, 2, 3]]`);
+ });
+});
+
+describe("ScalarValue", () => {
+ it("utcDateTime.fromISO correctly supplied", async () => {
+ const { minDate } = await Tester.compile(t.code`
+ model ${t.model("DateRange")} {
+ @encode("rfc7231")
+ ${t.modelProperty("minDate")}: ${t.type("utcDateTime")} = utcDateTime.fromISO("2024-02-15T18:36:03Z");
+ }
+ `);
+ await testValueExpression(
+ minDate.defaultValue!,
+ `from datetime import datetime
+from datetime import timezone
+
+
+datetime(2024, 2, 15, 18, 36, 3, tzinfo=timezone.utc)`,
+ );
+ });
+
+ it("Unsupported scalar constructor", async () => {
+ const { IpAddress } = await Tester.compile(t.code`
+ scalar ${t.scalar("ipv4")} extends ${t.scalar("string")} {
+ init fromInt(value: uint32);
+ }
+
+ @example (#{ip: ipv4.fromInt(2130706433)})
+ model ${t.model("IpAddress")} {
+ ${t.modelProperty("ip")}: ${t.type("ipv4")};
+ }
+ `);
+
+ const value = getExampleValue(IpAddress);
+ await expect(testValueExpression(value, ``)).rejects.toThrow(
+ /Unsupported scalar constructor fromInt/,
+ );
+ });
+});
+
+describe("ObjectValue", () => {
+ it("empty object", async () => {
+ // Can be replaced with TypeKit once #6976 is implemented
+ const { ObjectValue } = await Tester.compile(t.code`
+ @example(#{})
+ model ${t.model("ObjectValue")} {};
+ `);
+
+ const value = getExampleValue(ObjectValue);
+ await testValueExpression(value, `{}`);
+ });
+
+ it("object with properties", async () => {
+ // Can be replaced with TypeKit once #6976 is implemented
+ const { ObjectValue } = await Tester.compile(t.code`
+ @example(#{aNumber: 5, aString: "foo", aBoolean: true})
+ model ${t.model("ObjectValue")} {
+ ${t.modelProperty("aNumber")}: int32;
+ ${t.modelProperty("aString")}: string;
+ ${t.modelProperty("aBoolean")}: boolean;
+ };
+ `);
+ const value = getExampleValue(ObjectValue);
+ await testValueExpression(value, `{"aNumber": 5, "aString": "foo", "aBoolean": True}`);
+ });
+});
+
+describe("EnumValue", () => {
+ it("different EnumValue types", async () => {
+ // Can be replaced with TypeKit once #6976 is implemented
+ const { Red, Green, Blue } = await Tester.compile(t.code`
+ enum ${t.enum("Color")} {
+ Red,
+ Green: 3,
+ Blue: "cyan",
+ }
+ const ${t.value("Red")} = ${t.enumValue("Color.Red")};
+ const ${t.value("Green")} = ${t.enumValue("Color.Green")};
+ const ${t.value("Blue")} = ${t.enumValue("Color.Blue")};
+ `);
+
+ await testValueExpression(Red, `"Red"`);
+ await testValueExpression(Green, `3`);
+ await testValueExpression(Blue, `"cyan"`);
+ });
+});
+
+/**
+ * Helper that renders a value expression and checks the output against the expected value.
+ */
+async function testValueExpression(value: Value, expected: string) {
+ expect(getOutput(program, [])).toRenderTo(`${expected}`);
+}
+
+/**
+ * Extracts the value marked with the @example decorator from a model.
+ */
+function getExampleValue(model: Model): Value {
+ const decorator = model?.decorators.find((d) => d.definition?.name === "@example");
+ assert.exists(decorator?.args[0]?.value, "unable to find example decorator");
+ return decorator.args[0].value as Value;
+}
diff --git a/packages/emitter-framework/src/python/components/atom/atom.tsx b/packages/emitter-framework/src/python/components/atom/atom.tsx
new file mode 100644
index 00000000000..139fac2e217
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/atom/atom.tsx
@@ -0,0 +1,95 @@
+import { type Children } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import { compilerAssert, type Value } from "@typespec/compiler";
+import { datetimeModule } from "../../builtins.js";
+
+/**
+ * Base properties for the {@link Atom} component.
+ */
+interface BaseAtomProps {
+ /**
+ * The TypeSpec value to be converted to a Python expression.
+ */
+ value: Value;
+}
+
+/**
+ * Properties for the {@link Atom} component when dealing with numeric values.
+ */
+interface NumericAtomProps extends BaseAtomProps {
+ /**
+ * Hint that this numeric value should be emitted as a float (e.g., 42 -> 42.0).
+ * Only affects NumericValue.
+ */
+ float?: boolean;
+}
+
+/**
+ * All possible properties for the {@link Atom} component.
+ */
+type AtomProps = BaseAtomProps | NumericAtomProps;
+
+/**
+ * Generates a Python atom from a TypeSpec value.
+ * @param props properties for the atom
+ * @returns {@link Children} representing the Python value expression
+ */
+export function Atom(props: Readonly): Children {
+ switch (props.value.valueKind) {
+ case "StringValue":
+ case "BooleanValue":
+ case "NullValue":
+ return ;
+ case "NumericValue": {
+ const num = props.value.value.asNumber();
+ const isNonInteger = num != null && !Number.isInteger(num);
+ const numericProps = props as NumericAtomProps;
+ const asFloat = isNonInteger || numericProps.float;
+ return ;
+ }
+ case "ArrayValue":
+ return (
+ (
+
+ ))}
+ />
+ );
+ case "ScalarValue":
+ compilerAssert(
+ props.value.value.name === "fromISO",
+ `Unsupported scalar constructor ${props.value.value.name}`,
+ props.value,
+ );
+ return handleISOStringValue(props.value);
+ case "ObjectValue":
+ const jsProperties: Record = {};
+ for (const [key, value] of props.value.properties) {
+ jsProperties[key] = Atom({ value: value.value });
+ }
+ return ;
+ case "EnumValue":
+ return ;
+ }
+}
+
+/**
+ * Handles the conversion of ISO date strings to Python datetime objects.
+ * @param value the TypeSpec value containing the ISO string
+ * @returns {@link Children} representing the Python datetime expression
+ */
+function handleISOStringValue(value: Value & { valueKind: "ScalarValue" }): Children {
+ const arg0 = value.value.args[0];
+ if (arg0.valueKind !== "StringValue") {
+ throw new Error("Expected arg0 to be a StringValue");
+ }
+ const isoString = arg0.value;
+ const date = new Date(isoString);
+ return (
+ <>
+ {datetimeModule["."]["datetime"]}({date.getUTCFullYear()}, {date.getUTCMonth() + 1},{" "}
+ {date.getUTCDate()}, {date.getUTCHours()}, {date.getUTCMinutes()}, {date.getUTCSeconds()},
+ tzinfo={datetimeModule["."]["timezone"]}.utc)
+ >
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/atom/index.ts b/packages/emitter-framework/src/python/components/atom/index.ts
new file mode 100644
index 00000000000..b253f0db873
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/atom/index.ts
@@ -0,0 +1 @@
+export * from "./atom.js";
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-bases.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-bases.tsx
new file mode 100644
index 00000000000..8cfab143cb5
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-bases.tsx
@@ -0,0 +1,92 @@
+import { abcModule } from "#python/builtins.js";
+import { type Children } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import { isTemplateDeclarationOrInstance, type Interface, type Model } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { efRefkey } from "../../utils/refkey.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export interface ClassBasesProps {
+ /**
+ * The TypeSpec type to derive bases from. If not provided, only explicit bases are used.
+ */
+ type?: Model | Interface;
+
+ /**
+ * Explicit base classes to include.
+ */
+ bases?: Children[];
+
+ /**
+ * Whether the class is abstract. If true, ABC is added to the bases.
+ */
+ abstract?: boolean;
+
+ /**
+ * Additional bases to include (e.g., for future Generic[T] support).
+ */
+ extraBases?: Children[];
+}
+
+/**
+ * Computes the base classes for a Python class declaration.
+ *
+ * Combines:
+ * - Explicit bases from props
+ * - Type-derived bases (from TypeSpec model inheritance):
+ * - Template instances (e.g., `Response` → `Response[str]`)
+ * - Regular models (e.g., `BaseWidget`) via py.Reference
+ * - Arrays via TypeExpression for `typing.Sequence[T]` rendering
+ * - Records are not supported and ignored
+ * - Extra bases (for future generics support)
+ * - ABC if abstract (always last for proper Python MRO)
+ *
+ * For interfaces, type-derived bases are empty because TypeSpec flattens interface inheritance.
+ *
+ * @returns Array of base class Children, or empty array if none.
+ *
+ * @example
+ * ```tsx
+ * const bases = ClassBases({ type: model, abstract: true });
+ *
+ * ```
+ */
+export function ClassBases(props: ClassBasesProps): Children[] {
+ const { $ } = useTsp();
+ const extraBases = [...(props.extraBases ?? [])];
+
+ // Add extends/inheritance from the TypeSpec type if present
+ if (props.type && $.model.is(props.type)) {
+ const type = props.type;
+
+ if (type.baseModel) {
+ if ($.array.is(type.baseModel)) {
+ extraBases.push();
+ } else if ($.record.is(type.baseModel)) {
+ // Record-based scenarios are not supported, do nothing here
+ } else if (isTemplateDeclarationOrInstance(type.baseModel)) {
+ // Template type (declaration or instance) - needs TypeExpression for type parameter handling
+ extraBases.push();
+ } else {
+ // Regular model - use py.Reference for proper symbol resolution
+ extraBases.push();
+ }
+ }
+
+ // Handle index types: Arrays (int indexes) are supported, Records are not
+ const indexType = $.model.getIndexType(type);
+ if (indexType && !$.record.is(indexType)) {
+ extraBases.push();
+ }
+ }
+
+ // Combine explicit bases from props with extraBases (Generic, extends, etc.)
+ const allBases = (props.bases ?? []).concat(extraBases);
+
+ // For abstract classes, always include ABC (last for proper MRO)
+ if (props.abstract) {
+ return [...allBases, abcModule["."]["ABC"]];
+ }
+
+ return allBases;
+}
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-body.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-body.tsx
new file mode 100644
index 00000000000..a6f57ae4455
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-body.tsx
@@ -0,0 +1,56 @@
+import { For, type Children } from "@alloy-js/core";
+import { type Interface, type Model, type ModelProperty, type Operation } from "@typespec/compiler";
+import type { Typekit } from "@typespec/compiler/typekit";
+import { createRekeyableMap } from "@typespec/compiler/utils";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { ClassMember } from "./class-member.js";
+
+export interface ClassBodyProps {
+ type: Model | Interface;
+ abstract?: boolean;
+ methodType?: "method" | "class" | "static";
+ children?: Children;
+}
+
+/**
+ * Gets type members (properties or operations) from a Model or Interface.
+ */
+function getTypeMembers($: Typekit, type: Model | Interface): (ModelProperty | Operation)[] {
+ if ($.model.is(type)) {
+ return Array.from($.model.getProperties(type).values());
+ } else if (type.kind === "Interface") {
+ return Array.from(createRekeyableMap(type.operations).values());
+ } else {
+ throw new Error("Expected Model or Interface type");
+ }
+}
+
+/**
+ * Renders the body of a class declaration.
+ * For models, renders properties as dataclass fields.
+ * For interfaces, renders operations as abstract methods.
+ * Includes any additional children provided.
+ */
+export function ClassBody(props: ClassBodyProps): Children {
+ const { $ } = useTsp();
+ const typeMembers = getTypeMembers($, props.type);
+
+ // Throw error for models with additional properties (Record-based scenarios)
+ if ($.model.is(props.type)) {
+ const additionalPropsRecord = $.model.getAdditionalPropertiesRecord(props.type);
+ if (additionalPropsRecord) {
+ throw new Error("Models with additional properties (Record[…]) are not supported");
+ }
+ }
+
+ return (
+ <>
+
+ {(typeMember) => (
+
+ )}
+
+ {props.children}
+ >
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-declaration.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.test.tsx
new file mode 100644
index 00000000000..e9c574273c8
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.test.tsx
@@ -0,0 +1,1414 @@
+import { Tester } from "#test/test-host.js";
+import { List } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { ClassDeclaration } from "../../../../src/python/components/class-declaration/class-declaration.js";
+import { Method } from "../../../../src/python/components/class-declaration/class-method.js";
+import { EnumDeclaration } from "../../../../src/python/components/enum-declaration/enum-declaration.js";
+import { getOutput } from "../../test-utils.js";
+import { TypeAliasDeclaration } from "../type-alias-declaration/type-alias-declaration.js";
+
+describe("Python Class from model", () => {
+ it("creates a class", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ aliases: string[];
+ isActive: boolean;
+ color: "blue" | "red";
+ promotionalPrice: float64;
+ description?: string = "This is a widget";
+ createdAt: int64 = 1717334400;
+ tags: string[] = #["tag1", "tag2"];
+ isDeleted: boolean = false;
+ alternativeColor: "green" | "yellow" = "green";
+ price: float64 = 100.0;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Literal
+ from typing import Optional
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ weight: int
+ aliases: list[str]
+ is_active: bool
+ color: Literal["blue", "red"]
+ promotional_price: float
+ description: Optional[str] = "This is a widget"
+ created_at: int = 1717334400
+ tags: list[str] = ["tag1", "tag2"]
+ is_deleted: bool = False
+ alternative_color: Literal["green", "yellow"] = "green"
+ price: float = 100.0
+
+ `,
+ );
+ });
+
+ it("creates a class with non-default values followed by default values", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+
+ model ${t.model("Widget")} {
+ id: string;
+ description?: string = "This is a widget";
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Optional
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ description: Optional[str] = "This is a widget"
+
+ `,
+ );
+ });
+
+ it("creates a class with non-default values followed by default values", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+
+ model ${t.model("Widget")} {
+ description?: string = "This is a widget";
+ id: string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Optional
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ description: Optional[str] = "This is a widget"
+ id: str
+
+ `,
+ );
+ });
+
+ it("declares a class with multi line docs", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * This is a test
+ * with multiple lines
+ */
+ model ${t.model("Foo")} {
+ knownProp: string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ This is a test
+ with multiple lines
+ """
+
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("declares a class overriding docs", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * This is a test
+ * with multiple lines
+ */
+ model ${t.model("Foo")} {
+ knownProp: string;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ This is an overridden doc comment
+ with multiple lines
+ """
+
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("declares a class overriding docs with paragraphs array", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * Base doc will be overridden
+ */
+ model ${t.model("Foo")} {
+ knownProp: string;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ First paragraph
+
+ Second paragraph
+ """
+
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("declares a class overriding docs with prebuilt ClassDoc", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * Base doc will be overridden
+ */
+ model ${t.model("Foo")} {
+ knownProp: string;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ Alpha>, <>Beta>]} />} />,
+ ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ Alpha
+
+ Beta
+ """
+
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("declares a class from a model with @doc", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ @doc("This is a test")
+ model ${t.model("Foo")} {
+ knownProp: string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ This is a test
+ """
+
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("declares a model property with @doc", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * This is a test
+ */
+ model ${t.model("Foo")} {
+ @doc("This is a known property")
+ knownProp: string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ """
+ This is a test
+ """
+
+ # This is a known property
+ known_prop: str
+
+ `,
+ );
+ });
+
+ it("throws error for model is Record", async () => {
+ const { program, Person } = await Tester.compile(t.code`
+ model ${t.model("Person")} is Record;
+ `);
+
+ expect(() => {
+ expect(getOutput(program, [])).toRenderTo("");
+ }).toThrow(/Models with additional properties \(Record\[…\]\) are not supported/);
+ });
+
+ it("throws error for model is Record with properties", async () => {
+ const { program, Person } = await Tester.compile(t.code`
+ model ${t.model("Person")} is Record {
+ name: string;
+ }
+ `);
+
+ expect(() => {
+ expect(getOutput(program, [])).toRenderTo("");
+ }).toThrow(/Models with additional properties \(Record\[…\]\) are not supported/);
+ });
+
+ it("throws error for model extends Record", async () => {
+ const { program, Person } = await Tester.compile(t.code`
+ model ${t.model("Person")} extends Record {
+ name: string;
+ }
+ `);
+
+ expect(() => {
+ expect(getOutput(program, [])).toRenderTo("");
+ }).toThrow(/Models with additional properties \(Record\[…\]\) are not supported/);
+ });
+
+ it("throws error for model with ...Record", async () => {
+ const { program, Person } = await Tester.compile(t.code`
+ model ${t.model("Person")} {
+ age: int32;
+ ...Record;
+ }
+ `);
+
+ expect(() => {
+ expect(getOutput(program, [])).toRenderTo("");
+ }).toThrow(/Models with additional properties \(Record\[…\]\) are not supported/);
+ });
+
+ it("creates a class from a model that 'is' an array ", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ model ${t.model("Foo")} is Array;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ class Foo(list[str]):
+ pass
+
+ `,
+ );
+ });
+
+ it("handles a type reference to a union variant in a class property", async () => {
+ const { program, Color, Widget } = await Tester.compile(t.code`
+ union ${t.union("Color")} {
+ red: "RED",
+ blue: "BLUE",
+ }
+
+ model ${t.model("Widget")} {
+ id: string = "123";
+ weight: int32 = 100;
+ color: Color.blue;
+ }
+ `);
+
+ expect(
+ getOutput(program, [, ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from enum import StrEnum
+ from typing import Literal
+
+
+ class Color(StrEnum):
+ RED = "RED"
+ BLUE = "BLUE"
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str = "123"
+ weight: int = 100
+ color: Literal[Color.BLUE]
+
+ `,
+ );
+ });
+
+ it("handles a union of variant references in a class property", async () => {
+ const { program, Color, Widget } = await Tester.compile(t.code`
+ union ${t.union("Color")} {
+ red: "RED",
+ blue: "BLUE",
+ green: "GREEN",
+ }
+
+ model ${t.model("Widget")} {
+ id: string;
+ primaryColor: Color.red | Color.blue;
+ }
+ `);
+
+ expect(
+ getOutput(program, [, ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from enum import StrEnum
+ from typing import Literal
+
+
+ class Color(StrEnum):
+ RED = "RED"
+ BLUE = "BLUE"
+ GREEN = "GREEN"
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ primary_color: Literal[Color.RED, Color.BLUE]
+
+ `,
+ );
+ });
+
+ it("handles a union of integer literals in a class property", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ id: string;
+ priority: 1 | 2 | 3;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ priority: Literal[1, 2, 3]
+
+ `,
+ );
+ });
+
+ it("handles a union of boolean literals in a class property", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ id: string;
+ isActiveOrEnabled: true | false;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ is_active_or_enabled: Literal[True, False]
+
+ `,
+ );
+ });
+
+ it("handles a mixed union of literals and variant references", async () => {
+ const { program, Color, Widget } = await Tester.compile(t.code`
+ union ${t.union("Color")} {
+ red: "RED",
+ blue: "BLUE",
+ }
+
+ model ${t.model("Widget")} {
+ id: string;
+ mixedValue: "custom" | 42 | true | Color.red;
+ }
+ `);
+
+ expect(
+ getOutput(program, [, ]),
+ ).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from enum import StrEnum
+ from typing import Literal
+
+
+ class Color(StrEnum):
+ RED = "RED"
+ BLUE = "BLUE"
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ mixed_value: Literal["custom", 42, True, Color.RED]
+
+ `,
+ );
+ });
+
+ it("renders a never-typed member as typing.Never", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ property: never;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Never
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ property: Never
+
+ `);
+ });
+
+ it("can override class name", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ color: "blue" | "red";
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ @dataclass(kw_only=True)
+ class MyOperations:
+ id: str
+ weight: int
+ color: Literal["blue", "red"]
+
+ `);
+ });
+
+ it("can add a members to the class", async () => {
+ const { program, Widget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ color: "blue" | "red";
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+
+ <>custom_property: str>
+
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ @dataclass(kw_only=True)
+ class MyOperations:
+ id: str
+ weight: int
+ color: Literal["blue", "red"]
+ custom_property: str
+
+ `);
+ });
+ it("creates a class from a model with extends", async () => {
+ const { program, Widget, ErrorWidget } = await Tester.compile(t.code`
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ color: "blue" | "red";
+ }
+
+ model ${t.model("ErrorWidget")} extends Widget {
+ code: int32;
+ message: string;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ weight: int
+ color: Literal["blue", "red"]
+
+
+ @dataclass(kw_only=True)
+ class ErrorWidget(Widget):
+ code: int
+ message: str
+
+ `);
+ });
+});
+
+describe("Python Class from interface", () => {
+ it("creates a class from an interface declaration", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("should handle spread and non spread interface parameters", async () => {
+ const { program, Foo, WidgetOperations } = await Tester.compile(t.code`
+ model ${t.model("Foo")} {
+ name: string
+ }
+
+ interface ${t.interface("WidgetOperations")} {
+ op getName(foo: Foo): string;
+ op getOtherName(...Foo): string
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Foo:
+ name: str
+
+
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, foo: Foo) -> str:
+ pass
+
+ @abstractmethod
+ def get_other_name(self, name: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class from an interface with Model references", async () => {
+ const { program, WidgetOperations, Widget } = await Tester.compile(t.code`
+ /**
+ * Operations for Widget
+ */
+ interface ${t.interface("WidgetOperations")} {
+ /**
+ * Get the name of the widget
+ */
+ op getName(
+ /**
+ * The id of the widget
+ */
+ id: string
+ ): Widget;
+ }
+
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ color: "blue" | "red";
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ class WidgetOperations(ABC):
+ """
+ Operations for Widget
+ """
+
+ @abstractmethod
+ def get_name(self, id: str) -> Widget:
+ """
+ Get the name of the widget
+ """
+ pass
+
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ weight: int
+ color: Literal["blue", "red"]
+
+ `);
+ });
+
+ it("creates a class from an interface that extends another", async () => {
+ const { program, WidgetOperations, WidgetOperationsExtended, Widget } =
+ await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): Widget;
+ }
+
+ interface ${t.interface("WidgetOperationsExtended")} extends WidgetOperations{
+ op delete(id: string): void;
+ }
+
+ model ${t.model("Widget")} {
+ id: string;
+ weight: int32;
+ color: "blue" | "red";
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+ from dataclasses import dataclass
+ from typing import Literal
+
+
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> Widget:
+ pass
+
+
+
+ class WidgetOperationsExtended(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> Widget:
+ pass
+
+ @abstractmethod
+ def delete(self, id: str) -> None:
+ pass
+
+
+
+ @dataclass(kw_only=True)
+ class Widget:
+ id: str
+ weight: int
+ color: Literal["blue", "red"]
+
+ `);
+ });
+});
+
+describe("Python Class overrides", () => {
+ it("creates a class with a method if a model is provided and a class method is provided", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ model ${t.model("WidgetOperations")} {
+ id: string;
+ weight: int32;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+
+
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class WidgetOperations:
+ id: str
+ weight: int
+
+ def do_work(self) -> None:
+ """
+ This is a test
+ """
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with a method if a model is provided and a class method is provided and methodType is set to method", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ model ${t.model("WidgetOperations")} {
+ id: string;
+ weight: int32;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+
+
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class WidgetOperations:
+ id: str
+ weight: int
+
+ def do_work(self) -> None:
+ """
+ This is a test
+ """
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with a classmethod if a model is provided, a class method is provided and methodType is set to class", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ model ${t.model("WidgetOperations")} {
+ id: string;
+ weight: int32;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+
+
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class WidgetOperations:
+ id: str
+ weight: int
+
+ @classmethod
+ def do_work(cls) -> None:
+ """
+ This is a test
+ """
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with a staticmethod if a model is provided, a class method is provided and methodType is set to static", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ model ${t.model("WidgetOperations")} {
+ id: string;
+ weight: int32;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+
+
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class WidgetOperations:
+ id: str
+ weight: int
+
+ @staticmethod
+ def do_work() -> None:
+ """
+ This is a test
+ """
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with abstract method if an interface is provided", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with abstract method if an interface is provided and methodType is set to method", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with abstract classmethod if an interface is provided and methodType is set to class", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class WidgetOperations(ABC):
+ @classmethod
+ @abstractmethod
+ def get_name(cls, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class with abstract staticmethod if an interface is provided and methodType is set to static", async () => {
+ const { program, WidgetOperations } = await Tester.compile(t.code`
+ interface ${t.interface("WidgetOperations")} {
+ op getName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class WidgetOperations(ABC):
+ @staticmethod
+ @abstractmethod
+ def get_name(id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("Emits type alias to template instance as dataclass", async () => {
+ const { program, StringResponse } = await Tester.compile(t.code`
+ model Response {
+ data: T;
+ status: string;
+ }
+
+ alias ${t.type("StringResponse")} = Response;
+ `);
+
+ // Type alias to a template instance is emitted as a dataclass,
+ // since Python doesn't support parameterized type aliases like TypeScript.
+ // This is equivalent to: model StringResponse is Response;
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class StringResponse:
+ data: str
+ status: str
+
+ `);
+ });
+
+ it("Emits multiple concrete models from template instances using 'is'", async () => {
+ const { program, StringResult, IntResult } = await Tester.compile(t.code`
+ model Result {
+ value: T;
+ error: E;
+ }
+
+ model ${t.model("StringResult")} is Result;
+ model ${t.model("IntResult")} is Result;
+ `);
+
+ // TypeSpec 'is' copies all properties from the template instance.
+ // Each concrete model gets fully expanded properties with substituted types.
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class StringResult:
+ value: str
+ error: str
+
+
+ @dataclass(kw_only=True)
+ class IntResult:
+ value: int
+ error: str
+
+ `);
+ });
+
+ it("Emits concrete model using 'is' from template instance", async () => {
+ const { program, StringResponse } = await Tester.compile(t.code`
+ model Response {
+ data: T;
+ status: string;
+ }
+
+ model ${t.model("StringResponse")} is Response;
+ `);
+
+ // Using 'is' copies all properties from the template instance.
+ // StringResponse becomes a concrete model with all properties from Response.
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class StringResponse:
+ data: str
+ status: str
+
+ `);
+ });
+
+ it("Emits concrete interface extending template (operations use concrete types)", async () => {
+ const { program, StringRepository } = await Tester.compile(t.code`
+ interface Repository {
+ get(id: string): T;
+ list(): T[];
+ }
+
+ interface ${t.interface("StringRepository")} extends Repository {
+ findByPrefix(prefix: string): string[];
+ }
+ `);
+
+ // TypeSpec flattens interface inheritance - StringRepository gets all operations
+ // from Repository with T replaced by string.
+ expect(getOutput(program, [])).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class StringRepository(ABC):
+ @abstractmethod
+ def get(self, id: str) -> str:
+ pass
+
+ @abstractmethod
+ def list(self) -> list[str]:
+ pass
+
+ @abstractmethod
+ def find_by_prefix(self, prefix: str) -> list[str]:
+ pass
+
+
+ `);
+ });
+
+ it("Emits multiple concrete interfaces (templates are macros, no generics)", async () => {
+ const { program, UserRepository, ProductRepository } = await Tester.compile(t.code`
+ interface Repository {
+ get(id: string): T;
+ }
+
+ interface ${t.interface("UserRepository")} extends Repository {
+ findByEmail(email: string): string;
+ }
+
+ interface ${t.interface("ProductRepository")} extends Repository {
+ findByCategory(category: string): int32[];
+ }
+ `);
+
+ // Each concrete interface extends the template with different type arguments.
+ // TypeSpec flattens the inheritance with concrete types substituted.
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+
+
+ class UserRepository(ABC):
+ @abstractmethod
+ def get(self, id: str) -> str:
+ pass
+
+ @abstractmethod
+ def find_by_email(self, email: str) -> str:
+ pass
+
+
+
+ class ProductRepository(ABC):
+ @abstractmethod
+ def get(self, id: str) -> int:
+ pass
+
+ @abstractmethod
+ def find_by_category(self, category: str) -> list[int]:
+ pass
+
+
+ `);
+ });
+
+ it("Handles template instance with 'is' (copies properties)", async () => {
+ const { program, CanadaAddress } = await Tester.compile(t.code`
+ model Address {
+ state: TState;
+ city: string;
+ street: string;
+ }
+
+ model ${t.model("CanadaAddress")} is Address;
+ `);
+
+ // TypeSpec 'is' copies all properties from the template instance.
+ // CanadaAddress is a concrete type with all properties from Address.
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Never
+
+
+ @dataclass(kw_only=True)
+ class CanadaAddress:
+ state: Never
+ city: str
+ street: str
+
+ `);
+ });
+
+ it("Handles template with bounded type parameter using 'is'", async () => {
+ const { program, StringContainer } = await Tester.compile(t.code`
+ model Container {
+ value: T;
+ label: string;
+ }
+
+ model ${t.model("StringContainer")} is Container;
+ `);
+
+ // Bounded type parameters (T extends string) work the same as unbounded -
+ // the constraint is enforced by TypeSpec at compile time, and the concrete
+ // type gets the substituted value.
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class StringContainer:
+ value: str
+ label: str
+
+ `);
+ });
+
+ it("Handles template with multiple bounded and unbounded parameters using 'is'", async () => {
+ const { program, MyResult } = await Tester.compile(t.code`
+ model Result {
+ value: T;
+ error: E;
+ }
+
+ model ${t.model("MyResult")} is Result;
+ `);
+
+ // Mixed bounded/unbounded parameters are handled the same way -
+ // TypeSpec expands the template with concrete types.
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class MyResult:
+ value: str
+ error: int
+
+ `);
+ });
+
+ it("Handles 'extends' with concrete model (Python inheritance)", async () => {
+ const { program, Address, CanadaAddress } = await Tester.compile(t.code`
+ model ${t.model("Address")} {
+ city: string;
+ }
+
+ model ${t.model("CanadaAddress")} extends Address {
+ street: string;
+ }
+ `);
+
+ // TypeSpec 'extends' creates Python class inheritance when the base is a concrete model.
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Address:
+ city: str
+
+
+ @dataclass(kw_only=True)
+ class CanadaAddress(Address):
+ street: str
+
+ `);
+ });
+
+ it("Handles 'extends' with template instance base (references concrete base)", async () => {
+ const { program, Response, ConcreteResponse } = await Tester.compile(t.code`
+ model ${t.model("Response")} {
+ data: string;
+ status: string;
+ }
+
+ model ${t.model("ConcreteResponse")} extends Response {
+ timestamp: string;
+ }
+ `);
+
+ // When extending a concrete model, Python inheritance is used.
+ // The base class must be emitted first for the reference to resolve.
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class Response:
+ data: str
+ status: str
+
+
+ @dataclass(kw_only=True)
+ class ConcreteResponse(Response):
+ timestamp: str
+
+ `);
+ });
+
+ it("Handles template instance with 'extends' and never type using 'is' pattern", async () => {
+ const { program, CanadaAddress } = await Tester.compile(t.code`
+ model Address {
+ state: TState;
+ city: string;
+ }
+
+ // Using 'is' instead of 'extends' to copy all properties from the template instance
+ model ${t.model("CanadaAddress")} is Address {
+ street: string;
+ }
+ `);
+
+ // When extending template instances, prefer 'is' pattern which copies all properties.
+ // The 'extends' keyword with template instances would require the template declaration
+ // to be emitted, which is not supported since templates are macros.
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Never
+
+
+ @dataclass(kw_only=True)
+ class CanadaAddress:
+ state: Never
+ city: str
+ street: str
+
+ `);
+ });
+
+ it("creates an abstract dataclass when abstract prop is true with a model", async () => {
+ const { program, BaseEntity } = await Tester.compile(t.code`
+ model ${t.model("BaseEntity")} {
+ id: string;
+ createdAt: string;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from abc import ABC
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class BaseEntity(ABC):
+ id: str
+ created_at: str
+
+ `);
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx
new file mode 100644
index 00000000000..3b6fd3fcbc3
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx
@@ -0,0 +1,116 @@
+import * as py from "@alloy-js/python";
+import { type Interface, type Model } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { reportDiagnostic } from "../../../lib.js";
+import { declarationRefkeys } from "../../utils/refkey.js";
+import { DocElement } from "../doc-element/doc-element.js";
+import { ClassBases } from "./class-bases.js";
+import { ClassBody } from "./class-body.js";
+import { MethodProvider } from "./class-method.js";
+
+export interface ClassDeclarationPropsWithType extends Omit {
+ type: Model | Interface;
+ name?: string;
+ abstract?: boolean; // Global override for the abstract flag
+ methodType?: "method" | "class" | "static"; // Global override for the method type
+}
+
+export type ClassDeclarationProps = ClassDeclarationPropsWithType | py.ClassDeclarationProps;
+
+function isTypedClassDeclarationProps(
+ props: ClassDeclarationProps,
+): props is ClassDeclarationPropsWithType {
+ return "type" in props;
+}
+
+/**
+ * Converts TypeSpec Models and Interfaces to Python classes.
+ *
+ * - **Models** are converted into Dataclasses with `@dataclass(kw_only=True)` + fields
+ * - **Interfaces** are converted into Abstract classes (ABC) with abstract methods
+ * - For models that extends another model, we convert that into Python class inheritance
+ * - For interfaces that extends another interface, there's no inheritance, since
+ * TypeSpec flattens the inheritance
+ *
+ * @param props - The props for the class declaration.
+ * @returns The class declaration.
+ */
+export function ClassDeclaration(props: ClassDeclarationProps) {
+ const { $ } = useTsp();
+
+ // Interfaces are rendered as abstract classes (ABC) with abstract methods
+ // Models are rendered as concrete dataclasses with fields
+ // If we are explicitly overriding the class as abstract or the type is not a model, we need to create an abstract class
+ const abstract =
+ ("abstract" in props && props.abstract) || ("type" in props && !$.model.is(props.type));
+
+ const type = "type" in props ? props.type : undefined;
+ const docSource = props.doc ?? (type ? $.type.getDoc(type) : undefined);
+ const docElement = docSource ? : undefined;
+
+ // TODO: When TypeSpec adds true generics support, pass extraBases with Generic[T, ...] here.
+ // Currently, TypeSpec templates are macros that expand to concrete types, so we don't
+ // generate Python generics (TypeVar/Generic) for template declarations.
+ const basesType = ClassBases({
+ type: "type" in props ? props.type : undefined,
+ bases: props.bases,
+ abstract,
+ });
+
+ if (!isTypedClassDeclarationProps(props)) {
+ return (
+
+ );
+ }
+
+ 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");
+
+ const refkeys = declarationRefkeys(props.refkey, props.type);
+
+ // Check for models with additional properties (Record-based scenarios)
+ // This check must happen here (in addition to ClassBody) because models with no properties
+ // (e.g., `model Foo is Record`) won't render a ClassBody, so the error would never be thrown
+ if ($.model.is(props.type)) {
+ const additionalPropsRecord = $.model.getAdditionalPropertiesRecord(props.type);
+ if (additionalPropsRecord) {
+ throw new Error("Models with additional properties (Record[…]) are not supported");
+ }
+ }
+
+ // Array-based models (e.g., model Foo is Array) use regular classes, not dataclasses,
+ // since Array models in TypeSpec can't have properties, so they behave more like a class
+ // that inherits from a list.
+ // Similarly, interfaces should use regular classes (ABC) not dataclasses, since interfaces
+ // only define abstract methods, not fields.
+ const isArrayModel = $.model.is(props.type) && $.array.is(props.type);
+ const isInterface = props.type.kind === "Interface";
+ const useDataclass = !isArrayModel && !isInterface;
+
+ const ClassComponent = useDataclass ? py.DataclassDeclaration : py.ClassDeclaration;
+
+ return (
+
+
+
+ {props.children}
+
+
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-member.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-member.test.tsx
new file mode 100644
index 00000000000..47b71875bd5
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-member.test.tsx
@@ -0,0 +1,194 @@
+import { Tester } from "#test/test-host.js";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { ClassDeclaration } from "../../../../src/python/components/class-declaration/class-declaration.js";
+import { getOutput } from "../../test-utils.js";
+
+describe("Python Class Members", () => {
+ describe("default values", () => {
+ it("renders string default values", async () => {
+ const { program, MyModel } = await Tester.compile(t.code`
+ model ${t.model("MyModel")} {
+ name: string = "default";
+ description?: string = "optional with default";
+ emptyString: string = "";
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Optional
+
+
+ @dataclass(kw_only=True)
+ class MyModel:
+ name: str = "default"
+ description: Optional[str] = "optional with default"
+ empty_string: str = ""
+
+ `,
+ );
+ });
+
+ it("renders boolean default values", async () => {
+ const { program, BooleanModel } = await Tester.compile(t.code`
+ model ${t.model("BooleanModel")} {
+ isActive: boolean = true;
+ isDeleted: boolean = false;
+ optional?: boolean = true;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from typing import Optional
+
+
+ @dataclass(kw_only=True)
+ class BooleanModel:
+ is_active: bool = True
+ is_deleted: bool = False
+ optional: Optional[bool] = True
+
+ `,
+ );
+ });
+
+ it("renders array default values", async () => {
+ const { program, ArrayModel } = await Tester.compile(t.code`
+ model ${t.model("ArrayModel")} {
+ tags: string[] = #["tag1", "tag2"];
+ emptyArray: int32[] = #[];
+ numbers: int32[] = #[1, 2, 3];
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class ArrayModel:
+ tags: list[str] = ["tag1", "tag2"]
+ empty_array: list[int] = []
+ numbers: list[int] = [1, 2, 3]
+
+ `,
+ );
+ });
+
+ it("renders integer default values without .0 suffix", async () => {
+ const { program, IntegerModel } = await Tester.compile(t.code`
+ model ${t.model("IntegerModel")} {
+ count: int32 = 42;
+ bigNumber: int64 = 1000000;
+ smallNumber: int8 = 127;
+ unsignedValue: uint32 = 100;
+ safeIntValue: safeint = 999;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+
+
+ @dataclass(kw_only=True)
+ class IntegerModel:
+ count: int = 42
+ big_number: int = 1000000
+ small_number: int = 127
+ unsigned_value: int = 100
+ safe_int_value: int = 999
+
+ `,
+ );
+ });
+
+ it("renders float and decimal default values correctly", async () => {
+ const { program, NumericDefaults } = await Tester.compile(t.code`
+
+ scalar customFloat extends float;
+ scalar customDecimal extends decimal;
+
+ model ${t.model("NumericDefaults")} {
+ // Float variants with decimal values
+ floatBase: float = 1.5;
+ float32Value: float32 = 2.5;
+ float64Value: float64 = 3.5;
+ customFloatValue: customFloat = 4.5;
+
+ // Float variants with integer values (should render with .0)
+ floatInt: float = 10;
+ float32Int: float32 = 20;
+ float64Int: float64 = 30;
+
+ // Decimal variants
+ decimalBase: decimal = 100.25;
+ decimal128Value: decimal128 = 200.75;
+ customDecimalValue: customDecimal = 300.125;
+
+ // Decimal with integer values (should render with .0)
+ decimalInt: decimal = 400;
+ decimal128Int: decimal128 = 500;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from decimal import Decimal
+
+
+ @dataclass(kw_only=True)
+ class NumericDefaults:
+ float_base: float = 1.5
+ float32_value: float = 2.5
+ float64_value: float = 3.5
+ custom_float_value: float = 4.5
+ float_int: float = 10.0
+ float32_int: float = 20.0
+ float64_int: float = 30.0
+ decimal_base: Decimal = 100.25
+ decimal128_value: Decimal = 200.75
+ custom_decimal_value: Decimal = 300.125
+ decimal_int: Decimal = 400.0
+ decimal128_int: Decimal = 500.0
+
+ `,
+ );
+ });
+
+ it("distinguishes between integer and float types with same numeric value", async () => {
+ const { program, MixedNumeric } = await Tester.compile(t.code`
+ model ${t.model("MixedNumeric")} {
+ intValue: int32 = 100;
+ int64Value: int64 = 100;
+ floatValue: float = 100;
+ float64Value: float64 = 100;
+ decimalValue: decimal = 100;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(
+ `
+ from dataclasses import dataclass
+ from decimal import Decimal
+
+
+ @dataclass(kw_only=True)
+ class MixedNumeric:
+ int_value: int = 100
+ int64_value: int = 100
+ float_value: float = 100.0
+ float64_value: float = 100.0
+ decimal_value: Decimal = 100.0
+
+ `,
+ );
+ });
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx
new file mode 100644
index 00000000000..f493082745c
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx
@@ -0,0 +1,67 @@
+import { typingModule } from "#python/builtins.js";
+import { type Children } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import { type ModelProperty, type Operation } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+import { Method } from "./class-method.js";
+import { PrimitiveInitializer } from "./primitive-initializer.js";
+
+export interface ClassMemberProps {
+ type: ModelProperty | Operation;
+ doc?: Children;
+ optional?: boolean;
+ methodType?: "method" | "class" | "static";
+ abstract?: boolean;
+}
+
+/**
+ * Creates the class member for the property.
+ * @param props - The props for the class member.
+ * @returns The class member.
+ */
+export function ClassMember(props: ClassMemberProps) {
+ const { $ } = useTsp();
+ const namer = py.usePythonNamePolicy();
+ const name = namer.getName(props.type.name, "class-member");
+ const doc = props.doc ?? $.type.getDoc(props.type);
+
+ if ($.modelProperty.is(props.type)) {
+ // Map never-typed properties to typing.Never
+
+ const unpackedType = props.type.type;
+ const isOptional = props.optional ?? props.type.optional ?? false;
+ const defaultValue = props.type.defaultValue;
+ const initializer = defaultValue ? (
+
+ ) : undefined;
+ const unpackedTypeNode: Children = ;
+ const typeNode = isOptional ? (
+
+ ) : (
+ unpackedTypeNode
+ );
+
+ const classMemberProps = {
+ doc,
+ name,
+ optional: isOptional,
+ ...(typeNode ? { type: typeNode } : {}),
+ ...(initializer ? { initializer } : {}),
+ omitNone: !isOptional,
+ };
+ return ;
+ }
+
+ if ($.operation.is(props.type)) {
+ return (
+
+ );
+ }
+
+ // If type is neither ModelProperty nor Operation, return empty fragment
+ return <>>;
+}
diff --git a/packages/emitter-framework/src/python/components/class-declaration/class-method.test.tsx b/packages/emitter-framework/src/python/components/class-declaration/class-method.test.tsx
new file mode 100644
index 00000000000..2e8217c5383
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.test.tsx
@@ -0,0 +1,250 @@
+import { Tester } from "#test/test-host.js";
+import { getProgram } from "#test/utils.js";
+import * as py from "@alloy-js/python";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { ClassDeclaration } from "../../../../src/python/components/class-declaration/class-declaration.js";
+import { Method } from "../../../../src/python/components/class-declaration/class-method.js";
+import { getOutput } from "../../test-utils.js";
+
+describe("interface methods with a `type` prop", () => {
+ it("creates a class method from an interface method", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ async def get_name(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class method that is a classmethod", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ @classmethod
+ async def get_name(cls, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates a class method that is a staticmethod", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ @staticmethod
+ async def get_name(id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("creates an async class method from an asyncinterface method", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ async def get_name(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can append extra keyword-only parameters", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name(self, id: str, *, foo: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can add extra keyword-only parameters", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name(self, id: str, *, foo: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can replace parameters with raw params provided", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name(self, *, foo: str, bar: float) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can replace parameters with params having defaults (requires * marker)", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+ },
+ { name: "bar", type: "number", default: },
+ ]}
+ />
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name(self, *, foo: str = "default", bar: float = 42) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can override return type in a class method", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name(self, id: str) -> ASpecialString:
+ pass
+
+
+ `);
+ });
+
+ it("can override method name in a class method", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ @test op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def get_name_custom(self, id: str) -> str:
+ pass
+
+
+ `);
+ });
+});
+
+describe("interface methods without a `type` prop", () => {
+ it("renders a plain interface method from a class method without a `type` prop", async () => {
+ const program = await getProgram("");
+
+ expect(
+ getOutput(program, [
+
+
+ ,
+ ]),
+ ).toRenderTo(`
+ class BasicInterface:
+ def plain_method(self, param1: string) -> number:
+ pass
+
+
+ `);
+ });
+});
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
new file mode 100644
index 00000000000..da717d79694
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx
@@ -0,0 +1,97 @@
+import { 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";
+import { buildParameterDescriptors, getReturnType } from "../../utils/operation.js";
+import { DocElement } from "../doc-element/doc-element.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export const MethodContext = createContext<"method" | "static" | "class" | undefined>(undefined);
+export const MethodProvider = MethodContext.Provider;
+
+export interface MethodPropsWithType extends Omit {
+ type: Operation;
+ name?: string;
+ doc?: Children;
+ methodType?: "method" | "class" | "static";
+ abstract?: boolean;
+ /** If true, parameters replaces operation parameters instead of adding to them as keyword-only */
+ replaceParameters?: boolean;
+}
+
+export type MethodProps = MethodPropsWithType | py.MethodDeclarationBaseProps;
+
+/**
+ * Get the method component based on the resolved method type.
+ * We prioritize the methodType prop provided in the Method component,
+ * and then the one provided in the context, and then we default to "method".
+ */
+function getResolvedMethodType(props: MethodProps): "method" | "class" | "static" {
+ const ctxMethodType = useContext(MethodContext);
+ const propMethodType = "methodType" in props ? (props as any).methodType : undefined;
+ return (propMethodType ?? ctxMethodType ?? "method") as "method" | "class" | "static";
+}
+
+/**
+ * A Python class method. Pass the `type` prop to create the
+ * method by converting from a TypeSpec Operation. Any other props
+ * provided will take precedence.
+ */
+export function Method(props: Readonly) {
+ const { $ } = useTsp();
+ const isTypeSpecTyped = "type" in props;
+ const type = isTypeSpecTyped ? props.type : undefined;
+ const docSource = props.doc ?? (type ? $.type.getDoc(type) : undefined);
+ const docElement = docSource ? (
+
+ ) : undefined;
+ const resolvedMethodType = getResolvedMethodType(props);
+ const MethodComponent =
+ resolvedMethodType === "static"
+ ? py.StaticMethodDeclaration
+ : resolvedMethodType === "class"
+ ? py.ClassMethodDeclaration
+ : py.MethodDeclaration;
+
+ // Default to abstract when deriving from a TypeSpec operation (`type` prop present),
+ // unless explicitly overridden by props.abstract === false
+ const abstractFlag = (() => {
+ const explicit = (props as any).abstract as boolean | undefined;
+ return explicit ?? (!isTypeSpecTyped ? false : 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 [efProps, updateProps, forwardProps] = splitProps(
+ props,
+ ["type"],
+ ["returnType", "parameters"],
+ );
+
+ const name = props.name ?? py.usePythonNamePolicy().getName(efProps.type.name, "function");
+ const returnType = props.returnType ?? ;
+ const allParameters = buildParameterDescriptors(efProps.type.parameters, {
+ params: props.parameters,
+ replaceParameters: props.replaceParameters,
+ });
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/class-declaration/index.ts b/packages/emitter-framework/src/python/components/class-declaration/index.ts
new file mode 100644
index 00000000000..19f53a8ea60
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/index.ts
@@ -0,0 +1,6 @@
+export * from "./class-bases.js";
+export * from "./class-body.js";
+export * from "./class-declaration.js";
+export * from "./class-member.js";
+export * from "./class-method.js";
+export * from "./primitive-initializer.js";
diff --git a/packages/emitter-framework/src/python/components/class-declaration/primitive-initializer.tsx b/packages/emitter-framework/src/python/components/class-declaration/primitive-initializer.tsx
new file mode 100644
index 00000000000..65aeca9b2d5
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/primitive-initializer.tsx
@@ -0,0 +1,62 @@
+import { type Children } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { Type, Value } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { Atom } from "../atom/atom.js";
+
+export interface PrimitiveInitializerProps {
+ /**
+ * The default value to convert to a Python initializer expression.
+ */
+ defaultValue: Value;
+ /**
+ * The property type, used to determine float vs int formatting.
+ */
+ propertyType: Type;
+}
+
+/**
+ * Renders a Python primitive initializer from a TypeSpec default value.
+ *
+ * Handles StringValue, BooleanValue, NullValue, NumericValue, and ArrayValue.
+ * For numeric values, uses the propertyType to determine whether to render as float.
+ *
+ * @returns The Python initializer expression, or undefined if not supported.
+ */
+export function PrimitiveInitializer(props: PrimitiveInitializerProps): Children | undefined {
+ const { $ } = useTsp();
+ const { defaultValue, propertyType } = props;
+
+ if (!defaultValue) return undefined;
+
+ const valueKind = (defaultValue as any).valueKind ?? (defaultValue as any).kind;
+ switch (valueKind) {
+ case "StringValue":
+ case "BooleanValue":
+ case "NullValue":
+ return ;
+ case "NumericValue": {
+ // The Atom component converts NumericValue via asNumber(), which normalizes 100.0 to 100.
+ // Atom also has no access to the field type (float vs int), so it can't decide when to keep a trailing .0.
+ // Here we do have the propertyType so, for float/decimal fields, we render a raw value and append ".0"
+ // when needed. For non-float fields, default to a plain numeric Atom.
+
+ // Unwrap potential numeric wrapper shape and preserve float formatting
+ let raw: any = (defaultValue as any).value;
+ // Example: value is { value: "100", isInteger: true }
+ if (raw && typeof raw === "object" && "value" in raw) raw = raw.value;
+
+ // Float-like property types (including custom subtypes) should render with float hint
+ if ($.scalar.extendsFloat(propertyType) || $.scalar.extendsDecimal(propertyType)) {
+ return ;
+ }
+
+ // Otherwise output as a number atom
+ return ;
+ }
+ case "ArrayValue":
+ return ;
+ default:
+ return undefined;
+ }
+}
diff --git a/packages/emitter-framework/src/python/components/doc-element/doc-element.tsx b/packages/emitter-framework/src/python/components/doc-element/doc-element.tsx
new file mode 100644
index 00000000000..75ccdd35b6f
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/doc-element/doc-element.tsx
@@ -0,0 +1,83 @@
+import { type Children, type Component, List } from "@alloy-js/core";
+import type { Type } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+
+export interface DocElementProps {
+ /**
+ * The TypeSpec type to get documentation from.
+ * If provided and no `doc` override is given, documentation will be
+ * fetched via `$.type.getDoc(type)`.
+ */
+ type?: Type;
+
+ /**
+ * Optional documentation override. If provided, this takes precedence
+ * over documentation from the `type` prop.
+ *
+ * Accepts:
+ * - string - split into lines and render as a multi-line docstring
+ * - string[] | Children[] - rendered as separate paragraphs
+ * - Children (e.g., an explicit Doc component) - returned as-is
+ */
+ doc?: string | string[] | Children | Children[];
+
+ /**
+ * The Python doc component to use for rendering (ClassDoc, FunctionDoc, MethodDoc, etc.)
+ */
+ component: Component<{ description: Children[] }>;
+}
+
+/**
+ * Renders documentation for a Python declaration.
+ *
+ * This component handles fetching documentation from TypeSpec types and
+ * normalizing various doc formats into the appropriate Python doc component.
+ *
+ * @example
+ * ```tsx
+ * // With a TypeSpec type (fetches doc automatically)
+ *
+ *
+ * // With an explicit doc override
+ *
+ *
+ * // With both (doc takes precedence)
+ *
+ * ```
+ */
+export function DocElement(props: DocElementProps): Children {
+ const { $ } = useTsp();
+
+ // Resolve the documentation source: explicit doc takes precedence over type-derived doc
+ const source = props.doc ?? (props.type ? $.type.getDoc(props.type) : undefined);
+
+ if (!source) {
+ return undefined;
+ }
+
+ const DocComponent = props.component;
+
+ // Doc provided as an array (paragraphs/nodes) - preserve structure
+ if (Array.isArray(source)) {
+ return ;
+ }
+
+ // Doc provided as a string - preserve line breaks
+ if (typeof source === "string") {
+ const lines = source.split(/\r?\n/);
+ return (
+
+ {lines.map((line) => (
+ <>{line}>
+ ))}
+ ,
+ ]}
+ />
+ );
+ }
+
+ // Doc provided as JSX - pass through unchanged
+ return source as Children;
+}
diff --git a/packages/emitter-framework/src/python/components/doc-element/index.ts b/packages/emitter-framework/src/python/components/doc-element/index.ts
new file mode 100644
index 00000000000..918f7f95db9
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/doc-element/index.ts
@@ -0,0 +1 @@
+export * from "./doc-element.js";
diff --git a/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.test.tsx b/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.test.tsx
new file mode 100644
index 00000000000..a74c7abcdf7
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.test.tsx
@@ -0,0 +1,319 @@
+import { efRefkey } from "#python/utils/refkey.js";
+import { Tester } from "#test/test-host.js";
+import { d } from "@alloy-js/core/testing";
+import * as py from "@alloy-js/python";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { getOutput } from "../../test-utils.js";
+import { EnumDeclaration } from "./enum-declaration.js";
+
+describe("Python Enum Declaration", () => {
+ it("takes an enum type parameter", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ enum ${t.enum("Foo")} {
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+
+ `);
+ });
+
+ it("adds Python doc from TypeSpec", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * This is a test enum
+ */
+ enum ${t.enum("Foo")} {
+ @doc("This is one")
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ """
+ This is a test enum
+ """
+
+ ONE = 1
+ """
+ This is one
+ """
+ TWO = 2
+ THREE = 3
+
+
+ `);
+ });
+
+ it("explicit doc take precedence", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ /**
+ * This is a test enum
+ */
+ enum ${t.enum("Foo")} {
+ @doc("This is one")
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+ const output = getOutput(program, [
+ ,
+ ]);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ """
+ This is an explicit doc
+ """
+
+ ONE = 1
+ """
+ This is one
+ """
+ TWO = 2
+ THREE = 3
+
+
+ `);
+ });
+
+ it("takes a union type parameter", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ union ${t.union("Foo")} {
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+
+ `);
+ });
+
+ it("can be referenced", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ enum ${t.enum("Foo")} {
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+
+ const output = getOutput(program, [
+ ,
+
+ {efRefkey(Foo)}
+ {efRefkey(Foo.members.get("one"))}
+ ,
+ ]);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+
+ Foo
+ Foo.ONE
+ `);
+ });
+
+ it("can be referenced using union", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ union ${t.union("Foo")} {
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+
+ const output = getOutput(program, [
+ ,
+
+ {efRefkey(Foo)}
+ {efRefkey(Foo.variants.get("one"))}
+ ,
+ ]);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+
+ Foo
+ Foo.ONE
+ `);
+ });
+
+ describe("Enum Type Detection", () => {
+ it("generates IntEnum if all values are integer values", async () => {
+ const { program, StatusCode } = await Tester.compile(t.code`
+ enum ${t.enum("StatusCode")} {
+ success: 200,
+ notFound: 404,
+ serverError: 500
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class StatusCode(IntEnum):
+ SUCCESS = 200
+ NOT_FOUND = 404
+ SERVER_ERROR = 500
+
+
+ `);
+ });
+
+ it("generates StrEnum if all values are string values", async () => {
+ const { program, Color } = await Tester.compile(t.code`
+ enum ${t.enum("Color")} {
+ red: "red",
+ green: "green",
+ blue: "blue"
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import StrEnum
+
+
+ class Color(StrEnum):
+ RED = "red"
+ GREEN = "green"
+ BLUE = "blue"
+
+
+ `);
+ });
+
+ it("generates StrEnum if all values are string values with custom values", async () => {
+ const { program, Color } = await Tester.compile(t.code`
+ enum ${t.enum("Color")} {
+ red: "This is red",
+ green: "This is green",
+ blue: "This is blue"
+ }
+ `);
+ const output = getOutput(program, [
+ ,
+
+ {efRefkey(Color.members.get("red"))}
+ {efRefkey(Color.members.get("green"))}
+ {efRefkey(Color.members.get("blue"))}
+ ,
+ ]);
+
+ expect(output).toRenderTo(d`
+ from enum import StrEnum
+
+
+ class Color(StrEnum):
+ RED = "This is red"
+ GREEN = "This is green"
+ BLUE = "This is blue"
+
+
+ Color.RED
+ Color.GREEN
+ Color.BLUE
+
+ `);
+ });
+
+ it("generates Enum for mixed value types", async () => {
+ const { program, Mixed } = await Tester.compile(t.code`
+ enum ${t.enum("Mixed")} {
+ stringValue: "hello",
+ numericValue: 42,
+ autoValue
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import auto
+ from enum import Enum
+
+
+ class Mixed(Enum):
+ STRING_VALUE = "hello"
+ NUMERIC_VALUE = 42
+ AUTO_VALUE = auto()
+
+
+ `);
+ });
+
+ it("handles correctly enums without values", async () => {
+ const { program, EnumWithoutValues } = await Tester.compile(t.code`
+ enum ${t.enum("EnumWithoutValues")} {
+ someValue,
+ anotherValue,
+ yetAnotherValue
+ }
+ `);
+ const output = getOutput(program, []);
+
+ expect(output).toRenderTo(d`
+ from enum import auto
+ from enum import Enum
+
+
+ class EnumWithoutValues(Enum):
+ SOME_VALUE = auto()
+ ANOTHER_VALUE = auto()
+ YET_ANOTHER_VALUE = auto()
+
+
+ `);
+ });
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.tsx b/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.tsx
new file mode 100644
index 00000000000..7ce228ceb2b
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/enum-declaration/enum-declaration.tsx
@@ -0,0 +1,77 @@
+import { useTsp } from "#core/context/index.js";
+import { For, Prose } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { Enum, EnumMember as TspEnumMember, Union } from "@typespec/compiler";
+import { reportDiagnostic } from "../../../lib.js";
+import { declarationRefkeys, efRefkey } from "../../utils/refkey.js";
+import { EnumMember } from "./enum-member.js";
+
+export interface EnumDeclarationProps extends Omit {
+ name?: string;
+ type: Union | Enum;
+}
+
+// Determine the appropriate enum type based on the member values
+function determineEnumType(
+ members: Array<[string, TspEnumMember]>,
+): "IntEnum" | "StrEnum" | "Enum" {
+ const allInteger = members.every(([, member]) => {
+ const value = member.value;
+ return typeof value === "number" && Number.isInteger(value);
+ });
+
+ const allString = members.every(([, member]) => {
+ const value = member.value;
+ return typeof value === "string";
+ });
+
+ if (allInteger) {
+ return "IntEnum";
+ } else if (allString) {
+ return "StrEnum";
+ } else {
+ return "Enum";
+ }
+}
+
+export function EnumDeclaration(props: EnumDeclarationProps) {
+ const { $ } = useTsp();
+ let type: Enum;
+ if ($.union.is(props.type)) {
+ if (!$.union.isValidEnum(props.type)) {
+ throw new Error("The provided union type cannot be represented as an enum");
+ }
+ type = $.enum.createFromUnion(props.type);
+ } else {
+ type = props.type;
+ }
+
+ if (!props.type.name) {
+ reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type });
+ }
+ const refkeys = declarationRefkeys(props.refkey, props.type);
+ const name = props.name ?? py.usePythonNamePolicy().getName(props.type.name!, "enum");
+ const members = Array.from(type.members.entries());
+ const doc = props.doc ?? $.type.getDoc(type);
+ const docElement = doc ? {doc}]} /> : undefined;
+ const enumType = determineEnumType(members);
+
+ return (
+
+
+ {([key, value]) => {
+ const memberDoc = $.type.getDoc(value);
+ return (
+
+ );
+ }}
+
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/enum-declaration/enum-member.tsx b/packages/emitter-framework/src/python/components/enum-declaration/enum-member.tsx
new file mode 100644
index 00000000000..57a7f7e60ce
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/enum-declaration/enum-member.tsx
@@ -0,0 +1,21 @@
+import { type Children, type Refkey } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { EnumMember as TspEnumMember } from "@typespec/compiler";
+
+export interface EnumMemberProps {
+ type: TspEnumMember;
+ doc?: Children;
+ refkey?: Refkey;
+}
+
+export function EnumMember(props: EnumMemberProps) {
+ return (
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/enum-declaration/index.ts b/packages/emitter-framework/src/python/components/enum-declaration/index.ts
new file mode 100644
index 00000000000..c6a84680c17
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/enum-declaration/index.ts
@@ -0,0 +1,2 @@
+export * from "./enum-declaration.js";
+export * from "./enum-member.js";
diff --git a/packages/emitter-framework/src/python/components/function-declaration/function-declaration.test.tsx b/packages/emitter-framework/src/python/components/function-declaration/function-declaration.test.tsx
new file mode 100644
index 00000000000..fac6d007c1c
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/function-declaration/function-declaration.test.tsx
@@ -0,0 +1,582 @@
+import { getOutput } from "#python/test-utils.js";
+import { Tester } from "#test/test-host.js";
+import * as py from "@alloy-js/python";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { FunctionDeclaration } from "./function-declaration.js";
+
+describe("Python Function Declaration", () => {
+ it("creates a function with single positional param", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def get_name(id: str) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates an async function", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ async def get_name(id: str) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with a custom name", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def new_name(id: str) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with additional keyword-only parameters", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str, *, name: str, age: float) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with additional keyword-only parameters (string shorthand)", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str, *, name, age) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function replacing parameters with raw params provided", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(`
+ def create_person(*, name, age) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function replacing parameters with params having defaults (requires * marker)", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ { name: "age", type: "int", default: },
+ ]}
+ replaceParameters={true}
+ />,
+ ]),
+ ).toRenderTo(`
+ def create_person(*, name: str = "alice", age: int = 30) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with defaults in raw parameter descriptors", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string, locale: string = "en-US"): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ { name: "verbose", type: "bool", default: },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str, *, locale: str = "en-US", limit: int = 10, verbose: bool = True) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function building parameters from a model via parametersModel (will replace parameters)", async () => {
+ const { program, createPerson, Foo } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+
+ model ${t.model("Foo")} {
+ name: string;
+ age: int32;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def create_person(name: str, age: int) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with parametersModel applying defaults and optionals", async () => {
+ const { program, createPerson, Foo } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+
+ model ${t.model("Foo")} {
+ requiredName?: string = "alice";
+ optionalAge?: int32;
+ }
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def create_person(*, required_name: str = "alice", optional_age: int = None) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function that replaces parameters with parametersModel even when extras are provided", async () => {
+ const { program, createPerson, Foo } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+
+ model ${t.model("Foo")} {
+ name: string;
+ age: int32;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(`
+ def create_person(name: str, age: int) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function overriding the return type", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def get_name(id: str) -> ASpecialString:
+ pass
+
+ `);
+ });
+ it("creates a function with body", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+
+ <>print("Hello World!")>
+ ,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str) -> str:
+ print("Hello World!")
+
+ `);
+ });
+
+ it("creates a function with a doc", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def get_name(id: str) -> str:
+ """
+ This is a test doc
+ """
+
+ pass
+
+ `);
+ });
+
+ it("creates a function with a multi-paragraph FunctionDoc", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ First paragraph>, <>Second paragraph>]} />}
+ />,
+ ]),
+ ).toRenderTo(`
+ def get_name(id: str) -> str:
+ """
+ First paragraph
+
+ Second paragraph
+ """
+
+ pass
+
+ `);
+ });
+
+ it("creates a function with doc as string[]", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ]),
+ ).toRenderTo(`
+ def get_name(id: str) -> str:
+ """
+ First paragraph
+
+ Second paragraph
+ """
+
+ pass
+
+ `);
+ });
+
+ it("creates a function with doc as Children[]", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ First paragraph>, <>Second paragraph>]} />,
+ ]),
+ ).toRenderTo(`
+ def get_name(id: str) -> str:
+ """
+ First paragraph
+
+ Second paragraph
+ """
+
+ pass
+
+ `);
+ });
+
+ it("creates a function with doc lines (string with newlines)", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def get_name(id: str) -> str:
+ """
+ Line 1
+ Line 2
+ """
+
+ pass
+
+ `);
+ });
+
+ it("creates a function with no parameters", async () => {
+ const { program, ping } = await Tester.compile(t.code`
+ op ${t.op("ping")}(): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def ping() -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function that returns None for void", async () => {
+ const { program, ping } = await Tester.compile(t.code`
+ op ${t.op("ping")}(): void;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def ping() -> None:
+ pass
+
+ `);
+ });
+
+ it("creates a function that returns Never for never", async () => {
+ const { program, abort } = await Tester.compile(t.code`
+ op ${t.op("abort")}(): never;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from typing import Never
+
+
+ def abort() -> Never:
+ pass
+
+ `);
+ });
+
+ it("creates a function that correctly handles simple unions", async () => {
+ const { program, get } = await Tester.compile(t.code`
+ op ${t.op("get")}(): int32 | string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def get() -> int | str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with only keyword-only parameters (requires * marker for params with defaults)", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(name: string = "alice", age: int32 = 30): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def create_person(*, name: str = "alice", age: int = 30) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with operation params positional and additional params keyword-only", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string, locale: string = "en-US"): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str, *, version: int, locale: str = "en-US", debug: bool = False) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with TSP params in wrong order (default before required) - reorders correctly", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(locale: string = "en-US", id: string): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def create_person(id: str, *, locale: str = "en-US") -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with only positional params when adding params to empty operation", async () => {
+ const { program, ping } = await Tester.compile(t.code`
+ op ${t.op("ping")}(): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def ping(name, age) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with multiple positional params", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string, name: string, age: int32): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def create_person(id: str, name: str, age: int) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function adding keyword-only params to operation with only positional params", async () => {
+ const { program, createPerson } = await Tester.compile(t.code`
+ op ${t.op("createPerson")}(id: string, name: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def create_person(id: str, name: str, *, email, notify: bool = False) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with multiple params with defaults (all keyword-only)", async () => {
+ const { program, search } = await Tester.compile(t.code`
+ op ${t.op("search")}(
+ limit: int32 = 10,
+ offset: int32 = 0,
+ sortBy: string = "name"
+ ): string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ def search(*, limit: int = 10, offset: int = 0, sort_by: str = "name") -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function adding params without defaults to operation with only defaults", async () => {
+ const { program, search } = await Tester.compile(t.code`
+ op ${t.op("search")}(limit: int32 = 10, offset: int32 = 0): string;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ def search(*, query, limit: int = 10, offset: int = 0) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with complex parameter mix", async () => {
+ const { program, complexOp } = await Tester.compile(t.code`
+ op ${t.op("complexOp")}(
+ required1: string,
+ required2: int32,
+ optional1: string = "default",
+ optional2: int32 = 42
+ ): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def complex_op(required1: str, required2: int, *, additional_required, optional1: str = "default", optional2: int = 42, additional_optional: bool = True) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function with single positional param and additional keyword-only", async () => {
+ const { program, getUser } = await Tester.compile(t.code`
+ op ${t.op("getUser")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def get_user(id: str, *, include_deleted: bool = False) -> str:
+ pass
+
+ `);
+ });
+
+ it("creates a function adding only params with defaults to empty operation", async () => {
+ const { program, ping } = await Tester.compile(t.code`
+ op ${t.op("ping")}(): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ },
+ { name: "retries", type: "int", default: },
+ ]}
+ />,
+ ]),
+ ).toRenderTo(`
+ def ping(*, timeout: int = 30, retries: int = 3) -> str:
+ pass
+
+ `);
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/function-declaration/function-declaration.tsx b/packages/emitter-framework/src/python/components/function-declaration/function-declaration.tsx
new file mode 100644
index 00000000000..c77b2b9b5bd
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/function-declaration/function-declaration.tsx
@@ -0,0 +1,90 @@
+import { useTsp } from "#core/index.js";
+import { buildParameterDescriptors } from "#python/utils/operation.js";
+import { declarationRefkeys } from "#python/utils/refkey.js";
+import * as py from "@alloy-js/python";
+import type { Model, Operation } from "@typespec/compiler";
+import { DocElement } from "../doc-element/doc-element.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export interface FunctionDeclarationPropsWithType extends Omit<
+ py.FunctionDeclarationProps,
+ "name"
+> {
+ type: Operation;
+ name?: string;
+ parametersModel?: Model;
+ /** If true, parameters replaces operation parameters instead of adding to them as keyword-only */
+ replaceParameters?: boolean;
+}
+
+export type FunctionDeclarationProps =
+ | FunctionDeclarationPropsWithType
+ | py.FunctionDeclarationProps;
+
+/**
+ * A Python function declaration. Pass the `type` prop to create the
+ * function declaration by converting from a TypeSpec Operation. Any other props
+ * provided will take precedence.
+ *
+ * @remarks
+ * - `parametersModel`: When provided, it replaces the function parameters, ignoring
+ * the `parameters` option.
+ * - Model-derived parameters include default values when present on the model, and
+ * emit `= None` for optional parameters without an explicit default.
+ * - Additional parameters (from `parameters` prop) are always keyword-only.
+ */
+export function FunctionDeclaration(props: FunctionDeclarationProps) {
+ const { $ } = useTsp();
+
+ if (!isTypedFunctionDeclarationProps(props)) {
+ return ;
+ }
+
+ const refkeys = declarationRefkeys(props.refkey, props.type);
+
+ const name = props.name
+ ? props.name
+ : py.usePythonNamePolicy().getName(props.type.name, "function");
+
+ const returnType = props.returnType ?? ;
+ let allParameters: (py.ParameterDescriptor | string)[] | undefined;
+ if (props.parametersModel) {
+ // When a parametersModel is provided, it always replaces parameters,
+ // ignoring extra parameters.
+ allParameters = buildParameterDescriptors(props.parametersModel) ?? [];
+ } else {
+ allParameters = buildParameterDescriptors(props.type.parameters, {
+ params: props.parameters,
+ replaceParameters: props.replaceParameters,
+ });
+ }
+ const docSource = props.doc ?? $.type.getDoc(props.type);
+ const docElement = docSource ? (
+
+ ) : undefined;
+ const doc = docElement ? (
+ <>
+ {docElement}
+
+ >
+ ) : undefined;
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+function isTypedFunctionDeclarationProps(
+ props: FunctionDeclarationProps,
+): props is FunctionDeclarationPropsWithType {
+ return "type" in props;
+}
diff --git a/packages/emitter-framework/src/python/components/function-declaration/index.ts b/packages/emitter-framework/src/python/components/function-declaration/index.ts
new file mode 100644
index 00000000000..9b6812f3153
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/function-declaration/index.ts
@@ -0,0 +1 @@
+export * from "./function-declaration.js";
diff --git a/packages/emitter-framework/src/python/components/index.ts b/packages/emitter-framework/src/python/components/index.ts
new file mode 100644
index 00000000000..ca252a9df8b
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/index.ts
@@ -0,0 +1,11 @@
+export * from "./array-expression/index.js";
+export * from "./atom/index.js";
+export * from "./class-declaration/index.js";
+export * from "./doc-element/index.js";
+export * from "./enum-declaration/index.js";
+export * from "./function-declaration/index.js";
+export * from "./protocol-declaration/index.js";
+export * from "./record-expression/index.js";
+export * from "./type-alias-declaration/index.js";
+export * from "./type-declaration/index.js";
+export * from "./type-expression/index.js";
diff --git a/packages/emitter-framework/src/python/components/protocol-declaration/callable-parameters.tsx b/packages/emitter-framework/src/python/components/protocol-declaration/callable-parameters.tsx
new file mode 100644
index 00000000000..5381f17da40
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/protocol-declaration/callable-parameters.tsx
@@ -0,0 +1,44 @@
+import { refkey } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { Operation } from "@typespec/compiler";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { buildParameterDescriptor } from "../../utils/operation.js";
+
+export interface CallableParametersProps {
+ /**
+ * The operation to extract parameters from.
+ */
+ type: Operation;
+}
+
+/**
+ * Builds parameter descriptors for a callable (method/function) from an Operation's parameters.
+ *
+ * Iterates over the operation's parameters model and creates ParameterDescriptor objects
+ * with name, type expression, and default value (handles both explicit defaults and
+ * optional parameters without defaults which get `= None`).
+ *
+ * @returns Array of ParameterDescriptor objects for use with py.MethodDeclaration or similar.
+ *
+ * @example
+ * ```tsx
+ * const params = CallableParameters({ type: operation });
+ *
+ * ```
+ */
+export function CallableParameters(props: CallableParametersProps): py.ParameterDescriptor[] {
+ const { $ } = useTsp();
+ const paramsModel = props.type.parameters;
+
+ if (!paramsModel) {
+ return [];
+ }
+
+ const parameters: py.ParameterDescriptor[] = [];
+
+ for (const prop of $.model.getProperties(paramsModel).values()) {
+ parameters.push(buildParameterDescriptor(prop, refkey()));
+ }
+
+ return parameters;
+}
diff --git a/packages/emitter-framework/src/python/components/protocol-declaration/index.ts b/packages/emitter-framework/src/python/components/protocol-declaration/index.ts
new file mode 100644
index 00000000000..228289cca02
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/protocol-declaration/index.ts
@@ -0,0 +1,2 @@
+export * from "./callable-parameters.js";
+export * from "./protocol-declaration.js";
diff --git a/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.test.tsx b/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.test.tsx
new file mode 100644
index 00000000000..9af2d093eaf
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.test.tsx
@@ -0,0 +1,106 @@
+import { TypeExpression } from "#python/components/type-expression/type-expression.js";
+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 { ProtocolDeclaration } from "./protocol-declaration.js";
+
+describe("Python ProtocolDeclaration", () => {
+ it("emits a callback Protocol for an operation", async () => {
+ const { program, getName, getOtherName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ op ${t.op("getOtherName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from typing import Protocol
+
+
+ class GetName(Protocol):
+ def __call__(self, id: str) -> str:
+ ...
+
+
+
+ class GetOtherName(Protocol):
+ def __call__(self, id: str) -> str:
+ ...
+
+
+ `);
+ });
+
+ it("emits no return annotation when return type omitted", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ // Create a shallow clone without returnType to simulate missing return info
+ const getNameNoReturn: any = { ...(getName as any) };
+ delete getNameNoReturn.returnType;
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from typing import Protocol
+
+
+ class GetName(Protocol):
+ def __call__(self, id: str):
+ ...
+
+
+ `);
+ });
+
+ it("emits a Protocol for a TypeSpec interface (methods only)", async () => {
+ const { program, Greeter } = await Tester.compile(t.code`
+ interface ${t.interface("Greeter")} {
+ op getName(id: string): string;
+ op getOtherName(id: string): string;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from typing import Protocol
+
+
+ class Greeter(Protocol):
+ def get_name(self, id: str) -> str:
+ ...
+
+ def get_other_name(self, id: str) -> str:
+ ...
+
+
+ `);
+ });
+
+ it("emits both a Protocol and a Callable for the same operation", async () => {
+ const { program, getName } = await Tester.compile(t.code`
+ op ${t.op("getName")}(id: string): string;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from typing import Callable
+ from typing import Protocol
+
+
+ class GetName(Protocol):
+ def __call__(self, id: str) -> str:
+ ...
+
+
+
+ Callable[[str], str]
+ `);
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.tsx b/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.tsx
new file mode 100644
index 00000000000..55d77b7f2e2
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/protocol-declaration/protocol-declaration.tsx
@@ -0,0 +1,73 @@
+import { useTsp } from "#core/context/tsp-context.js";
+import { typingModule } from "#python/builtins.js";
+import { TypeExpression } from "#python/components/type-expression/type-expression.js";
+import { reportPythonDiagnostic } from "#python/lib.js";
+import { declarationRefkeys } from "#python/utils/refkey.js";
+import { mapJoin } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { Interface, Operation, Type } from "@typespec/compiler";
+import { CallableParameters } from "./callable-parameters.js";
+
+export interface ProtocolDeclarationProps extends Omit {
+ type: Interface | Operation;
+ name?: string;
+}
+
+export function ProtocolDeclaration(props: ProtocolDeclarationProps) {
+ const { $ } = useTsp();
+
+ const refkeys = declarationRefkeys(props.refkey, props.type);
+ const protocolBase = typingModule["."]["Protocol"];
+
+ const namePolicy = py.usePythonNamePolicy();
+ const originalName = props.name ?? (props.type as any)?.name ?? "";
+ const name = namePolicy.getName(originalName, "class");
+
+ // Interfaces will be converted to Protocols with method stubs for operations
+ if ((props.type as any)?.kind === "Interface") {
+ const iface = props.type as Interface;
+ const operations = ((iface as any).operations ?? new Map()) as Map;
+ const methods = mapJoin(
+ () => Array.from(operations.values()) as any[],
+ (op: any) => {
+ const methodName = namePolicy.getName(op.name, "function");
+ const prm = CallableParameters({ type: op as Operation }); // self injected by MethodDeclaration
+ const ret = (op as any)?.returnType ? (
+
+ ) : undefined;
+ return (
+
+ ...
+
+ );
+ },
+ );
+ return (
+
+ {methods}
+
+ );
+ }
+
+ if ((props.type as any)?.kind !== "Operation") {
+ reportPythonDiagnostic($.program, {
+ code: "python-unsupported-type",
+ target: props.type as any,
+ });
+ return <>>;
+ }
+
+ // Operations will be converted to Callback protocol using a dunder __call__ method
+ const op = props.type as Operation;
+ const cbParams = CallableParameters({ type: op });
+ const cbReturn = (op as any)?.returnType ? (
+
+ ) : undefined;
+ return (
+
+
+ ...
+
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/record-expression/index.ts b/packages/emitter-framework/src/python/components/record-expression/index.ts
new file mode 100644
index 00000000000..a50b3eb7a99
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/record-expression/index.ts
@@ -0,0 +1 @@
+export * from "./record-expression.js";
diff --git a/packages/emitter-framework/src/python/components/record-expression/record-expression.test.tsx b/packages/emitter-framework/src/python/components/record-expression/record-expression.test.tsx
new file mode 100644
index 00000000000..bf71036d97d
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/record-expression/record-expression.test.tsx
@@ -0,0 +1,14 @@
+import { Tester } from "#test/test-host.js";
+import { d } from "@alloy-js/core/testing";
+import { t } from "@typespec/compiler/testing";
+import { expect, it } from "vitest";
+import { getOutput } from "../../test-utils.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+it("maps Record to Python dict", async () => {
+ const { program, TestRecord } = await Tester.compile(t.code`
+ alias ${t.type("TestRecord")} = Record;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`dict[str, bool]`);
+});
diff --git a/packages/emitter-framework/src/python/components/record-expression/record-expression.tsx b/packages/emitter-framework/src/python/components/record-expression/record-expression.tsx
new file mode 100644
index 00000000000..bd6ba683210
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/record-expression/record-expression.tsx
@@ -0,0 +1,13 @@
+import { code } from "@alloy-js/core";
+import type { Type } from "@typespec/compiler";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export interface RecordExpressionProps {
+ elementType: Type;
+}
+
+export function RecordExpression({ elementType }: RecordExpressionProps) {
+ return code`
+ dict[str, ${()}]
+ `;
+}
diff --git a/packages/emitter-framework/src/python/components/type-alias-declaration/index.ts b/packages/emitter-framework/src/python/components/type-alias-declaration/index.ts
new file mode 100644
index 00000000000..57df7c72b96
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-alias-declaration/index.ts
@@ -0,0 +1 @@
+export * from "./type-alias-declaration.js";
diff --git a/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.test.tsx b/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.test.tsx
new file mode 100644
index 00000000000..8233243e4a5
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.test.tsx
@@ -0,0 +1,117 @@
+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 { TypeAliasDeclaration } from "./type-alias-declaration.js";
+
+describe("Python Declaration equivalency to Type Alias", () => {
+ describe("Type Alias Declaration bound to Typespec Scalar", () => {
+ describe("Scalar extends utcDateTime", () => {
+ it("creates a type alias declaration for a utcDateTime without encoding", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("creates a type alias declaration with doc", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ /**
+ * Type to represent a date
+ */
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ # Type to represent a date
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("can override JSDoc", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ /**
+ * Type to represent a date
+ */
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, []))
+ .toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ # Overridden Doc
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("creates a type alias declaration for a utcDateTime with unixTimeStamp encoding", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ @encode("unixTimestamp", int32)
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("creates a type alias declaration for a utcDateTime with rfc7231 encoding", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ @encode("rfc7231")
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("creates a type alias declaration for a utcDateTime with rfc3339 encoding", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ @encode("rfc3339")
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ my_date: TypeAlias = datetime`);
+ });
+ });
+ });
+
+ describe("Type Alias Declaration for Operation (Callable)", () => {
+ it("creates a type alias for an operation type reference", async () => {
+ const { program, Handler } = await Tester.compile(t.code`
+ op handleRequest(id: string): string;
+ alias ${t.type("Handler")} = handleRequest;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from typing import Callable
+ from typing import TypeAlias
+
+
+ handle_request: TypeAlias = Callable[[str], str]`);
+ });
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.tsx b/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.tsx
new file mode 100644
index 00000000000..8f35f858534
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-alias-declaration/type-alias-declaration.tsx
@@ -0,0 +1,67 @@
+import { useTsp } from "#core/context/index.js";
+import * as py from "@alloy-js/python";
+import { isTemplateInstance, type Model, type Type } from "@typespec/compiler";
+import { reportDiagnostic } from "../../../lib.js";
+import { typingModule } from "../../builtins.js";
+import { declarationRefkeys } from "../../utils/refkey.js";
+import { ClassDeclaration } from "../class-declaration/class-declaration.js";
+import { TypeExpression } from "../type-expression/type-expression.js";
+
+export interface TypedAliasDeclarationProps extends Omit {
+ type: Type;
+ name?: string;
+}
+
+/**
+ * Create a Python type alias declaration. Pass the `type` prop to emit the
+ * type alias as the provided TypeSpec type.
+ *
+ * For template instances (e.g., `alias StringResponse = Response`),
+ * this emits a dataclass instead of a type alias, since Python doesn't support
+ * parameterized type aliases the way TypeScript does.
+ */
+export function TypeAliasDeclaration(props: TypedAliasDeclarationProps) {
+ const { $ } = useTsp();
+
+ const originalName =
+ props.name ??
+ ("name" in props.type && typeof props.type.name === "string" ? props.type.name : "");
+
+ if (!originalName || originalName === "") {
+ reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type });
+ }
+
+ const doc = props.doc ?? $.type.getDoc(props.type);
+ const refkeys = declarationRefkeys(props.refkey, props.type);
+
+ // For Model template instances (e.g., alias StringResponse = Response),
+ // emit as a dataclass since Python doesn't support parameterized type aliases.
+ // TypeSpec templates are macros that expand to concrete types.
+ if ($.model.is(props.type) && isTemplateInstance(props.type)) {
+ const namePolicy = py.usePythonNamePolicy();
+ const plausibleName = $.type.getPlausibleName(props.type as Model);
+ const name = props.name ?? namePolicy.getName(plausibleName, "class");
+
+ return (
+
+ {props.children}
+
+ );
+ }
+
+ // For other types (scalars, unions, operations), emit as a type alias
+ const name = py.usePythonNamePolicy().getName(originalName, "variable");
+
+ return (
+ }
+ >
+ {props.children}
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/components/type-declaration/index.ts b/packages/emitter-framework/src/python/components/type-declaration/index.ts
new file mode 100644
index 00000000000..42e615a8166
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-declaration/index.ts
@@ -0,0 +1 @@
+export * from "./type-declaration.js";
diff --git a/packages/emitter-framework/src/python/components/type-declaration/type-declaration.test.tsx b/packages/emitter-framework/src/python/components/type-declaration/type-declaration.test.tsx
new file mode 100644
index 00000000000..e01adb0b7e1
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-declaration/type-declaration.test.tsx
@@ -0,0 +1,58 @@
+import { Tester } from "#test/test-host.js";
+import { d } from "@alloy-js/core/testing";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { getOutput } from "../../test-utils.js";
+import { TypeDeclaration } from "./type-declaration.js";
+
+describe("Python TypeDeclaration dispatcher", () => {
+ it("dispatches to EnumDeclaration for enums", async () => {
+ const { program, Foo } = await Tester.compile(t.code`
+ enum ${t.enum("Foo")} {
+ one: 1,
+ two: 2,
+ three: 3
+ }
+ `);
+
+ const output = getOutput(program, []);
+ expect(output).toRenderTo(d`
+ from enum import IntEnum
+
+
+ class Foo(IntEnum):
+ ONE = 1
+ TWO = 2
+ THREE = 3
+
+
+ `);
+ });
+
+ it("dispatches scalars to TypeAliasDeclaration", async () => {
+ const { program, MyDate } = await Tester.compile(t.code`
+ scalar ${t.scalar("MyDate")} extends utcDateTime;
+ `);
+
+ const output = getOutput(program, []);
+ expect(output).toRenderTo(d`
+ from datetime import datetime
+ from typing import TypeAlias
+
+
+ my_date: TypeAlias = datetime`);
+ });
+
+ it("dispatches arrays (model is Array) to type alias with list[T]", async () => {
+ const { program, Items } = await Tester.compile(t.code`
+ model ${t.model("Items")} is Array;
+ `);
+
+ const output = getOutput(program, []);
+ expect(output).toRenderTo(d`
+ from typing import TypeAlias
+
+
+ items: TypeAlias = list[int]`);
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/type-declaration/type-declaration.tsx b/packages/emitter-framework/src/python/components/type-declaration/type-declaration.tsx
new file mode 100644
index 00000000000..d1617bd8f8a
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-declaration/type-declaration.tsx
@@ -0,0 +1,26 @@
+import { useTsp } from "#core/context/index.js";
+import * as py from "@alloy-js/python";
+import type { Type } from "@typespec/compiler";
+import { EnumDeclaration } from "../enum-declaration/enum-declaration.js";
+import { TypeAliasDeclaration } from "../type-alias-declaration/type-alias-declaration.js";
+
+export interface TypeDeclarationProps extends Omit {
+ name?: string;
+ type: Type;
+}
+
+/**
+ * Single entry point to declare a Python symbol for any TypeSpec `Type`.
+ */
+export function TypeDeclaration(props: TypeDeclarationProps) {
+ const { $ } = useTsp();
+ const { type, ...restProps } = props;
+ const doc = props.doc ?? $.type.getDoc(type);
+ switch (type.kind) {
+ case "Enum":
+ return ;
+ default:
+ // All other kinds map to a Python type alias using TypeExpression
+ return ;
+ }
+}
diff --git a/packages/emitter-framework/src/python/components/type-expression/index.ts b/packages/emitter-framework/src/python/components/type-expression/index.ts
new file mode 100644
index 00000000000..9ffe8521a6e
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-expression/index.ts
@@ -0,0 +1 @@
+export * from "./type-expression.js";
diff --git a/packages/emitter-framework/src/python/components/type-expression/type-expression.test.tsx b/packages/emitter-framework/src/python/components/type-expression/type-expression.test.tsx
new file mode 100644
index 00000000000..67fe322e6bb
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-expression/type-expression.test.tsx
@@ -0,0 +1,463 @@
+import { Tester } from "#test/test-host.js";
+import { d } from "@alloy-js/core/testing";
+import { t } from "@typespec/compiler/testing";
+import { describe, expect, it } from "vitest";
+import { getOutput } from "../../test-utils.js";
+import { EnumDeclaration } from "../enum-declaration/enum-declaration.js";
+import { TypeExpression } from "./type-expression.js";
+
+describe("map Typespec types to Python built-in types", () => {
+ it.each([
+ ["unknown", "Any", "from typing import Any"],
+ ["string", "str"],
+ ["boolean", "bool"],
+ ["null", "None"],
+ ["void", "None"],
+ ["never", "Never", "from typing import Never"],
+ ["bytes", "bytes"],
+ ["numeric", "number"],
+ ["integer", "int"],
+ ["float", "float"],
+ ["decimal", "Decimal", "from decimal import Decimal"],
+ ["decimal128", "Decimal", "from decimal import Decimal"],
+ ["int64", "int"],
+ ["int32", "int"],
+ ["int16", "int"],
+ ["int8", "int"],
+ ["safeint", "int"],
+ ["uint64", "int"],
+ ["uint32", "int"],
+ ["uint16", "int"],
+ ["uint8", "int"],
+ ["float32", "float"],
+ ["float64", "float"],
+ ["plainDate", "str"],
+ ["plainTime", "str"],
+ ["utcDateTime", "datetime", "from datetime import datetime"],
+ ["offsetDateTime", "str"],
+ ["duration", "str"],
+ ["url", "str"],
+ ])("%s => %s", async (tspType, pythonType, extraImport = "") => {
+ const { program, Type } = await Tester.compile(t.code`
+ alias ${t.type("Type")} = ${t.type(tspType)};
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ ${extraImport ? `${extraImport}\n\n\n` : ""}${pythonType}
+ `);
+ });
+});
+
+describe("map scalar to Python types", () => {
+ it("Email => str (scalars are inlined to base type)", async () => {
+ const { program, Email } = await Tester.compile(t.code`
+ scalar ${t.scalar("Email")} extends string;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ str
+ `);
+ });
+
+ it("PhoneNumber => str (scalar in model property is inlined)", async () => {
+ const { program, _ } = await Tester.compile(t.code`
+ scalar PhoneNumber extends string;
+
+ model ${t.model("Widget")} {
+ phone: PhoneNumber;
+ }
+ `);
+
+ const phoneProperty = program.resolveTypeReference("Widget")[0]!;
+ const phoneProp = (phoneProperty as any).properties.get("phone");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ str
+ `);
+ });
+});
+
+describe("map tuple to Python types", () => {
+ it("[int32, int32] => tuple[int, int]", async () => {
+ const { program, Tuple } = await Tester.compile(t.code`
+ alias ${t.type("Tuple")} = [int32, int32];
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ tuple[int, int]
+ `);
+ });
+});
+
+describe("map operation (function type) to typing.Callable", () => {
+ it("op f(a: int32, b: string): void => Callable[[int, str], None]", async () => {
+ const { program, f } = await Tester.compile(t.code`
+ op ${t.op("f")}(a: int32, b: string): void;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Callable
+
+
+ Callable[[int, str], None]
+ `);
+ });
+
+ it("op g(): int32 => Callable[[], int]", async () => {
+ const { program, g } = await Tester.compile(t.code`
+ op ${t.op("g")}(): int32;
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Callable
+
+
+ Callable[[], int]
+ `);
+ });
+});
+
+describe("correctly solves a ModelProperty to Python types", () => {
+ it("[int32, int32] => tuple[int, int]", async () => {
+ const { program, tupleProperty } = await Tester.compile(t.code`
+ model Test {
+ ${t.modelProperty("tupleProperty")}: [int32, int32];
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ tuple[int, int]
+ `);
+ });
+});
+
+describe("handles UnionVariant types", () => {
+ it("renders named union variant with literal value as Literal[Union.MEMBER]", async () => {
+ const { program, Color } = await Tester.compile(t.code`
+ union ${t.union("Color")} {
+ red: "red",
+ blue: "blue",
+ }
+
+ model Widget {
+ color: Color.red;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const colorProperty = Widget.properties.get("color");
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(d`
+ from enum import StrEnum
+ from typing import Literal
+
+
+ class Color(StrEnum):
+ RED = "red"
+ BLUE = "blue"
+
+
+ Literal[Color.RED]
+ `);
+ });
+
+ it("renders named union variant with integer literal as Literal[Union.MEMBER]", async () => {
+ const { program, Status } = await Tester.compile(t.code`
+ union ${t.union("Status")} {
+ active: 1,
+ inactive: 0,
+ }
+
+ model Widget {
+ status: Status.active;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const statusProperty = Widget.properties.get("status");
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(d`
+ from enum import IntEnum
+ from typing import Literal
+
+
+ class Status(IntEnum):
+ ACTIVE = 1
+ INACTIVE = 0
+
+
+ Literal[Status.ACTIVE]
+ `);
+ });
+
+ it("unwraps union variant with non-literal type to reveal the inner type", async () => {
+ const { program } = await Tester.compile(t.code`
+ union Status {
+ active: string,
+ inactive: int32,
+ }
+
+ model Widget {
+ status: Status.inactive;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const statusProperty = Widget.properties.get("status");
+
+ // The UnionVariant should unwrap to reveal int32 (non-literal type)
+ expect(statusProperty.type.kind).toBe("UnionVariant");
+ expect(getOutput(program, [])).toRenderTo(d`
+ int
+ `);
+ });
+
+ it("handles named union variant in function return type", async () => {
+ const { program, Result } = await Tester.compile(t.code`
+ union ${t.union("Result")} {
+ success: "success",
+ failure: "failure",
+ }
+
+ op getResult(): Result.success;
+ `);
+
+ const getResult = program.resolveTypeReference("getResult")[0]! as any;
+ const returnType = getResult.returnType;
+
+ expect(
+ getOutput(program, [, ]),
+ ).toRenderTo(d`
+ from enum import StrEnum
+ from typing import Literal
+
+
+ class Result(StrEnum):
+ SUCCESS = "success"
+ FAILURE = "failure"
+
+
+ Literal[Result.SUCCESS]
+ `);
+ });
+
+ it("unwraps nested union variant with non-literal inner types", async () => {
+ const { program } = await Tester.compile(t.code`
+ union Inner {
+ a: string,
+ b: int32,
+ }
+
+ union Outer {
+ x: Inner.a,
+ y: Inner.b,
+ }
+
+ model Widget {
+ value: Outer.x;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const valueProperty = Widget.properties.get("value");
+
+ // Inner.a has type `string` (not a literal), so it unwraps
+ expect(getOutput(program, [])).toRenderTo(d`
+ str
+ `);
+ });
+});
+
+describe("handles literal types", () => {
+ it("renders union of string literals as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ status: "active" | "inactive" | "pending";
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const statusProperty = Widget.properties.get("status");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["active", "inactive", "pending"]
+ `);
+ });
+
+ it("renders union of integer literals as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ priority: 1 | 2 | 3;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const priorityProperty = Widget.properties.get("priority");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal[1, 2, 3]
+ `);
+ });
+
+ it("renders union of boolean literals as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ flag: true | false;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const flagProperty = Widget.properties.get("flag");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal[True, False]
+ `);
+ });
+
+ it("renders mixed literal union as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ value: "auto" | 0 | 100;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const valueProperty = Widget.properties.get("value");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["auto", 0, 100]
+ `);
+ });
+
+ it("renders function returning literal union as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ op getStatus(): "success" | "failure";
+ `);
+
+ const getStatus = program.resolveTypeReference("getStatus")[0]! as any;
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["success", "failure"]
+ `);
+ });
+
+ it("renders single string literal as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ constant: "fixed-value";
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const constantProperty = Widget.properties.get("constant");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["fixed-value"]
+ `);
+ });
+
+ it("renders function returning single string literal as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ op getSteve(): "steve";
+ `);
+
+ const getSteve = program.resolveTypeReference("getSteve")[0]! as any;
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["steve"]
+ `);
+ });
+
+ it("renders single numeric literal as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ op getAnswer(): 42;
+ `);
+
+ const getAnswer = program.resolveTypeReference("getAnswer")[0]! as any;
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal[42]
+ `);
+ });
+
+ it("renders single boolean literal as Literal[...]", async () => {
+ const { program } = await Tester.compile(t.code`
+ op isEnabled(): true;
+ `);
+
+ const isEnabled = program.resolveTypeReference("isEnabled")[0]! as any;
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal[True]
+ `);
+ });
+
+ it("renders union with non-literal types as regular union", async () => {
+ const { program } = await Tester.compile(t.code`
+ model Widget {
+ value: string | int32;
+ }
+ `);
+
+ const Widget = program.resolveTypeReference("Widget")[0]! as any;
+ const valueProperty = Widget.properties.get("value");
+
+ // Mixed literal and non-literal types render as regular union
+ expect(getOutput(program, [])).toRenderTo(d`
+ str | int
+ `);
+ });
+
+ it("renders function with literal parameter type", async () => {
+ const { program } = await Tester.compile(t.code`
+ op setMode(mode: "auto" | "manual"): void;
+ `);
+
+ const setMode = program.resolveTypeReference("setMode")[0]! as any;
+ const modeParam = setMode.parameters.properties.get("mode");
+
+ expect(getOutput(program, [])).toRenderTo(d`
+ from typing import Literal
+
+
+ Literal["auto", "manual"]
+ `);
+ });
+});
diff --git a/packages/emitter-framework/src/python/components/type-expression/type-expression.tsx b/packages/emitter-framework/src/python/components/type-expression/type-expression.tsx
new file mode 100644
index 00000000000..6bf20e9e2cf
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/type-expression/type-expression.tsx
@@ -0,0 +1,333 @@
+import { Experimental_OverridableComponent } from "#core/components/index.js";
+import { useTsp } from "#core/context/index.js";
+import { reportPythonDiagnostic } from "#python/lib.js";
+import { code, For, List, mapJoin } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import {
+ isNeverType,
+ type IntrinsicType,
+ type Model,
+ type Scalar,
+ type Type,
+} from "@typespec/compiler";
+import type { TemplateParameterDeclarationNode } from "@typespec/compiler/ast";
+import type { Typekit } from "@typespec/compiler/typekit";
+import { datetimeModule, decimalModule, typingModule } from "../../builtins.js";
+import { efRefkey } from "../../utils/refkey.js";
+import { ArrayExpression } from "../array-expression/array-expression.js";
+import { RecordExpression } from "../record-expression/record-expression.js";
+
+export interface TypeExpressionProps {
+ type: Type;
+
+ /**
+ * Whether to disallow references. Setting this will force the type to be
+ * emitted inline, even if it is a declaration that would otherwise be
+ * referenced.
+ */
+ noReference?: boolean;
+}
+
+export function TypeExpression(props: TypeExpressionProps) {
+ const { $ } = useTsp();
+ const type = props.type;
+ if (!props.noReference && isDeclaration($, type)) {
+ return (
+
+
+
+ );
+ }
+
+ switch (type.kind) {
+ case "Scalar": // Custom types based on primitives (Intrinsics)
+ case "Intrinsic": // Language primitives like `string`, `number`, etc.
+ if (isNeverType(type)) {
+ return typingModule["."]["Never"];
+ }
+ return <>{getScalarIntrinsicExpression($, type)}>;
+ case "Boolean":
+ case "Number":
+ case "String":
+ // Single literal values are wrapped in Literal[...]
+ return (
+ <>
+ {typingModule["."]["Literal"]}[{formatLiteralValue(type)}]
+ >
+ );
+ case "Tuple":
+ return (
+ <>
+ tuple[
+
+ {(element) => }
+
+ ]
+ >
+ );
+ case "Union": {
+ const variants = Array.from((type as any).variants?.values?.() ?? []);
+
+ // Check if all variants are literals or named union variant refs with literal values
+ const isLiteralOrVariantRef = (t: Type): boolean => {
+ if (!t) return false;
+ if (isLiteral($, t)) return true;
+ // Named union variant with a literal inner value
+ if (t.kind === "UnionVariant" && (t as any).union?.name) {
+ return isLiteral($, (t as any).type);
+ }
+ return false;
+ };
+
+ const innerTypes = variants.map((v: any) => v.type);
+ if (innerTypes.every(isLiteralOrVariantRef)) {
+ // All literals - render as Literal[...]
+ const literalValues = variants
+ .map((v: any) => {
+ const innerType = v.type;
+ // Named union variant ref with literal value
+ if (innerType.kind === "UnionVariant" && innerType.union?.name) {
+ const variantName = String(innerType.name).toUpperCase();
+ return code`${efRefkey(innerType.union)}.${variantName}`;
+ }
+ if (isLiteral($, innerType)) {
+ return formatLiteralValue(innerType);
+ }
+ return undefined;
+ })
+ .filter(Boolean);
+
+ return (
+ <>
+ {typingModule["."]["Literal"]}[
]
+ >
+ );
+ }
+
+ // Not all literals - render as union type
+ return mapJoin(
+ () => variants,
+ (v: any) => ,
+ { joiner: " | " },
+ );
+ }
+ case "UnionVariant": {
+ // Union variant from a named union with a literal value
+ if (type.union && (type.union as any).name && isLiteral($, type.type)) {
+ // Use the variant's name (e.g., "red", "active"), converted to UPPER_CASE by the enum
+ const variantName = String(type.name).toUpperCase();
+ return (
+ <>
+ {typingModule["."]["Literal"]}[{efRefkey(type.union)}.{variantName}]
+ >
+ );
+ }
+ // Unnamed union variant or non-literal value, unwrap to its inner type
+ return ;
+ }
+ case "ModelProperty":
+ return ;
+ case "Model":
+ if ($.array.is(type)) {
+ const elementType = type.indexer!.value;
+ return ;
+ }
+
+ if ($.record.is(type)) {
+ const elementType = (type as Model).indexer!.value;
+ return ;
+ }
+
+ // TODO: When TypeSpec adds true generics support, handle generic type references here.
+ // Currently, TypeSpec templates are macros that expand to concrete types, so template
+ // instances (e.g., Response) are treated as regular concrete types, not as
+ // parameterized generic types (e.g., Response[str]).
+ // When generics are implemented, this is where we would render: ClassName[TypeArg, ...]
+
+ // Regular named models should be handled as references
+ if (type.name) {
+ return (
+
+
+
+ );
+ }
+
+ reportPythonDiagnostic($.program, { code: "python-unsupported-type", target: type });
+ return <>>;
+ case "TemplateParameter":
+ return code`${String((type.node as TemplateParameterDeclarationNode).id.sv)}`;
+
+ case "Operation": {
+ // Render function types as typing.Callable[[ArgTypes...], ReturnType]
+ // If parameters cannot be enumerated, fall back to Callable[..., ReturnType]
+ let paramTypes: Type[] | null = null;
+ const op: any = type as any;
+ if (op.parameters) {
+ try {
+ const { $ } = useTsp();
+ const modelProps = $.model.getProperties(op.parameters);
+ paramTypes = Array.from(modelProps.values()).map((p: any) => p.type);
+ } catch {
+ // Unknown/unsupported params shape
+ paramTypes = null;
+ }
+ } else {
+ paramTypes = [];
+ }
+
+ return (
+ <>
+ {typingModule["."]["Callable"]}[
+ {paramTypes === null ? (
+ <>...>
+ ) : paramTypes.length > 0 ? (
+ <>
+ [
+
+ {(t) => }
+
+ ]
+ >
+ ) : (
+ <>[]>
+ )}
+ {", "}
+ ]
+ >
+ );
+ }
+ default:
+ reportPythonDiagnostic($.program, { code: "python-unsupported-type", target: type });
+ // Return empty fragment - the diagnostic has already been reported
+ return <>>;
+ }
+}
+
+/**
+ * Checks if a type is a literal (string, numeric, or boolean).
+ */
+function isLiteral($: Typekit, type: Type): boolean {
+ return $.literal.isString(type) || $.literal.isNumeric(type) || $.literal.isBoolean(type);
+}
+
+/**
+ * Formats a literal type value for use in Python's Literal[...] syntax.
+ */
+function formatLiteralValue(type: { kind: string; value: unknown }): string {
+ switch (type.kind) {
+ case "String":
+ return JSON.stringify(type.value);
+ case "Boolean":
+ return type.value ? "True" : "False";
+ case "Number":
+ return String(type.value);
+ default:
+ return String(type.value);
+ }
+}
+
+const intrinsicNameToPythonType = new Map([
+ // Core types
+ ["unknown", "Any"], // Matches Python's `Any`
+ ["string", "str"], // Matches Python's `str`
+ ["boolean", "bool"], // Matches Python's `bool`
+ ["null", "None"], // Matches Python's `None`
+ ["void", "None"], // Matches Python's `None`
+ ["never", "Never"], // Matches Python's `Never`
+ ["bytes", "bytes"], // Matches Python's `bytes`
+
+ // Numeric types
+ ["numeric", "number"], // Parent type for all numeric types
+ ["integer", "int"], // Broad integer category, maps to `int`
+ ["float", "float"], // Broad float category, maps to `float`
+ ["decimal", "Decimal"], // Broad decimal category, maps to `Decimal`
+ ["decimal128", "Decimal"], // 128-bit decimal category, maps to `Decimal`
+ ["int64", "int"], // Use `int` to handle large 64-bit integers
+ ["int32", "int"], // 32-bit integer fits in Python's `int`
+ ["int16", "int"], // 16-bit integer
+ ["int8", "int"], // 8-bit integer
+ ["safeint", "int"], // Safe integer fits within Python limits
+ ["uint64", "int"], // Use `int` for unsigned 64-bit integers
+ ["uint32", "int"], // 32-bit unsigned integer
+ ["uint16", "int"], // 16-bit unsigned integer
+ ["uint8", "int"], // 8-bit unsigned integer
+ ["float32", "float"], // Maps to Python's `float`
+ ["float64", "float"], // Maps to Python's `float`.
+
+ // Date and time types
+ ["plainDate", "str"], // Use `str` for plain calendar dates
+ ["plainTime", "str"], // Use `str` for plain clock times
+ ["utcDateTime", "datetime"], // Use `datetime` for UTC date-times
+ ["offsetDateTime", "str"], // Use `str` for timezone-specific date-times
+ ["duration", "str"], // Duration as an ISO 8601 string or custom format
+
+ // String types
+ ["url", "str"], // Matches Python's `str`
+]);
+
+const pythonTypeToImport = new Map([
+ ["Any", typingModule["."]["Any"]],
+ ["Never", typingModule["."]["Never"]],
+ ["datetime", datetimeModule["."]["datetime"]],
+ ["Decimal", decimalModule["."]["Decimal"]],
+]);
+
+function getScalarIntrinsicExpression($: Typekit, type: Scalar | IntrinsicType): string | null {
+ let intrinsicName: string;
+ if ($.scalar.is(type)) {
+ if ($.scalar.isUtcDateTime(type) || $.scalar.extendsUtcDateTime(type)) {
+ const encoding = $.scalar.getEncoding(type);
+ intrinsicName = "utcDateTime";
+ switch (encoding?.encoding) {
+ case "unixTimestamp":
+ case "rfc7231":
+ case "rfc3339":
+ default:
+ intrinsicName = `utcDateTime`;
+ break;
+ }
+ }
+ intrinsicName = $.scalar.getStdBase(type)?.name ?? "";
+ } else {
+ intrinsicName = type.name;
+ }
+
+ let pythonType = intrinsicNameToPythonType.get(intrinsicName);
+ const importModule = pythonTypeToImport.get(pythonType ?? "");
+ pythonType = importModule ? importModule : pythonType;
+
+ if (!pythonType) {
+ reportPythonDiagnostic($.program, { code: "python-unsupported-scalar", target: type });
+ return "any";
+ }
+
+ return pythonType;
+}
+
+// TODO: When TypeSpec adds true generics support, add helper to detect generic type instances.
+// Currently, TypeSpec templates expand to concrete types at compile time, so we treat all
+// template instances as regular concrete types.
+
+function isDeclaration($: Typekit, type: Type): boolean {
+ switch (type.kind) {
+ case "Namespace":
+ case "Interface":
+ case "Enum":
+ case "Operation":
+ case "EnumMember":
+ case "UnionVariant":
+ return false;
+
+ case "Model":
+ if ($.array.is(type) || $.record.is(type)) {
+ return false;
+ }
+
+ return Boolean(type.name);
+ case "Union":
+ return Boolean(type.name);
+ default:
+ return false;
+ }
+}
diff --git a/packages/emitter-framework/src/python/index.ts b/packages/emitter-framework/src/python/index.ts
new file mode 100644
index 00000000000..0b6cde696f0
--- /dev/null
+++ b/packages/emitter-framework/src/python/index.ts
@@ -0,0 +1,3 @@
+export * from "./builtins.js";
+export * from "./components/index.js";
+export * from "./utils/index.js";
diff --git a/packages/emitter-framework/src/python/lib.ts b/packages/emitter-framework/src/python/lib.ts
new file mode 100644
index 00000000000..df457071e5e
--- /dev/null
+++ b/packages/emitter-framework/src/python/lib.ts
@@ -0,0 +1,40 @@
+import { createTypeSpecLibrary } from "@typespec/compiler";
+
+export const $pythonLib = createTypeSpecLibrary({
+ name: "emitter-framework",
+ diagnostics: {
+ "python-unsupported-scalar": {
+ severity: "warning",
+ messages: {
+ default: "Unsupported scalar type, falling back to Any",
+ },
+ },
+ "python-unsupported-type": {
+ severity: "error",
+ messages: {
+ default: "Unsupported type, falling back to Any",
+ },
+ description: "This type is not supported by the Python emitter",
+ },
+ "python-unsupported-model-discriminator": {
+ severity: "error",
+ messages: {
+ default:
+ "Unsupported model discriminator, falling back to not discriminating on serialization/deserialization",
+ },
+ description: "Discriminators at the model are not supported",
+ },
+ "python-unsupported-type-transform": {
+ severity: "error",
+ messages: {
+ default: "Unsupported type for transformation, falling back to not transforming this type",
+ },
+ description: "This type cannot be transformed",
+ },
+ },
+});
+
+export const {
+ reportDiagnostic: reportPythonDiagnostic,
+ createDiagnostic: createPythonDiagnostic,
+} = $pythonLib;
diff --git a/packages/emitter-framework/src/python/test-utils.tsx b/packages/emitter-framework/src/python/test-utils.tsx
new file mode 100644
index 00000000000..c94e9f66dfd
--- /dev/null
+++ b/packages/emitter-framework/src/python/test-utils.tsx
@@ -0,0 +1,31 @@
+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";
+
+export const renderOptions = {
+ printWidth: 80,
+ tabWidth: 4,
+};
+
+export function getOutput(program: Program, children: Children[]): Children {
+ const policy = py.createPythonNamePolicy();
+ return (
+
+ );
+}
diff --git a/packages/emitter-framework/src/python/utils/index.ts b/packages/emitter-framework/src/python/utils/index.ts
new file mode 100644
index 00000000000..3640f7478ef
--- /dev/null
+++ b/packages/emitter-framework/src/python/utils/index.ts
@@ -0,0 +1,3 @@
+export * from "./operation.js";
+export * from "./refkey.js";
+export * from "./type.js";
diff --git a/packages/emitter-framework/src/python/utils/operation.ts b/packages/emitter-framework/src/python/utils/operation.ts
new file mode 100644
index 00000000000..0dea9f5f69a
--- /dev/null
+++ b/packages/emitter-framework/src/python/utils/operation.ts
@@ -0,0 +1,161 @@
+import { typingModule } from "#python/builtins.js";
+import { refkey, type Children, type Refkey } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import type { Model, ModelProperty, Operation, Type } from "@typespec/compiler";
+import { useTsp } from "../../core/index.js";
+import { Atom } from "../components/atom/atom.js";
+import { TypeExpression } from "../components/type-expression/type-expression.js";
+import { efRefkey } from "./refkey.js";
+
+export function getReturnType(
+ type: Operation,
+ options: { skipErrorFiltering: boolean } = { skipErrorFiltering: false },
+): Type {
+ const { $ } = useTsp();
+ let returnType = type.returnType;
+
+ if (!options.skipErrorFiltering && type.returnType.kind === "Union") {
+ returnType = $.union.filter(type.returnType, (variant) => !$.type.isError(variant.type));
+ }
+
+ return returnType;
+}
+
+export interface BuildParameterDescriptorsOptions {
+ params?: (py.ParameterDescriptor | string)[];
+ suffixRefkey?: Refkey;
+ /** If true, params replaces operation parameters instead of adding to them */
+ replaceParameters?: boolean;
+}
+
+/**
+ * Build a parameter descriptor array from a TypeSpec Model.
+ *
+ * Parameter ordering (unless replaceParameters is true):
+ * - Operation params without defaults: positional (e.g., URL path params)
+ * - "*" marker (if any keyword-only params exist)
+ * - Operation params with defaults: keyword-only (e.g., query params)
+ * - Additional params: keyword-only
+ */
+export function buildParameterDescriptors(
+ type: Model,
+ options: BuildParameterDescriptorsOptions = {},
+): (py.ParameterDescriptor | string)[] | undefined {
+ const { $ } = useTsp();
+ const suffixRefkey = options.suffixRefkey ?? refkey();
+ const optionsParams: py.ParameterDescriptor[] = normalizeParameters(options.params ?? []);
+
+ // If replaceParameters is true, ignore operation params and just return options params
+ // All replacement parameters are keyword-only (following "additional parameters are keyword-only" principle)
+ if (options.replaceParameters) {
+ const withoutDefaults = optionsParams.filter((p) => p.default === undefined);
+ const withDefaults = optionsParams.filter((p) => p.default !== undefined);
+
+ // Always add "*" marker since all replacement params should be keyword-only
+ const allParams: (py.ParameterDescriptor | string)[] =
+ optionsParams.length > 0 ? ["*", ...withoutDefaults, ...withDefaults] : [];
+
+ return allParams;
+ }
+
+ const modelProperties = $.model.getProperties(type);
+ const operationParams: py.ParameterDescriptor[] = [...modelProperties.values()].map((m) =>
+ buildParameterDescriptor(m, suffixRefkey),
+ );
+
+ // Split operation params: params without defaults are positional, params with defaults are keyword-only
+ const opParamsWithoutDefaults = operationParams.filter((p) => p.default === undefined);
+ const opParamsWithDefaults = operationParams.filter((p) => p.default !== undefined);
+
+ // Reorder additional params: params without defaults before params with defaults
+ const optionsWithoutDefaults = optionsParams.filter((p) => p.default === undefined);
+ const optionsWithDefaults = optionsParams.filter((p) => p.default !== undefined);
+
+ // Build final parameter list:
+ // - Operation params without defaults: positional (e.g., URL path params)
+ // - "*" marker (if there are keyword-only params)
+ // - Operation params with defaults: keyword-only (e.g., query params with defaults)
+ // - Additional params: keyword-only
+
+ // If there are no operation params at all, treat additional params without defaults as positional
+ const hasOperationParams = operationParams.length > 0;
+
+ const positionalParams = hasOperationParams
+ ? opParamsWithoutDefaults
+ : [...opParamsWithoutDefaults, ...optionsWithoutDefaults];
+
+ const keywordOnlyParams = hasOperationParams
+ ? [...optionsWithoutDefaults, ...opParamsWithDefaults, ...optionsWithDefaults]
+ : [...opParamsWithDefaults, ...optionsWithDefaults];
+
+ // Add "*" marker if we have keyword-only params
+ // This enforces that params with defaults are truly keyword-only
+ const allParams: (py.ParameterDescriptor | string)[] =
+ keywordOnlyParams.length > 0
+ ? [...positionalParams, "*", ...keywordOnlyParams]
+ : [...positionalParams];
+
+ return allParams;
+}
+
+/**
+ * Convert a TypeSpec ModelProperty into a Python ParameterDescriptor.
+ */
+export function buildParameterDescriptor(
+ modelProperty: ModelProperty,
+ suffixRefkey: Refkey,
+): py.ParameterDescriptor {
+ const { $ } = useTsp();
+ const namePolicy = py.usePythonNamePolicy();
+ const paramName = namePolicy.getName(modelProperty.name, "parameter");
+ const isOptional = modelProperty.optional || modelProperty.defaultValue !== undefined;
+ const doc = $.type.getDoc(modelProperty);
+ let defaultValueNode: Children | undefined = undefined;
+ const hasDefault =
+ modelProperty.defaultValue !== undefined && modelProperty.defaultValue !== null;
+ if (hasDefault) {
+ defaultValueNode = Atom({ value: (modelProperty as any).defaultValue });
+ } else if (isOptional) {
+ // Render Python None for optional parameters without explicit default
+ defaultValueNode = py.Atom({ jsValue: null }) as any;
+ }
+ return {
+ doc,
+ name: paramName,
+ refkey: efRefkey(modelProperty, suffixRefkey),
+ type: TypeExpression({ type: modelProperty.type }),
+ ...(defaultValueNode !== undefined ? { default: defaultValueNode } : {}),
+ };
+}
+
+const rawTypeMap = {
+ string: "str",
+ number: "float",
+ boolean: "bool",
+ any: typingModule["."]["Any"],
+ never: typingModule["."]["Never"],
+};
+
+/**
+ * Convert a parameter descriptor array to normalized parameter descriptors.
+ * String parameter names are converted to basic parameter descriptors.
+ */
+function normalizeParameters(
+ params: (py.ParameterDescriptor | string)[],
+): py.ParameterDescriptor[] {
+ if (!params) return [];
+
+ return params.map((param) => {
+ if (typeof param === "string") {
+ // Convert string names to parameter descriptors
+ return { name: param };
+ }
+ if (typeof (param as any).type === "string") {
+ return {
+ ...param,
+ type: rawTypeMap[param.type as keyof typeof rawTypeMap] ?? param.type,
+ } as py.ParameterDescriptor;
+ }
+ return param;
+ });
+}
diff --git a/packages/emitter-framework/src/python/utils/refkey.ts b/packages/emitter-framework/src/python/utils/refkey.ts
new file mode 100644
index 00000000000..8beaf1edcdc
--- /dev/null
+++ b/packages/emitter-framework/src/python/utils/refkey.ts
@@ -0,0 +1,36 @@
+import { refkey as ayRefkey, type Refkey } from "@alloy-js/core";
+
+const refKeyPrefix = Symbol.for("emitter-framework:python");
+
+/**
+ * A wrapper around `refkey` that uses a custom symbol to avoid collisions with
+ * other libraries that use `refkey`.
+ *
+ * @remarks
+ *
+ * The underlying refkey function is called with the {@link refKeyPrefix} symbol as the first argument.
+ *
+ * @param args The parameters of the refkey.
+ * @returns A refkey object that can be used to identify the value.
+ */
+export function efRefkey(...args: unknown[]): Refkey {
+ if (args.length === 0) {
+ return ayRefkey(); // Generates a unique refkey
+ }
+ return ayRefkey(refKeyPrefix, ...args);
+}
+
+/**
+ * Creates a refkey for a declaration by combining the provided refkey with an internal
+ * refkey generated from the provided arguments.
+ *
+ * @param refkey The refkey provided by the user to be passed as is.
+ * @param args The parameters of the refkey.
+ * @returns An array of refkeys that can be passed to an Alloy declaration.
+ */
+export function declarationRefkeys(refkey?: Refkey | Refkey[], ...args: unknown[]): Refkey[] {
+ if (refkey) {
+ return [refkey, efRefkey(...args)].flat();
+ }
+ return [efRefkey(...args)];
+}
diff --git a/packages/emitter-framework/src/python/utils/type.ts b/packages/emitter-framework/src/python/utils/type.ts
new file mode 100644
index 00000000000..8058f3a37af
--- /dev/null
+++ b/packages/emitter-framework/src/python/utils/type.ts
@@ -0,0 +1,31 @@
+import type { Typekit } from "@typespec/compiler/typekit";
+
+/**
+ * Check if a type is a literal type (string, numeric, boolean, or union variant).
+ * This is useful for determining if a type can be used in a Python Literal[] type.
+ *
+ * @param $ - The Typekit instance
+ * @param type - The type to check
+ * @returns true if the type is a literal type
+ */
+export function isLiteral($: Typekit, type: any): boolean {
+ if (!type) return false;
+
+ return (
+ $.literal.isString(type) ||
+ $.literal.isNumeric(type) ||
+ $.literal.isBoolean(type) ||
+ type.kind === "UnionVariant"
+ );
+}
+
+/**
+ * Check if all types in an array are literal types.
+ *
+ * @param $ - The Typekit instance
+ * @param types - Array of types to check
+ * @returns true if all types are literals
+ */
+export function areAllLiterals($: Typekit, types: any[]): boolean {
+ return types.every((type) => isLiteral($, type));
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 88b2f19eb07..8af70bccd41 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -421,6 +421,9 @@ importers:
'@alloy-js/core':
specifier: ^0.22.0
version: 0.22.0
+ '@alloy-js/python':
+ specifier: ^0.3.0
+ version: 0.3.0
'@alloy-js/rollup-plugin':
specifier: ^0.1.0
version: 0.1.0(@babel/core@7.28.5)(@types/babel__core@7.20.5)(rollup@4.49.0)
@@ -2732,6 +2735,9 @@ packages:
'@alloy-js/msbuild@0.22.0':
resolution: {integrity: sha512-Zi4bezFzQveW4u78qwSlIlscZd8y5OKCRitSOAiw0qLfF95L4+3wgQ6QpbYuqeRRZIa5kimhbogKTSrSxycrUg==}
+ '@alloy-js/python@0.3.0':
+ resolution: {integrity: sha512-VujQyh6aGC5oAXvmtKcDGYg3rRgU01IvN5uOu6u5m/kXR7rUTsndrv960r0bBniIL+EvRF3CMu7E6EEcbb3Pyg==}
+
'@alloy-js/rollup-plugin@0.1.0':
resolution: {integrity: sha512-MXR8mBdSh/pxMP8kIXAcMYKsm5yOWZ+igxcaRX1vBXFiHU4eK7gE/5q6Fk8Vdydh+MItWtgekwIhUWvcszdNFQ==}
engines: {node: '>=18.0.0'}
@@ -13740,6 +13746,12 @@ snapshots:
marked: 16.4.2
pathe: 2.0.3
+ '@alloy-js/python@0.3.0':
+ dependencies:
+ '@alloy-js/core': 0.22.0
+ change-case: 5.4.4
+ pathe: 2.0.3
+
'@alloy-js/rollup-plugin@0.1.0(@babel/core@7.28.5)(@types/babel__core@7.20.5)(rollup@4.49.0)':
dependencies:
'@alloy-js/babel-preset': 0.2.1(@babel/core@7.28.5)