-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
'Partial' Equivalent #1673
Comments
Generally I would make the Partial model a subclass of the Original: from typing import Optional
from pydantic import BaseModel
class Original(BaseModel):
a: int
b: str
c: Optional[str]
class PartialModel(Original):
a: Optional[int]
b: Optional[str]
c: Optional[str] which retains the validation and everything else about the original, but makes all the fields it defines Optional. In : Original(a=1, b="two")
Out: Original(a=1, b='two', c=None)
In : PartialModel(b="three")
Out: PartialModel(a=None, b='three', c=None) Sinc you already have some optional fields in your original, you don't need to redefine them, though it does help with explicitness: In : class PartialModel(Original):
...: a: Optional[int]
...: b: Optional[str]
...:
In : PartialModel(b="four")
Out: PartialModel(a=None, b='four', c=None) |
Thanks @StephenBrown2 - yeah, that’s what was suggested in the linked FastAPI issue and is what I’d consider the simplest approach. Simplicity is a plus but it has two important caveats:
|
I would agree, though:
Even with that said, I can see the benefit of a generated Partial model, for update requests for example. I also don't know the best way to go about that, though, so I'll defer to @samuelcolvin for his thoughts. |
Hi @kalzoo, we're using a different method that does not require code duplication: First define your Partial model without Optionals, but with a default value equal to a missing sentinel (#1761). That should allow you to create objects where the supplied fields must validate, and where the omitted fields are equal to the sentinel value. Then, create a Full model by subclassing the Partial model without redefining the fields, and adding a Config class that sets validate_all to True so that even omitted fields are validated. |
hey class Clonable(BaseModel):
@classmethod
def partial(cls):
return cls.clone(to_optional='__all__')
@classmethod
def clone(
cls,
*,
fields: Set[str] = None,
exclude: Set[str] = None,
to_optional: Union[Literal['__all__'], Set[str], Dict[str, Any]] = None
) -> 'Clonable':
if fields is None:
fields = set(cls.__fields__.keys())
if exclude is None:
exclude = set()
if to_optional == '__all__':
opt = {f: None for f in fields}
opt.update(cls.__field_defaults__)
elif isinstance(to_optional, set):
opt = {f: None for f in to_optional}
opt.update(cls.__field_defaults__)
else:
opt = cls.__field_defaults__.copy()
opt.update(to_optional or {})
model = create_model(
cls.__name__,
__base__=Clonable,
**{
field: (cls.__annotations__[field], opt.get(field, ...))
for field in fields - exclude
}
)
model.__name__ += str(id(model))
return model |
I needed this too. I have an implementation, but I think this is something that belongs in pydantic itself, and for that some more polish is needed. Does any maintainer have time to have a look and advise? Perhaps @dmontagu as it involves generics? The relevant commit is here: 4cbe5b0 I was going to create a Pull Request, but apparently I'm supposed to open an issue to discuss first, and this issue already exists. If this is welcome (once polished up), I think opening a Pull Request to discuss the details makes sense. Is that what is expected? |
This comment has been minimized.
This comment has been minimized.
Humm, just seen this while reviewing #2245. I've had this problem too, my solution is to use That way you get errors returned to you and can decide what to do with them. You could even then use The You could easily build a utility function for this, either shipped with pydantic or standalone. The main problem is that type hints and knowledge about the model are no longer valid. You thing you have an instance of (for example) That's one of the advantages of using my Overall, I see the problem here, but I'm not yet sure I know what the best solution is. |
Would love if a |
I had the need for this feature as well, and this is how I solved it: from typing import *
class Partial(Generic[T]):
'''Partial[<Type>] returns a pydantic BaseModel identic to the given one,
except all arguments are optional and defaults to None.
This is intended to be used with partial updates.'''
_types = {}
def __class_getitem__(cls: Type[T], item: Type[Any]) -> Type[Any]:
if isinstance(item, TypeVar):
# Handle the case when Partial[T] is being used in type hints,
# but T is a TypeVar, and the type hint is a generic. In this case,
# the actual value doesn't matter.
return item
if item in cls._types:
# If the value was already requested, return the same class. The main
# reason for doing this is ensuring isinstance(obj, Partial[MyModel])
# works properly, and all invocation of Partial[MyModel] return the
# same class.
new_model = cls._types[item]
else:
class new_model(item):
'''Wrapper class to inherit the given class'''
for _, field in new_model.__fields__.items():
field.required = False
if getattr(field, 'default_factory'):
field.default_factory = lambda: None
else:
field.default = None
cls._types[item] = new_model
return new_model Basically, the idea is to have |
I just created https://github.com/team23/pydantic-partial just to achieve this ;-) This is still an early version, but is fully tested. |
Hi all, thanks so much for your patience. I've been thinking about this more, prompted partly by someone creating a duplicate issue, #5031. I would love to support "partial-isation" of a pydantic model, or dataclass. My proposal would b a function which creates a partial variant of a model Usage would be something like this from pydantic import BaseModel, partial, PydanticUndefined
class MyModel(BaseModel):
x: int
y: str
PartialModel1 = partial(MyModel)
PartialModel2 = partial(MyModel, missing_value=PydanticUndefined) I know @dmontagu would prefer The real point here is that ideally this would be added to python itself so static typing libraries recognised Therefore, when we get the time we should submit a proposal to discuss.python.org, and be prepare to write a PEP if/when we get enough interest. We could also ask mypy/pyright to support this before the pep is accepted. Lastly, we could even add support for Obviously this will be pydantic V2 only, and probably won't be added until V2.1 or later, I don't think we'll have time to work on it before v2 is released, still, happy to hear input from others...? |
Hi @samuelcolvin, I've done some modification to the function I proposed into #5031 . Known issues:
Do you have any suggestion for improving this implementation? How to deal with the two previous issues? from pydantic import BaseModel, validator, create_model, ValidationError, class_validators
from typing import Type, Union, Optional, Any, get_origin, get_args
from pydantic.fields import ModelField
# Returns a new model class which is a copy of the input one
# having all inner fields and subfields as Optional
def make_optional(cls: Type[BaseModel]):
field_definitions: dict = {}
field: ModelField
validators_definitions: dict = {}
# exit condition: if 'cls' is not a BaseModel return the Optional version of it
# otherwise its fields must be optionalized
if not issubclass(cls, BaseModel):
return ( Optional[cls] )
for name, field in cls.__fields__.items():
# keep original validators but allow_reuse to avoid errors
v: class_validators.Validator
for k, v in field.class_validators.items() or []:
validators_definitions[k] = validator(
name,
allow_reuse=True, # avoid errors due the the same function owned by original class
always=v.always,
check_fields=v.check_fields,
pre=v.pre,
each_item=v.each_item
)(v.func)
# Compute field definitions for 'cls' inheriting from BaseModel recursively.
# If the field has an outer type (e.g. Union, Dict, List) then construct the outer type and trasform its inner arguments as Optional
# Otherwise the field can be either a BaseModel type (again) or a standard type (so just need to call the function recursively)
origin: Any | None = get_origin(field.outer_type_)
args = get_args(field.outer_type_)
if field.sub_fields and origin:
field_definitions[name] = ( (origin[ tuple( make_optional(arg_type) for arg_type in args ) ]), field.default or None )
else:
field_definitions[name] = ( make_optional(field.outer_type_), field.default or None )
return create_model(cls.__name__+"Optional", __base__=cls.__base__, __validators__=validators_definitions, **field_definitions)
class Fuel(BaseModel):
name: str
spec: str
class Engine(BaseModel):
type: str
fuel: Union[str, Fuel]
class Wheel(BaseModel):
id: int
name: str = "Pirelli"
@validator('id')
def check_id(cls, v):
if v <= 0:
raise ValueError('ID must be greater than 0')
return v
class Driver(BaseModel):
name: str
number: int
class Car(BaseModel):
name: str
driver: Driver
wheels: list[Wheel]
engine: Union[Engine, str]
# 0. Make Optional Car
CarOptional: Any | Type[None] = make_optional(Car)
print()
# 1. Create a standard Car
car1 = Car(name="Ferrari", driver=Driver(name='Schumacher', number=1), engine=Engine(type="V12", fuel=Fuel(name="Petrol",spec="XXX")), wheels=[{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}])
print("\n1) Standard Car \n", car1)
# 2. Create a CarOptional model having Optional fields also in nested objects (e.g. Fuel.name becomes Optional as well)
try:
car_opt1 = CarOptional(engine=dict(type="V12", fuel=dict(spec="XXX")), wheels=[{}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
assert False
except Exception as e:
assert False
else:
print("\n2) Optional Car1 with 'engine.fuel : Fuel \n", car_opt1)
assert True
try:
car_opt2 = CarOptional(driver=dict(name='Leclerc'), engine=dict(type="V12", fuel='ciao'), wheels=[{}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
assert False
except Exception as e:
assert False
else:
print("\n2) Optional Car2 with 'engine.fuel : str \n", car_opt2)
assert True
# 3. Validators are still executed for fields with a value (e.g. Wheel.id = 0)
print("\n3) Optional Car but WheelOptional.id not valid \n")
try:
car_not_valid = CarOptional(engine=dict(type="V12", fuel=dict(spec="XXX")), wheels=[{"id": 0}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
print(e)
assert True
else:
assert False
# 4. Must raise a validation error
print("\n4) Standard Car still working: should return error missing 'name', 'driver' and 'wheels' \n")
try:
car2: Car = Car(engine=Engine(type="V12", fuel=Fuel(name="Petrol",spec="XXX")))
except ValidationError as e:
print(e)
assert True
else:
assert False |
After some reasoning and hard fighting on that topic, I ended up with un updated version of the previous recursive algorithm which handles for example Anyway I wanted to share my effort with the community cause it may be useful to someone else. Moreover, I've just understood that V2 should be very close (Q1 2023) so probably it doesn't make sense to put this effort for having a Typescript's Partial equivalent in V1. In V2 it may be also simpler to implement if the new modeling logic fits it in a better way (at least I hope so). One thing I really don't understand in my code, is why the last test fails saying that function from pydantic import BaseModel, validator, create_model, ValidationError, class_validators, conlist
from typing import Type, Union, Optional, Any, List, Dict, Tuple, get_origin, get_args
from pydantic.fields import ModelField
from pydantic.class_validators import Validator
from types import UnionType
def print_check_model(cls):
print(f"\n------------------------------------ check '{cls}' model")
if issubclass(cls, BaseModel):
for field in cls.__fields__.values():
print("field", " ##### name '", cls.__name__+"."+field.name, "' ##### outer_type_", field.outer_type_, " ##### type_", field.type_, " ##### required", field.required, " ##### allow_none", field.allow_none)
if field.sub_fields:
for sub in field.sub_fields or [] :
print("sub", " ##### name '", cls.__name__+"."+sub.name, "' ##### outer_type_", sub.outer_type_, " ##### type_", sub.type_, " ##### required", sub.required, " ##### allow_none", sub.allow_none)
print_check_model(sub.type_)
else:
print(field.type_)
print_check_model(field.type_)
def copy_validator(field_name: str, v: Validator) -> classmethod:
return validator(
field_name,
allow_reuse=True, # avoid errors due the the same function owned by original class
always=v.always,
check_fields=v.check_fields,
pre=v.pre,
each_item=v.each_item
)(v.func)
# Returns a new model class which is a copy of the input one
# having all inner fields and subfields as non required
def make_optional(cls: Type[BaseModel], recursive: bool):
field_definitions: dict = {}
field: ModelField
validators_definitions: dict = {}
# if cls has args (e.g. list[str] or dict[int, Union[str, Driver]])
# then make optional types of its arguments
if get_origin(cls) and get_args(cls):
return get_origin(cls)[tuple(make_optional(arg, recursive) for arg in get_args(cls))]
# exit condition: if 'cls' is not a BaseModel return the Optional version of it
# otherwise its fields must be optionalized
if not issubclass(cls, BaseModel):
return cls
for name, field in cls.__fields__.items():
# keep original validators but allow_reuse to avoid errors
v: class_validators.Validator
for k, v in field.class_validators.items() or []:
validators_definitions[k] = copy_validator(name, v)
# Compute field definitions for 'cls' inheriting from BaseModel recursively.
# If the field has an outer type (e.g. Union, Dict, List) then construct the outer type and trasform its inner arguments as Optional
# Otherwise the field can be either a BaseModel type (again) or a standard type (so just need to call the function recursively)
origin: Any | None = get_origin(field.outer_type_)
args = get_args(field.outer_type_)
if not recursive:
field_definitions[name] = ( field.outer_type_, field.default or None )
elif origin in (dict, Dict, tuple, Tuple, list, List, Union, UnionType):
if origin is UnionType: # handles 'field: Engine | str'
origin = Union
field_definitions[name] = ( origin[ tuple( make_optional(arg_type, recursive) for arg_type in args ) ], field.default or None ) # type: ignore
# handle special cases not handled by previous if branch (e.g. conlist)
elif field.outer_type_ != field.type_:
if issubclass(field.outer_type_, list): # handles conlist
field_definitions[name] = ( list[ make_optional(field.type_, recursive) ], field.default or None )
else:
raise Exception(f"Case with outer_type_ {field.outer_type_} and type_ {field.type_} not handled!!")
else:
field_definitions[name] = ( make_optional(field.outer_type_, recursive), field.default or None )
return create_model(cls.__name__+"Optional", __config__=cls.__config__, __validators__=validators_definitions, **field_definitions)
# ____________________________________________________________________________________________________________________________________________________________
class Comp(BaseModel):
name: str
class Fuel(BaseModel):
name: str
spec: str
class Engine(BaseModel):
type: str
fuel: Union[str, Fuel]
eng_components: dict[str, list[Comp]]
eng_tuple_var: tuple[str, int, list[Comp]]
class Wheel(BaseModel):
id: int
name: str = "Pirelli"
def get_key(self) -> str:
return self.id
def __hash__(self) -> int:
return hash(self.get_key())
def __eq__(self, other) -> bool:
if not isinstance(other, Wheel):
return False
return self.get_key() == other.get_key()
@validator('id')
def check_id(cls, v):
if v <= 0:
raise ValueError('ID must be greater than 0')
return v
class Driver(BaseModel):
name: str
number: int
class Car(BaseModel):
class Config:
extra: str = "forbid"
name: str
driver: Driver
wheels: conlist(Wheel, unique_items=True)
engine: Engine | str | int | dict[str, Engine]
components: dict[str, list[Comp]]
tuple_var: tuple[str, int, list[Comp]]
# ____________________________________________________________________________________________________________________________________________________________________________
# 0. Make Optional Car
CarOptional: Any | Type[None] = make_optional(Car, True)
print()
print_check_model(CarOptional)
# 0. Create a standard Car
car0 = Car(name="Ferrari",
driver=Driver(name='Schumacher', number=1),
engine=Engine(type="V12", fuel=Fuel(name="Petrol",spec="XXX"), eng_components=dict({"c1": [{"name": "eng-comp1.1"}, {"name": "eng-comp1.2"}], "c2": [{"name": "eng-comp2.1"}, {"name": "eng-comp2.2"}]}), eng_tuple_var=tuple(["ciao", 34, [{"name": "tup-comp1"}, {"name": "tup-comp2"}]]) ),
wheels=[{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}],
components=dict({"c1": [{"name": "comp1.1"}, {"name": "comp1.2"}], "c2": [{"name": "comp2.1"}, {"name": "comp2.2"}]}),
tuple_var=tuple(["ciao", 34, [{"name": "comp1.1"}, {"name": "comp1.2"}]])
)
print("\n0) Standard Car \n", car0, "\n")
# 1. Create a CarOptional model having Optional fields also in nested objects (e.g. Fuel.name becomes Optional as well)
try:
car_opt1 = CarOptional(engine=dict(type="V12", fuel=dict(spec="XXX")), wheels=[{}, {"id": 2}, {"id": 3}, {"id": 4}])
print_check_model(CarOptional)
except ValidationError as e:
print("[KO] car_opt1")
assert False
except Exception as e:
print("[KO] car_opt1")
assert False
else:
print("\n[ok] 1) Optional Car1 with 'engine.fuel : Fuel\n", car_opt1)
assert True
# 2. Create a CarOptional model having Optional fields also in nested objects (e.g. Fuel.name becomes Optional as well)
try:
car_opt2 = CarOptional(driver=dict(name='Leclerc'), engine=dict(type="V12", fuel='ciao'), wheels=[{}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
print("[KO] car_opt2")
print(e)
assert False
except Exception as e:
print("[KO] car_opt2")
assert False
else:
print("\n[ok] 2) Optional Car2 with 'engine.fuel : str \n", car_opt2)
assert True
# 3. Validators are still executed for fields with a value (e.g. Wheel.id = 0)
print("\n3) Optional Car but WheelOptional.id = 0 not valid \n")
try:
car_not_valid = CarOptional(engine=dict(type="V12", fuel=dict(spec="XXX")), wheels=[{"id": 0}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
print("[ok] car_not_valid ")
print(e)
assert True
else:
print("[KO] car_not_valid")
assert False
# 4. Validators are still executed for conlist with duplicated items (e.g. having two items with Wheel.id = 2)
print("\n4) Optional Car but duplicated WheelOptionals with same 'id = 2', is not valid due to 'id' validator\n")
try:
car_not_valid = CarOptional(engine=dict(type="V12", fuel=dict(spec="XXX")), wheels=[{"id": 1}, {"id": 2}, {"id": 2}, {"id": 4}])
except ValidationError as e:
print("[ok] car_not_valid ")
print(e)
assert True
else:
print("[KO] car_not_valid")
assert False
# 5. Standard Car must still raise a validation error
print("\n5) Standard Car still working: should return error missing 'name', 'driver', 'wheels', 'components' and 'tuple_var' \n")
try:
car2: Car = Car(engine=Engine(type="V12", fuel=Fuel(name="Petrol",spec="XXX")))
except ValidationError as e:
print("[ok] car2 ")
print(e)
assert True
else:
print("[KO] car2")
assert False
# 6. Check inner Driver model not modified
print("\n6) Inner Driver model not modified \n")
try:
print("driver", Driver(number=12))
except ValidationError as e:
print("[ok] Driver ")
print(e)
assert True
else:
print("[KO] Driver")
assert False
CarOptionalNonRecursive = make_optional(Car, False)
# 7. Non recursive Optional Car 1
print("\n7) Non recursive Optional Car 1 \n")
try:
caroptnonrec: CarOptionalNonRecursive = CarOptionalNonRecursive(driver=dict(number=12))
except ValidationError as e:
print("[ok] caroptnonrec1 ")
print(e)
assert True
else:
print("[KO] caroptnonrec1")
print(caroptnonrec)
assert False
# 8. Non recursive Optional Car 2
print("\n8) Non recursive Optional Car 2 \n")
try:
caroptnonrec: CarOptionalNonRecursive = CarOptionalNonRecursive(name='test')
except ValidationError as e:
print("[KO] caroptnonrec2")
print(e)
assert False
else:
print("[ok] caroptnonrec2 ")
print(caroptnonrec)
assert True
# 9. Complex Tuple
print("\n9) Complex Tuple \n")
try:
caropttuple: CarOptional = CarOptional(name='test', tuple_var=tuple(["ciao", 34, [{}, {"name": "comp1.2"}]]), engine=dict(type="V12", fuel="fuelXX", eng_components=dict({"c1": [{}, {"name": "eng-comp1.2"}], "c2": [{"name": "eng-comp2.1"}, {"name": "eng-comp2.2"}]}), eng_tuple_var=tuple(["ciao", 34, [{}, {"name": "tup-comp2"}]]) ), )
except ValidationError as e:
print("[KO] caropttuple")
print(e)
assert False
else:
print("[ok] caropttuple ")
print(caropttuple)
assert True
# 10. Additional forbidden param
print("\n10) Additional forbidden param \n")
try:
caroptadditional: CarOptional = CarOptional(additional="non_existing" )
except ValidationError as e:
print("[ok] caroptadditional ")
print(e)
assert True
else:
print("[KO] caroptadditional")
print(caroptadditional)
assert False
# 11. Engine of type int but why wheel.get_key() not found?!?!?
print("\n11) Engine of type 'int' but why wheel.get_key() not found?!?!? \n")
try:
car_opt3 = CarOptional(driver=dict(name='Leclerc'), engine=2, wheels=[{}, {"id": 2}, {"id": 3}, {"id": 4}])
except ValidationError as e:
print("[KO] car_opt3")
print(e)
assert False
except Exception as e:
print("[KO] car_opt3")
assert False
else:
print("\n[ok] Optional Car3 with 'engine : int \n", car_opt3)
for w in car_opt3.wheels:
print(w.get_key())
assert True |
While I think we would like to better support this, ultimately there just isn't anything super analogous to typescript's I think we'd be open to revisiting this if a good proposal was brought forward, especially one that was compatible with type-checkers, etc. But for now I'm going to close this as "not planned" — at least until the language better supports these kinds of (more advanced) utility types. See #830 (comment) for a related comment. |
I adapted a Partial Model that I found on StackOverflow for Pydantic V1x to work well with Pydantic V2. from copy import deepcopy
from typing import Any, Callable, Optional, Type, TypeVar
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
Model = TypeVar("Model", bound=Type[BaseModel])
def partial_model(without_fields: Optional[list[str]] = None) -> Callable[[Model], Model]:
"""A decorator that create a partial model.
Args:
model (Type[BaseModel]): BaseModel model.
Returns:
Type[BaseModel]: ModelBase partial model.
"""
if without_fields is None:
without_fields = []
def wrapper(model: Type[Model]) -> Type[Model]:
base_model: Type[Model] = model
def make_field_optional(field: FieldInfo, default: Any = None) -> tuple[Any, FieldInfo]:
new = deepcopy(field)
new.default = default
new.annotation = Optional[field.annotation]
return new.annotation, new
if without_fields:
base_model = BaseModel
return create_model(
model.__name__,
__base__=base_model,
__module__=model.__module__,
**{
field_name: make_field_optional(field_info)
for field_name, field_info in model.model_fields.items()
if field_name not in without_fields
},
)
return wrapper How use class MyFullModel(BaseModel):
name: str
age: int
relation_id: int
@partial_model(without_fields=["relation_id"])
class MyPartialModel(MyFullModel):
pass |
Here Is my working solution. Similar to some of the solutions above except that it works recursively and is able to make all sub schemas partial aswell. from typing import Any, Optional, Type, Union, get_args, get_origin, Annotated
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
MaybePydantic = Type[Union[Any, BaseModel]]
def create_optional_field(field: Union[FieldInfo, MaybePydantic]) -> object:
field_type = field.annotation if isinstance(field, FieldInfo) else field
if origin := get_origin(field_type):
if origin is Annotated:
return Optional[field_type]
args = get_args(field_type)
optional_args = [Optional[create_optional_field(arg)] for arg in args]
return Optional[origin[*optional_args]]
# Handle BaseModel subclasses
if field_type and issubclass(field_type, BaseModel):
return Optional[Union[field_type, create_optional_model(field_type)]]
return Optional[field_type]
def create_optional_model(model: Type[BaseModel]) -> Type[BaseModel]:
"""
Make all fields in a pydantic model optional. Sub schemas will also become 'partialized'
"""
return create_model( # type: ignore
model.__name__ + "Optional",
__base__=model,
**{
name: (create_optional_field(field), None)
for name, field in model.model_fields.items()
},
) Usage example: class Bar(BaseModel):
id: str
class Baz(BaseModel):
items: List[Dict[str, Optional[Bar]]]
class Foo(BaseModel):
bar: Optional[HttpUrl]
baz: Baz
optional_cls = create_optional_model(Foo)
optional_cls() |
For anyone interested in this feature, I'd love to support it, but really we need a There's currently a discussion on |
This comment was marked as duplicate.
This comment was marked as duplicate.
@satheler Thank you for the snippet. Just want to point out that this part of the function will strip the resulting partial model of all validators, serializers, and private attributes that would've otherwise been inherited from the input model: if without_fields:
base_model = BaseModel I made some modifications that seem to be working. The idea is to still pass all fields to from copy import deepcopy
from dataclasses import asdict
from typing import Any, Callable, Optional, Type, TypeVar, ClassVar
from pydantic import BaseModel, create_model
from pydantic.fields import FieldInfo
Model = TypeVar("Model", bound=Type[BaseModel])
def partial_model(
without_fields: Optional[list[str]] = None,
) -> Callable[[Model], Model]:
"""A decorator that create a partial model.
Args:
model (Type[BaseModel]): BaseModel model.
Returns:
Type[BaseModel]: ModelBase partial model.
"""
if without_fields is None:
without_fields = []
def wrapper(model: Type[Model]) -> Type[Model]:
def make_field_optional(
field: FieldInfo, default: Any = None, omit: bool = False
) -> tuple[Any, FieldInfo]:
new = deepcopy(field)
new.default = default
new.annotation = Optional[field.annotation]
# Wrap annotation in ClassVar if field in without_fields
return ClassVar[new.annotation] if omit else new.annotation, new
model_copy = deepcopy(model)
# Pydantic will error if validators are present without the field
# so we set check_fields to false on all validators
for dec_group_label, decs in asdict(model_copy.__pydantic_decorators__).items():
for validator in decs.keys():
decorator_info = getattr(
getattr(model.__pydantic_decorators__, dec_group_label)[validator],
"info",
)
if hasattr(decorator_info, "check_fields"):
setattr(
decorator_info,
"check_fields",
False,
)
return create_model(
model.__name__,
__base__=model,
__module__=model.__module__,
**{
field_name: make_field_optional(
field_info, omit=(field_name in without_fields)
)
for field_name, field_info in model.model_fields.items()
},
)
return wrapper |
@samuelcolvin what would you suggest is the best current work around with Pydantic V2.8? I know you suggested Right now the method I am going with is this: from pydantic import BaseModel, Field
class Field3(BaseModel):
subfield1: str = Field(...)
subfield2: str = Field(...)
class MainClass(BaseModel):
field1: str = Field(...)
field2: str = Field(...)
field3: Field3 = Field(...)
class MainClassSubset(MainClass):
pass
if __name__ == "__main__":
main_class = MainClass(field1="field1", field2="field2", field3=Field3(subfield1="subfield1", subfield2="subfield2"))
print(main_class.dict())
exclude_dict = {
'field1': True,
'field2': True,
'field3': {
'subfield1': True
}
}
main_class_subset = MainClassSubset(**main_class.dict())
print(main_class_subset.dict(exclude=exclude_dict)) |
Question
Output of
python -c "import pydantic.utils; print(pydantic.utils.version_info())"
: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 apydantic
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:
verified with a simple test:
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:
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 functionto_partial
?Thanks!
The text was updated successfully, but these errors were encountered: