diff --git a/examples/generate-python-complete-models/__snapshots__/index.spec.ts.snap b/examples/generate-python-complete-models/__snapshots__/index.spec.ts.snap index b65fc34153..6dc7a78ecf 100644 --- a/examples/generate-python-complete-models/__snapshots__/index.spec.ts.snap +++ b/examples/generate-python-complete-models/__snapshots__/index.spec.ts.snap @@ -6,9 +6,9 @@ Array [ from typing import Any, Dict class ObjProperty: def __init__(self, input: Dict): - if hasattr(input, 'number'): + if 'number' in input: self._number: float = input['number'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any] = input['additional_properties'] @property @@ -34,9 +34,9 @@ Array [ from typing import Any, Dict class ObjProperty: def __init__(self, input: Dict): - if hasattr(input, 'number'): + if 'number' in input: self._number: float = input['number'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any] = input['additional_properties'] @property @@ -62,9 +62,9 @@ Array [ from typing import Any, Dict class Root: def __init__(self, input: Dict): - if hasattr(input, 'email'): + if 'email' in input: self._email: str = input['email'] - if hasattr(input, 'obj_property'): + if 'obj_property' in input: self._obj_property: ObjProperty = ObjProperty(input['obj_property']) @property @@ -90,9 +90,9 @@ Array [ from typing import Any, Dict class Root: def __init__(self, input: Dict): - if hasattr(input, 'email'): + if 'email' in input: self._email: str = input['email'] - if hasattr(input, 'obj_property'): + if 'obj_property' in input: self._obj_property: ObjProperty = ObjProperty(input['obj_property']) @property diff --git a/examples/generate-python-models/__snapshots__/index.spec.ts.snap b/examples/generate-python-models/__snapshots__/index.spec.ts.snap index d417a91f92..bf629a654a 100644 --- a/examples/generate-python-models/__snapshots__/index.spec.ts.snap +++ b/examples/generate-python-models/__snapshots__/index.spec.ts.snap @@ -4,7 +4,7 @@ exports[`Should be able to render python models and should log expected output t Array [ "class Root: def __init__(self, input: Dict): - if hasattr(input, 'email'): + if 'email' in input: self._email: str = input['email'] @property diff --git a/examples/python-generate-json-serializer-and-deserializer/__snapshots__/index.spec.ts.snap b/examples/python-generate-json-serializer-and-deserializer/__snapshots__/index.spec.ts.snap index 30113f1a8f..a64ad85d29 100644 --- a/examples/python-generate-json-serializer-and-deserializer/__snapshots__/index.spec.ts.snap +++ b/examples/python-generate-json-serializer-and-deserializer/__snapshots__/index.spec.ts.snap @@ -4,9 +4,9 @@ exports[`Should be able to render JSON serialization and deserialization functio Array [ "class Root: def __init__(self, input: Dict): - if hasattr(input, 'email'): + if 'email' in input: self._email: str = input['email'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any] = input['additional_properties'] @property diff --git a/src/generators/python/renderers/ClassRenderer.ts b/src/generators/python/renderers/ClassRenderer.ts index 326b758f03..50490e3dc8 100644 --- a/src/generators/python/renderers/ClassRenderer.ts +++ b/src/generators/python/renderers/ClassRenderer.ts @@ -1,8 +1,13 @@ import { PythonRenderer } from '../PythonRenderer'; import { + ConstrainedArrayModel, + ConstrainedDictionaryModel, + ConstrainedMetaModel, ConstrainedObjectModel, ConstrainedObjectPropertyModel, - ConstrainedReferenceModel + ConstrainedReferenceModel, + ConstrainedTupleModel, + ConstrainedUnionModel } from '../../../models'; import { PythonOptions } from '../PythonGenerator'; import { ClassPresetType } from '../PythonPreset'; @@ -72,6 +77,72 @@ ${this.indent(this.renderBlock(content, 2))} runSetterPreset(property: ConstrainedObjectPropertyModel): Promise { return this.runPreset('setter', { property }); } + + /** + * Return forward reference types (`Address` becomes `'Address'`) when it resolves around self-references that are + * either a direct dependency or part of another model such as array, union, dictionary and tuple. + * + * Otherwise just return the provided propertyType as is. + */ + returnPropertyType({ + model, + property, + propertyType, + visitedMap = [] + }: { + model: ConstrainedMetaModel; + property: ConstrainedMetaModel; + propertyType: string; + visitedMap?: string[]; + }): string { + if (visitedMap.includes(property.name)) { + return propertyType; + } + visitedMap.push(property.name); + const isSelfReference = + property instanceof ConstrainedReferenceModel && property.ref === model; + if (isSelfReference) { + // Use forward references for getters and setters + return propertyType.replace(property.ref.type, `'${property.ref.type}'`); + } else if (property instanceof ConstrainedArrayModel) { + return this.returnPropertyType({ + model, + property: property.valueModel, + propertyType, + visitedMap + }); + } else if (property instanceof ConstrainedTupleModel) { + let newPropType = propertyType; + for (const tupl of property.tuple) { + newPropType = this.returnPropertyType({ + model, + property: tupl.value, + propertyType, + visitedMap + }); + } + return newPropType; + } else if (property instanceof ConstrainedUnionModel) { + let newPropType = propertyType; + for (const unionModel of property.union) { + newPropType = this.returnPropertyType({ + model, + property: unionModel, + propertyType, + visitedMap + }); + } + return newPropType; + } else if (property instanceof ConstrainedDictionaryModel) { + return this.returnPropertyType({ + model, + property: property.value, + propertyType, + visitedMap + }); + } + return propertyType; + } } export const PYTHON_DEFAULT_CLASS_PRESET: ClassPresetType = { @@ -83,17 +154,18 @@ export const PYTHON_DEFAULT_CLASS_PRESET: ClassPresetType = { let body = ''; if (Object.keys(properties).length > 0) { const assignments = Object.values(properties).map((property) => { + const propertyType = property.property.type; if (property.property.options.const) { - return `self._${property.propertyName}: ${property.property.type} = ${property.property.options.const.value}`; + return `self._${property.propertyName}: ${propertyType} = ${property.property.options.const.value}`; } let assignment: string; if (property.property instanceof ConstrainedReferenceModel) { - assignment = `self._${property.propertyName}: ${property.property.type} = ${property.property.type}(input['${property.propertyName}'])`; + assignment = `self._${property.propertyName}: ${propertyType} = ${propertyType}(input['${property.propertyName}'])`; } else { - assignment = `self._${property.propertyName}: ${property.property.type} = input['${property.propertyName}']`; + assignment = `self._${property.propertyName}: ${propertyType} = input['${property.propertyName}']`; } if (!property.required) { - return `if hasattr(input, '${property.propertyName}'): + return `if '${property.propertyName}' in input: ${renderer.indent(assignment, 2)}`; } return assignment; @@ -108,20 +180,30 @@ No properties return `def __init__(self, input: Dict): ${renderer.indent(body, 2)}`; }, - getter({ property, renderer }) { + getter({ property, renderer, model }) { + const propertyType = renderer.returnPropertyType({ + model, + property: property.property, + propertyType: property.property.type + }); const propAssignment = `return self._${property.propertyName}`; return `@property -def ${property.propertyName}(self) -> ${property.property.type}: +def ${property.propertyName}(self) -> ${propertyType}: ${renderer.indent(propAssignment, 2)}`; }, - setter({ property, renderer }) { + setter({ property, renderer, model }) { // if const value exists we should not render a setter if (property.property.options.const?.value) { return ''; } + const propertyType = renderer.returnPropertyType({ + model, + property: property.property, + propertyType: property.property.type + }); const propAssignment = `self._${property.propertyName} = ${property.propertyName}`; - const propArgument = `${property.propertyName}: ${property.property.type}`; + const propArgument = `${property.propertyName}: ${propertyType}`; return `@${property.propertyName}.setter def ${property.propertyName}(self, ${propArgument}): diff --git a/test/generators/python/PythonGenerator.spec.ts b/test/generators/python/PythonGenerator.spec.ts index e9dee10f67..e183102e8b 100644 --- a/test/generators/python/PythonGenerator.spec.ts +++ b/test/generators/python/PythonGenerator.spec.ts @@ -81,6 +81,34 @@ describe('PythonGenerator', () => { expect(models).toHaveLength(1); expect(models[0].result).toMatchSnapshot(); }); + test('should handle self reference models', async () => { + const doc = { + $id: 'Address', + type: 'object', + properties: { + self_model: { $ref: '#' }, + array_model: { type: 'array', items: { $ref: '#' } }, + tuple_model: { + type: 'array', + items: [{ $ref: '#' }], + additionalItems: false + }, + map_model: { + type: 'object', + additionalProperties: { + $ref: '#' + } + }, + union_model: { + oneOf: [{ $ref: '#' }] + } + }, + additionalProperties: false + }; + const models = await generator.generate(doc); + expect(models).toHaveLength(1); + expect(models[0].result).toMatchSnapshot(); + }); test('should render `class` type', async () => { const doc = { $id: 'Address', diff --git a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap index be589e3ed0..545f57d6a4 100644 --- a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap +++ b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap @@ -1,9 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`PythonGenerator Class should handle self reference models 1`] = ` +"class Address: + def __init__(self, input: Dict): + if 'self_model' in input: + self._self_model: Address = Address(input['self_model']) + if 'array_model' in input: + self._array_model: List[Address] = input['array_model'] + if 'tuple_model' in input: + self._tuple_model: tuple[Address] = input['tuple_model'] + if 'map_model' in input: + self._map_model: dict[str, Address] = input['map_model'] + if 'union_model' in input: + self._union_model: Address = input['union_model'] + + @property + def self_model(self) -> 'Address': + return self._self_model + @self_model.setter + def self_model(self, self_model: 'Address'): + self._self_model = self_model + + @property + def array_model(self) -> List['Address']: + return self._array_model + @array_model.setter + def array_model(self, array_model: List['Address']): + self._array_model = array_model + + @property + def tuple_model(self) -> tuple['Address']: + return self._tuple_model + @tuple_model.setter + def tuple_model(self, tuple_model: tuple['Address']): + self._tuple_model = tuple_model + + @property + def map_model(self) -> dict[str, 'Address']: + return self._map_model + @map_model.setter + def map_model(self, map_model: dict[str, 'Address']): + self._map_model = map_model + + @property + def union_model(self) -> 'Address': + return self._union_model + @union_model.setter + def union_model(self, union_model: 'Address'): + self._union_model = union_model +" +`; + exports[`PythonGenerator Class should not render reserved keyword 1`] = ` "class Address: def __init__(self, input: Dict): - if hasattr(input, 'reserved_del'): + if 'reserved_del' in input: self._reserved_del: str = input['reserved_del'] @property @@ -22,12 +73,12 @@ exports[`PythonGenerator Class should render \`class\` type 1`] = ` self._city: str = input['city'] self._state: str = input['state'] self._house_number: float = input['house_number'] - if hasattr(input, 'marriage'): + if 'marriage' in input: self._marriage: bool = input['marriage'] - if hasattr(input, 'members'): + if 'members' in input: self._members: str | float | bool = input['members'] self._array_type: List[str | float | Any] = input['array_type'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any | str] = input['additional_properties'] @property @@ -107,9 +158,9 @@ exports[`PythonGenerator Class should work with custom preset for \`class\` type def __init__(self, input: Dict): - if hasattr(input, 'property'): + if 'property' in input: self._property: str = input['property'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any] = input['additional_properties'] test2 diff --git a/test/generators/python/presets/__snapshots__/JsonSerializer.spec.ts.snap b/test/generators/python/presets/__snapshots__/JsonSerializer.spec.ts.snap index b337ade90b..e71260fa49 100644 --- a/test/generators/python/presets/__snapshots__/JsonSerializer.spec.ts.snap +++ b/test/generators/python/presets/__snapshots__/JsonSerializer.spec.ts.snap @@ -3,9 +3,9 @@ exports[`PYTHON_JSON_SERIALIZER_PRESET should render serializer and deserializer for class 1`] = ` "class Test: def __init__(self, input: Dict): - if hasattr(input, 'prop'): + if 'prop' in input: self._prop: str = input['prop'] - if hasattr(input, 'additional_properties'): + if 'additional_properties' in input: self._additional_properties: dict[str, Any] = input['additional_properties'] @property