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

FormPage.__pydantic_init_subclass__ doesn't behave as expected #39

Open
Viicos opened this issue Feb 20, 2025 · 0 comments
Open

FormPage.__pydantic_init_subclass__ doesn't behave as expected #39

Viicos opened this issue Feb 20, 2025 · 0 comments

Comments

@Viicos
Copy link

Viicos commented Feb 20, 2025

I came across this implementation while trying to evaluate potential regressions in the next Pydantic release (2.11):

@classmethod
def __pydantic_init_subclass__(cls, /, **kwargs: Any) -> None:
# The default and requiredness of a field is not a property of a field
# In the case of DisplayOnlyFieldTypes, we do kind of want that.
# Using this method we set the right properties after the form is created
for field in cls.model_fields.values():
if field.frozen:
field.validate_default = False

This is not working as you would expect, because Pydantic constructs a core schema used during validation based on model fields; and this happens before this __pydantic_init_subclass__ hook is called.

This core schema is used to understand how validation should be applied, and it contains the logic related to validate_default:

from pydantic import BaseModel, Field

class FormPage(BaseModel):
    @classmethod
    def __pydantic_init_subclass__(cls, /, **kwargs):
        for field in cls.model_fields.values():
            if field.frozen:
                field.validate_default = False


class Sub(FormPage):
    a: int = Field(default="wrong", frozen=True, validate_default=True)

Sub.model_fields
{'a': FieldInfo(annotation=int, required=False, default='test', frozen=True, validate_default=False)}

Sub.__pydantic_core_schema__
{
│   'type': 'model',
│   'cls': <class '__main__.Sub'>,
│   'schema': {
│   │   'type': 'model-fields',
│   │   'fields': {
│   │   │   'a': {
│   │   │   │   'type': 'model-field',
│   │   │   │   'schema': {'type': 'default', 'schema': {'type': 'int'}, 'validate_default': True, 'default': 'test'},  # Still True!
│   │   │   │   'frozen': True,
│   │   │   │   'metadata': {}
│   │   │   }
│   │   },
│   │   'model_name': 'Sub',
│   │   'computed_fields': []
│   },
│   'config': {'title': 'Sub'},
│   'ref': '__main__.Sub:95507792084592',
│   'metadata': {'<stripped>'}
}

Model()
# ValidationError, default value is still validated

To make sure the core schema is reconstructed, you can call model_rebuild(force=True):

class FormPage(BaseModel):
    @classmethod
    def __pydantic_init_subclass__(cls, /, **kwargs):
        needs_rebuild = False
        for field in cls.model_fields.values():
            if field.frozen:
                field.validate_default = False
                needs_rebuild = True

        if needs_rebuild:
            cls.model_rebuild(force=True)

Note that Pydantic does not really explicitly support this pattern of rebuilding a model after altering the model fields.

On a related note, relying on model_fields just after class creation might lead to incorrect results. The reason for this is that subclasses of FormPage might use unresolvable forward references. This PR gives more context: pydantic/pydantic#11388.

In 2.11, we might raise a warning when incomplete model fields are being accessed (cross ref this issue), but be assured that if we do so, we will provide another kind of hook similar to __pydantic_init_subclass__, that is only called when fields are guaranteed to be complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant