From d890541e52c8b70d583c600dc4041b49426f03cf Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Tue, 4 Aug 2026 16:29:51 +0200 Subject: [PATCH 1/2] feat: introduce Opaque type --- src/graphty/__init__.py | 1 + src/graphty/utils/types.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/graphty/__init__.py b/src/graphty/__init__.py index e9295d0..8743e83 100644 --- a/src/graphty/__init__.py +++ b/src/graphty/__init__.py @@ -8,3 +8,4 @@ from graphty.utils.exceptions import MissingGroupByError as MissingGroupByError from graphty.utils.types import Agg as Agg from graphty.utils.types import ConfigDict as ConfigDict +from graphty.utils.types import Opaque as Opaque diff --git a/src/graphty/utils/types.py b/src/graphty/utils/types.py index 33e8da7..060b97a 100644 --- a/src/graphty/utils/types.py +++ b/src/graphty/utils/types.py @@ -34,3 +34,15 @@ def __iter__(self) -> Iterator[Callable[[pl.Expr], pl.Expr]]: def apply_to(self, expr: pl.Expr) -> pl.Expr: return reduce(lambda x, y: y(x), self, expr) + + +class Opaque: + """Type for marking a field as opaque for the planner. + + For Pydantic fields typed with `Annotated[, Opaque()]` + the GraphTy planner will forward the entire projection as a pl.Struct + and otherwise ignore that field. + + Disengaging the planner for a field with `Opaque` + allows a before-validator to act on the raw bindings on the Python level. + """ From f02cf961670c3fedfc785bd37696cc5664f76456 Mon Sep 17 00:00:00 2001 From: Lukas Plank Date: Tue, 4 Aug 2026 16:30:07 +0200 Subject: [PATCH 2/2] feat: implement Opaque feature --- src/graphty/planner.py | 18 +++++++++++++----- src/graphty/utils/model_info.py | 4 +++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/graphty/planner.py b/src/graphty/planner.py index b457c90..39b2a93 100644 --- a/src/graphty/planner.py +++ b/src/graphty/planner.py @@ -28,7 +28,7 @@ is_pydantic_model_static_type, is_pydantic_model_union_static_type, ) -from graphty.utils.types import Agg +from graphty.utils.types import Agg, Opaque class ModelUnionDispatch: @@ -199,10 +199,7 @@ def run(self) -> pl.LazyFrame: model_projection: set[str] = model_info.model_projection if group_by is None: - if self.model.model_fields: - # TODO: when Opacity/planner disengagement is implemented, fields marked - # as Opaque should not count as "model fields" for this check — a model - # with only Opaque fields should also return the raw frame. + if self._has_actionable_fields: return self.lazy_frame.with_columns( *self._generate_expressions(model=self.model) ).drop(self._base_cols.difference(model_projection)) @@ -217,10 +214,21 @@ def run(self) -> pl.LazyFrame: def _base_cols(self) -> set[str]: return set(self.lazy_frame.collect_schema().names()) + @property + def _has_actionable_fields(self) -> bool: + return any( + get_metadata(field_info=field_info, cls=Opaque) is None + for _, field_info in self.model.model_fields.items() + ) + def _generate_expressions( self, model: type[BaseModel], group_context: bool = False ) -> Iterator[pl.Expr]: for field_name, field_info in model.model_fields.items(): + if get_metadata(field_info=field_info, cls=Opaque) is not None: + yield pl.struct(self._base_cols).alias(field_name) + continue + annotation = cast(TypeForm, field_info.annotation) if is_pydantic_model_static_type(annotation): diff --git a/src/graphty/utils/model_info.py b/src/graphty/utils/model_info.py index 6b7d4c9..fe0d1d1 100644 --- a/src/graphty/utils/model_info.py +++ b/src/graphty/utils/model_info.py @@ -3,7 +3,8 @@ from graphty.utils.alias_map import AliasMap from graphty.utils.exceptions import InvalidGroupByError -from graphty.utils.type_utils import is_structured_field_static_type +from graphty.utils.type_utils import get_metadata, is_structured_field_static_type +from graphty.utils.types import Opaque from pydantic import BaseModel @@ -22,6 +23,7 @@ def model_projection(self) -> set[str]: self.alias_map[field_name] for field_name, field_info in self.model.model_fields.items() if not is_structured_field_static_type(field_info.annotation) + and not get_metadata(field_info=field_info, cls=Opaque) } @cached_property