Skip to content

Commit c28bdba

Browse files
adhami3310masenf
andauthored
move pydantic, sqlmodel, alembic to optional dependencies (#5438)
* move pydantic, sqlmodel, alembic to optional dependencies * fix greptile comments * and sqlmodel * sqlalchemy * add those guys to dev * fix some of the tests * fix that a bit more strongly * bruh * dang it darglint * fix prerequisites having unused fields * move model registry and sqla_session * rx.Base * remove fastapi dep * add fastapi for tests * some cleanup * guard sqlalchemy inside of import * make tests possible to run without sqlmodel, sqlalchemy, starlette admin, fastapi * make it possible to run without pydantic * delay httpx * do things in a different way * event . event = event * maybe? * add __file__ * remove extra for now * why not both * update pyi hashes * fix uv lock * post merge issues * starlette 47 * move some compat code inside of if statement * update uv.lock * rx.Model no longer inherits from rx.Base (and friends) * Move sqlalchemy-only helpers out of the sqlmodel section * Remove sqlmodel pydantic v2 hacks -- just use pydantic v2 now * Import guard other parts of the code that were importing sqlalchemy * move sqlmodel to pydantic v2 and remove compat code * rx.model: from __future__ import annotations * upgrade --------- Co-authored-by: Masen Furer <[email protected]>
1 parent c923a97 commit c28bdba

38 files changed

+1513
-1338
lines changed

pyi_hashes.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"reflex/__init__.pyi": "cc4f461d8244f0f372b7607eb1edd146",
2+
"reflex/__init__.pyi": "2fa0051c43f2d3d10114283480c666fb",
33
"reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb",
44
"reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a",
55
"reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1",
@@ -23,7 +23,7 @@
2323
"reflex/components/core/window_events.pyi": "76bf03a273a1fbbb3b333e10d5d08c30",
2424
"reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e",
2525
"reflex/components/datadisplay/code.pyi": "b86769987ef4d1cbdddb461be88539fd",
26-
"reflex/components/datadisplay/dataeditor.pyi": "82c652f0679148d8431a0cf645686a50",
26+
"reflex/components/datadisplay/dataeditor.pyi": "35391d4ba147cf20ce4ac7a782066d61",
2727
"reflex/components/datadisplay/shiki_code_block.pyi": "1d53e75b6be0d3385a342e7b3011babd",
2828
"reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc",
2929
"reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4",
@@ -118,5 +118,5 @@
118118
"reflex/components/recharts/general.pyi": "d87ff9b85b2a204be01753690df4fb11",
119119
"reflex/components/recharts/polar.pyi": "ad24bd37c6acc0bc9bd4ac01af3ffe49",
120120
"reflex/components/recharts/recharts.pyi": "c41d19ab67972246c574098929bea7ea",
121-
"reflex/components/sonner/toast.pyi": "1be74a25e344129b80f14165ae1a01f2"
121+
"reflex/components/sonner/toast.pyi": "3c27bad1aaeb5183eaa6a41e77e8d7f0"
122122
}

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ classifiers = [
5050
]
5151

5252
[project.optional-dependencies]
53+
db = [
54+
"alembic >=1.15.2,<2.0",
55+
"pydantic >=1.10.21,<3.0",
56+
"sqlmodel >=0.0.24,<0.1",
57+
]
5358
monitoring = ["pyleak >=0.1.14,<1.0"]
5459

5560
[project.urls]

reflex/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@
323323
"SessionStorage",
324324
],
325325
"middleware": ["middleware", "Middleware"],
326-
"model": ["asession", "session", "Model"],
326+
"model": ["asession", "session", "Model", "ModelRegistry"],
327327
"page": ["page"],
328328
"state": [
329329
"var",

reflex/app.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@
8383
get_hydrate_event,
8484
noop,
8585
)
86-
from reflex.model import Model, get_db_status
8786
from reflex.page import DECORATED_PAGES
8887
from reflex.route import (
8988
get_route_args,
@@ -648,7 +647,7 @@ def __call__(self) -> ASGIApp:
648647

649648
for api_transformer in api_transformers:
650649
if isinstance(api_transformer, Starlette):
651-
# Mount the api to the fastapi app.
650+
# Mount the api to the starlette app.
652651
App._add_cors(api_transformer)
653652
api_transformer.mount("", asgi_app)
654653
asgi_app = api_transformer
@@ -957,6 +956,8 @@ def _setup_admin_dash(self):
957956
try:
958957
from starlette_admin.contrib.sqla.admin import Admin
959958
from starlette_admin.contrib.sqla.view import ModelView
959+
960+
from reflex.model import Model
960961
except ImportError:
961962
return
962963

@@ -1849,6 +1850,8 @@ async def health(_request: Request) -> JSONResponse:
18491850
tasks = []
18501851

18511852
if prerequisites.check_db_used():
1853+
from reflex.model import get_db_status
1854+
18521855
tasks.append(get_db_status())
18531856
if prerequisites.check_redis_used():
18541857
tasks.append(prerequisites.get_redis_status())

reflex/base.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,75 @@
11
"""Define the base Reflex class."""
22

3-
from pydantic.v1 import BaseModel
3+
from importlib.util import find_spec
44

5+
if find_spec("pydantic") and find_spec("pydantic.v1"):
6+
from pydantic.v1 import BaseModel
57

6-
class Base(BaseModel):
7-
"""The base class subclassed by all Reflex classes.
8+
class Base(BaseModel):
9+
"""The base class subclassed by all Reflex classes.
810
9-
This class wraps Pydantic and provides common methods such as
10-
serialization and setting fields.
11+
This class wraps Pydantic and provides common methods such as
12+
serialization and setting fields.
1113
12-
Any data structure that needs to be transferred between the
13-
frontend and backend should subclass this class.
14-
"""
14+
Any data structure that needs to be transferred between the
15+
frontend and backend should subclass this class.
16+
"""
1517

16-
class Config:
17-
"""Pydantic config."""
18+
class Config:
19+
"""Pydantic config."""
1820

19-
arbitrary_types_allowed = True
20-
use_enum_values = True
21-
extra = "allow"
21+
arbitrary_types_allowed = True
22+
use_enum_values = True
23+
extra = "allow"
2224

23-
def json(self) -> str:
24-
"""Convert the object to a json string.
25+
def __init__(self, *args, **kwargs):
26+
"""Initialize the base class.
2527
26-
Returns:
27-
The object as a json string.
28-
"""
29-
from reflex.utils.serializers import serialize
28+
Args:
29+
*args: Positional arguments.
30+
**kwargs: Keyword arguments.
31+
"""
32+
from reflex.utils import console
3033

31-
return self.__config__.json_dumps(
32-
self.dict(),
33-
default=serialize,
34-
)
34+
console.deprecate(
35+
feature_name="rx.Base",
36+
reason="You can subclass from `pydantic.BaseModel` directly instead or use dataclasses if possible.",
37+
deprecation_version="0.8.2",
38+
removal_version="0.9.0",
39+
)
40+
super().__init__(*args, **kwargs)
3541

36-
def set(self, **kwargs: object):
37-
"""Set multiple fields and return the object.
42+
def json(self) -> str:
43+
"""Convert the object to a json string.
3844
39-
Args:
40-
**kwargs: The fields and values to set.
45+
Returns:
46+
The object as a json string.
47+
"""
48+
from reflex.utils.serializers import serialize
4149

42-
Returns:
43-
The object with the fields set.
44-
"""
45-
for key, value in kwargs.items():
46-
setattr(self, key, value)
47-
return self
50+
return self.__config__.json_dumps(
51+
self.dict(),
52+
default=serialize,
53+
)
54+
else:
55+
56+
class PydanticNotFoundFallback:
57+
"""Fallback base class for environments without Pydantic."""
58+
59+
def __init__(self, *args, **kwargs):
60+
"""Initialize the base class.
61+
62+
Args:
63+
*args: Positional arguments.
64+
**kwargs: Keyword arguments.
65+
66+
Raises:
67+
ImportError: As Pydantic is not installed.
68+
"""
69+
msg = (
70+
"Pydantic is not installed. Please install it to use rx.Base."
71+
"You can install it with `pip install pydantic`."
72+
)
73+
raise ImportError(msg)
74+
75+
Base = PydanticNotFoundFallback # pyright: ignore[reportAssignmentType]

reflex/components/datadisplay/dataeditor.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from __future__ import annotations
44

5+
import dataclasses
56
from collections.abc import Mapping, Sequence
67
from enum import Enum
78
from typing import Any, Literal, TypedDict
89

9-
from reflex.base import Base
1010
from reflex.components.component import Component, NoSSRComponent
1111
from reflex.components.literals import LiteralRowMarker
1212
from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
@@ -50,7 +50,8 @@ class GridColumnIcons(Enum):
5050
VideoUri = "video_uri"
5151

5252

53-
class DataEditorTheme(Base):
53+
@dataclasses.dataclass
54+
class DataEditorThemeBase:
5455
"""The theme for the DataEditor component."""
5556

5657
accent_color: str | None = None
@@ -87,6 +88,20 @@ class DataEditorTheme(Base):
8788
text_medium: str | None = None
8889

8990

91+
@dataclasses.dataclass(init=False)
92+
class DataEditorTheme(DataEditorThemeBase):
93+
"""The theme for the DataEditor component."""
94+
95+
def __init__(self, **kwargs: Any):
96+
"""Initialize the DataEditorTheme.
97+
98+
Args:
99+
**kwargs: The keyword arguments to initialize the theme.
100+
"""
101+
kwargs = {format.to_snake_case(k): v for k, v in kwargs.items()}
102+
super().__init__(**kwargs)
103+
104+
90105
class Bounds(TypedDict):
91106
"""The bounds of the group header."""
92107

reflex/components/sonner/toast.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from __future__ import annotations
44

5+
import dataclasses
56
from typing import Any, Literal
67

7-
from reflex.base import Base
88
from reflex.components.component import Component, ComponentNamespace
99
from reflex.components.lucide.icon import Icon
1010
from reflex.components.props import NoExtrasAllowedProps
@@ -35,7 +35,8 @@
3535
)
3636

3737

38-
class ToastAction(Base):
38+
@dataclasses.dataclass
39+
class ToastAction:
3940
"""A toast action that render a button in the toast."""
4041

4142
label: str

reflex/environment.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525

2626
from reflex import constants
27+
from reflex.constants.base import LogLevel
2728
from reflex.plugins import Plugin
2829
from reflex.utils.exceptions import EnvironmentVarValueError
2930
from reflex.utils.types import GenericType, is_union, value_inside_optional
@@ -204,7 +205,8 @@ def interpret_env_var_value(
204205
The interpreted value.
205206
206207
Raises:
207-
ValueError: If the environment variable type is invalid.
208+
ValueError: If the value is invalid.
209+
EnvironmentVarValueError: If the value is invalid for the specific type.
208210
"""
209211
field_type = value_inside_optional(field_type)
210212

@@ -218,6 +220,12 @@ def interpret_env_var_value(
218220
return interpret_boolean_env(value, field_name)
219221
if field_type is str:
220222
return value
223+
if field_type is LogLevel:
224+
loglevel = LogLevel.from_string(value)
225+
if loglevel is None:
226+
msg = f"Invalid log level value: {value} for {field_name}"
227+
raise EnvironmentVarValueError(msg)
228+
return loglevel
221229
if field_type is int:
222230
return interpret_int_env(value, field_name)
223231
if field_type is Path:

reflex/istate/proxy.py

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,16 @@
99
import inspect
1010
import json
1111
from collections.abc import Callable, Sequence
12+
from importlib.util import find_spec
1213
from types import MethodType
1314
from typing import TYPE_CHECKING, Any, SupportsIndex, TypeVar
1415

15-
import pydantic
1616
import wrapt
17-
from pydantic import BaseModel as BaseModelV2
18-
from pydantic.v1 import BaseModel as BaseModelV1
19-
from sqlalchemy.orm import DeclarativeBase
2017

2118
from reflex.base import Base
2219
from reflex.utils import prerequisites
2320
from reflex.utils.exceptions import ImmutableStateError
24-
from reflex.utils.serializers import serializer
21+
from reflex.utils.serializers import can_serialize, serialize, serializer
2522
from reflex.vars.base import Var
2623

2724
if TYPE_CHECKING:
@@ -339,6 +336,34 @@ def mark_dirty(self):
339336
raise NotImplementedError(msg)
340337

341338

339+
if find_spec("pydantic"):
340+
import pydantic
341+
342+
NEVER_WRAP_BASE_ATTRS = set(Base.__dict__) - {"set"} | set(
343+
pydantic.BaseModel.__dict__
344+
)
345+
else:
346+
NEVER_WRAP_BASE_ATTRS = {}
347+
348+
MUTABLE_TYPES = (
349+
list,
350+
dict,
351+
set,
352+
Base,
353+
)
354+
355+
if find_spec("sqlalchemy"):
356+
from sqlalchemy.orm import DeclarativeBase
357+
358+
MUTABLE_TYPES += (DeclarativeBase,)
359+
360+
if find_spec("pydantic"):
361+
from pydantic import BaseModel as BaseModelV2
362+
from pydantic.v1 import BaseModel as BaseModelV1
363+
364+
MUTABLE_TYPES += (BaseModelV1, BaseModelV2)
365+
366+
342367
class MutableProxy(wrapt.ObjectProxy):
343368
"""A proxy for a mutable object that tracks changes."""
344369

@@ -371,11 +396,6 @@ class MutableProxy(wrapt.ObjectProxy):
371396
"setdefault",
372397
}
373398

374-
# These internal attributes on rx.Base should NOT be wrapped in a MutableProxy.
375-
__never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set(
376-
pydantic.BaseModel.__dict__
377-
)
378-
379399
# Dynamically generated classes for tracking dataclass mutations.
380400
__dataclass_proxies__: dict[type, type] = {}
381401

@@ -539,7 +559,7 @@ def __getattr__(self, __name: str) -> Any:
539559

540560
if (
541561
isinstance(self.__wrapped__, Base)
542-
and __name not in self.__never_wrap_base_attrs__
562+
and __name not in NEVER_WRAP_BASE_ATTRS
543563
and hasattr(value, "__func__")
544564
):
545565
# Wrap methods called on Base subclasses, which might do _anything_
@@ -669,7 +689,10 @@ def serialize_mutable_proxy(mp: MutableProxy):
669689
Returns:
670690
The wrapped object.
671691
"""
672-
return mp.__wrapped__
692+
obj = mp.__wrapped__
693+
if can_serialize(type(obj)):
694+
return serialize(obj)
695+
return obj
673696

674697

675698
_orig_json_encoder_default = json.JSONEncoder.default
@@ -739,18 +762,6 @@ def _mark_dirty(
739762
)
740763

741764

742-
# These types will be wrapped in MutableProxy
743-
MUTABLE_TYPES = (
744-
list,
745-
dict,
746-
set,
747-
Base,
748-
DeclarativeBase,
749-
BaseModelV2,
750-
BaseModelV1,
751-
)
752-
753-
754765
@functools.lru_cache(maxsize=1024)
755766
def is_mutable_type(type_: type) -> bool:
756767
"""Check if a type is mutable and should be wrapped.

0 commit comments

Comments
 (0)