-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
don't use _outer_type if we don't have to #4528
base: main
Are you sure you want to change the base?
Conversation
Returns: | ||
The type of the field, if it exists, else None. | ||
""" | ||
type_hints = get_type_hints(cls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_type_hints
is a really slow function, it doesn't make sense to call it multiple times for the same cls
without some kind of caching.
we should profile and see what affect this change may or may not have on component __init__
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea i was thinking the same, we could also be more specific when we use this vs the other one
I get these errors when I test with this code: from typing import Optional
import reflex as rx
class State(rx.State):
foo: rx.Field[Optional[str]] = rx.field(None) # throws a compile time error (see below)
simple: rx.Field[str | None] = rx.field(None) # throws a compile time error (see below)
bar: str | None = "world" # runtime error(see below)
name: rx.Field[str | None] = rx.field(None) # compile time error(see below)
def index() -> rx.Component:
return rx.container(
rx.button("Clear", on_click=lambda: State.set_name(None)),
rx.text("rx.input"),
rx.input(value=State.name, on_change=State.set_name),
rx.text("rx.el.input"),
rx.el.input(value=State.name),
)
app = rx.App()
app.add_page(index)
foo: rx.Field[Optional[str]]
simple: rx.Field[str | None] = rx.field(None) [same as above]
bar: str | None = "world"
name: rx.Field[str | None] = rx.field(None) [same as |
field = cls.__fields__[name] | ||
type_ = field.outer_type_ | ||
if isinstance(type_, ModelField): | ||
type_ = type_.type_ | ||
if ( | ||
not field.required | ||
and field.default is None | ||
and field.default_factory is None | ||
): | ||
# Ensure frontend uses null coalescing when accessing. | ||
type_ = Optional[type_] | ||
return type_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm really hesitant about removing these lines, unless we're sure this is not going to be hit.
@benedikt-bartscher do you know under what circumstances this bit of code gets used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import reflex as rx
class M1(rx.Base):
foo: str = "foo"
class M2(rx.Base):
m1: M1 = None
class State(rx.State):
m2: M2 = M2()
def index() -> rx.Component:
return rx.container(
rx.text(State.m2.m1.foo),
)
app = rx.App()
app.add_page(index)
This is at least one of the cases; default is None
, but the field is not annotated as Optional
(which is incorrect and i'm surprised pydantic doesn't yell), but Reflex still try to be helpful and use "null coalescing" to avoid frontend exceptions.
Again, why pydantic allows the default to not match the annotation and pass validation i'm unsure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is interesting, default
is None
when it's unset and when it's set to None
:(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@masenf sorry, I did not see your question for some reason. But it seems like you found a case.
This one is interesting, default is None when it's unset and when it's set to None :(
@adhami3310 you can use this to determine unset defaults
from pydantic.v1 import BaseModel
class User(BaseModel):
no_default: str | None
none_default: str | None = None
no_default_field = User.__fields__["no_default"]
none_default_field = User.__fields__["none_default"]
print(no_default_field.field_info.default)
print(none_default_field.field_info.default)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL PydanticUndefined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last time i checked, FieldInfo
doesn't contain the type, but I wouldn't be surprised if i missed something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean for default and default factory values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i made the check for default use None, there's little I can do with default_factory returning None (i hope no one thinks that's a good idea)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this is a bad idea?
diff --git a/reflex/state.py b/reflex/state.py
index e7e6bcf3..46ba3133 100644
--- a/reflex/state.py
+++ b/reflex/state.py
@@ -1099,7 +1099,10 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if (
not field.required
and field.default is None
- and field.default_factory is None
+ and (
+ field.default_factory is None
+ or types.default_factory_can_be_none(field.default_factory)
+ )
and not types.is_optional(prop._var_type)
):
# Ensure frontend uses null coalescing when accessing.
diff --git a/reflex/utils/types.py b/reflex/utils/types.py
index b8bcbf2d..60eb456d 100644
--- a/reflex/utils/types.py
+++ b/reflex/utils/types.py
@@ -333,7 +333,10 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
if (
not field.required
and field.default is None
- and field.default_factory is None
+ and (
+ field.default_factory is None
+ or default_factory_can_be_none(field.default_factory)
+ )
):
# Ensure frontend uses null coalescing when accessing.
type_ = Optional[type_]
@@ -893,3 +896,18 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
for provided_arg, accepted_arg in zip(provided_args, accepted_args)
if accepted_arg is not Any
)
+
+
+def default_factory_can_be_none(default_factory: Callable) -> bool:
+ """Check if the default factory can return None.
+
+ Args:
+ default_factory: The default factory to check.
+
+ Returns:
+ Whether the default factory can return None.
+ """
+ type_hints = get_type_hints(default_factory)
+ if hint := type_hints.get("return"):
+ return is_optional(hint)
+ return default_factory() is None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
most default factories are lambdas so i would imagine you're not getting a type hint in most cases, calling it could give you should be a reliable indicator but i'm afraid of side effects or performance costs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something else is going on with this PR too,
DeprecationWarning: mismatched-type-assignment has been deprecated in version 0.6.5 Tried to assign value None of
type <class 'NoneType'> to field State.parent_state of type <class 'reflex.state.BaseState'>. This might lead to
unexpected behavior. It will be completely removed in 0.7.0
This seems to come from setting parent_state
to None in BaseState.__init__
, but it's annotated as an Optional
so passing None
should be legitimate
# The parent state.
parent_state: Optional[BaseState] = None
So this issue is annoying, it's almost impossible to know if something is truly optional or not, one thing we could check is if |
…d tests without rx.Field
got unblocked on pyright, will investigate later to see if it's working |
turns out _outer_type strips out None