workfloworchestrator / pydantic-forms

Define JSON scheme with pydantic so a frontend can generate forms with pydantic validators
Apache License 2.0
8 stars 0 forks source link

Improve ReadOnlyField after Pydantic 2.x #16

Open Mark90 opened 10 months ago

Mark90 commented 10 months ago

Tasks

https://github.com/workfloworchestrator/pydantic-forms/pull/14#discussion_r1415813050_

https://github.com/workfloworchestrator/pydantic-forms/pull/14#discussion_r1415813304_

Sparrow1029 commented 7 months ago

Hello! Could this issue possibly be expanded to include "wrapped" types for ReadOnly field? For instance, our input forms for NSO "dry-runs" in v1.0 of orch-core (before pydantic-forms was the norm) were annotated like this:

class SomeProductInputForm(FormPage):
    dry_run_field: LongText = ReadOnly(dry_run_text)

which would appear correctly on the frontend UI as a disabled "long text" field...

If I change it to use the new style:

class SomeProductInputForm(FormPage):
    dry_run_field: ReadOnly(dry_run_text)

it is just a regular, horizontally scrollable text block, and if I attempt other configurations like:

    dry_run_field: Annotated[ReadOnly, LongText(dry_run_text)]
    # or
    dry_run_field: ReadOnly(dry_run_text) =  LongText(dry_run_text)
    # etc...

These configurations just error out, or result in the ReadOnly attribute being ignored. I believe maybe there should be a way to annotate something like

    dry_run_field: ReadOnly(LongText(dry_run_text))

or similar, in which the _read_only_field fn merges the json_schema object to contain both "disabled": true and "format": "longText" (or "organisationId", etc)

This may be the wrong place to ask this question, but is there already a recommended method to achieve this, and, if not, does this seem like a good idea?

Mark90 commented 7 months ago

Hey! Sorry for the late reply. Yeah, that is a good suggestion, I'll add it.

The problem is indeed that the LongText json schema should be merged with that of the ReadOnlyField.

This is something that pydantic v2 intentionally doesn't do, because it is not trivial to handle conflicting keys in the schema.

I've written a workaround function somewhere that takes 2 annotated types with json schema's and returns them combined, but I have no idea where 😅 I'll keep looking for it

Mark90 commented 7 months ago

Found it!

@Sparrow1029 This should do it as a workaround. I recommend to add the code to a separate file

from typing import Annotated, Any, Iterable, get_args

from more_itertools import first
from pydantic import Field
from pydantic.fields import FieldInfo

def _get_field_info_with_schema(type_: Any) -> Iterable[FieldInfo]:
    for annotation in get_args(type_):
        if isinstance(annotation, FieldInfo) and annotation.json_schema_extra:
            yield annotation

def update_json_schema(type_: Any, json_schema: dict[str, Any]) -> Any:
    """Add json_schema to type_'s annotations.

    Existing json schema annotations are updated in a dict.update() fashion.
    """
    if not (field_info := first(_get_field_info_with_schema(type_), None)):
        return Annotated[type_, Field(json_schema_extra=json_schema)]

    match field_info.json_schema_extra:
        case dict() as existing_schema:
            existing_schema.update(json_schema)
        case _ as unknown:
            raise TypeError(f"Cannot update json_schema_extra of type {type(unknown)}")
    return type_

def merge_json_schema(target_type: Any, source_type: Any) -> Any:
    """Add json_schema from source_type to target_type."""
    if not (source_field_info := first(_get_field_info_with_schema(source_type), None)):
        raise TypeError("Source type has no json_schema_extra")

    match source_field_info.json_schema_extra:
        case dict() as existing_source_schema:
            return update_json_schema(target_type, existing_source_schema)
        case _ as unknown:
            raise TypeError(f"Cannot merge source_type json_schema_extra from type {type(unknown)}")

This is an example usage;

class SomeProductInputForm(FormPage):
    dry_run_field: merge_json_schema(ReadOnlyField(dry_run_text), LongText)  # type: ignore[valid-type]

The # type: ignore is only needed if you're using a type checker like mypy.

Sparrow1029 commented 7 months ago

@Mark90 Thanks for the code snippet! That will definitely work for my use case 👍