Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pydantic_model_creator refactorization #1745

Merged
merged 28 commits into from
Nov 17, 2024

Conversation

markus-96
Copy link
Contributor

@markus-96 markus-96 commented Oct 22, 2024

The method pydantic_model_creator was very difficult to understand and therefore also very hard to maintain

Description

I have refactored to use a class-based approche. I also added some dataclasses to describe the Models and Fields so that typing is much more consistent.

Motivation and Context

I encountered many issues using pydantic_model_creator. The results are very unpredictable. For example: Why is MinRelation in tests.testmodels never referenced if you call pydantic_model_creator with any of Event, Tournament, Team, Address? Why should someone want a model to be named tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf?

How Has This Been Tested?

since nearly every current written test fails because of the changes how names are generated and which fields are included, it is hard to say that everything is working as intended.

Edit: It is now tested against the existing test, that needed to be updated to match the new naming scheme.

I have created some pydantic models and reviewed the changes with Meld:

here is the comparison old vs new
--- <unnamed>
+++ <unnamed>
@@ -1,6 +1,6 @@
 {
   "$defs": {
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf": {
+    "Address_obakhj_leaf": {
       "additionalProperties": false,
       "properties": {
         "city": {
@@ -28,7 +28,31 @@
       "title": "Address",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf": {
+    "MinRelation_tqv6dk_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "participants": {
+          "items": {
+            "$ref": "#/$defs/Team_yndv6t_leaf"
+          },
+          "title": "Participants",
+          "type": "array"
+        }
+      },
+      "required": [
+        "id",
+        "participants"
+      ],
+      "title": "MinRelation",
+      "type": "object"
+    },
+    "Reporter_mnvssl_leaf": {
       "additionalProperties": false,
       "description": "Whom is assigned as the reporter",
       "properties": {
@@ -50,7 +74,7 @@
       "title": "Reporter",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf": {
+    "Team_yndv6t_leaf": {
       "additionalProperties": false,
       "description": "Team that is a playing",
       "properties": {
@@ -87,7 +111,7 @@
       "title": "Team",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf": {
+    "Tournament_y6uxv3_leaf": {
       "additionalProperties": false,
       "properties": {
         "id": {
@@ -118,15 +142,53 @@
           "readOnly": true,
           "title": "Created",
           "type": "string"
+        },
+        "minrelations": {
+          "items": {
+            "$ref": "#/$defs/MinRelation_tqv6dk_leaf"
+          },
+          "title": "Minrelations",
+          "type": "array"
+        },
+        "uniquetogetherfieldswithfks": {
+          "items": {
+            "$ref": "#/$defs/UniqueTogetherFieldsWithFK_vj7nws_leaf"
+          },
+          "title": "Uniquetogetherfieldswithfks",
+          "type": "array"
         }
       },
       "required": [
         "id",
         "name",
         "desc",
-        "created"
+        "created",
+        "minrelations",
+        "uniquetogetherfieldswithfks"
       ],
       "title": "Tournament",
+      "type": "object"
+    },
+    "UniqueTogetherFieldsWithFK_vj7nws_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "text": {
+          "maxLength": 64,
+          "title": "Text",
+          "type": "string"
+        }
+      },
+      "required": [
+        "id",
+        "text"
+      ],
+      "title": "UniqueTogetherFieldsWithFK",
       "type": "object"
     }
   },
@@ -145,13 +207,13 @@
       "type": "string"
     },
     "tournament": {
-      "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf",
+      "$ref": "#/$defs/Tournament_y6uxv3_leaf",
       "description": "What tournaments is a happenin'"
     },
     "reporter": {
       "anyOf": [
         {
-          "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf"
+          "$ref": "#/$defs/Reporter_mnvssl_leaf"
         },
         {
           "type": "null"
@@ -162,7 +224,7 @@
     },
     "participants": {
       "items": {
-        "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf"
+        "$ref": "#/$defs/Team_yndv6t_leaf"
       },
       "title": "Participants",
       "type": "array"
@@ -201,7 +263,7 @@
     "address": {
       "anyOf": [
         {
-          "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf"
+          "$ref": "#/$defs/Address_obakhj_leaf"
         },
         {
           "type": "null"
@@ -227,15 +289,43 @@
 }
 {
   "$defs": {
-    "Event_bliobj": {
-      "additionalProperties": false,
-      "description": "Events on the calendar",
-      "properties": {
+    "Address_obakhj_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "city": {
+          "maxLength": 64,
+          "title": "City",
+          "type": "string"
+        },
+        "street": {
+          "maxLength": 128,
+          "title": "Street",
+          "type": "string"
+        },
         "event_id": {
           "maximum": 9223372036854775807,
           "minimum": -9223372036854775808,
           "title": "Event Id",
           "type": "integer"
+        }
+      },
+      "required": [
+        "city",
+        "street",
+        "event_id"
+      ],
+      "title": "Address",
+      "type": "object"
+    },
+    "Event_mrdcym": {
+      "additionalProperties": false,
+      "description": "Events on the calendar",
+      "properties": {
+        "event_id": {
+          "maximum": 9223372036854775807,
+          "minimum": -9223372036854775808,
+          "title": "Event Id",
+          "type": "integer"
         },
         "name": {
           "description": "The name",
@@ -243,13 +333,13 @@
           "type": "string"
         },
         "tournament": {
-          "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf",
+          "$ref": "#/$defs/Tournament_y6uxv3_leaf",
           "description": "What tournaments is a happenin'"
         },
         "reporter": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf"
+              "$ref": "#/$defs/Reporter_mnvssl_leaf"
             },
             {
               "type": "null"
@@ -260,7 +350,7 @@
         },
         "participants": {
           "items": {
-            "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf"
+            "$ref": "#/$defs/Team_yndv6t_leaf"
           },
           "title": "Participants",
           "type": "array"
@@ -299,7 +389,7 @@
         "address": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf"
+              "$ref": "#/$defs/Address_obakhj_leaf"
             },
             {
               "type": "null"
@@ -323,35 +413,31 @@
       "title": "Event",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf": {
-      "additionalProperties": false,
-      "properties": {
-        "city": {
-          "maxLength": 64,
-          "title": "City",
-          "type": "string"
-        },
-        "street": {
-          "maxLength": 128,
-          "title": "Street",
-          "type": "string"
-        },
-        "event_id": {
-          "maximum": 9223372036854775807,
-          "minimum": -9223372036854775808,
-          "title": "Event Id",
-          "type": "integer"
-        }
-      },
-      "required": [
-        "city",
-        "street",
-        "event_id"
-      ],
-      "title": "Address",
-      "type": "object"
-    },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf": {
+    "MinRelation_tqv6dk_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "participants": {
+          "items": {
+            "$ref": "#/$defs/Team_yndv6t_leaf"
+          },
+          "title": "Participants",
+          "type": "array"
+        }
+      },
+      "required": [
+        "id",
+        "participants"
+      ],
+      "title": "MinRelation",
+      "type": "object"
+    },
+    "Reporter_mnvssl_leaf": {
       "additionalProperties": false,
       "description": "Whom is assigned as the reporter",
       "properties": {
@@ -373,7 +459,7 @@
       "title": "Reporter",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf": {
+    "Team_yndv6t_leaf": {
       "additionalProperties": false,
       "description": "Team that is a playing",
       "properties": {
@@ -410,7 +496,7 @@
       "title": "Team",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf": {
+    "Tournament_y6uxv3_leaf": {
       "additionalProperties": false,
       "properties": {
         "id": {
@@ -441,36 +527,102 @@
           "readOnly": true,
           "title": "Created",
           "type": "string"
+        },
+        "minrelations": {
+          "items": {
+            "$ref": "#/$defs/MinRelation_tqv6dk_leaf"
+          },
+          "title": "Minrelations",
+          "type": "array"
+        },
+        "uniquetogetherfieldswithfks": {
+          "items": {
+            "$ref": "#/$defs/UniqueTogetherFieldsWithFK_vj7nws_leaf"
+          },
+          "title": "Uniquetogetherfieldswithfks",
+          "type": "array"
         }
       },
       "required": [
         "id",
         "name",
         "desc",
-        "created"
+        "created",
+        "minrelations",
+        "uniquetogetherfieldswithfks"
       ],
       "title": "Tournament",
+      "type": "object"
+    },
+    "UniqueTogetherFieldsWithFK_vj7nws_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "text": {
+          "maxLength": 64,
+          "title": "Text",
+          "type": "string"
+        }
+      },
+      "required": [
+        "id",
+        "text"
+      ],
+      "title": "UniqueTogetherFieldsWithFK",
       "type": "object"
     }
   },
   "description": "Events on the calendar",
   "items": {
-    "$ref": "#/$defs/Event_bliobj"
+    "$ref": "#/$defs/Event_mrdcym"
   },
   "title": "Event_list",
   "type": "array"
 }
 {
   "$defs": {
-    "Event_ml4ytz": {
-      "additionalProperties": false,
-      "description": "Events on the calendar",
-      "properties": {
+    "Address_obakhj_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "city": {
+          "maxLength": 64,
+          "title": "City",
+          "type": "string"
+        },
+        "street": {
+          "maxLength": 128,
+          "title": "Street",
+          "type": "string"
+        },
         "event_id": {
           "maximum": 9223372036854775807,
           "minimum": -9223372036854775808,
           "title": "Event Id",
           "type": "integer"
+        }
+      },
+      "required": [
+        "city",
+        "street",
+        "event_id"
+      ],
+      "title": "Address",
+      "type": "object"
+    },
+    "Event_mrdcym_leaf": {
+      "additionalProperties": false,
+      "description": "Events on the calendar",
+      "properties": {
+        "event_id": {
+          "maximum": 9223372036854775807,
+          "minimum": -9223372036854775808,
+          "title": "Event Id",
+          "type": "integer"
         },
         "name": {
           "description": "The name",
@@ -480,7 +632,7 @@
         "reporter": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf"
+              "$ref": "#/$defs/Reporter_mnvssl_leaf"
             },
             {
               "type": "null"
@@ -491,7 +643,7 @@
         },
         "participants": {
           "items": {
-            "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf"
+            "$ref": "#/$defs/Team_yndv6t_leaf"
           },
           "title": "Participants",
           "type": "array"
@@ -530,7 +682,7 @@
         "address": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf"
+              "$ref": "#/$defs/Address_obakhj_leaf"
             },
             {
               "type": "null"
@@ -553,35 +705,31 @@
       "title": "Event",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf": {
-      "additionalProperties": false,
-      "properties": {
-        "city": {
-          "maxLength": 64,
-          "title": "City",
-          "type": "string"
-        },
-        "street": {
-          "maxLength": 128,
-          "title": "Street",
-          "type": "string"
-        },
-        "event_id": {
-          "maximum": 9223372036854775807,
-          "minimum": -9223372036854775808,
-          "title": "Event Id",
-          "type": "integer"
-        }
-      },
-      "required": [
-        "city",
-        "street",
-        "event_id"
-      ],
-      "title": "Address",
-      "type": "object"
-    },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf": {
+    "MinRelation_tqv6dk_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "participants": {
+          "items": {
+            "$ref": "#/$defs/Team_yndv6t_leaf"
+          },
+          "title": "Participants",
+          "type": "array"
+        }
+      },
+      "required": [
+        "id",
+        "participants"
+      ],
+      "title": "MinRelation",
+      "type": "object"
+    },
+    "Reporter_mnvssl_leaf": {
       "additionalProperties": false,
       "description": "Whom is assigned as the reporter",
       "properties": {
@@ -603,7 +751,7 @@
       "title": "Reporter",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf": {
+    "Team_yndv6t_leaf": {
       "additionalProperties": false,
       "description": "Team that is a playing",
       "properties": {
@@ -638,6 +786,28 @@
         "alias"
       ],
       "title": "Team",
+      "type": "object"
+    },
+    "UniqueTogetherFieldsWithFK_vj7nws_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "text": {
+          "maxLength": 64,
+          "title": "Text",
+          "type": "string"
+        }
+      },
+      "required": [
+        "id",
+        "text"
+      ],
+      "title": "UniqueTogetherFieldsWithFK",
       "type": "object"
     }
   },
@@ -675,9 +845,23 @@
     "events": {
       "description": "What tournaments is a happenin'",
       "items": {
-        "$ref": "#/$defs/Event_ml4ytz"
+        "$ref": "#/$defs/Event_mrdcym_leaf"
       },
       "title": "Events",
+      "type": "array"
+    },
+    "minrelations": {
+      "items": {
+        "$ref": "#/$defs/MinRelation_tqv6dk_leaf"
+      },
+      "title": "Minrelations",
+      "type": "array"
+    },
+    "uniquetogetherfieldswithfks": {
+      "items": {
+        "$ref": "#/$defs/UniqueTogetherFieldsWithFK_vj7nws_leaf"
+      },
+      "title": "Uniquetogetherfieldswithfks",
       "type": "array"
     }
   },
@@ -686,36 +870,62 @@
     "name",
     "desc",
     "created",
-    "events"
+    "events",
+    "minrelations",
+    "uniquetogetherfieldswithfks"
   ],
   "title": "Tournament",
   "type": "object"
 }
 {
   "$defs": {
-    "Event_vrm2bi": {
-      "additionalProperties": false,
-      "description": "Events on the calendar",
-      "properties": {
+    "Address_obakhj_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "city": {
+          "maxLength": 64,
+          "title": "City",
+          "type": "string"
+        },
+        "street": {
+          "maxLength": 128,
+          "title": "Street",
+          "type": "string"
+        },
         "event_id": {
           "maximum": 9223372036854775807,
           "minimum": -9223372036854775808,
           "title": "Event Id",
           "type": "integer"
+        }
+      },
+      "required": [
+        "city",
+        "street",
+        "event_id"
+      ],
+      "title": "Address",
+      "type": "object"
+    },
+    "Event_mrdcym_leaf": {
+      "additionalProperties": false,
+      "description": "Events on the calendar",
+      "properties": {
+        "event_id": {
+          "maximum": 9223372036854775807,
+          "minimum": -9223372036854775808,
+          "title": "Event Id",
+          "type": "integer"
         },
         "name": {
           "description": "The name",
           "title": "Name",
           "type": "string"
         },
-        "tournament": {
-          "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf",
-          "description": "What tournaments is a happenin'"
-        },
         "reporter": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf"
+              "$ref": "#/$defs/Reporter_mnvssl_leaf"
             },
             {
               "type": "null"
@@ -723,6 +933,13 @@
           ],
           "nullable": true,
           "title": "Reporter"
+        },
+        "participants": {
+          "items": {
+            "$ref": "#/$defs/Team_yndv6t_leaf"
+          },
+          "title": "Participants",
+          "type": "array"
         },
         "modified": {
           "format": "date-time",
@@ -758,7 +975,7 @@
         "address": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf"
+              "$ref": "#/$defs/Address_obakhj_leaf"
             },
             {
               "type": "null"
@@ -771,8 +988,8 @@
       "required": [
         "event_id",
         "name",
-        "tournament",
         "reporter",
+        "participants",
         "modified",
         "token",
         "alias",
@@ -781,35 +998,31 @@
       "title": "Event",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Address__leaf": {
-      "additionalProperties": false,
-      "properties": {
-        "city": {
-          "maxLength": 64,
-          "title": "City",
-          "type": "string"
-        },
-        "street": {
-          "maxLength": 128,
-          "title": "Street",
-          "type": "string"
-        },
-        "event_id": {
-          "maximum": 9223372036854775807,
-          "minimum": -9223372036854775808,
-          "title": "Event Id",
-          "type": "integer"
-        }
-      },
-      "required": [
-        "city",
-        "street",
-        "event_id"
-      ],
-      "title": "Address",
-      "type": "object"
-    },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf": {
+    "MinRelation_tqv6dk_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "participants": {
+          "items": {
+            "$ref": "#/$defs/Team_yndv6t_leaf"
+          },
+          "title": "Participants",
+          "type": "array"
+        }
+      },
+      "required": [
+        "id",
+        "participants"
+      ],
+      "title": "MinRelation",
+      "type": "object"
+    },
+    "Reporter_mnvssl_leaf": {
       "additionalProperties": false,
       "description": "Whom is assigned as the reporter",
       "properties": {
@@ -831,46 +1044,41 @@
       "title": "Reporter",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf": {
-      "additionalProperties": false,
-      "properties": {
-        "id": {
-          "maximum": 32767,
-          "minimum": -32768,
-          "title": "Id",
-          "type": "integer"
-        },
-        "name": {
-          "maxLength": 255,
-          "title": "Name",
-          "type": "string"
-        },
-        "desc": {
-          "anyOf": [
-            {
-              "type": "string"
-            },
-            {
-              "type": "null"
-            }
-          ],
-          "nullable": true,
-          "title": "Desc"
-        },
-        "created": {
-          "format": "date-time",
-          "readOnly": true,
-          "title": "Created",
-          "type": "string"
+    "Team_yndv6t_leaf": {
+      "additionalProperties": false,
+      "description": "Team that is a playing",
+      "properties": {
+        "id": {
+          "maximum": 2147483647,
+          "minimum": -2147483648,
+          "title": "Id",
+          "type": "integer"
+        },
+        "name": {
+          "title": "Name",
+          "type": "string"
+        },
+        "alias": {
+          "anyOf": [
+            {
+              "maximum": 2147483647,
+              "minimum": -2147483648,
+              "type": "integer"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "nullable": true,
+          "title": "Alias"
         }
       },
       "required": [
         "id",
         "name",
-        "desc",
-        "created"
-      ],
-      "title": "Tournament",
+        "alias"
+      ],
+      "title": "Team",
       "type": "object"
     }
   },
@@ -903,9 +1111,16 @@
     },
     "events": {
       "items": {
-        "$ref": "#/$defs/Event_vrm2bi"
+        "$ref": "#/$defs/Event_mrdcym_leaf"
       },
       "title": "Events",
+      "type": "array"
+    },
+    "minrelations": {
+      "items": {
+        "$ref": "#/$defs/MinRelation_tqv6dk_leaf"
+      },
+      "title": "Minrelations",
       "type": "array"
     }
   },
@@ -913,36 +1128,61 @@
     "id",
     "name",
     "alias",
-    "events"
+    "events",
+    "minrelations"
   ],
   "title": "Team",
   "type": "object"
 }
 {
   "$defs": {
-    "Event_jim4na": {
-      "additionalProperties": false,
-      "description": "Events on the calendar",
-      "properties": {
+    "Address_obakhj_leaf": {
+      "additionalProperties": false,
+      "properties": {
+        "city": {
+          "maxLength": 64,
+          "title": "City",
+          "type": "string"
+        },
+        "street": {
+          "maxLength": 128,
+          "title": "Street",
+          "type": "string"
+        },
         "event_id": {
           "maximum": 9223372036854775807,
           "minimum": -9223372036854775808,
           "title": "Event Id",
           "type": "integer"
+        }
+      },
+      "required": [
+        "city",
+        "street",
+        "event_id"
+      ],
+      "title": "Address",
+      "type": "object"
+    },
+    "Event_mrdcym_leaf": {
+      "additionalProperties": false,
+      "description": "Events on the calendar",
+      "properties": {
+        "event_id": {
+          "maximum": 9223372036854775807,
+          "minimum": -9223372036854775808,
+          "title": "Event Id",
+          "type": "integer"
         },
         "name": {
           "description": "The name",
           "title": "Name",
           "type": "string"
         },
-        "tournament": {
-          "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf",
-          "description": "What tournaments is a happenin'"
-        },
         "reporter": {
           "anyOf": [
             {
-              "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf"
+              "$ref": "#/$defs/Reporter_mnvssl_leaf"
             },
             {
               "type": "null"
@@ -953,7 +1193,7 @@
         },
         "participants": {
           "items": {
-            "$ref": "#/$defs/tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf"
+            "$ref": "#/$defs/Team_yndv6t_leaf"
           },
           "title": "Participants",
           "type": "array"
@@ -988,22 +1228,34 @@
           ],
           "nullable": true,
           "title": "Alias"
+        },
+        "address": {
+          "anyOf": [
+            {
+              "$ref": "#/$defs/Address_obakhj_leaf"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "nullable": true,
+          "title": "Address"
         }
       },
       "required": [
         "event_id",
         "name",
-        "tournament",
         "reporter",
         "participants",
         "modified",
         "token",
-        "alias"
+        "alias",
+        "address"
       ],
       "title": "Event",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Reporter__leaf": {
+    "Reporter_mnvssl_leaf": {
       "additionalProperties": false,
       "description": "Whom is assigned as the reporter",
       "properties": {
@@ -1025,7 +1277,7 @@
       "title": "Reporter",
       "type": "object"
     },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Team__leaf": {
+    "Team_yndv6t_leaf": {
       "additionalProperties": false,
       "description": "Team that is a playing",
       "properties": {
@@ -1060,48 +1312,6 @@
         "alias"
       ],
       "title": "Team",
-      "type": "object"
-    },
-    "tortoise__contrib__pydantic__creator__tests__testmodels__Tournament__leaf": {
-      "additionalProperties": false,
-      "properties": {
-        "id": {
-          "maximum": 32767,
-          "minimum": -32768,
-          "title": "Id",
-          "type": "integer"
-        },
-        "name": {
-          "maxLength": 255,
-          "title": "Name",
-          "type": "string"
-        },
-        "desc": {
-          "anyOf": [
-            {
-              "type": "string"
-            },
-            {
-              "type": "null"
-            }
-          ],
-          "nullable": true,
-          "title": "Desc"
-        },
-        "created": {
-          "format": "date-time",
-          "readOnly": true,
-          "title": "Created",
-          "type": "string"
-        }
-      },
-      "required": [
-        "id",
-        "name",
-        "desc",
-        "created"
-      ],
-      "title": "Tournament",
       "type": "object"
     }
   },
@@ -1118,7 +1328,7 @@
       "type": "string"
     },
     "event": {
-      "$ref": "#/$defs/Event_jim4na"
+      "$ref": "#/$defs/Event_mrdcym_leaf"
     },
     "event_id": {
       "maximum": 9223372036854775807,

produced with the following code:

the code...
import json

import tortoise
from tests.testmodels import Event, Tournament, Team, Address
from tortoise import connections
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator


async def run() -> None:
    await tortoise.Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["tests.testmodels"]})
    await tortoise.Tortoise.generate_schemas()

    Event_Pydantic = pydantic_model_creator(Event)
    Event_Pydantic_List = pydantic_queryset_creator(Event)
    Tournament_Pydantic = pydantic_model_creator(Tournament)
    Team_Pydantic = pydantic_model_creator(Team)
    Address_Pydantic = pydantic_model_creator(Address)

    print(json.dumps(Event_Pydantic.model_json_schema(), indent=2))
    print(json.dumps(Event_Pydantic_List.model_json_schema(), indent=2))
    print(json.dumps(Tournament_Pydantic.model_json_schema(), indent=2))
    print(json.dumps(Team_Pydantic.model_json_schema(), indent=2))
    print(json.dumps(Address_Pydantic.model_json_schema(), indent=2))

    print(tortoise.__version__)
    await connections.close_all()

if __name__ == "__main__":
    tortoise.run_async(run())

Checklist:

  • [? ] My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added the changelog accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@abondar
Copy link
Member

abondar commented Oct 22, 2024

Hi!

Thank you for looking into it
Model creator indeed long gone out of control and it's hard to maintain it

Although before I would be able to give any constructive feedback - you would still need to fix testes (and rebase to actual develop), as in most cases tests were written with holding some edgecase in mind, so I wouldn't want to risk just throwing them all away.
Although I see no problem if you will update tests to have sane names for generated schemas and other minor changes to them

I am also okay with changes in API of model creator if there is need in that, as long as it won't be confusing for people updating lib

@@ -441,3 +463,21 @@ def default_name(default: Any) -> Optional[Union[int, float, str, bool]]:
desc["db_field_types"] = self.get_db_field_types()

return desc

def describe_by_dataclass(self) -> FieldDescriptionBase:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer: I'm quite new to the project but I looked into pydantic_model_creator a bit and it's obvious that it's convoluted, it has issues and begs for refactoring. So thank you for working on this!

I'm not sure though that extending the core classes like Field, RelationalField and others with functionality required for one particular extension is optimal. Basically contrib.pydantic is not a part of ORM, it's an extension that only some users use. So it seems to me that it would make more sense to have the describe_by_dataclass logic inside of contrib.pydantic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point. Originally I planned to change the describe() method to use the describe_by_dataclass() method, with something like asdict(self.describe_by_dataclass()). That is because describe() only returns a dict, so typing is not working with describe(). But this change would become a bit more larger and out of focus of the original intention of this PR. with other words: I think describe_by_dataclass() could be helpfull in many points, but I can see why it should be moved to a dedicated place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to tortoise.contrib.pydantic.dataclasses

@markus-96
Copy link
Contributor Author

It is now fully tested, as far as I can test it, since I fail to set up the dev environment via make up on a linux machine. The only errors I get are in test_decorator.py, test_queryset.py and test_update.py, which are not using anything I changed, and are throwing errors because I can only use sqlite.
MyPy is also pleased, and also Codacy.

With other words: I think it is ready for a review!

Copy link
Contributor

@henadzit henadzit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, seems like you haven't gotten a review for a while, so I decided to leave a few comments. Thank you for looking into it! pydantic_model_creator begs for refactoring!

I would like to complain a bit by saying that it took me a while to do a review, even though I haven't really dug deep because it is a lot of changes. Looks like the PR does a few things:

  • Introduces dataclasses as a conversion step from tortoise to pydantic
  • refactors a lot of the code
  • changes the pydantic model naming

It would definitely make a review much easier if the PR was smaller! Anyway, please have a look at my comments and let me know what you think.

@@ -1335,44 +1336,7 @@ def test_schema(self):
"title": "Employee",
"type": "object",
},
"Employee_obdn4z": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this has to be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is the exact same def as "Employee_dkvdqq_leaf". Here, _MODEL_INDEX kicks in and returns the same model. This is because of the different way how the names are generated now. Before, the current stack was also hashed. But for a model-name to be unique, the stack is not needed. What is needed is the information about what fields are included in the model.

"title": "Event_list",
"type": "array",
},
)

def test_address_schema(self):
# print(json.dumps(self.Address_Pydantic.model_json_schema(), indent=2))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, will remove the other print statements as well :)

_MODEL_INDEX[self._name] = model
return model

def process_field(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this could be a private method as well as construct_field_map and others. Having private methods would help to provide a clear interface of the class and make it easier to read the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did as you said, but I left get_name public, since it might be useful for others

return self.given_name, self.given_name
hashval = (
f"{self._fqname};{self.meta.exclude};{self.meta.include};{self.meta.computed};"
f"{self.meta.sort_alphabetically}:{self.meta.allow_cycles}:{self._exclude_read_only}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since all names are changing anyway, maybe we can decide on having either ; or : as separators :)


:param _stack: Internal parameter to track recursion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like all the documentation got removed.

)


def describe_field_by_dataclass(field: Field) -> FieldDescriptionBase:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please help me to understand what is the benefit of converting the model and field metadata to a dataclass, so then we can convert it to a pydantic model? Not a criticism! Just trying to understand the logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before using dataclasses, everything was inside a simple dict. A dict does not provide any typing information, and you also do not know what is inside a dict, with other words: you have no control about what keys are in a dict and also have no clue about of what type the values are. Since in this case it is very predictable what is needed for pydantic_model_creator, I found it much more consistent to rely on dataclasses, that do exactly what dicts don't. In my opinion, it is a good compromise between light weight and structural integrity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another approach would be to not use dataclasses or a dict and directly read the needed values from the Field Instances

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another approach would be to not use dataclasses or a dict and directly read the needed values from the Field Instances

that's not possible because Field.constraints is modified during the process.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's not possible because Field.constraints is modified during the process.

This is kinda weird. I would assume that the "description" should stay constant and not be modified. Should it be refactored instead of adding dataclasses?

It also looks like when instantiating dataclasses, we just pass into the constructor the field values:

 return FieldDescription(
        name=field.model_field_name,
        field_type=field.__class__,
        python_type=field.field_type,
        nullable=field.null,
        default=field.default,
        description=field.description,
        docstring=field.docstring,
        constraints=field.constraints,
    )

Can we just refer to them in the code?

@markus-96
Copy link
Contributor Author

Hey, seems like you haven't gotten a review for a while, so I decided to leave a few comments. Thank you for looking into it! pydantic_model_creator begs for refactoring!

Thanks a lot for reviewing!

I would like to complain a bit by saying that it took me a while to do a review, even though I haven't really dug deep because it is a lot of changes. Looks like the PR does a few things:

* Introduces dataclasses as a conversion step from tortoise to pydantic

* refactors a lot of the code

* changes the pydantic model naming

that is exactly what it is doing, the main part being the refactoring.

It would definitely make a review much easier if the PR was smaller! Anyway, please have a look at my comments and let me know what you think.

That is true, but in my eyes there were many thinks that could be improved, but I did not see the point to split this into multiple PRs because in the end everything would be rewritten, like it is now. Now, I can at least try to justify my changes ;)

Copy link
Contributor

@henadzit henadzit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, but in my eyes there were many thinks that could be improved, but I did not see the point to split this into multiple PRs because in the end everything would be rewritten, like it is now. Now, I can at least try to justify my changes ;)

The issue is that it is hard to understand what exactly has been changed and if the change is positive. I'm not familiar with this code close enough and it hard for me to say whether the code quality has improved. I see that the amount of code increased by a few hundreds lines which is not necessarily good, I see that there is more typing which is good. But I also see that tests keep failing and it makes approving it quite a leap of faith. Since it is a massive change, it should be tested thoroughly.

return None

def _process_json_field_description(self) -> Any:
return Any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as it is right now, also claimed here: #1702

I made it to an actual function so that in the future someone could add the functionality described there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this method

) -> Type[PydanticModel]:
"""
Function to build `Pydantic Model <https://pydantic-docs.helpmanual.io/usage/models/>`__ off Tortoise Model.
@dataclasses.dataclass
Copy link
Contributor

@henadzit henadzit Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be this in dataclasses.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to dataclasses.py

# ForeignKeyFieldInstance -> RelationalField
if isinstance(field, OneToOneFieldInstance):
# OneToOneFieldInstance -> ForeignKeyFieldInstance -> RelationalField
return OneToOneFieldInstanceDescription(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like all contructors are almost the same. Can it be changed to be a class method in FieldDescriptionBase?

class FieldDescriptionBase:
  @classmethod
  def from_tortoise_field(field): 
    return cls(...)

This would make this method shorter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did as you said

@@ -0,0 +1,191 @@
import dataclasses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that dataclasses is a great name for a module. It's like calling a file classes.py or functions.py, it does not really convey information about what is exactly in the file. Maybe, descriptions.py? I know naming is hard.

)


def describe_field_by_dataclass(field: Field) -> FieldDescriptionBase:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's not possible because Field.constraints is modified during the process.

This is kinda weird. I would assume that the "description" should stay constant and not be modified. Should it be refactored instead of adding dataclasses?

It also looks like when instantiating dataclasses, we just pass into the constructor the field values:

 return FieldDescription(
        name=field.model_field_name,
        field_type=field.__class__,
        python_type=field.field_type,
        nullable=field.null,
        default=field.default,
        description=field.description,
        docstring=field.docstring,
        constraints=field.constraints,
    )

Can we just refer to them in the code?

@markus-96
Copy link
Contributor Author

Could anyone review how I changed the creation of the hash? The hash is needed to guarantee the uniqunes of the created model. So I thought about what makes a model unique:

  1. the name of the model,
  2. the contained fields in their order,
  3. if there are relational fields, to what model do they relate to

This should be covering the current state.

maybe it is also important to include the optional fields? This is not done right now. The following is producing the same model:

Employee_Pydantic = pydantic_model_creator(Employee, exclude=())
Employee_Pydantic_optional = pydantic_model_creator(Employee, optional=("manager",))
print(Employee_Pydantic.__name__, Employee_Pydantic_optional.__name__)
print(Employee_Pydantic is Employee_Pydantic_optional)  # True

@markus-96
Copy link
Contributor Author

I have removed the dataclasses that describe the fields and am now working with the fields directly. Should I push or revert the changes? The tests are passing.

Also, I have noticed that a line that should have been removed still exists: #1465 (comment). Therefore, there are also a lot of tests that should be updated if the line is removed.

@markus-96
Copy link
Contributor Author

The pydantic_json_schema warning is because of the naming conflict between tortoise.Field and pydantic.Field that is covered by from pydantic import Field as PydanticField and should be resolved now.

The IntegrityError is a mystery for me. The only reason I could think of right now is an integrity error because of the randomly generated UUIDs as pks, that is very unlikely but could happen.. Also, I did not touch anything that could possibly affect this, as far as I can tell.

I was able to set up a test environment with docker, the test does not fail here with python 3.12 or 3.11 and mssql as database.

Another problem however I simply want to point out here is that during the test test_concurrency_transactions_concurrent in tests/test_concurrency.py using psycopg I always encounter the following error: psycopg_pool.PoolTimeout: couldn't get a connection after 15.00 sec, also with the current develop branch. Maybe this is because my computer is too damn slow (the tests take more than twice as long on my machine as in the github actions..)?

Dockerfile
FROM python:3.11-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    apt-transport-https \
    gnupg \
    build-essential \
    unixodbc-dev \
    libpq-dev \
    lsb-release\
    && rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
RUN curl https://packages.microsoft.com/config/debian/$(lsb_release -rs)/prod.list \
    -o /etc/apt/sources.list.d/mssql-release.list && \
    apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql18

COPY pyproject.toml poetry.lock ./

RUN pip install -U pip poetry && poetry config virtualenvs.create false

RUN poetry install --no-dev

COPY . .

ENV TORTOISE_TEST_MODULES=tests.testmodels \
    TORTOISE_MYSQL_PASS=123456 \
    TORTOISE_POSTGRES_PASS=123456 \
    TORTOISE_MSSQL_PASS=Abcd12345678 \
    TORTOISE_MSSQL_DRIVER="ODBC Driver 18 for SQL Server" \
    PYTHONDEVMODE=1 \
    PYTEST_ARGS="-n auto --cov=tortoise --cov-append --tb=native -q"

RUN apt-get update && apt-get install -y make && rm -rf /var/lib/apt/lists/*

CMD ["make", "style"]
CMD ["make", "ci"]
docker-compose.yml
services:
  app:
    build: .
    depends_on:
      - postgres
      - mysql
      - mssql
    environment:
      TORTOISE_TEST_MODULES: tests.testmodels
      TORTOISE_MYSQL_PASS: 123456
      TORTOISE_POSTGRES_PASS: 123456
      TORTOISE_MSSQL_PASS: Abcd12345678
      TORTOISE_MSSQL_DRIVER: "ODBC Driver 18 for SQL Server"
  postgres:
    image: postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: 123456
      POSTGRES_USER: postgres
  mysql:
    image: mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: 123456
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-CU15-ubuntu-22.04
    ports:
      - "1433:1433"
    environment:
      ACCEPT_EULA: Y
      SA_PASSWORD: Abcd12345678

(And changed the database-URLs in the Makefile accordingly)

@coveralls
Copy link

coveralls commented Nov 14, 2024

Pull Request Test Coverage Report for Build 11880457757

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 304 of 306 (99.35%) changed or added relevant lines in 2 files are covered.
  • 85 unchanged lines in 9 files lost coverage.
  • Overall coverage increased (+0.2%) to 89.269%

Changes Missing Coverage Covered Lines Changed/Added Lines %
tortoise/contrib/pydantic/creator.py 226 228 99.12%
Files with Coverage Reduction New Missed Lines %
tortoise/backends/mssql/executor.py 1 94.12%
tortoise/query_utils.py 1 97.37%
tortoise/models.py 4 95.46%
tortoise/backends/sqlite/executor.py 5 77.14%
tortoise/expressions.py 7 97.1%
tortoise/backends/base/executor.py 9 91.35%
tortoise/backends/base_postgres/client.py 12 89.06%
tortoise/fields/data.py 13 94.29%
tortoise/queryset.py 33 94.37%
Totals Coverage Status
Change from base Build 11435130298: 0.2%
Covered Lines: 6211
Relevant Lines: 6840

💛 - Coveralls

@abondar
Copy link
Member

abondar commented Nov 14, 2024

Seems like it was some flaky test, restarting it helped

Please bear with me before I will be able to review it properly, probably on weekend, as it's quite hard to review massive refactoring like that

model = create_model(
lname,
__base__=PydanticListModel,
root=(List[submodel], Field(default_factory=list)), # type: ignore
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to be PydanticField, not Field. Will be fixed

abondar
abondar previously approved these changes Nov 16, 2024
Copy link
Member

@abondar abondar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall - I am happy with the changes
Thank you for working on it

Although model creator is still hard to get through, but at least now it is more readable and typed

return pconfig

def _initialize_field_map(self) -> FieldMap:
return FieldMap(self.meta) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reformat to

        return (
            FieldMap(self.meta)
            if self._exclude_read_only
            else FieldMap(self.meta, pk_field=self._model_description.pk_field)
        )

Not sure why black didn't reformat it itself 🤔

Comment on lines 529 to 537
exclude=tuple(
str(v[prefix_len:]) for v in self.meta.exclude if v.startswith(field_name + ".")
),
include=tuple(
str(v[prefix_len:]) for v in self.meta.include if v.startswith(field_name + ".")
),
computed=tuple(
str(v[prefix_len:]) for v in self.meta.computed if v.startswith(field_name + ".")
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we can make this transformation into a function? Inline function would be good enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not want to use a lambda expression here (pycharm yelled at me "PEP 8: E731 do not assign a lambda expression, use a def"), so I put it in an inner function

Comment on lines 253 to 254
self.meta = meta_from_class.finalize_meta(
exclude, include, computed, allow_cycles, sort_alphabetically, model_config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please - use kwargs here, it looks like too many arguments for using args here

@abondar abondar merged commit ea34c3d into tortoise:develop Nov 17, 2024
7 checks passed
@markus-96
Copy link
Contributor Author

Thanks a lot @abondar and @henadzit!

@markus-96 markus-96 deleted the pydantic-model-creator branch December 3, 2024 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants