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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/emitter-framework/src/python/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
},
});
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in this function are aimed at allowing us to specify additional bases for the purpose of adding Generic.

Those bases are generated in ClassDeclaration.

$: 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];
}

Expand All @@ -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
) {
Comment on lines +195 to +198
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mauriciogardini this is what I was referring to here #45 (comment)

const templateParameters = (props.type.node as TemplateDeclarationNode)?.templateParameters;
typeVars = (
<>
<For each={templateParameters} hardline>
{(node) => {
const typeVar = (
<py.FunctionCallExpression
target={typingModule["."].TypeVar}
args={[<py.Atom jsValue={node.id.sv} />]}
/>
);
return <py.VariableDeclaration name={node.id.sv} initializer={typeVar} />;

// ("${node.id.sv}")`;
// return <py.Declaration name={String(node.id.sv)}></py.Declaration>;
}}
</For>
</>
);
for (const templateParamter of templateParameters) {
typeArgs.push(templateParamter.id.sv);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't great, we're just making a list of strings. These should probably be references but I didn't have time to figure out how to make that work.

}
}
Comment on lines +195 to +221
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is all aimed at determining if there are template parameters that are unfulfilled and, if so, adding them as type arguments to a Generic base.


if (typeArgs.length > 0) {
extraBases.push(<py.TypeReference refkey={typingModule["."].Generic} typeArgs={typeArgs} />);
}
}

const basesType = createBasesType($, props, abstract, extraBases);

if (!isTypedClassDeclarationProps(props)) {
return (
Expand All @@ -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<T>)
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<T>)
const isArrayModel = $.model.is(props.type) && $.array.is(props.type);
if (!isArrayModel) {
dataclass = dataclassesModule["."]["dataclass"];
}
const classBody = createClassBody($, props, abstract);

Expand All @@ -218,11 +264,13 @@ export function ClassDeclaration(props: ClassDeclarationProps) {

return (
<>
<Show when={dataclass}>
@{dataclass}
<Show when={!!typeVars}>
{typeVars}
<hbr />
<line />
</Show>
Comment on lines +267 to 271
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typeVars should probably go at the top of the module, but I didn't want to figure out how to do that.

Even then, I'm not sure there is a defined style for where TypeVars should go.

<Show when={abstract}>
<Show when={dataclass}>
@{dataclass}(kw_only=True)
<hbr />
</Show>
<MethodProvider value={props.methodType}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 ?? <TypeExpression type={unpackedType} />);
const unpackedTypeNode: Children = literalTypeNode ?? <TypeExpression type={unpackedType} />;
const typeNode = isOptional ? (
<py.TypeReference
refkey={typingModule["."].Optional}
typeArgs={[unpackedTypeNode]}
></py.TypeReference>
) : (
unpackedTypeNode
);

const interfaceMemberProps = {
doc,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions packages/emitter-framework/src/python/components/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -64,8 +75,13 @@ export function TypeExpression(props: TypeExpressionProps) {
return <RecordExpression elementType={elementType} />;
}

if (isTemplateVar(type)) {
return <TypeExpression type={type.templateMapper?.args[0] as Type} />;
}
Comment on lines +78 to +80
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weird thing is that models used as template arguments are treated as models here.

reportPythonDiagnostic($.program, { code: "python-unsupported-type", target: type });
break;
case "TemplateParameter":
return code`${String((type.node as TemplateParameterDeclarationNode).id.sv)}`;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably can do a lot better than a string! Should be a reference.


// TODO: Models will be implemented separately
// return <InterfaceExpression type={type} />;
Expand Down Expand Up @@ -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":
Expand Down
1 change: 1 addition & 0 deletions packages/emitter-framework/src/python/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./builtins.js";
export * from "./components/index.js";
6 changes: 0 additions & 6 deletions packages/emitter-framework/src/python/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Output
program={program}
Expand All @@ -29,7 +24,6 @@ export function getOutput(program: Program, children: Children[]): Children {
py.abcModule,
py.enumModule,
]}
printOptions={printOptions}
namePolicy={policy}
>
<py.SourceFile path="test.py">{children}</py.SourceFile>
Expand Down
9 changes: 9 additions & 0 deletions packages/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# MacOS
.DS_Store

# Default TypeSpec output
tsp-output/
dist/

# Dependency directories
node_modules/
11 changes: 11 additions & 0 deletions packages/python/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-check
import eslint from "@eslint/js";
import tsEslint from "typescript-eslint";

export default tsEslint.config(
{
ignores: ["**/dist/**/*", "**/.temp/**/*"],
},
eslint.configs.recommended,
...tsEslint.configs.recommended,
);
44 changes: 44 additions & 0 deletions packages/python/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "python",
"version": "0.1.0",
"type": "module",
"main": "dist/src/index.js",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./testing": {
"types": "./dist/src/testing/index.d.ts",
"default": "./dist/src/testing/index.js"
}
},
"peerDependencies": {
"@typespec/compiler": "workspace:^"
},
"dependencies": {
"@alloy-js/core": "^0.20.0",
"@alloy-js/python": "link:/Users/srice/code/alloy/packages/python",
"@typespec/emitter-framework": "workspace:^",
"prettier": "~3.6.2"
},
"devDependencies": {
"@alloy-js/cli": "^0.20.0",
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@typespec/compiler": "workspace:^",
"eslint": "^9.15.0",
"prettier": "^3.3.3",
"typescript": "^5.3.3"
},
"scripts": {
"build": "alloy build",
"test": "node --test 'dist/test**/*.test.js'",
"lint": "eslint src/ test/ --report-unused-disable-directives --max-warnings=0",
"lint:fix": "eslint . --report-unused-disable-directives --fix",
"format": "prettier . --write",
"format:check": "prettier --check ."
},
"private": true
}
8 changes: 8 additions & 0 deletions packages/python/prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
trailingComma: "all"
printWidth: 120
quoteProps: "consistent"
endOfLine: lf
arrowParens: always
plugins:
- "./node_modules/@typespec/prettier-plugin-typespec/dist/index.js"
overrides: [{ "files": "*.tsp", "options": { "parser": "typespec" } }]
Loading