Skip to content

Commit fcefc31

Browse files
committed
add docstring to the INIBasedModel class
1 parent 8d721a6 commit fcefc31

File tree

1 file changed

+226
-9
lines changed

1 file changed

+226
-9
lines changed

hydrolib/core/dflowfm/ini/models.py

Lines changed: 226 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,57 @@ class INIBasedModel(BaseModel, ABC):
5252
"""INIBasedModel defines the base model for blocks/chapters
5353
inside an INIModel (*.ini file).
5454
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.
5960
6061
Attributes:
6162
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.
64106
"""
65107

66108
_header: str = ""
@@ -75,14 +117,33 @@ class Config:
75117

76118
@classmethod
77119
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+
"""
78127
return UnknownKeywordErrorManager()
79128

80129
@classmethod
81130
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+
"""
82137
return True
83138

84139
@classmethod
85140
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+
"""
86147
return False
87148

88149
@classmethod
@@ -107,6 +168,9 @@ def get_list_field_delimiter(cls, field_key: str) -> str:
107168
108169
Args:
109170
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.
110174
"""
111175
delimiter = None
112176
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:
117181
return delimiter
118182

119183
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+
"""
121196

122197
class Config:
123198
extra = Extra.allow
@@ -127,6 +202,18 @@ class Config:
127202

128203
@root_validator(pre=True)
129204
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+
"""
130217
unknown_keyword_error_manager = cls._get_unknown_keyword_error_manager()
131218
do_not_validate = cls._exclude_from_validation(values)
132219
if unknown_keyword_error_manager:
@@ -140,7 +227,16 @@ def _validate_unknown_keywords(cls, values):
140227

141228
@root_validator(pre=True)
142229
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+
"""
144240
dropkeys = []
145241
for k, v in values.items():
146242
if v is None and k in cls.__fields__.keys():
@@ -157,20 +253,48 @@ def _skip_nones_and_set_header(cls, values):
157253

158254
@validator("comments", always=True, allow_reuse=True)
159255
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+
"""
160265
if not cls._supports_comments() and v is not None:
161266
logging.warning(f"Dropped unsupported comments from {cls.__name__} init.")
162267
v = None
163268
return v
164269

165270
@validator("*", pre=True, allow_reuse=True)
166271
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+
"""
167282
if field.type_ != float:
168283
return value
169284

170285
return cls._replace_fortran_scientific_notation(value)
171286

172287
@classmethod
173288
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+
"""
174298
if isinstance(value, str):
175299
return cls._scientific_notation_regex.sub(r"\1e\3", value)
176300
if isinstance(value, list):
@@ -182,6 +306,15 @@ def _replace_fortran_scientific_notation(cls, value):
182306

183307
@classmethod
184308
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+
"""
185318
if isinstance(value, Section):
186319
value = value.flatten(
187320
cls._duplicate_keys_as_list(), cls._supports_comments()
@@ -191,11 +324,25 @@ def validate(cls: Type["INIBasedModel"], value: Any) -> "INIBasedModel":
191324

192325
@classmethod
193326
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+
"""
195336
return set()
196337

197338
@classmethod
198339
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+
"""
199346
return {"comments", "datablock", "_header"}
200347

201348
def _convert_value(
@@ -205,6 +352,18 @@ def _convert_value(
205352
config: INISerializerConfig,
206353
save_settings: ModelSaveSettings,
207354
) -> 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+
"""
208367
if isinstance(v, bool):
209368
return str(int(v))
210369
elif isinstance(v, list):
@@ -228,6 +387,16 @@ def _convert_value(
228387
def _to_section(
229388
self, config: INISerializerConfig, save_settings: ModelSaveSettings
230389
) -> 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+
"""
231400
props = []
232401
for key, value in self:
233402
if not self._should_be_serialized(key, value, save_settings):
@@ -248,6 +417,17 @@ def _to_section(
248417
def _should_be_serialized(
249418
self, key: str, value: Any, save_settings: ModelSaveSettings
250419
) -> 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+
"""
251431
if key in self._exclude_fields():
252432
return False
253433

@@ -269,18 +449,55 @@ def _should_be_serialized(
269449

270450
@staticmethod
271451
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+
"""
272461
return get_origin(field_type) is Union
273462

274463
@staticmethod
275464
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+
"""
276474
return any(issubclass(arg, FileModel) for arg in get_args(field_type))
277475

278476
@staticmethod
279477
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+
"""
280487
return get_origin(field_type) is List
281488

282489
@staticmethod
283490
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+
"""
284501
return value is not None or issubclass(field_type, FileModel)
285502

286503

0 commit comments

Comments
 (0)