11import typing
2- from typing import Any , get_args , get_origin , TypeVar
2+ from typing import Any , get_args , get_origin , TypeVar , Iterable
33
44from pydantic import BaseModel
55
99from xsentinels .default import DefaultType
1010
1111from .config import PartialConfigDict
12- from .sentinels import Missing , MissingType
12+ from .sentinels import Missing , MissingType , AutoPartialExcludeMarker
1313
1414from logging import getLogger
1515
@@ -46,6 +46,7 @@ def __new__(
4646 * ,
4747
4848 auto_partials : bool | DefaultType = Default ,
49+ auto_partials_exclude : Iterable [str ] | DefaultType = Default ,
4950
5051 # A private/internal detail for generic base subclasses that want to also change the fields,
5152 # this prevents having to rebuild the class a second time; if this is True then the subclass
@@ -62,6 +63,13 @@ def __new__(
6263 If `True` (default): Will automatically make all fields on the model `Partial`.
6364 If `False`: User needs to mark individual fields as `Partial` where they want.
6465
66+ auto_partials_exclude: A set of strings of field names to exclude from automatic partials.
67+ If you explicitly mark a field as a Partial, this won't effect that. THis only effects
68+ automatically applied partial fields.
69+
70+ You can also use `pydantic_partials.partial.AutoPartialExclude` to more easily mark fields as excluded.
71+ For more details see `pydantic_partials.config.PartialConfigDict.auto_partials_exclude`.
72+
6573 **kwargs: Passed along other class arguments to Pydantic and any __init_subclass__ methods.
6674 """
6775 # Create the class first...
@@ -74,19 +82,41 @@ def __new__(
7482 if auto_partials is not Default :
7583 cls .model_config ['auto_partials' ] = auto_partials
7684
85+ if auto_partials_exclude :
86+ cls .model_config ['auto_partials_exclude' ] = set (auto_partials_exclude )
87+
88+ final_auto_exclude = cls .model_config .get ('auto_partials_exclude' , set ())
89+ for c in cls .__mro__ :
90+ parent_config = getattr (c , 'model_config' , set ())
91+ if not parent_config :
92+ continue
93+
94+ final_auto_exclude .update (parent_config .get ('auto_partials_exclude' , set ()))
95+
7796 need_rebuild = False
7897
7998 partial_fields = set ()
8099 for k , v in cls .model_fields .items ():
81100 field_type = v .annotation
82- if get_origin (field_type ) is None :
101+ origin = get_origin (field_type )
102+ if origin is None :
83103 if field_type is MissingType :
84104 partial_fields .add (k )
85105
106+ # TODO: Check that `field_type` is a union?
86107 for arg_type in get_args (field_type ):
87108 if arg_type is MissingType :
88109 partial_fields .add (k )
89110
111+ for mdv in v .metadata :
112+ # If we find the marker AND we are not already marked as a partial_field,
113+ # add field to auto-exclude list.
114+ if mdv is AutoPartialExcludeMarker and k not in partial_fields :
115+ final_auto_exclude .add (k )
116+
117+ if final_auto_exclude :
118+ cls .model_config ['auto_partials_exclude' ] = final_auto_exclude
119+
90120 final_partial_auto = cls .model_config .get ('auto_partials' , True )
91121 if final_partial_auto is not False :
92122 # I'll be putting in more options for `final_partial_auto` in the near future,
@@ -99,6 +129,10 @@ def __new__(
99129 # The field is already a Partial
100130 continue
101131
132+ if k in final_auto_exclude :
133+ # The field is excluded.
134+ continue
135+
102136 if v .default is PydanticUndefined and v .default_factory is None :
103137 v .annotation = v .annotation | MissingType
104138 partial_fields .add (k )
0 commit comments