base.py
1 from typing import TYPE_CHECKING 2 from typing import Dict 3 from typing import List 4 from typing import Optional 5 from typing import Type 6 from typing import TypeVar 7 from typing import Union 8 9 from evidently._pydantic_compat import BaseModel 10 from evidently.legacy.options import ColorOptions 11 from evidently.legacy.options.agg_data import DataDefinitionOptions 12 from evidently.legacy.options.agg_data import RenderOptions 13 from evidently.legacy.options.option import Option 14 15 if TYPE_CHECKING: 16 from evidently._pydantic_compat import AbstractSetIntStr 17 from evidently._pydantic_compat import DictStrAny 18 from evidently._pydantic_compat import MappingIntStrAny 19 TypeParam = TypeVar("TypeParam", bound=Option) 20 21 22 class Options(BaseModel): 23 color: Optional[ColorOptions] = None 24 render: Optional[RenderOptions] = None 25 custom: Dict[Type[Option], Option] = {} 26 data_definition: Optional[DataDefinitionOptions] = None 27 28 @property 29 def color_options(self) -> ColorOptions: 30 return self.color or ColorOptions() 31 32 @property 33 def render_options(self) -> RenderOptions: 34 return self.render or RenderOptions() 35 36 @property 37 def data_definition_options(self) -> DataDefinitionOptions: 38 return self.data_definition or DataDefinitionOptions() 39 40 def get(self, option_type: Type[TypeParam]) -> TypeParam: 41 if option_type in _option_cls_mapping: 42 res = getattr(self, _option_cls_mapping[option_type]) 43 if res is None: 44 return option_type() 45 return res 46 if option_type in self.custom: 47 return self.custom[option_type] # type: ignore[return-value] 48 for possible_subclass in self.custom.keys(): 49 if issubclass(possible_subclass, option_type): 50 return self.custom[possible_subclass] # type: ignore[return-value] 51 return option_type() 52 53 @classmethod 54 def from_list(cls, values: List[Option]) -> "Options": 55 kwargs: Dict = {"custom": {}} 56 for value in values: 57 field = _option_cls_mapping.get(type(value), None) 58 if field is not None: 59 kwargs[field] = value 60 else: 61 kwargs["custom"][type(value)] = value 62 return Options(**kwargs) 63 64 @classmethod 65 def from_any_options(cls, options: "AnyOptions") -> "Options": 66 """Options can be provided as Options object, list of Option classes or raw dict""" 67 _options = None 68 if isinstance(options, dict): 69 _options = Options(**options) 70 if isinstance(options, Option): 71 options = [options] 72 if isinstance(options, list): 73 _options = Options.from_list(options) 74 if isinstance(options, Options): 75 _options = options 76 77 return _options or Options() 78 79 def override(self, other: "Options") -> "Options": 80 res = Options() 81 res.custom = self.custom.copy() 82 for key, value in other.custom.items(): 83 res.custom[key] = value 84 for name in self.__fields__: 85 if name == "custom": 86 continue 87 override = getattr(other, name) 88 if override is None: 89 override = getattr(self, name) 90 setattr(res, name, override) 91 92 return res 93 94 def __hash__(self): 95 value_pairs = [(f, getattr(self, f)) for f in self.__fields__ if f != "custom"] 96 value_pairs.extend(sorted(list(self.custom.items()))) 97 return hash((type(self),) + tuple(value_pairs)) 98 99 def dict( 100 self, 101 *, 102 include: Optional[Union["AbstractSetIntStr", "MappingIntStrAny"]] = None, 103 exclude: Optional[Union["AbstractSetIntStr", "MappingIntStrAny"]] = None, 104 by_alias: bool = False, 105 skip_defaults: Optional[bool] = None, 106 exclude_unset: bool = False, 107 exclude_defaults: bool = False, 108 exclude_none: bool = False, 109 ) -> "DictStrAny": 110 # todo 111 # for now custom options will not be saved at all 112 # if we want them to be saved, custom field needs to be Dict[str, Option] so it is json-able 113 if exclude is None: 114 exclude = {"custom"} 115 elif isinstance(exclude, set): 116 exclude.add("custom") 117 elif isinstance(exclude, dict): 118 exclude["custom"] = False 119 else: 120 raise TypeError("exclude must be either a dict or a set") 121 return super().dict( 122 include=include, 123 exclude=exclude, 124 by_alias=by_alias, 125 skip_defaults=skip_defaults, 126 exclude_unset=exclude_unset, 127 exclude_defaults=exclude_defaults, 128 exclude_none=exclude_none, 129 ) 130 131 132 _option_cls_mapping = {field.type_: name for name, field in Options.__fields__.items()} 133 134 AnyOptions = Union[Options, Option, dict, List[Option], None]