Skip to content

Commit

Permalink
fix: python self referencing property types not using forward referen…
Browse files Browse the repository at this point in the history
…ce (#1893)
  • Loading branch information
jonaslagoni committed Mar 26, 2024
1 parent fcf4b7c commit 7d69b70
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 91 additions & 9 deletions src/generators/python/renderers/ClassRenderer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -72,6 +77,72 @@ ${this.indent(this.renderBlock(content, 2))}
runSetterPreset(property: ConstrainedObjectPropertyModel): Promise<string> {
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<PythonOptions> = {
Expand All @@ -83,17 +154,18 @@ export const PYTHON_DEFAULT_CLASS_PRESET: ClassPresetType<PythonOptions> = {
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;
Expand All @@ -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}):
Expand Down
28 changes: 28 additions & 0 deletions test/generators/python/PythonGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
63 changes: 57 additions & 6 deletions test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7d69b70

Please sign in to comment.