diff --git a/packages/emitter-framework/src/python/builtins.ts b/packages/emitter-framework/src/python/builtins.ts
index 87ea1063a0b..c7b88c95427 100644
--- a/packages/emitter-framework/src/python/builtins.ts
+++ b/packages/emitter-framework/src/python/builtins.ts
@@ -35,6 +35,16 @@ export const decimalModule = createModule({
export const typingModule = createModule({
name: "typing",
descriptor: {
- ".": ["Any", "Literal", "Never", "NoReturn", "Protocol", "Tuple"],
+ ".": [
+ "Any",
+ "Literal",
+ "Never",
+ "NoReturn",
+ "Protocol",
+ "Tuple",
+ "Generic",
+ "TypeVar",
+ "Optional",
+ ],
},
});
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
index 4b1c645a939..cbb1bf14893 100644
--- 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
@@ -1,4 +1,3 @@
-import { getOutput } from "#python/test-utils.jsx";
import { Tester } from "#test/test-host.js";
import { List } from "@alloy-js/core";
import * as py from "@alloy-js/python";
@@ -7,6 +6,7 @@ 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.jsx";
describe("Python Class from model", () => {
it("creates a class", async () => {
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
index 4833d9541c3..5499c6f09bf 100644
--- a/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-declaration.tsx
@@ -1,7 +1,8 @@
-import { abcModule, dataclassesModule } from "#python/builtins.js";
+import { abcModule, dataclassesModule, typingModule } from "#python/builtins.js";
import { type Children, For, List, mapJoin, Show } from "@alloy-js/core";
import * as py from "@alloy-js/python";
import { 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";
@@ -152,16 +153,25 @@ function getExtendsType($: Typekit, type: Model | Interface): Children | undefin
* @param abstract - Whether the class is abstract.
* @returns The bases type for the class declaration.
*/
-function createBasesType($: Typekit, props: ClassDeclarationProps, abstract: boolean) {
- const globalBasesType = isTypedClassDeclarationProps(props)
- ? getExtendsType($, props.type)
- : undefined;
- let basesType = props.bases ? props.bases : (globalBasesType ?? undefined);
+function createBasesType(
+ $: Typekit,
+ props: ClassDeclarationProps,
+ abstract: boolean,
+ extraBases: Children[] = [],
+) {
+ if (isTypedClassDeclarationProps(props)) {
+ const extend = getExtendsType($, props.type);
+ if (extend) {
+ extraBases.push(extend);
+ }
+ }
+ const allBases = (props.bases ? props.bases : []).concat(extraBases);
+ const basesType = allBases.length > 0 ? allBases : undefined;
if (!abstract) return basesType;
const abcBase = abcModule["."]["ABC"];
- if (Array.isArray(basesType)) return [abcBase, ...basesType];
- if (basesType != null) return [abcBase, basesType];
+ if (Array.isArray(basesType)) return [...basesType, abcBase];
+ if (basesType != null) return [basesType, abcBase];
return [abcBase];
}
@@ -174,10 +184,48 @@ export function ClassDeclaration(props: ClassDeclarationProps) {
const { $ } = useTsp();
// If we are explicitly overriding the class as abstract or the type is not a model, we need to create an abstract class
- let abstract =
+ const abstract =
("abstract" in props && props.abstract) || ("type" in props && !$.model.is(props.type));
- let docElement = createDocElement($, props);
- let basesType = createBasesType($, props, abstract);
+ const docElement = createDocElement($, props);
+
+ const extraBases = [];
+ let typeVars = null;
+ const typeArgs = [];
+ if (isTypedClassDeclarationProps(props)) {
+ if (
+ !props.type.isFinished &&
+ (props.type.node as TemplateDeclarationNode)?.templateParameters
+ ) {
+ const templateParameters = (props.type.node as TemplateDeclarationNode)?.templateParameters;
+ typeVars = (
+ <>
+
+ {(node) => {
+ const typeVar = (
+ ]}
+ />
+ );
+ return ;
+
+ // ("${node.id.sv}")`;
+ // return ;
+ }}
+
+ >
+ );
+ for (const templateParamter of templateParameters) {
+ typeArgs.push(templateParamter.id.sv);
+ }
+ }
+
+ if (typeArgs.length > 0) {
+ extraBases.push();
+ }
+ }
+
+ const basesType = createBasesType($, props, abstract, extraBases);
if (!isTypedClassDeclarationProps(props)) {
return (
@@ -199,12 +247,10 @@ export function ClassDeclaration(props: ClassDeclarationProps) {
const refkeys = declarationRefkeys(props.refkey, props.type);
let dataclass: any = null;
- if (!abstract) {
- // Array-based models should be rendered as normal classes, not dataclasses (e.g., model Foo is Array)
- const isArrayModel = $.model.is(props.type) && $.array.is(props.type);
- if (!isArrayModel) {
- dataclass = dataclassesModule["."]["dataclass"];
- }
+ // Array-based models should be rendered as normal classes, not dataclasses (e.g., model Foo is Array)
+ const isArrayModel = $.model.is(props.type) && $.array.is(props.type);
+ if (!isArrayModel) {
+ dataclass = dataclassesModule["."]["dataclass"];
}
const classBody = createClassBody($, props, abstract);
@@ -218,11 +264,13 @@ export function ClassDeclaration(props: ClassDeclarationProps) {
return (
<>
-
- @{dataclass}
+
+ {typeVars}
+
-
+
+ @{dataclass}(kw_only=True)
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
index 909a232b435..d1ab297bca8 100644
--- a/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx
+++ b/packages/emitter-framework/src/python/components/class-declaration/class-member.tsx
@@ -1,7 +1,7 @@
import { typingModule } from "#python/builtins.js";
import { type Children } from "@alloy-js/core";
import * as py from "@alloy-js/python";
-import { isNeverType, type ModelProperty, type Operation } from "@typespec/compiler";
+import { type ModelProperty, type Operation } from "@typespec/compiler";
import { useTsp } from "../../../core/context/tsp-context.js";
import { efRefkey } from "../../utils/refkey.js";
import { Atom } from "../atom/atom.jsx";
@@ -125,16 +125,21 @@ export function ClassMember(props: ClassMemberProps) {
if ($.modelProperty.is(props.type)) {
// Map never-typed properties to typing.Never
- const isNever = isNeverType(props.type.type);
- const unpackedType = isNever ? (typingModule["."]["Never"] as any) : (props.type.type as any);
+ const unpackedType = props.type.type;
const isOptional = props.optional ?? props.type.optional ?? false;
- const defaultValue: any = (props.type as any).defaultValue;
+ const defaultValue = props.type.defaultValue;
const literalTypeNode = buildTypeNodeForProperty(unpackedType);
const initializer = buildPrimitiveInitializerFromDefault(defaultValue, unpackedType, $);
- const typeNode: Children = isNever
- ? (typingModule["."]["Never"] as any)
- : (literalTypeNode ?? );
+ const unpackedTypeNode: Children = literalTypeNode ?? ;
+ const typeNode = isOptional ? (
+
+ ) : (
+ unpackedTypeNode
+ );
const interfaceMemberProps = {
doc,
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
index 5eb7f4f9691..199cb034563 100644
--- 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
@@ -1,10 +1,10 @@
-import { getOutput } from "#python/test-utils.jsx";
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 () => {
diff --git a/packages/emitter-framework/src/python/components/index.ts b/packages/emitter-framework/src/python/components/index.ts
index 8a91b6e3d26..e641cd3a541 100644
--- a/packages/emitter-framework/src/python/components/index.ts
+++ b/packages/emitter-framework/src/python/components/index.ts
@@ -1,3 +1,4 @@
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";
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 6b9d27cef92..ecc225a28f9 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 } from "@alloy-js/core";
+import { code, For } 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":
@@ -64,8 +75,13 @@ export function TypeExpression(props: TypeExpressionProps) {
return ;
}
+ if (isTemplateVar(type)) {
+ 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 ;
@@ -157,7 +173,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 65709974f65..16163171173 100644
--- a/packages/emitter-framework/src/python/test-utils.tsx
+++ b/packages/emitter-framework/src/python/test-utils.tsx
@@ -12,11 +12,6 @@ import {
export function getOutput(program: Program, children: Children[]): Children {
const policy = py.createPythonNamePolicy();
- const printOptions = {
- printWidth: 80,
- tabWidth: 4,
- insertFinalNewLine: false,
- };
return (