diff --git a/packages/emitter-framework/src/python/builtins.ts b/packages/emitter-framework/src/python/builtins.ts
index 13934272067..d796b1606ea 100644
--- a/packages/emitter-framework/src/python/builtins.ts
+++ b/packages/emitter-framework/src/python/builtins.ts
@@ -4,6 +4,13 @@ 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: {
@@ -21,6 +28,17 @@ export const decimalModule = createModule({
export const typingModule = createModule({
name: "typing",
descriptor: {
- ".": ["Any", "Never", "NoReturn", "TypeAlias", "Tuple", "Callable", "Protocol"],
+ ".": [
+ "Any",
+ "Callable",
+ "Generic",
+ "Literal",
+ "Never",
+ "Optional",
+ "Protocol",
+ "TypeAlias",
+ "TypeVar",
+ "Tuple",
+ ],
},
});
diff --git a/packages/emitter-framework/src/python/components/atom/atom.test.tsx b/packages/emitter-framework/src/python/components/atom/atom.test.tsx
index 03b145b3de5..fef589ed6b3 100644
--- a/packages/emitter-framework/src/python/components/atom/atom.test.tsx
+++ b/packages/emitter-framework/src/python/components/atom/atom.test.tsx
@@ -57,9 +57,54 @@ describe("NumericValue", () => {
});
it("decimals", async () => {
- const value = $(program).value.createNumeric(42.5);
+ 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 assumeFloat", 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 assumeFloat", 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 assumeFloat", async () => {
+ const value = $(program).value.createNumeric(1e3);
+ expect(getOutput(program, [])).toRenderTo(`1000.0`);
+ });
- await testValueExpression(value, `42.5`);
+ it("small decimal via exponent", async () => {
+ const value = $(program).value.createNumeric(1e-3);
+ await testValueExpression(value, `0.001`);
});
});
diff --git a/packages/emitter-framework/src/python/components/atom/atom.tsx b/packages/emitter-framework/src/python/components/atom/atom.tsx
index 4771c30fcc9..b4606ef3077 100644
--- a/packages/emitter-framework/src/python/components/atom/atom.tsx
+++ b/packages/emitter-framework/src/python/components/atom/atom.tsx
@@ -10,6 +10,11 @@ interface AtomProps {
* The TypeSpec value to be converted to a Python expression.
*/
value: Value;
+ /**
+ * Hint that this numeric value should be emitted as a float (e.g., 42 -> 42.0).
+ * Only affects NumericValue.
+ */
+ assumeFloat?: boolean;
}
/**
@@ -23,8 +28,12 @@ export function Atom(props: Readonly): Children {
case "BooleanValue":
case "NullValue":
return ;
- case "NumericValue":
- return ;
+ case "NumericValue": {
+ const num = props.value.value.asNumber();
+ const isNonInteger = num != null && !Number.isInteger(num);
+ const asFloat = isNonInteger || props.assumeFloat === true;
+ return ;
+ }
case "ArrayValue":
return (
{
+ 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"
+
+ `,
+ );
+ });
+
+ // TODO: Change this test, as this isn't valid Python
+ 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
+ from dataclasses import dataclass
+
+ @dataclass(kw_only=True)
+ 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
+
+
+ @dataclass(kw_only=True)
+ 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
+
+ @dataclass(kw_only=True)
+ 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
+
+ @dataclass(kw_only=True)
+ class WidgetOperations(ABC):
+ @abstractmethod
+ def get_name(self, id: str) -> Widget:
+ pass
+
+
+
+ @dataclass(kw_only=True)
+ 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
+ from dataclasses import dataclass
+
+ @dataclass(kw_only=True)
+ 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
+ from dataclasses import dataclass
+
+ @dataclass(kw_only=True)
+ 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
+ from dataclasses import dataclass
+
+ @dataclass(kw_only=True)
+ 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
+ from dataclasses import dataclass
+
+ @dataclass(kw_only=True)
+ class WidgetOperations(ABC):
+ @staticmethod
+ @abstractmethod
+ def get_name(id: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("Adds a Generic import if the model has template parameters", async () => {
+ const { program, Response, StringResponse } = await Tester.compile(t.code`
+ model ${t.model("Response")} {
+ data: T;
+ status: string;
+ }
+
+ alias ${t.type("StringResponse")} = Response;
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeAlias
+ from typing import TypeVar
+
+ t = TypeVar("T")
+
+ @dataclass(kw_only=True)
+ class Response(Generic[T]):
+ data: T
+ status: str
+
+
+ StringResponse: TypeAlias = Response[str]
+ `);
+ });
+
+ it("Handles multiple template parameters", async () => {
+ const { program, Result } = await Tester.compile(t.code`
+ model ${t.model("Result")} {
+ value: T;
+ error: E;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T")
+ e = TypeVar("E")
+
+ @dataclass(kw_only=True)
+ class Result(Generic[T, E]):
+ value: T
+ error: E
+
+ `);
+ });
+
+ it("Handles template parameter with constraint (bound)", async () => {
+ const { program, Container } = await Tester.compile(t.code`
+ model ${t.model("Container")} {
+ value: T;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T", bound=str)
+
+ @dataclass(kw_only=True)
+ class Container(Generic[T]):
+ value: T
+
+ `);
+ });
+
+ it("Handles multiple template parameters with mixed constraints", async () => {
+ const { program, Result } = await Tester.compile(t.code`
+ model ${t.model("Result")} {
+ value: T;
+ error: E;
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T", bound=str)
+ e = TypeVar("E")
+
+ @dataclass(kw_only=True)
+ class Result(Generic[T, E]):
+ value: T
+ error: E
+
+ `);
+ });
+
+ it("Does not add Generic for template instances", async () => {
+ const { program, Response, ConcreteResponse } = await Tester.compile(t.code`
+ model ${t.model("Response")} {
+ data: T;
+ status: string;
+ }
+
+ model ${t.model("ConcreteResponse")} extends Response {
+ timestamp: string;
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T")
+
+ @dataclass(kw_only=True)
+ class Response(Generic[T]):
+ data: T
+ status: str
+
+
+ @dataclass(kw_only=True)
+ class ConcreteResponse(Response[str]):
+ timestamp: str
+
+ `);
+ });
+
+ it("Generates TypeVars for templated interfaces", async () => {
+ const { program, Repository } = await Tester.compile(t.code`
+ interface ${t.interface("Repository")} {
+ get(id: string): T;
+ list(): T[];
+ }
+ `);
+
+ expect(getOutput(program, [])).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T")
+
+ @dataclass(kw_only=True)
+ class Repository(Generic[T], ABC):
+ @abstractmethod
+ def get(self, id: str) -> T:
+ pass
+
+ @abstractmethod
+ def list(self) -> Array[T]:
+ pass
+
+
+ `);
+ });
+
+ it("Does not generate TypeVars for interface instances", async () => {
+ const { program, Repository, StringRepository } = await Tester.compile(t.code`
+ interface ${t.interface("Repository")} {
+ get(id: string): T;
+ list(): T[];
+ }
+
+ interface ${t.interface("StringRepository")} extends Repository {
+ findByPrefix(prefix: string): string[];
+ }
+ `);
+
+ expect(
+ getOutput(program, [
+ ,
+ ,
+ ]),
+ ).toRenderTo(`
+ from abc import ABC
+ from abc import abstractmethod
+ from dataclasses import dataclass
+ from typing import Generic
+ from typing import TypeVar
+
+ t = TypeVar("T")
+
+ @dataclass(kw_only=True)
+ class Repository(Generic[T], ABC):
+ @abstractmethod
+ def get(self, id: str) -> T:
+ pass
+
+ @abstractmethod
+ def list(self) -> Array[T]:
+ pass
+
+
+
+ @dataclass(kw_only=True)
+ 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
+
+
+ `);
+ });
+});
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..5c73a54251c
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx
@@ -0,0 +1,384 @@
+import { abcModule, typingModule } from "#python/builtins.js";
+import { code, createContentSlot, For, mapJoin, Show, type Children } from "@alloy-js/core";
+import * as py from "@alloy-js/python";
+import {
+ isTemplateDeclaration,
+ isTemplateDeclarationOrInstance,
+ type Interface,
+ type Model,
+ type ModelProperty,
+ type Operation,
+} from "@typespec/compiler";
+import type { TemplateDeclarationNode } from "@typespec/compiler/ast";
+import type { Typekit } from "@typespec/compiler/typekit";
+import { createRekeyableMap } from "@typespec/compiler/utils";
+import { useTsp } from "../../../core/context/tsp-context.js";
+import { reportDiagnostic } from "../../../lib.js";
+import { createDocElement } from "../../utils/doc.jsx";
+import { declarationRefkeys, efRefkey } from "../../utils/refkey.js";
+import { TypeExpression } from "../type-expression/type-expression.jsx";
+import { ClassMember } from "./class-member.jsx";
+import { MethodProvider } from "./class-method.jsx";
+
+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;
+}
+
+/**
+ * Gets type members (properties or operations) from a Model or Interface.
+ * @param $ - The Typekit.
+ * @param type - The model or interface type.
+ * @returns Array of model properties or operations.
+ */
+function getTypeMembers($: Typekit, type: Model | Interface): (ModelProperty | Operation)[] {
+ if ($.model.is(type)) {
+ // For models, extract properties to render as dataclass fields
+ return Array.from($.model.getProperties(type).values());
+ } else if (type.kind === "Interface") {
+ // For interfaces, extract operations to render as abstract methods
+ return Array.from(createRekeyableMap(type.operations).values());
+ } else {
+ throw new Error("Expected Model or Interface type");
+ }
+}
+
+/**
+ * Creates the class body for the class declaration.
+ * Returns a ClassBody component if there are members or children to render,
+ * otherwise returns undefined (which will render "pass" in Python).
+ *
+ * @param $ - The Typekit.
+ * @param props - The props for the class declaration.
+ * @param abstract - Whether the class is abstract.
+ * @returns The class body component, or undefined for an empty class.
+ */
+function createClassBody($: Typekit, props: ClassDeclarationProps, abstract: boolean) {
+ if (!isTypedClassDeclarationProps(props)) {
+ const ContentSlot = createContentSlot();
+ return (
+ <>
+ {props.children}
+ {undefined}
+ >
+ );
+ }
+
+ const validTypeMembers = getTypeMembers($, props.type);
+
+ return ;
+}
+
+/**
+ * Creates the extends types for the class declaration.
+ *
+ * - Template instances (e.g., `Response` → `Response[str]`) - Use TypeExpression to render with type args
+ * - Partial templates (e.g., `Response -> Response[T]`) - Use TypeExpression to render with type args
+ * - Regular models (e.g., `BaseWidget`) - Use py.Reference for simple name resolution
+ * - Arrays - Use TypeExpression for `typing.Sequence[T]` rendering
+ * - Records - Not supported, ignored
+ *
+ * @param $ - The Typekit.
+ * @param type - The type to create the extends type for.
+ * @returns The extends types for the class declaration, or undefined for interfaces.
+ */
+function getExtendsType($: Typekit, type: Model | Interface): Children | undefined {
+ // For interfaces, return undefined because inheritance is flattened by TypeSpec
+ if (!$.model.is(type)) {
+ return undefined;
+ }
+
+ const extending: Children[] = [];
+
+ if (type.baseModel) {
+ if ($.array.is(type.baseModel)) {
+ extending.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
+ // This covers: Response, Response, and other templated scenarios
+ extending.push();
+ } else {
+ // Regular model - use py.Reference for proper symbol resolution
+ extending.push();
+ }
+ }
+
+ // Handle index types: Arrays (int indexes) are supported, while Records (string indexes) are not
+ // Note: TypeSpec prevents array models from having properties, so indexType is only for empty arrays
+ const indexType = $.model.getIndexType(type);
+ if (indexType && !$.record.is(indexType)) {
+ extending.push();
+ }
+
+ return extending.length > 0
+ ? mapJoin(
+ () => extending,
+ (ext) => ext,
+ { joiner: "," },
+ )
+ : undefined;
+}
+
+/**
+ * Creates the bases (inheritance) list for the class declaration.
+ * Combines explicit bases from props, inherited bases from the type, and ABC if abstract.
+ * ABC is always added last to maintain proper Python MRO.
+ *
+ * @param $ - The Typekit.
+ * @param props - The props for the class declaration.
+ * @param abstract - Whether the class is abstract.
+ * @param extraBases - Additional bases to include (e.g., Generic[T]). Will be mutated.
+ * @returns The bases type for the class declaration, or undefined if no bases.
+ */
+function createBasesType(
+ $: Typekit,
+ props: ClassDeclarationProps,
+ abstract: boolean,
+ extraBases: Children[] = [],
+) {
+ // Add extends/inheritance from the TypeSpec type if present
+ if (isTypedClassDeclarationProps(props)) {
+ const extend = getExtendsType($, props.type);
+ if (extend) {
+ extraBases.push(extend);
+ }
+ }
+
+ // Combine explicit bases from props with extraBases (Generic, extends, etc.)
+ const allBases = (props.bases ?? []).concat(extraBases);
+
+ // For non-abstract classes, return bases or undefined
+ if (!abstract) {
+ return allBases.length > 0 ? allBases : undefined;
+ }
+
+ // For abstract classes, add ABC (always last for proper MRO)
+ const abcBase = abcModule["."]["ABC"];
+ return allBases.length > 0 ? [...allBases, abcBase] : [abcBase];
+}
+
+/**
+ * Builds TypeVar declarations and the Generic[...] base for templated types.
+ *
+ * **Template Detection Logic**:
+ * Only generates TypeVars for true template declarations (e.g., `model Response` or `interface Foo`).
+ *
+ * Skips TypeVars for:
+ * - **Template Instances** - e.g., `Response` (concrete type instantiation)
+ * - **Operations in Template Interfaces** - e.g., `interface Foo { op(item: T): T }` (operations inherit parent's template params)
+ * - **Regular Types** - e.g., `model Widget` (no template parameters)
+ *
+ * @param $ - The Typekit
+ * @param type - The model or interface type to analyze
+ * @returns TypeVar declarations and Generic base, or null if not a template declaration
+ */
+function buildTypeVarsAndGenericBase(
+ $: Typekit,
+ type: Model | Interface,
+): { typeVars: Children | null; genericBase?: Children } {
+ // Only generate TypeVars for true template declarations
+ // (skips template instances, operations in template interfaces, and regular types)
+ if (!isTemplateDeclaration(type)) {
+ return { typeVars: null };
+ }
+
+ // Get template parameters from the validated template declaration
+ const templateParameters = (type.node as TemplateDeclarationNode).templateParameters;
+
+ // Generate TypeVars for the template declaration
+ const typeVars = (
+ <>
+
+ {(node) => {
+ // Build TypeVar arguments: name + optional bound
+ const typeVarArgs: Children[] = [];
+
+ // Check if template parameter has a constraint (bound)
+ if (node.constraint) {
+ // Converts the AST node to a TypeSpec type
+ const constraintType = $.program.checker.getTypeForNode(node.constraint);
+ typeVarArgs.push(
+ <>
+ bound=
+
+ >,
+ );
+ }
+
+ const typeVar = (
+
+ );
+ return ;
+ }}
+
+ >
+ );
+
+ const typeArgs: Children[] = [];
+ for (const templateParameter of templateParameters) {
+ typeArgs.push(code`${templateParameter.id.sv}`);
+ }
+
+ const genericBase = ;
+
+ return { typeVars, genericBase };
+}
+
+/**
+ * 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 docSource = props.doc ?? ("type" in props ? $.type.getDoc(props.type) : undefined);
+ const docElement = createDocElement(docSource, py.ClassDoc);
+
+ // Build template-related bases (Generic[T, ...]) if this is a template declaration
+ const extraBases: Children[] = [];
+ let typeVars: Children | null = null;
+ if (isTypedClassDeclarationProps(props)) {
+ const generic = buildTypeVarsAndGenericBase($, props.type);
+ typeVars = generic.typeVars;
+ if (generic.genericBase) {
+ extraBases.push(generic.genericBase);
+ }
+ }
+
+ const basesType = createBasesType($, props, abstract, extraBases);
+
+ 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.
+ const isArrayModel = $.model.is(props.type) && $.array.is(props.type);
+ const useDataclass = !isArrayModel;
+
+ const classBody = createClassBody($, props, abstract);
+ const ClassComponent = useDataclass ? py.DataclassDeclaration : py.ClassDeclaration;
+
+ return (
+ <>
+
+ {typeVars}
+
+
+
+
+
+ {classBody}
+
+
+ >
+ );
+}
+
+interface ClassBodyProps extends ClassDeclarationPropsWithType {
+ abstract?: boolean; // Global override for the abstract flag
+ methodType?: "method" | "class" | "static"; // Global override for the method 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.
+ */
+function ClassBody(
+ props: ClassBodyProps & { validTypeMembers?: (ModelProperty | Operation)[] },
+): Children {
+ const { $ } = useTsp();
+ const validTypeMembers = props.validTypeMembers ?? getTypeMembers($, props.type);
+ const ContentSlot = createContentSlot();
+
+ // Throw error for models with additional properties (Record-based scenarios)
+ // This is checked in ClassDeclaration before calling createClassBody, but kept here
+ // as a safety measure in case ClassBody is called directly
+ if ($.model.is(props.type)) {
+ const additionalPropsRecord = $.model.getAdditionalPropertiesRecord(props.type);
+ if (additionalPropsRecord) {
+ // Python dataclasses don't support dynamic properties, so an additionalProperties
+ // field would just be another fixed field, not a "catch-all" for arbitrary properties.
+ throw new Error("Models with additional properties (Record[…]) are not supported");
+ }
+ }
+
+ return (
+ <>
+
+
+ {(typeMember) => (
+
+ )}
+
+ {props.children}
+
+ {undefined}
+ >
+ );
+}
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..ad86d37a2e2
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-member.test.tsx
@@ -0,0 +1,188 @@
+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.jsx";
+
+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..27dab446ceb
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx
@@ -0,0 +1,189 @@
+import { typingModule } from "#python/builtins.js";
+import { type Children, code, mapJoin } 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 { efRefkey } from "../../utils/refkey.js";
+import { areAllLiterals } from "../../utils/type.js";
+import { Atom } from "../atom/atom.jsx";
+import { TypeExpression } from "../type-expression/type-expression.jsx";
+import { Method } from "./class-method.jsx";
+
+export interface ClassMemberProps {
+ type: ModelProperty | Operation;
+ doc?: Children;
+ optional?: boolean;
+ methodType?: "method" | "class" | "static";
+ abstract?: boolean;
+}
+
+/**
+ * Builds the primitive initializer from the default value.
+ * @param defaultValue - The default value.
+ * @returns The primitive initializer.
+ */
+function buildPrimitiveInitializerFromDefault(
+ defaultValue: any,
+ propertyType: any,
+ $: ReturnType["$"],
+): Children | undefined {
+ 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;
+ }
+}
+
+/**
+ * Builds the type node for the property. This handles various literal and union variant scenarios:
+ * - Single union variant reference: Color.blue produces Literal[Color.BLUE]
+ * - Union of string literals: "a" | "b" produces Literal["a", "b"]
+ * - Union of integer literals: 1 | 2 | 3 produces Literal[1, 2, 3]
+ * - Union of boolean literals: true | false produces Literal[True, False]
+ * - Union of variant references: Color.red | Color.blue produces Literal[Color.RED, Color.BLUE]
+ * - Mixed literal unions: "a" | 1 | true | Color.RED produces Literal["a", 1, True, Color.RED]
+ *
+ * @param unpackedType - The unpacked type.
+ * @returns The type node, or undefined if the type doesn't match any supported literal pattern.
+ */
+function buildTypeNodeForProperty(unpackedType: any): Children | undefined {
+ const { $ } = useTsp();
+
+ // Single union variant reference - Literal[Color.MEMBER]
+ if (unpackedType && unpackedType.kind === "UnionVariant" && unpackedType.union) {
+ const unionType = unpackedType.union;
+ const variantValue = unpackedType.type;
+ const enumMemberName =
+ variantValue && typeof variantValue.value === "string"
+ ? variantValue.value
+ : String(variantValue?.value ?? "");
+ return (
+ <>
+ {typingModule["."]["Literal"]}[{efRefkey(unionType)}.{enumMemberName}]
+ >
+ );
+ }
+
+ // Union of literals or variant references (including mixed)
+ if (
+ unpackedType &&
+ unpackedType.kind === "Union" &&
+ Array.isArray((unpackedType as any).options)
+ ) {
+ const opts: any[] = (unpackedType as any).options;
+
+ // Check if all options are valid literal types
+ if (areAllLiterals($, opts)) {
+ const literalValues = opts
+ .map((opt) => {
+ if ($.literal.isString(opt)) {
+ // String literals need quotes
+ return JSON.stringify(opt.value);
+ } else if ($.literal.isNumeric(opt)) {
+ // Number literals render directly
+ return String(opt.value);
+ } else if ($.literal.isBoolean(opt)) {
+ // Boolean literals render as True/False (Python capitalization)
+ return opt.value ? "True" : "False";
+ } else if (opt.kind === "UnionVariant") {
+ // Variant references need enum reference
+ const variantValue = opt.type;
+ const enumMemberName =
+ variantValue && typeof variantValue.value === "string"
+ ? variantValue.value
+ : String(variantValue?.value ?? "");
+ return code`${efRefkey(opt.union)}.${enumMemberName}`;
+ }
+ return undefined;
+ })
+ .filter(Boolean);
+
+ return (
+ <>
+ {typingModule["."]["Literal"]}[
+ {mapJoin(
+ () => literalValues,
+ (val) => val,
+ { joiner: ", " },
+ )}
+ ]
+ >
+ );
+ }
+ }
+
+ return undefined;
+}
+
+/**
+ * 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 literalTypeNode = buildTypeNodeForProperty(unpackedType);
+ const initializer = buildPrimitiveInitializerFromDefault(defaultValue, unpackedType, $);
+ const unpackedTypeNode: Children = literalTypeNode ?? ;
+ const typeNode = isOptional ? (
+
+ ) : (
+ unpackedTypeNode
+ );
+
+ const classMemberProps = {
+ doc,
+ name,
+ optional: isOptional,
+ type: typeNode,
+ ...(initializer ? { initializer } : {}),
+ omitNone: !isOptional,
+ };
+ return ;
+ }
+
+ if ($.operation.is(props.type)) {
+ 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..6bf4fcfb9f7
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.test.tsx
@@ -0,0 +1,230 @@
+import { Tester } from "#test/test-host.js";
+import { getProgram } from "#test/utils.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 { Method } from "../../../../src/python/components/class-declaration/class-method.js";
+import { getOutput } from "../../test-utils.jsx";
+
+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 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, id: str, foo: str) -> str:
+ pass
+
+
+ `);
+ });
+
+ it("can prepend extra 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, id: 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 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..0298e6338ca
--- /dev/null
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-method.tsx
@@ -0,0 +1,93 @@
+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 { createDocElement } from "../../utils/doc.jsx";
+import { buildParameterDescriptors, getReturnType } from "../../utils/operation.js";
+import { TypeExpression } from "../type-expression/type-expression.jsx";
+
+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;
+ parametersMode?: "prepend" | "append" | "replace";
+ methodType?: "method" | "class" | "static";
+ abstract?: 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 docSource = props.doc ?? (isTypeSpecTyped && $.type.getDoc(props.type)) ?? undefined;
+ const docElement = createDocElement(docSource, py.MethodDoc);
+ 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,
+ mode: props.parametersMode,
+ });
+
+ return (
+ <>
+
+ >
+ );
+}
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
index 833366ed7e4..79d64aa7b49 100644
--- a/packages/emitter-framework/src/python/components/function-declaration/function-declaration.tsx
+++ b/packages/emitter-framework/src/python/components/function-declaration/function-declaration.tsx
@@ -1,9 +1,9 @@
import { useTsp } from "#core/index.js";
import { buildParameterDescriptors } from "#python/utils/operation.js";
import { declarationRefkeys } from "#python/utils/refkey.js";
-import { type Children, List } from "@alloy-js/core";
import * as py from "@alloy-js/python";
import type { Model, Operation } from "@typespec/compiler";
+import { createDocElement } from "../../utils/doc.jsx";
import { TypeExpression } from "../type-expression/type-expression.jsx";
export interface FunctionDeclarationPropsWithType
@@ -18,39 +18,6 @@ export type FunctionDeclarationProps =
| FunctionDeclarationPropsWithType
| py.FunctionDeclarationProps;
-/**
- * Normalize various doc sources into a Python FunctionDoc element.
- *
- * Accepts:
- * - string → split into lines and render as a multi-line docstring
- * - string[] | Children[] → rendered as separate paragraphs
- * - Children (e.g., an explicit ) → returned as-is
- */
-function createDocElement(
- $: ReturnType["$"],
- source?: string | string[] | Children | Children[],
-): Children | undefined {
- if (!source) return undefined;
- if (Array.isArray(source)) {
- return ;
- } else if (typeof source === "string") {
- const lines = source.split(/\r?\n/);
- return (
-
- {lines.map((line) => (
- <>{line}>
- ))}
- ,
- ]}
- />
- );
- } else {
- return source as Children | undefined;
- }
-}
-
/**
* A Python function declaration. Pass the `type` prop to create the
* function declaration by converting from a TypeSpec Operation. Any other props
@@ -88,7 +55,7 @@ export function FunctionDeclaration(props: FunctionDeclarationProps) {
});
}
const rawDoc = props.doc ?? $.type.getDoc(props.type);
- const docElement = createDocElement($, rawDoc);
+ const docElement = createDocElement(rawDoc, py.FunctionDoc);
const doc = docElement ? (
<>
{docElement}
diff --git a/packages/emitter-framework/src/python/components/index.ts b/packages/emitter-framework/src/python/components/index.ts
index 2a1585fb46b..9366e04b74f 100644
--- a/packages/emitter-framework/src/python/components/index.ts
+++ b/packages/emitter-framework/src/python/components/index.ts
@@ -1,4 +1,6 @@
export * from "./atom/atom.jsx";
+export * from "./class-declaration/class-declaration.jsx";
+export * from "./enum-declaration/enum-declaration.jsx";
export * from "./function-declaration/function-declaration.jsx";
export * from "./protocol-declaration/protocol-declaration.jsx";
export * from "./type-declaration/type-declaration.jsx";
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
index 3e39bc66dca..50f13e14b0d 100644
--- 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
@@ -1,4 +1,5 @@
import { useTsp } from "#core/context/index.js";
+import { namekey } from "@alloy-js/core";
import * as py from "@alloy-js/python";
import type { Type } from "@typespec/compiler";
import { reportDiagnostic } from "../../../lib.js";
@@ -29,7 +30,14 @@ export function TypeAliasDeclaration(props: TypedAliasDeclarationProps) {
const doc = props.doc ?? $.type.getDoc(props.type);
const refkeys = declarationRefkeys(props.refkey, props.type);
- const name = py.usePythonNamePolicy().getName(originalName, "variable");
+ let name: any;
+ if ("templateMapper" in (props.type as any) && (props.type as any).templateMapper) {
+ // Template instance alias: use the alias name (like StringResponse in alias StringResponse = Response)
+ const plausibleName = $.type.getPlausibleName(props.type as any);
+ name = namekey(plausibleName, { ignoreNamePolicy: true });
+ } else {
+ name = py.usePythonNamePolicy().getName(originalName, "variable");
+ }
// TODO: See how we will handle this kind of scenario:
// type Foo {
// bar(id: String): BarResponse
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
index f8a501041b6..058626a3305 100644
--- a/packages/emitter-framework/src/python/components/type-expression/type-expression.tsx
+++ b/packages/emitter-framework/src/python/components/type-expression/type-expression.tsx
@@ -1,9 +1,17 @@
import { Experimental_OverridableComponent } from "#core/components/index.js";
import { useTsp } from "#core/context/index.js";
import { reportPythonDiagnostic } from "#python/lib.js";
-import { For, mapJoin } from "@alloy-js/core";
+import { code, For, mapJoin } from "@alloy-js/core";
import * as py from "@alloy-js/python";
-import type { IntrinsicType, Model, Scalar, Type } from "@typespec/compiler";
+import {
+ isNeverType,
+ type IntrinsicType,
+ type Model,
+ type Scalar,
+ type TemplatedTypeBase,
+ 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";
@@ -36,6 +44,9 @@ export function TypeExpression(props: TypeExpressionProps) {
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":
@@ -72,8 +83,35 @@ export function TypeExpression(props: TypeExpressionProps) {
return ;
}
+ if (isTemplateVar(type)) {
+ // Handles scenarios like Response, rendering Response[str]
+ const args = (type.templateMapper?.args ?? []) as Type[];
+ const typeArgs = args.map((a) => );
+ const baseName = py.usePythonNamePolicy().getName((type as Model).name, "class");
+ return (
+ <>
+ {baseName}[
+
+ {(a) => a}
+
+ ]
+ >
+ );
+ }
+
+ // Regular named models should be handled as references
+ if (type.name) {
+ return (
+
+
+
+ );
+ }
+
reportPythonDiagnostic($.program, { code: "python-unsupported-type", target: type });
break;
+ case "TemplateParameter":
+ return code`${String((type.node as TemplateParameterDeclarationNode).id.sv)}`;
// TODO: Models will be implemented separately
// return ;
@@ -118,7 +156,6 @@ export function TypeExpression(props: TypeExpressionProps) {
}
default:
reportPythonDiagnostic($.program, { code: "python-unsupported-type", target: type });
- return "any";
}
}
@@ -201,7 +238,12 @@ function getScalarIntrinsicExpression($: Typekit, type: Scalar | IntrinsicType):
return pythonType;
}
+function isTemplateVar(type: Type): boolean {
+ return (type as TemplatedTypeBase).templateMapper !== undefined;
+}
+
function isDeclaration($: Typekit, type: Type): boolean {
+ if (isTemplateVar(type)) return false;
switch (type.kind) {
case "Namespace":
case "Interface":
diff --git a/packages/emitter-framework/src/python/index.ts b/packages/emitter-framework/src/python/index.ts
index abfe9e01158..5308987703b 100644
--- a/packages/emitter-framework/src/python/index.ts
+++ b/packages/emitter-framework/src/python/index.ts
@@ -1 +1,2 @@
+export * from "./builtins.js";
export * from "./components/index.js";
diff --git a/packages/emitter-framework/src/python/test-utils.tsx b/packages/emitter-framework/src/python/test-utils.tsx
index 6d8d4e16c73..c94e9f66dfd 100644
--- a/packages/emitter-framework/src/python/test-utils.tsx
+++ b/packages/emitter-framework/src/python/test-utils.tsx
@@ -2,14 +2,27 @@ 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 { datetimeModule, decimalModule, typingModule } from "./builtins.js";
+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 (