Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Serialize query lookups with pydantic_core based on the provided schema #49

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 3 additions & 19 deletions django_pydantic_field/v2/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
import pydantic
from django.core import checks, exceptions
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.expressions import BaseExpression, Col, Value
from django.db.models.expressions import BaseExpression, Value
from django.db.models.fields import NOT_PROVIDED
from django.db.models.fields.json import JSONField
from django.db.models.lookups import Transform
from django.db.models.query_utils import DeferredAttribute

from django_pydantic_field.compat import deprecation
from django_pydantic_field.compat.django import GenericContainer

from . import forms, types
from . import forms, types, lookups

if ty.TYPE_CHECKING:
import json
Expand Down Expand Up @@ -174,7 +173,7 @@ def get_prep_value(self, value: ty.Any):
def get_transform(self, lookup_name: str):
transform: ty.Any = super().get_transform(lookup_name)
if transform is not None:
transform = SchemaKeyTransformAdapter(transform)
transform = lookups.SchemaKeyTransformAdapter(transform, lookup_name)
return transform

def get_default(self) -> types.ST:
Expand All @@ -198,21 +197,6 @@ def value_to_string(self, obj: Model):
return self.get_prep_value(value)


class SchemaKeyTransformAdapter:
"""An adapter for creating key transforms for schema field lookups."""

def __init__(self, transform: type[Transform]):
self.transform = transform

def __call__(self, col: Col | None = None, *args, **kwargs) -> Transform | None:
"""All transforms should bypass the SchemaField's adaptaion with `get_prep_value`,
and routed to JSONField's `get_prep_value` for further processing."""
if isinstance(col, BaseExpression):
col = col.copy()
col.output_field = super(PydanticSchemaField, col.output_field) # type: ignore
return self.transform(col, *args, **kwargs)


@ty.overload
def SchemaField(
schema: type[types.ST | None] | ty.ForwardRef = ...,
Expand Down
43 changes: 43 additions & 0 deletions django_pydantic_field/v2/lookups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

import typing as ty

from django.db.models.expressions import BaseExpression, Col
from django.db.models.lookups import Transform

if ty.TYPE_CHECKING:
ExprT = ty.TypeVar("ExprT", bound=BaseExpression)


class SchemaKeyTransformAdapter:
"""An adapter class that modifies the key transformation behavior for schema fields.
This class acts as an adapter, altering how key transformations are performed on schema fields.
It circumvents the usual adaptation process for `PydanticSchemaField` objects,
instead opting to use the `JSONField`'s own transformation methods.
The goal is to utilize the lookup transformation features provided by the `JSONField.encoder` class.
With the current limitations in Pydantic, it's not feasible to conduct partial value adaptations.
While this approach is not ideal for QuerySet lookups,
it allows `JSONField.encoder` (which defaults to `DjangoJSONEncoder`) to perform essential transformations.
"""

def __init__(self, transform: type[Transform], lookup_name: str):
self.transform = transform
self.lookup_name = lookup_name

def __call__(self, col: Col | None = None, *args, **kwargs) -> Transform | None:
"""All transforms should bypass the SchemaField's adaptaion with `get_prep_value`,
and routed to JSONField's `get_prep_value` for further processing."""
if isinstance(col, BaseExpression):
col = self._get_prep_expression(col)
return self.transform(col, *args, **kwargs)

def _get_prep_expression(self, expr: ExprT) -> ExprT:
from .fields import PydanticSchemaField

if isinstance(expr.output_field, PydanticSchemaField):
expr = expr.copy()
expr.output_field = super(PydanticSchemaField, expr.output_field) # type: ignore
return expr