/ src / evidently / legacy / options / base.py
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]