Skip to content

Commit 60a63a6

Browse files
committed
add test suite
1 parent 591f3a9 commit 60a63a6

13 files changed

Lines changed: 1714 additions & 73 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/node_modules
22
/build
33
/yarn-error.log
4+
/dist

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,34 @@
44
"main": "index.js",
55
"license": "MIT",
66
"dependencies": {
7+
"@commander-js/extra-typings": "^11.1.0",
78
"typescript": "^5.3.3"
89
},
910
"devDependencies": {
1011
"@babel/core": "^7.23.7",
1112
"@babel/node": "^7.22.19",
1213
"@babel/preset-env": "^7.23.7",
1314
"@babel/preset-typescript": "^7.23.3",
15+
"@ts-morph/bootstrap": "^0.22.0",
16+
"@types/jest": "^29.5.11",
1417
"@types/node": "^20.10.6",
1518
"@typescript-eslint/eslint-plugin": "^6.17.0",
1619
"@typescript-eslint/parser": "^6.17.0",
1720
"eslint": "^8.56.0",
1821
"eslint-config-prettier": "^9.1.0",
1922
"eslint-plugin-prettier": "^5.1.2",
23+
"jest": "^29.7.0",
2024
"prettier": "^3.1.1"
2125
},
2226
"scripts": {
2327
"watch": "babel-node --watch -x .ts --",
2428
"single": "babel-node -x .ts",
2529
"develop": "yarn watch src/index.ts",
26-
"lint": "eslint src/**/*.ts"
30+
"lint": "eslint src/**/*.ts",
31+
"build": "tsc -p .",
32+
"test": "jest"
33+
},
34+
"bin": {
35+
"typescript2python": "./dist/index.js"
2736
}
2837
}

src/index.ts

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as ts from "typescript";
22
import path from "path";
3-
import { ParserState } from "./ParserState";
4-
import { parseExports } from "./parseExports";
3+
import { program } from "@commander-js/extra-typings";
4+
import { typeScriptToPython } from "./typeScriptToPython";
55

6-
function compile(fileNames: string[]): void {
6+
const compile = (fileNames: string[]) => {
77
const program = ts.createProgram(fileNames, {
88
noEmit: true,
99
allowJs: true,
@@ -19,38 +19,16 @@ function compile(fileNames: string[]): void {
1919
.reduce((a, b) => a || b),
2020
);
2121

22-
const typechecker = program.getTypeChecker();
23-
24-
const knownTypes = new Map<ts.Type, string>();
25-
knownTypes.set(typechecker.getAnyType(), "Any");
26-
knownTypes.set(typechecker.getVoidType(), "None");
27-
knownTypes.set(typechecker.getUndefinedType(), "None");
28-
knownTypes.set(typechecker.getStringType(), "str");
29-
knownTypes.set(typechecker.getBooleanType(), "bool");
30-
knownTypes.set(typechecker.getNumberType(), "float");
31-
knownTypes.set(typechecker.getTrueType(), "Literal[True]");
32-
knownTypes.set(typechecker.getFalseType(), "Literal[False]");
33-
34-
const state: ParserState = {
35-
statements: [],
36-
helperCount: 0,
37-
typechecker,
38-
knownTypes,
39-
};
40-
41-
state.statements.push(
42-
`from typing_extensions import Literal, TypedDict, List, Union, NotRequired, Optional, Tuple, Dict, Any`,
43-
);
44-
45-
relevantSourceFiles.forEach((f) => {
46-
parseExports(state, f);
47-
});
48-
49-
for (const statement of state.statements) {
50-
// we want a single log statement printing the resulting python code
51-
// eslint-disable-next-line no-console
52-
console.log(statement, "\n");
53-
}
22+
const transpiled = typeScriptToPython(program.getTypeChecker(), relevantSourceFiles)
23+
console.log(transpiled);
5424
}
5525

56-
compile(process.argv.slice(2));
26+
program
27+
.name("typescript2python")
28+
.description("A program that converts TypeScript type definitions to Python")
29+
.arguments("<input...>")
30+
.action(args => {
31+
compile(args)
32+
})
33+
.parse(process.argv)
34+

src/parseInlineType.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const tryToParseInlineType = (
1818
globalScope?: boolean,
1919
): string | undefined => {
2020
const known = state.knownTypes.get(type);
21+
2122
if (known !== undefined) {
2223
return known;
2324
} else if (type.isLiteral()) {
@@ -31,8 +32,8 @@ export const tryToParseInlineType = (
3132
.getTypeArguments(type as ts.TypeReference)
3233
.map((v) => parseInlineType(state, v))
3334
.join(",")}]`;
34-
} else if (type.getStringIndexType()) {
35-
return `Dict[str, ${parseInlineType(state, type.getStringIndexType()!)}]`;
35+
} else if (type.getStringIndexType() !== undefined) {
36+
return `Dict[str,${parseInlineType(state, type.getStringIndexType()!)}]`;
3637
} else if (state.typechecker.isArrayLikeType(type)) {
3738
const typeArguments = state.typechecker.getTypeArguments(
3839
type as ts.TypeReference,
@@ -41,7 +42,7 @@ export const tryToParseInlineType = (
4142
return `List[${parseInlineType(state, typeArguments[0]!)}]`;
4243
} else {
4344
// TODO: figure out why we reach this and replace with correct type definition
44-
return `List[object]`;
45+
return `object`;
4546
}
4647
} else {
4748
// assume interface or object

src/testing/.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { transpileString } from "./utils";
2+
3+
describe("transpiling dictionaries types", () => {
4+
it("can transpile dicts", async () => {
5+
const result = await transpileString(`export type A = {
6+
foo: string,
7+
bar: number,
8+
}`);
9+
expect(result).toContain(`class A(TypedDict):\n foo: str\n bar: float`);
10+
});
11+
12+
it("keeps docstrings", async () => {
13+
const result = await transpileString(`
14+
/** This is A */
15+
export type A = {
16+
/** this is foo */
17+
foo: string,
18+
/** this is bar */
19+
bar: number,
20+
}
21+
`);
22+
expect(result).toContain(
23+
`class A(TypedDict):
24+
"""
25+
This is A
26+
"""
27+
foo: str
28+
"""
29+
this is foo
30+
"""
31+
bar: float
32+
"""
33+
this is bar
34+
"""`,
35+
);
36+
});
37+
38+
it("can transpile nested dicts", async () => {
39+
const result = await transpileString(`export type A = {
40+
outer: {
41+
inner: string
42+
},
43+
extra: number,
44+
}`);
45+
expect(result).toContain(
46+
`class __HelperType1__(TypedDict):
47+
inner: str
48+
49+
class A(TypedDict):
50+
outer: __HelperType1__
51+
extra: float`
52+
)
53+
});
54+
});
55+
56+
export type T = Record<string, string>;

src/testing/basic.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { transpileString } from "./utils";
2+
3+
describe("transpiling basic types", () => {
4+
it.each([
5+
["export type T = any;", "T = Any"],
6+
["export type T = boolean;", "T = bool"],
7+
["export type T = number;", "T = float"],
8+
["export type T = string;", "T = str"],
9+
["export type T = undefined;", "T = None"],
10+
["export type T = void;", "T = None"],
11+
["export type T = true;", "T = Literal[True]"],
12+
["export type T = false;", "T = Literal[False]"],
13+
["export type T = 42;", "T = Literal[42]"],
14+
["export type T = 'foo';", 'T = Literal["foo"]'],
15+
["export type T = {[key: string]: boolean};", "T = Dict[str,bool]"],
16+
["export type T = {[key: string]: number};", "T = Dict[str,float]"],
17+
[
18+
"export type T = {[key: string]: {[key: string]: number}};",
19+
"T = Dict[str,Dict[str,float]]",
20+
],
21+
["export type T = Record<string, number>;", "T = Dict[str,float]"],
22+
[
23+
"export type T = number | string | Record<string, boolean>",
24+
"T = Union[str,float,Dict[str,bool]]",
25+
],
26+
])("transpiles %p to %p", async (input, expected) => {
27+
const result = await transpileString(input);
28+
expect(result.split("\n")[2]).toEqual(expected);
29+
});
30+
31+
it("only transpiles exported types", async () => {
32+
const result = await transpileString(`
33+
type NotExported = number;
34+
const notExported: NotExported = 42;
35+
export type Exported = number;
36+
export const exported: Exported = 42;
37+
`);
38+
expect(result).not.toContain("NotExported");
39+
expect(result).not.toContain("exported");
40+
expect(result).toContain("Exported = float");
41+
});
42+
});
43+
44+
export type T = Record<string, string>;

src/testing/dicts.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { transpileString } from "./utils";
2+
3+
describe("transpiling dictionaries types", () => {
4+
it("can transpile dicts", async () => {
5+
const result = await transpileString(`export type A = {
6+
foo: string,
7+
bar: number,
8+
}`);
9+
expect(result).toContain(`class A(TypedDict):\n foo: str\n bar: float`);
10+
});
11+
12+
it("keeps docstrings", async () => {
13+
const result = await transpileString(`
14+
/** This is A */
15+
export type A = {
16+
/** this is foo */
17+
foo: string,
18+
/** this is bar */
19+
bar: number,
20+
}
21+
`);
22+
expect(result).toContain(
23+
`class A(TypedDict):
24+
"""
25+
This is A
26+
"""
27+
foo: str
28+
"""
29+
this is foo
30+
"""
31+
bar: float
32+
"""
33+
this is bar
34+
"""`,
35+
);
36+
});
37+
38+
it("can transpile nested dicts", async () => {
39+
const result = await transpileString(`export type A = {
40+
outer: {
41+
inner: string
42+
},
43+
extra: number,
44+
}`);
45+
expect(result).toContain(
46+
`class __HelperType1__(TypedDict):
47+
inner: str
48+
49+
class A(TypedDict):
50+
outer: __HelperType1__
51+
extra: float`,
52+
);
53+
});
54+
});
55+
56+
export type T = Record<string, string>;

src/testing/imports.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { typeScriptToPython } from "../typeScriptToPython";
2+
import { createProject, ts } from "@ts-morph/bootstrap";
3+
4+
describe("transpiling referenced types", () => {
5+
it("can refer to types defined in other files", async () => {
6+
const project = await createProject();
7+
8+
project.createSourceFile("foo.ts", `export type Foo = { foo: number }`);
9+
const barSource = project.createSourceFile(
10+
"bar.ts",
11+
`import {Foo} from './foo';\nexport type Bar = {foo: Foo}`,
12+
);
13+
14+
const program = project.createProgram();
15+
const diagnostics = ts.getPreEmitDiagnostics(program);
16+
17+
if (diagnostics.length > 0) {
18+
throw new Error(
19+
`code compiled with errors: ${project.formatDiagnosticsWithColorAndContext(
20+
diagnostics,
21+
)}`,
22+
);
23+
}
24+
25+
const transpiled = typeScriptToPython(program.getTypeChecker(), [
26+
barSource,
27+
]);
28+
expect(transpiled).toEqual(
29+
`from typing_extensions import Literal, TypedDict, List, Union, NotRequired, Optional, Tuple, Dict, Any
30+
31+
class __HelperType1__Foo(TypedDict):
32+
foo: float
33+
34+
class Bar(TypedDict):
35+
foo: __HelperType1__Foo`,
36+
);
37+
});
38+
});
39+
40+
export type T = Record<string, string>;

src/testing/reference.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { transpileString } from "./utils";
2+
3+
describe("transpiling referenced types", () => {
4+
it("can refer to previously defined types", async () => {
5+
const result = await transpileString(`
6+
type A = { foo: number }
7+
type B = A | { [key: string]: boolean } | { bar: string }
8+
export type C = { flat: number, outer: B }
9+
`);
10+
expect(result).toEqual(
11+
`from typing_extensions import Literal, TypedDict, List, Union, NotRequired, Optional, Tuple, Dict, Any
12+
13+
class __HelperType1__A(TypedDict):
14+
foo: float
15+
16+
class __HelperType2__(TypedDict):
17+
bar: str
18+
19+
class C(TypedDict):
20+
flat: float
21+
outer: Union[__HelperType1__A,Dict[str,bool],__HelperType2__]`,
22+
);
23+
});
24+
});
25+
26+
export type T = Record<string, string>;

src/testing/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { typeScriptToPython } from "../typeScriptToPython";
2+
import { createProject, ts } from "@ts-morph/bootstrap";
3+
4+
export const transpileString = async (code: string) => {
5+
const project = await createProject();
6+
const fileName = "test.ts";
7+
8+
const sourceFile = project.createSourceFile(fileName, code);
9+
const program = project.createProgram();
10+
const diagnostics = ts.getPreEmitDiagnostics(program);
11+
12+
if (diagnostics.length > 0) {
13+
throw new Error(
14+
`code compiled with errors: ${project.formatDiagnosticsWithColorAndContext(
15+
diagnostics,
16+
)}`,
17+
);
18+
}
19+
20+
return typeScriptToPython(program.getTypeChecker(), [sourceFile]);
21+
};

0 commit comments

Comments
 (0)