From c62048aa0e31fdade7c0ca6d00ee443f6c1df0f2 Mon Sep 17 00:00:00 2001 From: Lars Melchior Date: Tue, 21 Jan 2025 17:03:59 +0100 Subject: [PATCH 1/3] add --strict option --- README.md | 4 ++++ src/index.ts | 6 ++++-- src/testing/basic.test.ts | 14 ++++++++++++++ src/testing/dicts.test.ts | 2 +- src/testing/utils.ts | 9 +++++++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7624a37..c19e31e 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ TypeScript2Python supports many of TypeScripts type constructs, including: ## Transpiler options +### Strict + +Use the `--strict` flag to enable all strict type-checking options to ensure `undefined` and `null` properties are not ignored during transpilation. + ### Nullable optionals In TypeScript objects, optional values can also be set to `undefined`. By default we assume the according Python diff --git a/src/index.ts b/src/index.ts index 2a76405..8477e36 100755 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,15 @@ import path from "path"; import { program } from "@commander-js/extra-typings"; import { typeScriptToPython } from "./typeScriptToPython"; import { Ts2PyConfig } from "./config"; +import { readFileSync } from "fs"; -const compile = (fileNames: string[], config: Ts2PyConfig) => { +const compile = (fileNames: string[], config: Ts2PyConfig & {strict?: boolean}) => { const program = ts.createProgram(fileNames, { noEmit: true, allowJs: true, resolveJsonModule: true, skipLibCheck: true, + strict: config.strict, }); const relevantSourceFiles = program @@ -21,7 +23,6 @@ const compile = (fileNames: string[], config: Ts2PyConfig) => { .map((fn) => path.relative(fn, f.fileName) === "") .reduce((a, b) => a || b), ); - const transpiled = typeScriptToPython(program.getTypeChecker(), relevantSourceFiles, config) console.log(transpiled); } @@ -30,6 +31,7 @@ program .name("typescript2python") .description("A program that converts TypeScript type definitions to Python") .option("--nullable-optionals", "if set, optional entries in dictionaries will be nullable, e.g. `NotRequired[Optional[T]]`") + .option("--strict", "Enable all strict type-checking options.") .arguments("") .action((args, options) => { compile(args, options) diff --git a/src/testing/basic.test.ts b/src/testing/basic.test.ts index eb7ebdc..26edbab 100644 --- a/src/testing/basic.test.ts +++ b/src/testing/basic.test.ts @@ -62,6 +62,20 @@ describe("transpiling basic types", () => { expect(result).toEqual(expected); }); + it.each([ + [ + "export type T = number | undefined", + "from typing_extensions import Union\n\nT = Union[None,float]", + ], + [ + "export type T = number | null", + "from typing_extensions import Union\n\nT = Union[None,float]", + ], + ])("transpiles %p to %p when strict", async (input, expected) => { + const result = await transpileString(input, {}, { strict: true }); + expect(result).toEqual(expected); + }); + it("only transpiles exported types", async () => { const result = await transpileString(` type NotExported = number; diff --git a/src/testing/dicts.test.ts b/src/testing/dicts.test.ts index 73279b3..7438a1a 100644 --- a/src/testing/dicts.test.ts +++ b/src/testing/dicts.test.ts @@ -64,7 +64,7 @@ class A(TypedDict): expect(result).toContain(`class A(TypedDict):\n foo: NotRequired[str]`); }); - it("transpiles optional values with non-null optionals as NotRequired[T]", async () => { + it.only("transpiles optional values with non-null optionals as NotRequired[T]", async () => { const result = await transpileString(`export type A = { foo?: string }`, { nullableOptionals: true, }); diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 2eb5503..e9bda7e 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -7,10 +7,12 @@ import { createProject, ts } from "@ts-morph/bootstrap"; * transpiling multiple files. **/ let globalProject: ReturnType | undefined; +let i = 0; export const transpileString = async ( code: string, config: Ts2PyConfig = {}, + compilerOptions: ts.CompilerOptions = {}, ) => { if (globalProject === undefined) { globalProject = createProject({ @@ -19,11 +21,14 @@ export const transpileString = async ( } const project = await globalProject; - const fileName = `source.ts`; + const fileName = `source${i++}.ts`; // instead of adding a new source file for each program, we update the existing one. const sourceFile = project.updateSourceFile(fileName, code); - const program = project.createProgram(); + const program = project.createProgram({ + rootNames: [fileName], + options: { ...project.compilerOptions, ...compilerOptions }, + }); const diagnostics = ts.getPreEmitDiagnostics(program); if (diagnostics.length > 0) { From 8fc43865a8537d95f93fce03cbc0779a060b150c Mon Sep 17 00:00:00 2001 From: Lars Melchior Date: Tue, 21 Jan 2025 17:06:35 +0100 Subject: [PATCH 2/3] add test case for non strict mode --- src/testing/basic.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/testing/basic.test.ts b/src/testing/basic.test.ts index 26edbab..c0cd21b 100644 --- a/src/testing/basic.test.ts +++ b/src/testing/basic.test.ts @@ -57,6 +57,11 @@ describe("transpiling basic types", () => { "export type T = number | string | Record", "from typing_extensions import Dict, Union\n\nT = Union[str,float,Dict[str,bool]]", ], + [ + "export type T = number | undefined", + // without strict mode the `undefined` gets lost here + "T = float", + ], ])("transpiles %p to %p", async (input, expected) => { const result = await transpileString(input); expect(result).toEqual(expected); From ac58384c0d525458fe742434511f491a3eaefde9 Mon Sep 17 00:00:00 2001 From: Lars Melchior Date: Tue, 21 Jan 2025 17:10:31 +0100 Subject: [PATCH 3/3] add docstring for source name counter --- src/testing/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/testing/utils.ts b/src/testing/utils.ts index e9bda7e..0b136e8 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -7,6 +7,8 @@ import { createProject, ts } from "@ts-morph/bootstrap"; * transpiling multiple files. **/ let globalProject: ReturnType | undefined; + +/** Each file should get a unique name to avoid issues. */ let i = 0; export const transpileString = async ( @@ -21,7 +23,7 @@ export const transpileString = async ( } const project = await globalProject; - const fileName = `source${i++}.ts`; + const fileName = `source_${i++}.ts`; // instead of adding a new source file for each program, we update the existing one. const sourceFile = project.updateSourceFile(fileName, code);