@@ -52,15 +52,57 @@ class INIBasedModel(BaseModel, ABC):
52
52
"""INIBasedModel defines the base model for blocks/chapters
53
53
inside an INIModel (*.ini file).
54
54
55
- INIBasedModel instances can be created from Section instances
56
- obtained through parsing ini documents. It further supports
57
- adding arbitrary fields to it, which will be written to file.
58
- Lastly, no arbitrary types are allowed for the defined fields.
55
+ - Abstract base class for representing INI-style configuration file blocks or chapters.
56
+ - This class serves as the foundational model for handling blocks within INI configuration files.
57
+ It supports creating instances from parsed INI sections, adding arbitrary fields, and ensuring
58
+ well-defined serialization and deserialization behavior. Subclasses are expected to define
59
+ specific behavior and headers for their respective INI blocks.
59
60
60
61
Attributes:
61
62
comments (Optional[Comments]):
62
- Optional Comments if defined by the user, containing
63
- descriptions for all data fields.
63
+ Optional Comments if defined by the user, containing descriptions for all data fields.
64
+
65
+ Args:
66
+ comments (Optional[Comments], optional):
67
+ Comments for the model fields. Defaults to None.
68
+
69
+ Returns:
70
+ None
71
+
72
+ Raises:
73
+ ValueError: If unknown fields are encountered during validation.
74
+
75
+ See Also:
76
+ BaseModel: The Pydantic base model extended by this class.
77
+ INISerializerConfig: Provides configuration for INI serialization.
78
+
79
+
80
+ Examples:
81
+ Define a custom INI block subclass:
82
+
83
+ >>> class MyModel(INIBasedModel):
84
+ ... _header = "MyHeader"
85
+ ... field_a: str = "default_value"
86
+
87
+ Parse an INI section:
88
+
89
+ >>> from hydrolib.core.dflowfm.ini.io_models import Section
90
+ >>> section = Section(header="MyHeader", content=[{"key": "field_a", "value": "value"}])
91
+ >>> model = MyModel.parse_obj(section.flatten())
92
+ >>> print(model.field_a)
93
+ value
94
+
95
+ Serialize a model to an INI format:
96
+ >>> from hydrolib.core.dflowfm.ini.serializer import INISerializerConfig
97
+ >>> from hydrolib.core.basemodel import ModelSaveSettings
98
+ >>> config = INISerializerConfig()
99
+ >>> section = model._to_section(config, save_settings=ModelSaveSettings())
100
+ >>> print(section.header)
101
+ MyHeader
102
+
103
+ Notes:
104
+ - Subclasses can override the `_header` attribute to define the INI block header.
105
+ - Arbitrary fields can be added dynamically and are included during serialization.
64
106
"""
65
107
66
108
_header : str = ""
@@ -75,14 +117,33 @@ class Config:
75
117
76
118
@classmethod
77
119
def _get_unknown_keyword_error_manager (cls ) -> Optional [UnknownKeywordErrorManager ]:
120
+ """
121
+ Retrieves the error manager for handling unknown keywords in INI files.
122
+
123
+ Returns:
124
+ Optional[UnknownKeywordErrorManager]:
125
+ An instance of the error manager or None if unknown keywords are allowed.
126
+ """
78
127
return UnknownKeywordErrorManager ()
79
128
80
129
@classmethod
81
130
def _supports_comments (cls ):
131
+ """
132
+ Indicates whether the model supports comments for its fields.
133
+
134
+ Returns:
135
+ bool: True if comments are supported; otherwise, False.
136
+ """
82
137
return True
83
138
84
139
@classmethod
85
140
def _duplicate_keys_as_list (cls ):
141
+ """
142
+ Indicates whether duplicate keys in INI sections should be treated as lists.
143
+
144
+ Returns:
145
+ bool: True if duplicate keys should be treated as lists; otherwise, False.
146
+ """
86
147
return False
87
148
88
149
@classmethod
@@ -107,6 +168,9 @@ def get_list_field_delimiter(cls, field_key: str) -> str:
107
168
108
169
Args:
109
170
field_key (str): the original field key (not its alias).
171
+
172
+ Returns:
173
+ str: the delimiter string to be used for serializing the given field.
110
174
"""
111
175
delimiter = None
112
176
if (field := cls .__fields__ .get (field_key )) and isinstance (field , ModelField ):
@@ -117,7 +181,18 @@ def get_list_field_delimiter(cls, field_key: str) -> str:
117
181
return delimiter
118
182
119
183
class Comments (BaseModel , ABC ):
120
- """Comments defines the comments of an INIBasedModel"""
184
+ """
185
+ Represents the comments associated with fields in an INIBasedModel.
186
+
187
+ Attributes:
188
+ Arbitrary fields can be added dynamically to store comments.
189
+
190
+ Config:
191
+ extra: Extra.allow
192
+ Allows dynamic fields for comments.
193
+ arbitrary_types_allowed: bool
194
+ Indicates that only known types are allowed.
195
+ """
121
196
122
197
class Config :
123
198
extra = Extra .allow
@@ -127,6 +202,18 @@ class Config:
127
202
128
203
@root_validator (pre = True )
129
204
def _validate_unknown_keywords (cls , values ):
205
+ """
206
+ Validates fields and raises errors for unknown keywords.
207
+
208
+ Args:
209
+ values (dict): Dictionary of field values to validate.
210
+
211
+ Returns:
212
+ dict: Validated field values.
213
+
214
+ Raises:
215
+ ValueError: If unknown keywords are found.
216
+ """
130
217
unknown_keyword_error_manager = cls ._get_unknown_keyword_error_manager ()
131
218
do_not_validate = cls ._exclude_from_validation (values )
132
219
if unknown_keyword_error_manager :
@@ -140,7 +227,16 @@ def _validate_unknown_keywords(cls, values):
140
227
141
228
@root_validator (pre = True )
142
229
def _skip_nones_and_set_header (cls , values ):
143
- """Drop None fields for known fields."""
230
+ """Drop None fields for known fields.
231
+
232
+ Filters out None values and sets the model header.
233
+
234
+ Args:
235
+ values (dict): Dictionary of field values.
236
+
237
+ Returns:
238
+ dict: Updated field values with None values removed.
239
+ """
144
240
dropkeys = []
145
241
for k , v in values .items ():
146
242
if v is None and k in cls .__fields__ .keys ():
@@ -157,20 +253,48 @@ def _skip_nones_and_set_header(cls, values):
157
253
158
254
@validator ("comments" , always = True , allow_reuse = True )
159
255
def comments_matches_has_comments (cls , v ):
256
+ """
257
+ Validates the presence of comments if supported by the model.
258
+
259
+ Args:
260
+ v (Any): The comments field value.
261
+
262
+ Returns:
263
+ Any: Validated comments field value.
264
+ """
160
265
if not cls ._supports_comments () and v is not None :
161
266
logging .warning (f"Dropped unsupported comments from { cls .__name__ } init." )
162
267
v = None
163
268
return v
164
269
165
270
@validator ("*" , pre = True , allow_reuse = True )
166
271
def replace_fortran_scientific_notation_for_floats (cls , value , field ):
272
+ """
273
+ Converts FORTRAN-style scientific notation to standard notation for float fields.
274
+
275
+ Args:
276
+ value (Any): The field value to process.
277
+ field (Field): The field being processed.
278
+
279
+ Returns:
280
+ Any: The processed field value.
281
+ """
167
282
if field .type_ != float :
168
283
return value
169
284
170
285
return cls ._replace_fortran_scientific_notation (value )
171
286
172
287
@classmethod
173
288
def _replace_fortran_scientific_notation (cls , value ):
289
+ """
290
+ Replaces FORTRAN-style scientific notation in a value.
291
+
292
+ Args:
293
+ value (Any): The value to process.
294
+
295
+ Returns:
296
+ Any: The processed value.
297
+ """
174
298
if isinstance (value , str ):
175
299
return cls ._scientific_notation_regex .sub (r"\1e\3" , value )
176
300
if isinstance (value , list ):
@@ -182,6 +306,15 @@ def _replace_fortran_scientific_notation(cls, value):
182
306
183
307
@classmethod
184
308
def validate (cls : Type ["INIBasedModel" ], value : Any ) -> "INIBasedModel" :
309
+ """
310
+ Validates a value as an instance of INIBasedModel.
311
+
312
+ Args:
313
+ value (Any): The value to validate.
314
+
315
+ Returns:
316
+ INIBasedModel: The validated instance.
317
+ """
185
318
if isinstance (value , Section ):
186
319
value = value .flatten (
187
320
cls ._duplicate_keys_as_list (), cls ._supports_comments ()
@@ -191,11 +324,25 @@ def validate(cls: Type["INIBasedModel"], value: Any) -> "INIBasedModel":
191
324
192
325
@classmethod
193
326
def _exclude_from_validation (cls , input_data : Optional = None ) -> Set :
194
- """Fields that should not be checked when validating existing fields as they will be dynamically added."""
327
+ """
328
+ Fields that should not be checked when validating existing fields as they will be dynamically added.
329
+
330
+ Args:
331
+ input_data (Optional): Input data to process.
332
+
333
+ Returns:
334
+ Set: Set of field names to exclude from validation.
335
+ """
195
336
return set ()
196
337
197
338
@classmethod
198
339
def _exclude_fields (cls ) -> Set :
340
+ """
341
+ Defines fields to exclude from serialization.
342
+
343
+ Returns:
344
+ Set: Set of field names to exclude.
345
+ """
199
346
return {"comments" , "datablock" , "_header" }
200
347
201
348
def _convert_value (
@@ -205,6 +352,18 @@ def _convert_value(
205
352
config : INISerializerConfig ,
206
353
save_settings : ModelSaveSettings ,
207
354
) -> str :
355
+ """
356
+ Converts a field value to its serialized string representation.
357
+
358
+ Args:
359
+ key (str): The field key.
360
+ v (Any): The field value.
361
+ config (INISerializerConfig): Configuration for serialization.
362
+ save_settings (ModelSaveSettings): Settings for saving the model.
363
+
364
+ Returns:
365
+ str: The serialized value.
366
+ """
208
367
if isinstance (v , bool ):
209
368
return str (int (v ))
210
369
elif isinstance (v , list ):
@@ -228,6 +387,16 @@ def _convert_value(
228
387
def _to_section (
229
388
self , config : INISerializerConfig , save_settings : ModelSaveSettings
230
389
) -> Section :
390
+ """
391
+ Converts the model to an INI section.
392
+
393
+ Args:
394
+ config (INISerializerConfig): Configuration for serialization.
395
+ save_settings (ModelSaveSettings): Settings for saving the model.
396
+
397
+ Returns:
398
+ Section: The INI section representation of the model.
399
+ """
231
400
props = []
232
401
for key , value in self :
233
402
if not self ._should_be_serialized (key , value , save_settings ):
@@ -248,6 +417,17 @@ def _to_section(
248
417
def _should_be_serialized (
249
418
self , key : str , value : Any , save_settings : ModelSaveSettings
250
419
) -> bool :
420
+ """
421
+ Determines if a field should be serialized.
422
+
423
+ Args:
424
+ key (str): The field key.
425
+ value (Any): The field value.
426
+ save_settings (ModelSaveSettings): Settings for saving the model.
427
+
428
+ Returns:
429
+ bool: True if the field should be serialized; otherwise, False.
430
+ """
251
431
if key in self ._exclude_fields ():
252
432
return False
253
433
@@ -269,18 +449,55 @@ def _should_be_serialized(
269
449
270
450
@staticmethod
271
451
def _is_union (field_type : type ) -> bool :
452
+ """
453
+ Checks if a type is a Union.
454
+
455
+ Args:
456
+ field_type (type): The type to check.
457
+
458
+ Returns:
459
+ bool: True if the type is a Union; otherwise, False.
460
+ """
272
461
return get_origin (field_type ) is Union
273
462
274
463
@staticmethod
275
464
def _union_has_filemodel (field_type : type ) -> bool :
465
+ """
466
+ Checks if a Union type includes a FileModel subtype.
467
+
468
+ Args:
469
+ field_type (type): The type to check.
470
+
471
+ Returns:
472
+ bool: True if the Union includes a FileModel; otherwise, False.
473
+ """
276
474
return any (issubclass (arg , FileModel ) for arg in get_args (field_type ))
277
475
278
476
@staticmethod
279
477
def _is_list (field_type : type ) -> bool :
478
+ """
479
+ Checks if a type is a list.
480
+
481
+ Args:
482
+ field_type (type): The type to check.
483
+
484
+ Returns:
485
+ bool: True if the type is a list; otherwise, False.
486
+ """
280
487
return get_origin (field_type ) is List
281
488
282
489
@staticmethod
283
490
def _value_is_not_none_or_type_is_filemodel (field_type : type , value : Any ) -> bool :
491
+ """
492
+ Checks if a value is not None or if its type is FileModel.
493
+
494
+ Args:
495
+ field_type (type): The expected type of the field.
496
+ value (Any): The value to check.
497
+
498
+ Returns:
499
+ bool: True if the value is valid; otherwise, False.
500
+ """
284
501
return value is not None or issubclass (field_type , FileModel )
285
502
286
503
0 commit comments