Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions news/+pydantic-optional.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`pip install reflex` no longer installs `pydantic`; pydantic model support activates when it is installed. Use the new `reflex[pydantic]` extra (or `reflex[db]`) to keep it.
1 change: 1 addition & 0 deletions packages/reflex-base/news/+pydantic-optional.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`pydantic` is no longer a hard dependency; pydantic model support activates when it is installed. Use the `reflex-base[pydantic]` extra (or `reflex[db]`) to keep it.
4 changes: 3 additions & 1 deletion packages/reflex-base/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }]
requires-python = ">=3.10"
dependencies = [
"packaging >=24.2,<27",
"pydantic >=2.12.0,<3.0",
"rich >=13,<15",
"typing_extensions >=4.13.0",
"platformdirs >=4.3.7,<5.0",
]

[project.optional-dependencies]
pydantic = ["pydantic >=2.12.0,<3.0"]

[tool.hatch.version]
source = "uv-dynamic-versioning"

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ db = [
"pydantic >=2.12.0,<3.0",
"sqlmodel >=0.0.24,<0.1",
]
pydantic = ["reflex-base[pydantic]"]

[project.urls]
homepage = "https://reflex.dev"
Expand Down
73 changes: 73 additions & 0 deletions tests/units/test_optional_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Verify reflex imports and works when pydantic is not installed."""

import subprocess
import sys

# Runs in a subprocess with pydantic (and the db extras that require it)
# blocked, mirroring the "without db dependencies" CI job.
_SCRIPT = """
import sys

BLOCKED = ("pydantic", "sqlmodel", "alembic", "sqlalchemy")


class _Filter:
# Hide blocked packages from every finder so both ``import`` and
# ``find_spec`` see them as absent.
def __init__(self, inner):
self.inner = inner

def find_spec(self, name, path=None, target=None):
if name.partition(".")[0] in BLOCKED:
return None
finder = getattr(self.inner, "find_spec", None)
return finder(name, path, target) if finder is not None else None


sys.meta_path = [_Filter(f) for f in sys.meta_path]

# find_spec consults sys.modules before meta_path — purge anything a .pth or
# startup hook may have imported eagerly.
for mod in [m for m in sys.modules if m.partition(".")[0] in BLOCKED]:
del sys.modules[mod]

from importlib.util import find_spec

assert find_spec("pydantic") is None
Comment thread
greptile-apps[bot] marked this conversation as resolved.

import datetime

import reflex as rx
from reflex_base.utils import serializers


class S(rx.State):
val: rx.Field[str] = rx.field("hello")


comp = rx.text(S.val)
assert "val" in str(comp.render())

assert serializers.serialize(datetime.datetime(2026, 1, 2, 3, 4, 5)) == "2026-01-02 03:04:05"
assert not any(
getattr(cls, "__module__", "").startswith("pydantic")
for cls in serializers.SERIALIZERS
)

print("OK")
"""


def test_reflex_works_without_pydantic() -> None:
"""Core import, state, component render, and serializers work without pydantic."""
result = subprocess.run(
[sys.executable, "-c", _SCRIPT],
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert result.returncode == 0, (
f"reflex failed without pydantic:\nstdout: {result.stdout}\nstderr: {result.stderr}"
)
assert "OK" in result.stdout
15 changes: 12 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading