@@ -89,38 +89,50 @@ def _is_submodule(obj: Any, module_name: str) -> bool:
89
89
90
90
def _is_pydantic_v1_model (obj : Any ) -> bool :
91
91
"""
92
- Return true if an object is a pydantic V1 model.
93
- """
94
- return inspect .isclass (obj ) and (
95
- (obj is not BaseModelV1 and issubclass (obj , BaseModelV1 ))
96
- or (
97
- GenericModelV1 is not None
98
- and issubclass (obj , GenericModelV1 )
99
- and getattr (obj , "__concrete__" , False )
100
- )
101
- )
92
+ Return true if the object is a 'concrete' pydantic V1 model.
93
+ """
94
+ if not inspect .isclass (obj ):
95
+ return False
96
+ elif obj is BaseModelV1 or obj is GenericModelV1 :
97
+ return False
98
+ elif GenericModelV1 and issubclass (obj , GenericModelV1 ):
99
+ return getattr (obj , "__concrete__" , False )
100
+ return issubclass (obj , BaseModelV1 )
102
101
103
102
104
103
def _is_pydantic_v2_model (obj : Any ) -> bool :
105
104
"""
106
- Return true if an object is a pydantic V2 model.
105
+ Return true if an object is a 'concrete' pydantic V2 model.
107
106
"""
108
- return inspect .isclass (obj ) and (
109
- BaseModelV2 is not None
110
- and obj is not BaseModelV2
111
- and issubclass (obj , BaseModelV2 )
112
- and not getattr (obj , "__pydantic_generic_metadata__" , {}).get ("parameters" )
113
- )
107
+ if not inspect .isclass (obj ):
108
+ return False
109
+ elif obj is BaseModelV2 or BaseModelV2 is None :
110
+ return False
111
+ return issubclass (obj , BaseModelV2 ) and not getattr (
112
+ obj , "__pydantic_generic_metadata__" , {}
113
+ ).get ("parameters" )
114
114
115
115
116
116
def _is_pydantic_model (obj : Any ) -> bool :
117
117
"""
118
- Return true if an object is a concrete subclass of pydantic's BaseModel.
119
- 'concrete' meaning that it's not a generic model.
118
+ Return true if an object is a valid model for either V1 or V2 of pydantic.
120
119
"""
121
120
return _is_pydantic_v1_model (obj ) or _is_pydantic_v2_model (obj )
122
121
123
122
123
+ def _has_null_variant (schema : Dict [str , Any ]) -> bool :
124
+ """
125
+ Return true if a JSON schema has 'null' as one of its types.
126
+ """
127
+ if schema .get ("type" ) == "null" :
128
+ return True
129
+ if isinstance (schema .get ("type" ), list ) and "null" in schema ["type" ]:
130
+ return True
131
+ if isinstance (schema .get ("anyOf" ), list ):
132
+ return any (_has_null_variant (s ) for s in schema ["anyOf" ])
133
+ return False
134
+
135
+
124
136
def _get_model_config (model : Type [Any ]) -> "Union[ConfigDict, Type[BaseConfig]]" :
125
137
"""
126
138
Return the 'config' for a pydantic model.
@@ -156,47 +168,9 @@ def _extract_pydantic_models(module: ModuleType) -> List[type]:
156
168
return models
157
169
158
170
159
- def _clean_output_file (output_filename : str ) -> None :
160
- """
161
- Clean up the output file typescript definitions were written to by:
162
- 1. Removing the 'master model'.
163
- This is a faux pydantic model with references to all the *actual* models necessary for generating
164
- clean typescript definitions without any duplicates. We don't actually want it in the output, so
165
- this function removes it from the generated typescript file.
166
- 2. Adding a banner comment with clear instructions for how to regenerate the typescript definitions.
167
- """
168
- with open (output_filename , "r" ) as f :
169
- lines = f .readlines ()
170
-
171
- start , end = None , None
172
- for i , line in enumerate (lines ):
173
- if line .rstrip ("\r \n " ) == "export interface _Master_ {" :
174
- start = i
175
- elif (start is not None ) and line .rstrip ("\r \n " ) == "}" :
176
- end = i
177
- break
178
-
179
- assert start is not None , "Could not find the start of the _Master_ interface."
180
- assert end is not None , "Could not find the end of the _Master_ interface."
181
-
182
- banner_comment_lines = [
183
- "/* tslint:disable */\n " ,
184
- "/* eslint-disable */\n " ,
185
- "/**\n " ,
186
- "/* This file was automatically generated from pydantic models by running pydantic2ts.\n " ,
187
- "/* Do not modify it by hand - just update the pydantic models and then re-run the script\n " ,
188
- "*/\n \n " ,
189
- ]
190
-
191
- new_lines = banner_comment_lines + lines [:start ] + lines [(end + 1 ) :]
192
-
193
- with open (output_filename , "w" ) as f :
194
- f .writelines (new_lines )
195
-
196
-
197
171
def _clean_json_schema (schema : Dict [str , Any ], model : Any = None ) -> None :
198
172
"""
199
- Clean up the resulting JSON schemas by :
173
+ Clean up the resulting JSON schemas via the following steps :
200
174
201
175
1) Get rid of the useless "An enumeration." description applied to Enums
202
176
which don't have a docstring.
@@ -220,24 +194,58 @@ def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None:
220
194
try :
221
195
if not field .allow_none :
222
196
continue
223
- prop = properties .get (field .alias )
197
+ name = field .alias
198
+ prop = properties .get (name )
224
199
if prop is None :
225
200
continue
226
- prop_types : List [Any ] = prop .setdefault ("anyOf" , [])
227
- if any (t .get ("type" ) == "null" for t in prop_types ):
201
+ if _has_null_variant (prop ):
228
202
continue
229
- if "type" in prop :
230
- prop_types .append ({"type" : prop .pop ("type" )})
231
- if "$ref" in prop :
232
- prop_types .append ({"$ref" : prop .pop ("$ref" )})
233
- prop_types .append ({"type" : "null" })
203
+ properties [name ] = {"anyOf" : [prop , {"type" : "null" }]}
234
204
except Exception :
235
205
LOG .error (
236
206
f"Failed to ensure nullability for field { field .alias } ." ,
237
207
exc_info = True ,
238
208
)
239
209
240
210
211
+ def _clean_output_file (output_filename : str ) -> None :
212
+ """
213
+ Clean up the resulting typescript definitions via the following steps:
214
+
215
+ 1. Remove the "_Master_" model.
216
+ It exists solely to serve as a namespace for the target models.
217
+ By rolling them all up into a single model, we can generate a single output file.
218
+ 2. Add a banner comment with clear instructions for regenerating the typescript definitions.
219
+ """
220
+ with open (output_filename , "r" ) as f :
221
+ lines = f .readlines ()
222
+
223
+ start , end = None , None
224
+ for i , line in enumerate (lines ):
225
+ if line .rstrip ("\r \n " ) == "export interface _Master_ {" :
226
+ start = i
227
+ elif (start is not None ) and line .rstrip ("\r \n " ) == "}" :
228
+ end = i
229
+ break
230
+
231
+ assert start is not None , "Could not find the start of the _Master_ interface."
232
+ assert end is not None , "Could not find the end of the _Master_ interface."
233
+
234
+ banner_comment_lines = [
235
+ "/* tslint:disable */\n " ,
236
+ "/* eslint-disable */\n " ,
237
+ "/**\n " ,
238
+ "/* This file was automatically generated from pydantic models by running pydantic2ts.\n " ,
239
+ "/* Do not modify it by hand - just update the pydantic models and then re-run the script\n " ,
240
+ "*/\n \n " ,
241
+ ]
242
+
243
+ new_lines = banner_comment_lines + lines [:start ] + lines [(end + 1 ) :]
244
+
245
+ with open (output_filename , "w" ) as f :
246
+ f .writelines (new_lines )
247
+
248
+
241
249
@contextmanager
242
250
def _schema_generation_overrides (
243
251
model : Type [Any ],
@@ -246,6 +254,8 @@ def _schema_generation_overrides(
246
254
Temporarily override the 'extra' setting for a model,
247
255
changing it to 'forbid' unless it was EXPLICITLY set to 'allow'.
248
256
This prevents '[k: string]: any' from automatically being added to every interface.
257
+
258
+ TODO: check if overriding 'schema_extra' is necessary, or if there's a better way.
249
259
"""
250
260
revert : Dict [str , Any ] = {}
251
261
config = _get_model_config (model )
@@ -254,10 +264,14 @@ def _schema_generation_overrides(
254
264
if config .get ("extra" ) != "allow" :
255
265
revert ["extra" ] = config .get ("extra" )
256
266
config ["extra" ] = "forbid"
267
+ revert ["json_schema_extra" ] = config .get ("json_schema_extra" )
268
+ config ["json_schema_extra" ] = staticmethod (_clean_json_schema )
257
269
else :
258
270
if config .extra != "allow" :
259
271
revert ["extra" ] = config .extra
260
272
config .extra = "forbid" # type: ignore
273
+ revert ["schema_extra" ] = config .schema_extra
274
+ config .schema_extra = staticmethod (_clean_json_schema ) # type: ignore
261
275
yield
262
276
finally :
263
277
for key , value in revert .items ():
0 commit comments