From 09c8efe3c8fc439a543e6e31e1745ee723a53342 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 22 Feb 2024 17:31:32 +0400 Subject: [PATCH 1/3] Add exclude tag for field path --- setup.cfg | 3 +++ src/evidently/pydantic_utils.py | 34 +++++++++++++++++++++++++++--- tests/utils/test_pydantic_utils.py | 30 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index b0fa42f141..cf4488bf9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,9 @@ ignore_missing_imports = True [mypy-dynaconf.*] ignore_missing_imports = True +[mypy-litestar.*] +ignore_missing_imports = True + [tool:pytest] testpaths=tests python_classes=*Test diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index c9a35bea97..1a0f569889 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from evidently._pydantic_compat import DictStrAny from evidently._pydantic_compat import Model + from evidently.core import IncludeTags T = TypeVar("T") @@ -229,23 +230,50 @@ def child(self, item: str) -> "FieldPath": return FieldPath(self._path + [item], field_value, is_mapping=True) return FieldPath(self._path + [item], field_value, is_mapping=is_mapping) - def list_nested_fields(self) -> List[str]: + @staticmethod + def _get_field_tags_rec(mro, name): + from evidently.base_metric import BaseResult + + cls = mro[0] + if not issubclass(cls, BaseResult): + return None + if name in cls.__config__.field_tags: + return cls.__config__.field_tags[name] + return FieldPath._get_field_tags_rec(mro[1:], name) + + @staticmethod + def _get_field_tags(cls, name, type_) -> Optional[Set["IncludeTags"]]: + from evidently.base_metric import BaseResult + + if not issubclass(cls, BaseResult): + return None + field_tags = FieldPath._get_field_tags_rec(cls.__mro__, name) + if field_tags is not None: + return field_tags + if isinstance(type_, type) and issubclass(type_, BaseResult): + return type_.__config__.tags + return set() + + def list_nested_fields(self, exclude: Set["IncludeTags"] = None) -> List[str]: if not isinstance(self._cls, type) or not issubclass(self._cls, BaseModel): return [repr(self)] res = [] for name, field in self._cls.__fields__.items(): field_value = field.type_ + field_tags = self._get_field_tags(self._cls, name, field_value) + if field_tags is not None and (exclude is not None and any(t in exclude for t in field_tags)): + continue is_mapping = field.shape == SHAPE_DICT if self.has_instance: field_value = getattr(self._instance, name) if is_mapping and isinstance(field_value, dict): for key, value in field_value.items(): - res.extend(FieldPath(self._path + [name, str(key)], value).list_nested_fields()) + res.extend(FieldPath(self._path + [name, str(key)], value).list_nested_fields(exclude=exclude)) continue else: if is_mapping: name = f"{name}.*" - res.extend(FieldPath(self._path + [name], field_value).list_nested_fields()) + res.extend(FieldPath(self._path + [name], field_value).list_nested_fields(exclude=exclude)) return res def __repr__(self): diff --git a/tests/utils/test_pydantic_utils.py b/tests/utils/test_pydantic_utils.py index 0dad8eac90..a6ee62f749 100644 --- a/tests/utils/test_pydantic_utils.py +++ b/tests/utils/test_pydantic_utils.py @@ -5,6 +5,7 @@ from evidently.base_metric import Metric from evidently.base_metric import MetricResult +from evidently.core import IncludeTags from evidently.pydantic_utils import PolymorphicModel @@ -130,3 +131,32 @@ class Config: obj = parse_obj_as(SomeModel, {"type": "othersubclass"}) assert obj.__class__ == SomeOtherSubclass + + +def test_include_exclude(): + class SomeModel(MetricResult): + class Config: + field_tags = {"f1": {IncludeTags.Render}} + + f1: str + f2: str + + assert SomeModel.fields.list_nested_fields(exclude={IncludeTags.Render, IncludeTags.TypeField}) == ["f2"] + # assert SomeModel.fields.list_nested_fields(include={IncludeTags.Render}) == ["f1"] + + class SomeNestedModel(MetricResult): + class Config: + tags = {IncludeTags.Render} + + f1: str + + class SomeOtherModel(MetricResult): + f1: str + f2: SomeNestedModel + f3: SomeModel + + assert SomeOtherModel.fields.list_nested_fields(exclude={IncludeTags.Render, IncludeTags.TypeField}) == [ + "f1", + "f3.f2", + ] + # assert SomeOtherModel.fields.list_nested_fields(include={IncludeTags.Render}) == ["f2.f1", "f3.f1"] From a24426b22f71df1a17437db7e8e831085666c30f Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 22 Feb 2024 20:36:16 +0400 Subject: [PATCH 2/3] add get fields tags by path --- src/evidently/pydantic_utils.py | 13 ++++++++++++ tests/utils/test_pydantic_utils.py | 32 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index 1a0f569889..ab4cce5941 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -288,6 +288,19 @@ def __dir__(self) -> Iterable[str]: res.extend(self.list_fields()) return res + def get_field_tags(self, path: List[str]) -> Optional[Set["IncludeTags"]]: + from evidently.base_metric import BaseResult + + if not isinstance(self._cls, type) or not issubclass(self._cls, BaseResult): + return None + self_tags = self._cls.__config__.tags + if len(path) == 0: + return self_tags + field_name, *path = path + + field_tags = self._get_field_tags(self._cls, field_name, self._cls.__fields__[field_name].type_) or set() + return self_tags.union(field_tags).union(self.child(field_name).get_field_tags(path) or tuple()) + @pydantic_type_validator(FieldPath) def series_validator(value): diff --git a/tests/utils/test_pydantic_utils.py b/tests/utils/test_pydantic_utils.py index a6ee62f749..d107a7510d 100644 --- a/tests/utils/test_pydantic_utils.py +++ b/tests/utils/test_pydantic_utils.py @@ -160,3 +160,35 @@ class SomeOtherModel(MetricResult): "f3.f2", ] # assert SomeOtherModel.fields.list_nested_fields(include={IncludeTags.Render}) == ["f2.f1", "f3.f1"] + + +def test_get_field_tags(): + class SomeModel(MetricResult): + class Config: + field_tags = {"f1": {IncludeTags.Render}} + + f1: str + f2: str + + assert SomeModel.fields.get_field_tags(["type"]) == {IncludeTags.TypeField} + assert SomeModel.fields.get_field_tags(["f1"]) == {IncludeTags.Render} + assert SomeModel.fields.get_field_tags(["f2"]) == set() + + class SomeNestedModel(MetricResult): + class Config: + tags = {IncludeTags.Render} + + f1: str + + class SomeOtherModel(MetricResult): + f1: str + f2: SomeNestedModel + f3: SomeModel + + assert SomeOtherModel.fields.get_field_tags(["type"]) == {IncludeTags.TypeField} + assert SomeOtherModel.fields.get_field_tags(["f1"]) == set() + assert SomeOtherModel.fields.get_field_tags(["f2"]) == {IncludeTags.Render} + assert SomeOtherModel.fields.get_field_tags(["f2", "f1"]) == {IncludeTags.Render} + assert SomeOtherModel.fields.get_field_tags(["f3"]) == set() + assert SomeOtherModel.fields.get_field_tags(["f3", "f1"]) == {IncludeTags.Render} + assert SomeOtherModel.fields.get_field_tags(["f3", "f2"]) == set() From d4b0d6728e500936fa278fbb40adc16929e3c6a6 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Fri, 23 Feb 2024 19:17:42 +0400 Subject: [PATCH 3/3] List fields with tags --- src/evidently/pydantic_utils.py | 29 +++++++++++++++++++++++ tests/utils/test_pydantic_utils.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/evidently/pydantic_utils.py b/src/evidently/pydantic_utils.py index ab4cce5941..b680e9ce5d 100644 --- a/src/evidently/pydantic_utils.py +++ b/src/evidently/pydantic_utils.py @@ -11,6 +11,7 @@ from typing import List from typing import Optional from typing import Set +from typing import Tuple from typing import Type from typing import TypeVar from typing import Union @@ -276,6 +277,34 @@ def list_nested_fields(self, exclude: Set["IncludeTags"] = None) -> List[str]: res.extend(FieldPath(self._path + [name], field_value).list_nested_fields(exclude=exclude)) return res + def _list_with_tags(self, current_tags: Set["IncludeTags"]) -> List[Tuple[str, Set["IncludeTags"]]]: + if not isinstance(self._cls, type) or not issubclass(self._cls, BaseModel): + return [(repr(self), current_tags)] + res = [] + for name, field in self._cls.__fields__.items(): + field_value = field.type_ + field_tags = self._get_field_tags(self._cls, name, field_value) or set() + + is_mapping = field.shape == SHAPE_DICT + if self.has_instance: + field_value = getattr(self._instance, name) + if is_mapping and isinstance(field_value, dict): + for key, value in field_value.items(): + res.extend( + FieldPath(self._path + [name, str(key)], value)._list_with_tags( + current_tags.union(field_tags) + ) + ) + continue + else: + if is_mapping: + name = f"{name}.*" + res.extend(FieldPath(self._path + [name], field_value)._list_with_tags(current_tags.union(field_tags))) + return res + + def list_nested_fields_with_tags(self) -> List[Tuple[str, Set["IncludeTags"]]]: + return self._list_with_tags(set()) + def __repr__(self): return self.get_path() diff --git a/tests/utils/test_pydantic_utils.py b/tests/utils/test_pydantic_utils.py index d107a7510d..575f08532e 100644 --- a/tests/utils/test_pydantic_utils.py +++ b/tests/utils/test_pydantic_utils.py @@ -142,6 +142,7 @@ class Config: f2: str assert SomeModel.fields.list_nested_fields(exclude={IncludeTags.Render, IncludeTags.TypeField}) == ["f2"] + # assert SomeModel.fields.list_nested_fields(include={IncludeTags.Render}) == ["f1"] class SomeNestedModel(MetricResult): @@ -192,3 +193,39 @@ class SomeOtherModel(MetricResult): assert SomeOtherModel.fields.get_field_tags(["f3"]) == set() assert SomeOtherModel.fields.get_field_tags(["f3", "f1"]) == {IncludeTags.Render} assert SomeOtherModel.fields.get_field_tags(["f3", "f2"]) == set() + + +def test_list_with_tags(): + class SomeModel(MetricResult): + class Config: + field_tags = {"f1": {IncludeTags.Render}} + + f1: str + f2: str + + assert SomeModel.fields.list_nested_fields_with_tags() == [ + ("type", {IncludeTags.TypeField}), + ("f1", {IncludeTags.Render}), + ("f2", set()), + ] + + class SomeNestedModel(MetricResult): + class Config: + tags = {IncludeTags.Render} + + f1: str + + class SomeOtherModel(MetricResult): + f1: str + f2: SomeNestedModel + f3: SomeModel + + assert SomeOtherModel.fields.list_nested_fields_with_tags() == [ + ("type", {IncludeTags.TypeField}), + ("f1", set()), + ("f2.type", {IncludeTags.Render, IncludeTags.TypeField}), + ("f2.f1", {IncludeTags.Render}), + ("f3.type", {IncludeTags.TypeField}), + ("f3.f1", {IncludeTags.Render}), + ("f3.f2", set()), + ]