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

[wip] Pydantic v2 #2751

Draft
wants to merge 52 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
0b4b7c0
bump reflex_hosting_cli, pydantic and fastapi deps
benedikt-bartscher Mar 1, 2024
25d0585
prepare base
benedikt-bartscher Feb 28, 2024
8e692cd
state adjustments
benedikt-bartscher Feb 28, 2024
95f9b12
types adjustments
benedikt-bartscher Feb 28, 2024
8b2d0b4
outer_type_ -> annotation
benedikt-bartscher Feb 28, 2024
ce06bf0
migrate config to pydantic v2
benedikt-bartscher Feb 28, 2024
67c8148
migrate remaining __fields__ to model_fields
benedikt-bartscher Feb 28, 2024
66f1e21
fix backend vars
benedikt-bartscher Feb 28, 2024
fd32942
parse_obj is deprecated in favor of model_validate
benedikt-bartscher Feb 28, 2024
6291c39
minor state fixes, and var serialization
benedikt-bartscher Feb 28, 2024
a5d197c
serialization fixes
benedikt-bartscher Feb 28, 2024
faebd22
black fixes
benedikt-bartscher Feb 28, 2024
2def8b9
FieldInfo has no name anymore
benedikt-bartscher Feb 28, 2024
c057f2e
minor pydantic fixups
benedikt-bartscher Feb 29, 2024
7f8a457
migrate to field.annotation and fix default var wrapping for undefined
benedikt-bartscher Feb 29, 2024
5c584da
add missing default values to icon and link
benedikt-bartscher Feb 29, 2024
1abc57c
fix default value for theme in ChakraProvider
benedikt-bartscher Feb 29, 2024
4d497e0
hacky workaround to allow __class_getitem__ with pydantic
benedikt-bartscher Feb 29, 2024
80a25d6
add missing type annotation for initialColorMode
benedikt-bartscher Feb 29, 2024
8c7dd39
fix EventSpec args type annotation for pydantic v2
benedikt-bartscher Feb 29, 2024
6d3809e
ModelField doesn't exist in pydantic v2
benedikt-bartscher Feb 29, 2024
1c89f01
optionalize some Component props
benedikt-bartscher Feb 29, 2024
fd5a5b9
Bare.contents should be a Var
benedikt-bartscher Feb 29, 2024
d0f15c5
optionalize all Var props without defaults
benedikt-bartscher Feb 29, 2024
78db90d
forgot to migrate one type_ to annotation
benedikt-bartscher Feb 29, 2024
211f9f3
fix auto-var conversion
benedikt-bartscher Feb 29, 2024
7789118
prevent calling __bool__ for is_hydrated
benedikt-bartscher Feb 29, 2024
4380cd2
fix pydantic _get_value for MutableProxy
benedikt-bartscher Feb 29, 2024
80f0275
add some missing type hints to test state vars
benedikt-bartscher Feb 29, 2024
37c360e
fix init_subclass for pydantic v2, add some missing type hints, forma…
benedikt-bartscher Mar 1, 2024
45cb36f
pydantic copy is deprecated in favor of model_copy
benedikt-bartscher Mar 1, 2024
b65c34b
update_forward_refs -> model_rebuild
benedikt-bartscher Mar 1, 2024
b565e89
dict -> model_dump and field_info fixes
benedikt-bartscher Mar 1, 2024
b359353
fix VarData json loads
benedikt-bartscher Mar 1, 2024
0240541
migrate pydantic config classes to ConfigDict
benedikt-bartscher Mar 1, 2024
db3c791
add hint for VarData deserialization
benedikt-bartscher Mar 1, 2024
32bc17b
minor IconButton default value fix
benedikt-bartscher Mar 1, 2024
99c28f8
add proper pydantic v2 FieldInfo annotation
benedikt-bartscher Mar 1, 2024
7534b2c
my regex was not smart enough
benedikt-bartscher Mar 1, 2024
935286b
add Component.tag str type annotation
benedikt-bartscher Mar 1, 2024
f44d0ed
add Component.is_default bool type annotation
benedikt-bartscher Mar 1, 2024
16b7bae
add Component.library str type annotation
benedikt-bartscher Mar 1, 2024
bcaab30
add Component.alias str type annotation
benedikt-bartscher Mar 1, 2024
16b838d
format component.library black
benedikt-bartscher Mar 1, 2024
3c30ceb
new regex missed f-strings
benedikt-bartscher Mar 1, 2024
bcd29a1
use typing.get_args instead of __args__
benedikt-bartscher Mar 2, 2024
7ac184f
rebuild pydantic model after dropping id field
benedikt-bartscher Mar 2, 2024
30df32b
add default value for new lang prop in Html
benedikt-bartscher Mar 5, 2024
be0a77a
pydantic_init_subclass
benedikt-bartscher Mar 5, 2024
16a7c1f
fix bug in memoization mode copy
benedikt-bartscher Mar 5, 2024
6d96a94
fix chakra list, allow Tag.tag to be None in validation
benedikt-bartscher Mar 5, 2024
cdc2f4f
cleanup unneeded init_subclass super calls
benedikt-bartscher Mar 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/test_event_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def on_click2(self):
class EventFiringComponent(rx.Component):
"""A component that fires onClick event without passing DOM event."""

tag = "EventFiringComponent"
tag: str = "EventFiringComponent"

def _get_custom_code(self) -> str | None:
return """
Expand Down
201 changes: 138 additions & 63 deletions poetry.lock

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ packages = [
{include = "reflex"}
]

[[tool.poetry.source]]
name = "test"
url = "https://test.pypi.org/simple/"
priority = "explicit"

[tool.poetry.dependencies]
python = "^3.8"
cloudpickle = "^2.2.1"
fastapi = "^0.96.0"
fastapi = "^0.110"
gunicorn = "^20.1.0"
jinja2 = "^3.1.2"
psutil = "^5.9.4"
pydantic = "^1.10.2"
pydantic = "^2"
python-multipart = "^0.0.5"
python-socketio = "^5.7.0"
redis = "^4.3.5"
Expand All @@ -55,7 +60,7 @@ wrapt = [
{version = "^1.11.0", python = "<3.11"},
]
packaging = "^23.1"
reflex-hosting-cli = ">=0.1.2"
reflex-hosting-cli = {version = "0.1.9dev0", source = "test"}
charset-normalizer = "^3.3.2"
wheel = "^0.42.0"
build = "^1.0.3"
Expand Down
37 changes: 18 additions & 19 deletions reflex/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

import pydantic
from pydantic import BaseModel
from pydantic.fields import ModelField
from pydantic.fields import FieldInfo

from reflex import constants


# TODO: migrate to pydantic v2
def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None:
"""Ensure that the field's name does not shadow an existing attribute of the model.

Expand All @@ -35,7 +36,8 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None

# monkeypatch pydantic validate_field_name method to skip validating
# shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True)
pydantic.main.validate_field_name = validate_field_name # type: ignore
# TODO
# pydantic.main.validate_field_name = validate_field_name # type: ignore


class Base(pydantic.BaseModel):
Expand All @@ -48,12 +50,12 @@ class Base(pydantic.BaseModel):
frontend and backend should subclass this class.
"""

class Config:
"""Pydantic config."""

arbitrary_types_allowed = True
use_enum_values = True
extra = "allow"
# Pydantic config
model_config = pydantic.ConfigDict(
arbitrary_types_allowed=True,
use_enum_values=True,
extra="allow",
)

def json(self) -> str:
"""Convert the object to a json string.
Expand All @@ -63,7 +65,9 @@ def json(self) -> str:
"""
from reflex.utils.serializers import serialize

return self.__config__.json_dumps(self.dict(), default=serialize)
return self.__pydantic_serializer__.to_json(
value=self, fallback=serialize
).decode()

def set(self, **kwargs):
"""Set multiple fields and return the object.
Expand All @@ -85,7 +89,7 @@ def get_fields(cls) -> dict[str, Any]:
Returns:
The fields of the object.
"""
return cls.__fields__
return cls.model_fields

@classmethod
def add_field(cls, var: Any, default_value: Any):
Expand All @@ -97,14 +101,9 @@ def add_field(cls, var: Any, default_value: Any):
var: The variable to add a pydantic field for.
default_value: The default value of the field
"""
new_field = ModelField.infer(
name=var._var_name,
value=default_value,
annotation=var._var_type,
class_validators=None,
config=cls.__config__,
)
cls.__fields__.update({var._var_name: new_field})
field_info = FieldInfo(default=default_value, annotation=var._var_type)
cls.model_fields.update({var._var_name: field_info})
cls.model_rebuild(force=True)

def get_value(self, key: str) -> Any:
"""Get the value of a field.
Expand All @@ -115,7 +114,7 @@ def get_value(self, key: str) -> Any:
Returns:
The value of the field.
"""
if isinstance(key, str) and key in self.__fields__:
if isinstance(key, str) and key in self.get_fields():
# Seems like this function signature was wrong all along?
# If the user wants a field that we know of, get it and pass it off to _get_value
key = getattr(self, key)
Expand Down
13 changes: 7 additions & 6 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import os
from typing import Any, Callable, Dict, Optional, Type, Union
from urllib.parse import urlparse

from pydantic.fields import ModelField
from pydantic.fields import FieldInfo

from reflex import constants
from reflex.components.base import (
Expand Down Expand Up @@ -149,7 +148,7 @@ def compile_state(state: Type[BaseState]) -> dict:


def _compile_client_storage_field(
field: ModelField,
field: FieldInfo,
) -> tuple[Type[Cookie] | Type[LocalStorage] | None, dict[str, Any] | None]:
"""Compile the given cookie or local_storage field.

Expand All @@ -162,8 +161,10 @@ def _compile_client_storage_field(
for field_type in (Cookie, LocalStorage):
if isinstance(field.default, field_type):
cs_obj = field.default
elif isinstance(field.type_, type) and issubclass(field.type_, field_type):
cs_obj = field.type_()
elif isinstance(field.annotation, type) and issubclass(
field.annotation, field_type
):
cs_obj = field.annotation()
else:
continue
return field_type, cs_obj.options()
Expand All @@ -188,7 +189,7 @@ def _compile_client_storage_recursive(
cookies = {}
local_storage = {}
state_name = state.get_full_name()
for name, field in state.__fields__.items():
for name, field in state.model_fields.items():
if name in state.inherited_vars:
# only include vars defined in this state
continue
Expand Down
6 changes: 3 additions & 3 deletions reflex/components/base/bare.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""A bare component."""
from __future__ import annotations

from typing import Any, Iterator
from typing import Any, Iterator, Optional

from reflex.components.component import Component
from reflex.components.tags import Tag
Expand All @@ -12,7 +12,7 @@
class Bare(Component):
"""A component with no tag."""

contents: Var[str]
contents: Optional[Var[str]] = None

@classmethod
def create(cls, contents: Any) -> Component:
Expand All @@ -27,7 +27,7 @@ def create(cls, contents: Any) -> Component:
if isinstance(contents, Var) and contents._var_data:
contents = contents.to(str)
else:
contents = str(contents)
contents = Var.create(str(contents))
return cls(contents=contents) # type: ignore

def _render(self) -> Tag:
Expand Down
2 changes: 1 addition & 1 deletion reflex/components/base/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
class Body(Component):
"""A body component."""

tag = "body"
tag: str = "body"
12 changes: 6 additions & 6 deletions reflex/components/base/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@
class NextDocumentLib(Component):
"""Root document components."""

library = "next/document"
library: str = "next/document"


class Html(NextDocumentLib):
"""The document html."""

tag = "Html"
tag: str = "Html"

lang: Optional[str]
lang: Optional[str] = None


class DocumentHead(NextDocumentLib):
"""The document head."""

tag = "Head"
tag: str = "Head"


class Main(NextDocumentLib):
"""The document main section."""

tag = "Main"
tag: str = "Main"


class NextScript(NextDocumentLib):
"""The document main scripts."""

tag = "NextScript"
tag: str = "NextScript"
4 changes: 2 additions & 2 deletions reflex/components/base/fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
class Fragment(Component):
"""A React fragment to return multiple components from a function without wrapping it in a container."""

library = "react"
tag = "Fragment"
library: str = "react"
tag: str = "Fragment"
6 changes: 3 additions & 3 deletions reflex/components/base/head.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
class NextHeadLib(Component):
"""Header components."""

library = "next/head"
library: str = "next/head"


class Head(NextHeadLib, MemoizationLeaf):
"""Head Component."""

tag = "NextHead"
tag: str = "NextHead"

is_default = True
is_default: bool = True
24 changes: 12 additions & 12 deletions reflex/components/base/link.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Display the title of the current page."""

from typing import Optional

from reflex.components.component import Component
from reflex.vars import Var
Expand All @@ -8,37 +8,37 @@
class RawLink(Component):
"""A component that displays the title of the current page."""

tag = "link"
tag: str = "link"

# The href.
href: Var[str]
href: Optional[Var[str]] = None

# The type of link.
rel: Var[str]
rel: Optional[Var[str]] = None


class ScriptTag(Component):
"""A script tag with the specified type and source."""

tag = "script"
tag: str = "script"

# The type of script represented.
type_: Var[str]
type_: Optional[Var[str]] = None

# The URI of an external script.
source: Var[str]
source: Optional[Var[str]] = None

# Metadata to verify the content of the script.
integrity: Var[str]
integrity: Optional[Var[str]] = None

# Whether to allow cross-origin requests.
crossorigin: Var[str]
crossorigin: Optional[Var[str]] = None

# Indicates which referrer to send when fetching the script.
referrer_policy: Var[str]
referrer_policy: Optional[Var[str]] = None

# Whether to asynchronously load the script.
is_async: Var[bool]
is_async: Optional[Var[bool]] = None

# Whether to defer loading the script.
defer: Var[bool]
defer: Optional[Var[bool]] = None
4 changes: 2 additions & 2 deletions reflex/components/base/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class Title(Component):
"""A component that displays the title of the current page."""

tag = "title"
tag: str = "title"

def render(self) -> dict:
"""Render the title component.
Expand All @@ -29,7 +29,7 @@ def render(self) -> dict:
class Meta(Component):
"""A component that displays metadata for the current page."""

tag = "meta"
tag: str = "meta"

# The description of character encoding.
char_set: Optional[str] = None
Expand Down
13 changes: 6 additions & 7 deletions reflex/components/base/script.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Next.js script wrappers and inline script functionality.

https://nextjs.org/docs/app/api-reference/components/script
https://nextjs.org/docs/app/api-reference/components/script.
"""
from __future__ import annotations

from typing import Any, Union
from typing import Any, Optional, Union

from reflex.components.component import Component
from reflex.vars import Var
Expand All @@ -20,12 +19,12 @@ class Script(Component):
HTML <script> tag which does not work when rendering a component.
"""

library = "next/script"
tag = "Script"
is_default = True
library: str = "next/script"
tag: str = "Script"
is_default: bool = True

# Required unless inline script is used
src: Var[str]
src: Optional[Var[str]] = None

# When the script will execute: afterInteractive | beforeInteractive | lazyOnload
strategy: Var[str] = "afterInteractive" # type: ignore
Expand Down
Loading
Loading