2323
2424import logging
2525import warnings
26- from abc import ABC
27- from dataclasses import asdict , dataclass , field , fields
26+ from dataclasses import MISSING , dataclass , field , fields
2827from datetime import datetime
29- from typing import Dict , Optional , Tuple , Union
28+ from typing import Any , Dict , Optional , Tuple
3029
3130import numpy as np
3231import pandas as pd
32+ import yaml
3333
3434LOGGER = logging .getLogger (__name__ )
3535
36+ __all__ = [
37+ "HazardModifierConfig" ,
38+ "ExposuresModifierConfig" ,
39+ "ImpfsetModifierConfig" ,
40+ "CostIncomeConfig" ,
41+ "MeasureConfig" ,
42+ ]
43+
3644
3745@dataclass
38- class _ModifierConfig :
46+ class ModifierConfig :
3947 """
4048 Abstract base class for all modifier configuration dataclasses.
4149
@@ -44,7 +52,7 @@ class _ModifierConfig:
4452 be instantiated directly.
4553 """
4654
47- def _filter_out_default_fields (self ) -> dict [str , Any ]:
55+ def _filter_out_default_fields (self ) -> tuple [ dict [str , Any ], dict [ str , Any ] ]:
4856 """
4957 Partition the instance's fields into non-default and default groups.
5058
@@ -64,7 +72,7 @@ def _filter_out_default_fields(self) -> dict[str, Any]:
6472 for defined_field in fields (self ):
6573 val = getattr (self , defined_field .name )
6674 default = defined_field .default
67- if default is MISSING :
75+ if defined_field . default_factory is not MISSING :
6876 default = defined_field .default_factory ()
6977
7078 if val != default :
@@ -76,7 +84,7 @@ def _filter_out_default_fields(self) -> dict[str, Any]:
7684 non_defaults .pop ("haz_type" )
7785 return non_defaults , defaults
7886
79- def to_dict (self , omit_default : bool = True ) -> dict [str , Any ]:
87+ def to_dict (self , omit_default : bool = True ) -> dict [str , Any ]:
8088 """
8189 Serialize the config to a flat dictionary, omitting default values.
8290
@@ -89,8 +97,11 @@ def to_dict(self, omit_default : bool = True) -> dict[str, Any]:
8997 Dictionary containing only fields whose values differ from
9098 their dataclass defaults.
9199 """
92- non_default , _ = self ._filter_out_default_fields ()
93- return non_default
100+ non_defaults , defaults = self ._filter_out_default_fields ()
101+ if omit_default :
102+ return non_defaults
103+
104+ return defaults | non_defaults
94105
95106 @classmethod
96107 def from_dict (cls , kwargs_dict : dict ):
@@ -129,8 +140,8 @@ def _filter_dict_to_fields(cls, to_filter: dict):
129140 dataclass fields on this class.
130141 """
131142
132- fields = [f .name for f in fields (cls )]
133- return {key : val for key , val in to_filter .items () if key in fields }
143+ field_names = [f .name for f in fields (cls )]
144+ return {key : val for key , val in to_filter .items () if key in field_names }
134145
135146 def __repr__ (self ) -> str :
136147 """
@@ -166,7 +177,7 @@ def __repr__(self) -> str:
166177
167178
168179@dataclass (repr = False )
169- class ImpfsetModifierConfig (_ModifierConfig ):
180+ class ImpfsetModifierConfig (ModifierConfig ):
170181 """
171182 Configuration for modifications to an impact function set.
172183
@@ -242,7 +253,7 @@ def __post_init__(self):
242253
243254
244255@dataclass (repr = False )
245- class HazardModifierConfig (_ModifierConfig ):
256+ class HazardModifierConfig (ModifierConfig ):
246257 """
247258 Configuration for modifications to a hazard.
248259
@@ -303,7 +314,7 @@ def __post_init__(self):
303314
304315
305316@dataclass (repr = False )
306- class ExposuresModifierConfig (_ModifierConfig ):
317+ class ExposuresModifierConfig (ModifierConfig ):
307318 """
308319 Configuration for modifications to an exposures object.
309320
@@ -349,7 +360,7 @@ def __post_init__(self):
349360
350361
351362@dataclass (repr = False )
352- class CostIncomeConfig (_ModifierConfig ):
363+ class CostIncomeConfig (ModifierConfig ):
353364 """
354365 Serializable configuration for a ``CostIncome`` object.
355366
@@ -371,13 +382,14 @@ class CostIncomeConfig(_ModifierConfig):
371382 income_yearly_growth_rate : float, optional
372383 Annual growth rate applied to periodic income. Default is ``0.0``.
373384 freq : str, optional
374- Pandas offset alias defining the period length (e.g. ``"Y"`` for
385+ Pandas period alias defining the period length (e.g. ``"Y"`` for
375386 yearly, ``"M"`` for monthly). Default is ``"Y"``.
387+ See [pandas documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#period-aliases).
376388 custom_cash_flows : list of dict, optional
377389 Explicit cash flow schedule as a list of records with at minimum
378390 a ``"date"`` key (ISO 8601 string) and a value key. If provided,
379391 overrides the periodic cost/income logic.
380- """
392+ """ # noqa
381393
382394 mkt_price_year : Optional [int ] = field (default_factory = lambda : datetime .today ().year )
383395 init_cost : float = 0.0
@@ -426,7 +438,7 @@ def from_cost_income(cls, cost_income: "CostIncome") -> "CostIncomeConfig":
426438
427439
428440@dataclass (repr = False )
429- class MeasureConfig (_ModifierConfig ):
441+ class MeasureConfig (ModifierConfig ):
430442 """
431443 Top-level serializable configuration for a single adaptation measure.
432444
@@ -453,7 +465,7 @@ class MeasureConfig(_ModifierConfig):
453465 cost_income : CostIncomeConfig
454466 Financial parameters associated with implementing this measure.
455467 implementation_duration : str, optional
456- Pandas offset alias (e.g. ``"2Y"``) representing the time before
468+ Pandas period alias (e.g. ``"2Y"``) representing the time before
457469 the measure is fully operational. If ``None``, the measure takes
458470 effect immediately.
459471 color_rgb : tuple of float, optional
@@ -486,7 +498,7 @@ def __repr__(self) -> str:
486498 fields_str = "\n \t " .join (f"{ k } ={ v !r} " for k , v in self .__dict__ .items ())
487499 return f"{ self .__class__ .__name__ } (\n \t { fields_str } )"
488500
489- def to_dict (self ) -> dict :
501+ def to_dict (self , omit_default : bool = True ) -> dict :
490502 """
491503 Serialize the measure configuration to a flat dictionary.
492504
@@ -504,10 +516,10 @@ def to_dict(self) -> dict:
504516 return {
505517 "name" : self .name ,
506518 "haz_type" : self .haz_type ,
507- ** self .impfset_modifier .to_dict (),
508- ** self .hazard_modifier .to_dict (),
509- ** self .exposures_modifier .to_dict (),
510- ** self .cost_income .to_dict (),
519+ ** self .impfset_modifier .to_dict (omit_default ),
520+ ** self .hazard_modifier .to_dict (omit_default ),
521+ ** self .exposures_modifier .to_dict (omit_default ),
522+ ** self .cost_income .to_dict (omit_default ),
511523 "implementation_duration" : self .implementation_duration ,
512524 "color_rgb" : list (self .color_rgb ) if self .color_rgb is not None else None ,
513525 }
@@ -579,8 +591,6 @@ def to_yaml(self, path: str) -> None:
579591 Destination file path. Will be created or overwritten.
580592 """
581593
582- import yaml
583-
584594 with open (path , "w" ) as opened_file :
585595 yaml .dump (
586596 {"measures" : [self .to_dict ()]},
@@ -609,8 +619,6 @@ def from_yaml(cls, path: str) -> "MeasureConfig":
609619 ``measures``.
610620 """
611621
612- import yaml
613-
614622 with open (path ) as opened_file :
615623 return cls .from_dict (yaml .safe_load (opened_file )["measures" ][0 ])
616624
0 commit comments