From 0ab3d72bbd258488956e050b24f6d80722dbbcfa Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:50:49 +0100 Subject: [PATCH 1/2] test(python): cover property names that from_dict uses `from_str` (with a string value), `isinstance` and `obj` join the keywords; `re`, `len` and `cast` are added as plain properties next to the `pattern`, `minItems` and `minLength` checks that call them. In Python's `from_dict` each becomes a local variable that shadows what it is named after. Co-Authored-By: Claude Opus 5.5 --- test/inputs/json/priority/keywords.json | 21 ++++++++++++--------- test/inputs/schema/keyword-enum.schema | 3 +++ test/inputs/schema/keyword-unions.schema | 21 +++++++++++++++++++++ test/inputs/schema/min-max-items.1.json | 1 + test/inputs/schema/min-max-items.schema | 3 +++ test/inputs/schema/minmaxlength.1.json | 1 + test/inputs/schema/minmaxlength.schema | 3 +++ test/inputs/schema/pattern.1.json | 1 + test/inputs/schema/pattern.schema | 3 +++ test/keywords.txt | 3 +++ 10 files changed, 51 insertions(+), 9 deletions(-) diff --git a/test/inputs/json/priority/keywords.json b/test/inputs/json/priority/keywords.json index 8956a74978..6056fee68a 100644 --- a/test/inputs/json/priority/keywords.json +++ b/test/inputs/json/priority/keywords.json @@ -130,10 +130,11 @@ "from": { "from": 123 }, "from_dict": { "from_dict": 123 }, "from_json": { "from_json": 123 }, - "func": { "func": 123 }, + "from_str": { "from_str": "converted" }, "dummy": 123 }, "obj3": { + "func": { "func": 123 }, "function": { "function": 123 }, "get": { "get": 123 }, "global": { "global": 123 }, @@ -161,6 +162,7 @@ "IntNode": { "IntNode": 123 }, "interface": { "interface": 123 }, "internal": { "internal": 123 }, + "isinstance": { "isinstance": 123 }, "IsoDateTimeOffsetConverter": { "IsoDateTimeOffsetConverter": 123 }, "iterable": { "iterable": 123 }, "is": { "is": 123 }, @@ -196,11 +198,11 @@ "map": { "map": 123 }, "MarshalJSON": { "MarshalJSON": 123 }, "MapEntry": { "MapEntry": 123 }, - "metadata_property_handling": { "metadata_property_handling": 123 }, - "module": { "module": 123 }, "dummy": 123 }, "obj4": { + "metadata_property_handling": { "metadata_property_handling": 123 }, + "module": { "module": 123 }, "mutable": { "mutable": 123 }, "mutating": { "mutating": 123 }, "namespace": { "namespace": 123 }, @@ -226,6 +228,7 @@ "null": { "null": 123 }, "nullptr": { "nullptr": 123 }, "number": { "number": 123 }, + "obj": { "obj": 123 }, "object": { "object": 123 }, "OffsetDateTime": { "OffsetDateTime": 123 }, "OffsetTime": { "OffsetTime": 123 }, @@ -262,12 +265,12 @@ "range": { "range": 123 }, "readonly": { "readonly": 123 }, "ref": { "ref": 123 }, - "RegExp": { "RegExp": "550e8400-e29b-41d4-a716-446655440000" }, - "register": { "register": 123 }, - "reinterpret_cast": { "reinterpret_cast": 123 }, "dummy": 123 }, "obj5": { + "RegExp": { "RegExp": "550e8400-e29b-41d4-a716-446655440000" }, + "register": { "register": 123 }, + "reinterpret_cast": { "reinterpret_cast": 123 }, "repeat": { "repeat": 123 }, "require": { "require": 123 }, "required": { "required": 123 }, @@ -329,12 +332,12 @@ "top_level": { "top_level": 123 }, "toString": { "toString": 123 }, "to_dict": { "to_dict": 123 }, - "transient": { "transient": 123 }, - "True": { "True": 123 }, - "true": { "true": 123 }, "dummy": 123 }, "obj6": { + "transient": { "transient": 123 }, + "True": { "True": 123 }, + "true": { "true": 123 }, "try": { "try": 123 }, "Type": { "Type": 123 }, "type": { "type": 123 }, diff --git a/test/inputs/schema/keyword-enum.schema b/test/inputs/schema/keyword-enum.schema index ed5a54c698..eac0de8740 100644 --- a/test/inputs/schema/keyword-enum.schema +++ b/test/inputs/schema/keyword-enum.schema @@ -131,6 +131,7 @@ "from", "from_dict", "from_json", + "from_str", "func", "function", "get", @@ -159,6 +160,7 @@ "IntNode", "interface", "internal", + "isinstance", "IsoDateTimeOffsetConverter", "iterable", "is", @@ -221,6 +223,7 @@ "null", "nullptr", "number", + "obj", "object", "OffsetDateTime", "OffsetTime", diff --git a/test/inputs/schema/keyword-unions.schema b/test/inputs/schema/keyword-unions.schema index c9fb819790..86e1c75c7b 100644 --- a/test/inputs/schema/keyword-unions.schema +++ b/test/inputs/schema/keyword-unions.schema @@ -890,6 +890,13 @@ ], "title": "union_from_json" }, + "from_str": { + "oneOf": [ + { "type": "number" }, + { "type": "object", "additionalProperties": false, "title": "from_str" } + ], + "title": "union_from_str" + }, "func": { "oneOf": [ { "type": "number" }, @@ -1086,6 +1093,13 @@ ], "title": "union_internal" }, + "isinstance": { + "oneOf": [ + { "type": "number" }, + { "type": "object", "additionalProperties": false, "title": "isinstance" } + ], + "title": "union_isinstance" + }, "IsoDateTimeOffsetConverter": { "oneOf": [ { "type": "number" }, @@ -1520,6 +1534,13 @@ ], "title": "union_number" }, + "obj": { + "oneOf": [ + { "type": "number" }, + { "type": "object", "additionalProperties": false, "title": "obj" } + ], + "title": "union_obj" + }, "object": { "oneOf": [ { "type": "number" }, diff --git a/test/inputs/schema/min-max-items.1.json b/test/inputs/schema/min-max-items.1.json index 0d5c53a959..d93908bf06 100644 --- a/test/inputs/schema/min-max-items.1.json +++ b/test/inputs/schema/min-max-items.1.json @@ -1,4 +1,5 @@ { + "len": "x", "minOnly": ["one", "two"], "maxOnly": [1, 2, 3], "minAndMax": [1.25, 2.5], diff --git a/test/inputs/schema/min-max-items.schema b/test/inputs/schema/min-max-items.schema index 7aaf9e8092..213dbb96d7 100644 --- a/test/inputs/schema/min-max-items.schema +++ b/test/inputs/schema/min-max-items.schema @@ -1,6 +1,9 @@ { "type": "object", "properties": { + "len": { + "type": "string" + }, "minOnly": { "type": "array", "items": { diff --git a/test/inputs/schema/minmaxlength.1.json b/test/inputs/schema/minmaxlength.1.json index b9ea9aba78..5a75c5455d 100644 --- a/test/inputs/schema/minmaxlength.1.json +++ b/test/inputs/schema/minmaxlength.1.json @@ -1,4 +1,5 @@ { + "cast": "x", "minlength": "aaaa", "maxlength": "bbb", "minmaxlength": "cccc", diff --git a/test/inputs/schema/minmaxlength.schema b/test/inputs/schema/minmaxlength.schema index 147e5e0e5d..f68dab4a39 100644 --- a/test/inputs/schema/minmaxlength.schema +++ b/test/inputs/schema/minmaxlength.schema @@ -1,6 +1,9 @@ { "type": "object", "properties": { + "cast": { + "type": "string" + }, "minlength": { "type": "string", "minLength": 3 diff --git a/test/inputs/schema/pattern.1.json b/test/inputs/schema/pattern.1.json index 7a82d4ecab..a1b9d25977 100644 --- a/test/inputs/schema/pattern.1.json +++ b/test/inputs/schema/pattern.1.json @@ -1,4 +1,5 @@ { + "re": "x", "pattern1": "aa", "pattern2": "ba", "union": "ccc" diff --git a/test/inputs/schema/pattern.schema b/test/inputs/schema/pattern.schema index 3e7610a6a7..77d1aa6f0d 100644 --- a/test/inputs/schema/pattern.schema +++ b/test/inputs/schema/pattern.schema @@ -1,6 +1,9 @@ { "type": "object", "properties": { + "re": { + "type": "string" + }, "pattern1": { "type": "string", "pattern": "a[.]*" diff --git a/test/keywords.txt b/test/keywords.txt index af0cc55145..c02d939fb4 100644 --- a/test/keywords.txt +++ b/test/keywords.txt @@ -125,6 +125,7 @@ friend from from_dict from_json +from_str "converted" func function get @@ -153,6 +154,7 @@ int IntNode interface internal +isinstance IsoDateTimeOffsetConverter iterable is @@ -215,6 +217,7 @@ NULL null nullptr number +obj object OffsetDateTime OffsetTime From 55091f608af974c80ab881156da9f3f37a37dd71 Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:50:49 +0100 Subject: [PATCH 2/2] fix(python): reserve names that from_dict uses `from_dict` assigns each property to a local variable of the property's name, so a property named like something `from_dict` uses shadowed it for the whole function: a converter it calls (`from_date` in `misc/a0496.json` gave `UnboundLocalError`, and mypy `"date" not callable`), its parameter `obj`, `isinstance`, or what the constraint checks call (`len`, `re`, `cast`). Reserve those names for properties. The converters come from a `Record` of the ones `from_dict` calls, named as `conv` names them, so a new converter cannot be missed; names only `to_dict` uses (`to_class`, `to_enum`, `to_float`, `is_type`) are not reserved. Co-Authored-By: Claude Opus 5.5 --- .../src/language/Python/JSONPythonRenderer.ts | 105 +++++++++++++++--- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts index 738748a087..9c6458d293 100644 --- a/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts +++ b/packages/quicktype-core/src/language/Python/JSONPythonRenderer.ts @@ -52,6 +52,42 @@ export type ConverterFunction = | "from-stringified-bool" | "is-type"; +// The name of a converter function in the generated code. +function converterName(cf: ConverterFunction): string { + const name = cf.replace(/-/g, "_"); + if (cf.startsWith("from-") || cf.startsWith("to-") || cf.startsWith("is-")) + return name; + return `from_${name}`; +} + +// What the constraint checks of `from_dict` call: the built-in `len`, the +// module `re` and `typing.cast`. +const lenFunction = "len"; +const reModule = "re"; +const castFunction = "cast"; + +// Whether `from_dict` may call a converter function: the deserializers +// and the decoding transformers' converters. The others only `to_dict` +// calls. +const calledByFromDict: Record = { + none: true, + bool: true, + int: true, + "from-float": true, + "to-float": false, + str: true, + "to-enum": false, + list: true, + "to-class": false, + dict: true, + union: true, + "from-date": true, + "from-time": true, + "from-datetime": true, + "from-stringified-bool": true, + "is-type": false, +}; + interface TopLevelConverterNames { fromDict: Name; toDict: Name; @@ -197,7 +233,27 @@ export class JSONPythonRenderer extends PythonRenderer { protected forbiddenForObjectProperties(c: ClassType, className: Name) { const result = super.forbiddenForObjectProperties(c, className); - result.names = result.names.concat(["from_dict", "to_dict"]); + // `from_dict` assigns each property to a local variable of the + // property's name, so no property may be named like something + // `from_dict` uses: its parameter, `isinstance`, the converter + // functions it calls (`from_dict`, the "dict" converter, is + // already the method's name) and what its constraint checks call. + const fromDictConverters = ( + Object.keys(calledByFromDict) as ConverterFunction[] + ) + .filter((cf) => calledByFromDict[cf]) + .map(converterName) + .filter((name) => name !== "from_dict"); + result.names = result.names.concat([ + "from_dict", + "to_dict", + "obj", + "isinstance", + ...fromDictConverters, + lenFunction, + reModule, + castFunction, + ]); return result; } @@ -242,7 +298,7 @@ export class JSONPythonRenderer extends PythonRenderer { return v; } - return [this.withTyping("cast"), "(", type, ", ", v, ")"]; + return [this.withTyping(castFunction), "(", type, ", ", v, ")"]; } protected emitNoneConverter(): void { @@ -677,14 +733,7 @@ export class JSONPythonRenderer extends PythonRenderer { // Return the name of the Python converter function `cf`. protected conv(cf: ConverterFunction): Sourcelike { this._deserializerFunctions.add(cf); - const name = cf.replace(/-/g, "_"); - if ( - cf.startsWith("from-") || - cf.startsWith("to-") || - cf.startsWith("is-") - ) - return name; - return ["from_", name]; + return converterName(cf); } // Applies the converter function to `arg` @@ -1123,9 +1172,22 @@ export class JSONPythonRenderer extends PythonRenderer { condition, ); if (min !== undefined) - check([min.toString(), " <= len(", name, ")"]); + check([ + min.toString(), + " <= ", + lenFunction, + "(", + name, + ")", + ]); if (max !== undefined) - check(["len(", name, ") <= ", max.toString()]); + check([ + lenFunction, + "(", + name, + ") <= ", + max.toString(), + ]); const [minValue, maxValue] = minMaxValueForType(cp.type) ?? []; if (minValue !== undefined) @@ -1136,14 +1198,27 @@ export class JSONPythonRenderer extends PythonRenderer { if (lo !== undefined || hi !== undefined) { const raw = this.cast("str", property.value); if (lo !== undefined) - check([lo.toString(), " <= len(", raw, ")"]); + check([ + lo.toString(), + " <= ", + lenFunction, + "(", + raw, + ")", + ]); if (hi !== undefined) - check(["len(", raw, ") <= ", hi.toString()]); + check([ + lenFunction, + "(", + raw, + ") <= ", + hi.toString(), + ]); } const pattern = patternForType(cp.type); if (pattern !== undefined) check([ - this.withModuleImport("re"), + this.withModuleImport(reModule), ".search(", this.string(pattern), ", ",