diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 20d5fc5aa8..59c6bd481d 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -334,12 +334,20 @@ export class JSONPythonRenderer extends PythonRenderer { ); } + protected typingTypeHint(tvar: Sourcelike): Sourcelike { + if (!this.pyOptions.features.typingType) { + return []; + } + + return this.typeHint(": ", this.withTyping("Type"), "[", tvar, "]"); + } + protected emitToEnumConverter(): void { const tvar = this.enumTypeVar(); this.emitBlock( [ "def to_enum(c", - this.typeHint(": ", this.withTyping("Type"), "[", tvar, "]"), + this.typingTypeHint(tvar), ", ", this.typingDecl("x", "Any"), ")", @@ -393,7 +401,7 @@ export class JSONPythonRenderer extends PythonRenderer { this.emitBlock( [ "def to_class(c", - this.typeHint(": ", this.withTyping("Type"), "[", tvar, "]"), + this.typingTypeHint(tvar), ", ", this.typingDecl("x", "Any"), ")", @@ -500,7 +508,7 @@ export class JSONPythonRenderer extends PythonRenderer { this.emitBlock( [ "def is_type(t", - this.typeHint(": ", this.withTyping("Type"), "[", tvar, "]"), + this.typingTypeHint(tvar), ", ", this.typingDecl("x", "Any"), ")", diff --git a/packages/quicktype-core/src/language/Python/language.ts b/packages/quicktype-core/src/language/Python/language.ts index 263026b7e5..42ae31ea3c 100644 --- a/packages/quicktype-core/src/language/Python/language.ts +++ b/packages/quicktype-core/src/language/Python/language.ts @@ -25,6 +25,8 @@ export interface PythonFeatures { builtinGenerics: boolean; dataClasses: boolean; typeHints: boolean; + /** `typing.Type`, unavailable in Python 3.6.0 */ + typingType: boolean; /** PEP 604 union operators (`str | None`), Python 3.10+ */ unionOperators: boolean; } @@ -36,30 +38,35 @@ export const pythonOptions = { { "3.5": { typeHints: false, + typingType: false, dataClasses: false, builtinGenerics: false, unionOperators: false, }, "3.6": { typeHints: true, + typingType: false, dataClasses: false, builtinGenerics: false, unionOperators: false, }, "3.7": { typeHints: true, + typingType: true, dataClasses: true, builtinGenerics: false, unionOperators: false, }, "3.9": { typeHints: true, + typingType: true, dataClasses: true, builtinGenerics: true, unionOperators: false, }, "3.10": { typeHints: true, + typingType: true, dataClasses: true, builtinGenerics: true, unionOperators: true, diff --git a/test/unit/python-typing-type.test.ts b/test/unit/python-typing-type.test.ts new file mode 100644 index 0000000000..110d6fe1a6 --- /dev/null +++ b/test/unit/python-typing-type.test.ts @@ -0,0 +1,79 @@ +// Python fixtures exercise every version preset, but cannot verify that the +// generated imports also work on the first Python 3.6 micro-version. +import { describe, expect, test } from "vitest"; + +import { + InputData, + JSONSchemaInput, + jsonInputForTargetLanguage, + quicktype, +} from "quicktype-core"; + +const schema = { + type: "object", + properties: { + child: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + status: { type: "string", enum: ["ready", "done"] }, + value: { oneOf: [{ type: "integer" }, { type: "string" }] }, + }, + required: ["child", "status", "value"], +}; + +async function pythonFor(version: "3.6" | "3.7"): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "TopLevel", + schema: JSON.stringify(schema), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + const schemaResult = await quicktype({ + inputData, + lang: "python", + rendererOptions: { "python-version": version }, + }); + + // An inferred integer-string union exercises the is_type helper. + const jsonInput = jsonInputForTargetLanguage("python"); + await jsonInput.addSource({ + name: "MixedValues", + samples: ['{"mixed":[null,1,"1",{}]}'], + }); + const jsonInputData = new InputData(); + jsonInputData.addInput(jsonInput); + const jsonResult = await quicktype({ + inputData: jsonInputData, + lang: "python", + rendererOptions: { "python-version": version }, + }); + + return [...schemaResult.lines, ...jsonResult.lines].join("\n"); +} + +describe("Python typing.Type compatibility (issue #1728)", () => { + test("Python 3.6 does not import or use typing.Type", async () => { + const output = await pythonFor("3.6"); + + expect(output).toContain('T = TypeVar("T")'); + expect(output).not.toMatch(/from typing import [^\n]*\bType\b/); + expect(output).not.toMatch(/\bType\[/); + expect(output).toContain("def to_class(c, x: Any) -> dict:"); + expect(output).toContain("def to_enum(c, x: Any) -> EnumT:"); + expect(output).toContain("def is_type(t, x: Any) -> T:"); + }); + + test("Python 3.7 keeps typing.Type annotations", async () => { + const output = await pythonFor("3.7"); + + expect(output).toMatch(/from typing import [^\n]*\bType\b/); + expect(output).toContain("def to_class(c: Type[T], x: Any) -> dict:"); + expect(output).toContain( + "def to_enum(c: Type[EnumT], x: Any) -> EnumT:", + ); + expect(output).toContain("def is_type(t: Type[T], x: Any) -> T:"); + }); +});