Skip to content

'Partial' Equivalent #1673

Closed as not planned
Closed as not planned
@kalzoo

Description

@kalzoo

Question

Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())":

             pydantic version: 1.5.1
            pydantic compiled: True
                 install path: /.../venv/lib/python3.7/site-packages/pydantic
               python version: 3.7.4 (default, Sep  7 2019, 18:27:02)  [Clang 10.0.1 (clang-1001.0.46.4)]
                     platform: Darwin-19.4.0-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']

Hey Samuel & team,

First of all, huge fan of Pydantic, it makes working in Python so much better. I've been through the docs and many of the issues several times, and until this point I've found all the answers I was looking for. If I missed one in this case - sorry in advance!

Looking to be able to transform a model at runtime to another with the same fields but where all are Optional, akin to Typescript's Partial (hence the issue title). Something similar is discussed in part in at least one FastAPI issue, but since the answer there is basically "duplicate your model" and since this is inherently a pydantic thing, I thought I'd ask here.

By way of a little bit of hacking on model internals, I've been able to get the functionality I need for a relatively trivial use case:

from __future__ import annotations

from pydantic import BaseModel, create_model
from typing import Optional

class BaseSchemaModel(BaseModel):
    # ...

    @classmethod
    def to_partial(cls) -> BaseSchemaModel:
        return get_partial_model(cls)


def get_partial_model(model: BaseModel) -> BaseModel:
    """
    Return a model similar to the original, but where all fields are Optional.

    Note: this minimal implementation means that many Pydantic features will be discarded
    (such as alias). This is not by design and could stand to improve.
    """
    new_fields = {
        name: (Optional[model.__annotations__.get(name)], model.__fields__[name].default or None)
        for name in model.__fields__
    }
    return create_model("GeneratedPartialModel", **new_fields, __config__=model.__config__)

verified with a simple test:

import pytest

from pydantic import ValidationError
from typing import Optional

from app.utilities.pydantic import BaseSchemaModel


def test_partial_model():
    class TestModel(BaseSchemaModel):
        a: int
        b: int = 2
        c: str
        d: Optional[str]

    with pytest.raises(ValidationError):
        TestModel()

    model = TestModel(a=1, c='hello')
    assert model.dict() == {'a': 1, 'b': 2, 'c': 'hello', 'd': None}

    PartialTestModel = TestModel.to_partial()

    partial_model = PartialTestModel()
    assert partial_model.dict() == {'a': None, 'b': 2, 'c': None, 'd': None}

However this doesn't support much of Pydantic's functionality (aliases, for a start). In the interest of a better issue report and possibly a PR, I tried a couple other things, but in the time I allocated did not get them to work:

from pydantic import BaseModel, create_model
from pydantic.fields import ModelField

class Original(BaseModel):
  a: int
  b: str
  c: Optional[str]

class PartialModel(BaseModel):
  a: Optional[int]
  b: Optional[str]
  c: Optional[str]

def copy_model_field(field: ModelField, **kwargs) -> ModelField:
    params = (
      'name',
      'type_',
      'class_validators',
      'model_config',
      'default',
      'default_factory',
      'required',
      'alias',
      'field_info'
    )
    return ModelField(**{param: kwargs.get(param, getattr(field, param)) for param in params})

# Doesn't work - ModelField not acceptable in place of FieldInfo
GeneratedPartialModel = create_model('GeneratedPartialModel', **{name: copy_model_field(field, required=False) for name, field in Original.__fields__.items()})

# Doesn't work - field_info doesn't contain all the necessary information
GeneratedPartialModel = create_model('GeneratedPartialModel', **{name: copy_model_field(field, required=False).field_info for name, field in Original.__fields__.items()})

# This works for my use case - but without aliases and probably without some other functionality as well
new_fields = {name: (Optional[Original.__annotations__.get(name)], Original.__fields__[name].default or None) for name in Original.__fields__}
GeneratedPartialModel = create_model('GeneratedPartialModel', **new_fields)

Would be happy to put in a PR for this, if

a. it doesn't exist already
b. it would be useful
c. I knew where that would best fit - on ModelMetaclass? BaseModel? A utility function to_partial?

Thanks!

Metadata

Metadata

Assignees

Labels

deferredDeferred until future release or until something else gets donefeature request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions