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

Introspection of fields during validation #6429

Open
3 of 13 tasks
ofek opened this issue Jul 4, 2023 · 9 comments
Open
3 of 13 tasks

Introspection of fields during validation #6429

ofek opened this issue Jul 4, 2023 · 9 comments
Assignees
Labels
feature request unconfirmed Bug not yet confirmed as valid/applicable

Comments

@ofek
Copy link
Contributor

ofek commented Jul 4, 2023

Initial Checks

  • I have searched Google & GitHub for similar requests and couldn't find anything
  • I have read and followed the docs and still think this feature is missing

Description

Here is an example of what we are currently doing in v1:

We define the configuration for each integration as OpenAPI and generate the model code. Basically, we:

  1. Set the default based on any manually defined functions that define complex logic like fallbacks
  2. Set the default value for each option based on its type e.g. a string would default to an empty string rather than None
  3. Run any manually defined functions for each option
  4. Do a final pass over all values and convert mutable types like lists into immutable types like tuples

Is there a way in v2 to achieve this? Specifically, it looks like we need replacements for field.required, field.shape, and always=True.

Affected Components

Selected Assignee: @hramezani

@pydantic-hooky pydantic-hooky bot added the unconfirmed Bug not yet confirmed as valid/applicable label Jul 4, 2023
@adriangb
Copy link
Member

adriangb commented Jul 5, 2023

I couldn't tell immediately from the links what is going on, a self contained example would help.

In v2 you can access some field info via BaseModel.model_fields:

model_fields: ClassVar[dict[str, FieldInfo]]

The FieldInfo object has an is_required() method:
https://github.com/pydantic/pydantic/blob/c3fc4e6953d816e0e09c77d6913a6ecf50e19ee0/pydantic/fields.py#L471C6-L471C6

For always=True you can either use validate_default=True in Field or validate_defaults=True in model_config:

from datetime import datetime
from typing import Annotated

from pydantic import BaseModel, ConfigDict, Field, field_validator


class DemoModel(BaseModel):
    ts: Annotated[datetime | None, Field(validate_default=True)] = None

    @field_validator('ts', mode='before')
    def set_ts_now(cls, v: datetime | None) -> datetime:
        return v or datetime(2032, 1, 2, 3, 4, 5, 6)

    model_config = ConfigDict(validate_default=True)  #  either option works, you don't need both


print(DemoModel())
#> ts=datetime.datetime(2032, 1, 2, 3, 4, 5, 6)
print(DemoModel(ts=datetime(2022, 1, 2, 3, 4, 5, 6)))
#> ts=datetime.datetime(2022, 1, 2, 3, 4, 5, 6)

field.shape is gone, the whole shape thing was a sort of strange IR of the type system. I'd recommend you introspect BaseModel.__annotations__, FieldInfo.annotation or one of the several other ways to get the type hints / annotations for a given field.

If you're trying to do some of this from within a validator function you can get the field from the model using the field name:

from datetime import datetime
from typing import Any

from pydantic import BaseModel, FieldValidationInfo, field_validator


def validator_func(cls: type[BaseModel], v: Any, info: FieldValidationInfo) -> Any:
    print(repr(cls.model_fields[info.field_name]))
    return v


class DemoModel(BaseModel):
    ts: datetime | None
    val_ts = field_validator('ts', mode='before')(classmethod(validator_func))


DemoModel(ts=None)
#> FieldInfo(annotation=Union[datetime, NoneType], required=True)

You could also use a model_validator with mode='before' to do this for every field in the model.

Let me know if this helps or if you have any more questions.

@dmontagu
Copy link
Contributor

dmontagu commented Jul 5, 2023

I'll just note that, in pydantic-settings, we needed to port some functionality that relied on shape. The pydantic_settings.sources._annotation_is_complex and pydantic_settings.sources._annotation_is_complex_inner functions replace logic that previously relied on the value of field.shape. (Your use case will be quite different, but just showing those as an example.)

Also, I had a chance to discuss this with @samuelcolvin this morning, and he noted that the right way to inject defaults might be to instead post-process the __pydantic_core_schema__ in some way to insert a DefaultSchema wrapper around certain forms of core schemas. I'm not sure whether that would be easier in your case, but it is another option that could be pursued if it turns out to be problematic to introspect the annotation directly for some reason. (It would also be significantly more performant than adding a validator to set the default, but I don't know how performance-critical this application is.)

@ofek
Copy link
Contributor Author

ofek commented Jul 6, 2023

Thanks! I am at the last step I think. Is there a way to perform validation/update the raw dictionary of fields after all validation right before the model gets created? It looks like the after mode for model validators returns the final object which in our case cannot be modified because we enable frozen. I tried the wrap mode but am getting an error (this might not even provide the dictionary but I was just trying stuff):

    @model_validator(mode='wrap')
    def _final_validation(cls, values, handler):
        return values
Traceback (most recent call last):
  File "C:\Users\ofek\Desktop\pd.py", line 12, in <module>
    main()
  File "C:\Users\ofek\Desktop\pd.py", line 8, in main
    print(o)
  File "C:\USERS\OFEK\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON311\Lib\site-packages\pydantic\main.py", line 859, in __str__
    return self.__repr_str__(' ')
           ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\USERS\OFEK\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON311\Lib\site-packages\pydantic\_internal\_repr.py", line 55, in __repr_str__
    return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\USERS\OFEK\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON311\Lib\site-packages\pydantic\_internal\_repr.py", line 55, in <genexpr>
    return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__())
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\USERS\OFEK\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON311\Lib\site-packages\pydantic\main.py", line 847, in __repr_args__
    pydantic_extra = self.__pydantic_extra__
                     ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\USERS\OFEK\APPDATA\LOCAL\PROGRAMS\PYTHON\PYTHON311\Lib\site-packages\pydantic\main.py", line 699, in __getattr__
    pydantic_extra = object.__getattribute__(self, '__pydantic_extra__')
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'SharedConfig' object has no attribute '__pydantic_extra__'. Did you mean: '__pydantic_private__'?

@adriangb
Copy link
Member

adriangb commented Jul 6, 2023

That implementation would work with before mode

@ofek
Copy link
Contributor Author

ofek commented Jul 6, 2023

It looks like that is not true but rather model validation with before mode gets run before field validation

@adriangb
Copy link
Member

adriangb commented Jul 6, 2023

My point was that wrap mode expect you to return an instance of the model, eg return handler(values) would work. before mode let’s you validate / replace the values which is what you are doing in the comment above. Also note that values isn’t necessarily a dictionary it is the raw input to validation eg in the case of Model.model_validate_python(Model()) it’ll be a Model instance.

@ofek
Copy link
Contributor Author

ofek commented Jul 6, 2023

Is there a way to modify the dictionary at the very end after all field validators have executed?

@adriangb
Copy link
Member

adriangb commented Jul 6, 2023

There is no more dictionary at that point, just the model

@adriangb
Copy link
Member

adriangb commented Jul 6, 2023

I do think there's two very reasonable feature requests here:

  1. Don't freeze the model until after @model_validators are done running
  2. A way to run validation for individual fields (e.g. values['x'] = handler.validate_field(values['x']))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request unconfirmed Bug not yet confirmed as valid/applicable
Projects
None yet
Development

No branches or pull requests

4 participants