Skip to content

Commit 8dbd276

Browse files
committed
Merge branch 'main' into release/reflex-0.7.6
2 parents 45c9cef + 216552d commit 8dbd276

File tree

9 files changed

+144
-33
lines changed

9 files changed

+144
-33
lines changed

.github/workflows/pre-commit.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ jobs:
2323
with:
2424
python-version: 3.13.2
2525
run-uv-sync: true
26-
- run: uv run pre-commit run --all-files
26+
- uses: actions/checkout@v4
27+
with:
28+
clean: false
29+
- run: uv run pre-commit run --all-files --show-diff-on-failure

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"reflex/components/el/element.pyi": "06ac2213b062119323291fa66a1ac19e",
3030
"reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
3131
"reflex/components/el/elements/base.pyi": "6e533348b5e1a88cf62fbb5a38dbd795",
32-
"reflex/components/el/elements/forms.pyi": "dca85624142e170709dbecdbffdff4ee",
32+
"reflex/components/el/elements/forms.pyi": "e05f3ed762ea47f37f32550f8b9105e5",
3333
"reflex/components/el/elements/inline.pyi": "33d9d860e75dd8c4769825127ed363bb",
3434
"reflex/components/el/elements/media.pyi": "addd6872281d65d44a484358b895432f",
3535
"reflex/components/el/elements/metadata.pyi": "974a86d9f0662f6fc15a5bb4b3a87862",

reflex/components/el/elements/forms.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,12 @@ class Select(BaseHTML):
572572
# Fired when the select value changes
573573
on_change: EventHandler[input_event]
574574

575+
# The controlled value of the select, read only unless used with on_change
576+
value: Var[str]
577+
578+
# The default value of the select when initially rendered
579+
default_value: Var[str]
580+
575581

576582
AUTO_HEIGHT_JS = """
577583
const autoHeightOnInput = (e, is_enabled) => {

reflex/utils/decorator.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def once(f: Callable[[], T]) -> Callable[[], T]:
1818
unset = object()
1919
value: object | T = unset
2020

21+
@functools.wraps(f)
2122
def wrapper() -> T:
2223
nonlocal value
2324
value = f() if value is unset else value
@@ -26,6 +27,26 @@ def wrapper() -> T:
2627
return wrapper
2728

2829

30+
def once_unless_none(f: Callable[[], T | None]) -> Callable[[], T | None]:
31+
"""A decorator that calls the function once and caches the result unless it is None.
32+
33+
Args:
34+
f: The function to call.
35+
36+
Returns:
37+
A function that calls the function once and caches the result unless it is None.
38+
"""
39+
value: T | None = None
40+
41+
@functools.wraps(f)
42+
def wrapper() -> T | None:
43+
nonlocal value
44+
value = f() if value is None else value
45+
return value
46+
47+
return wrapper
48+
49+
2950
P = ParamSpec("P")
3051

3152

reflex/utils/pyi_generator.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,12 +1250,20 @@ def scan_all(
12501250
file_parent = file_parent.parent
12511251
top_dir = top_dir.parent
12521252

1253-
(top_dir.parent / "pyi_hashes.json").write_text(
1253+
pyi_hashes_file = top_dir / "pyi_hashes.json"
1254+
if not pyi_hashes_file.exists():
1255+
while top_dir.parent and not (top_dir / "pyi_hashes.json").exists():
1256+
top_dir = top_dir.parent
1257+
another_pyi_hashes_file = top_dir / "pyi_hashes.json"
1258+
if another_pyi_hashes_file.exists():
1259+
pyi_hashes_file = another_pyi_hashes_file
1260+
1261+
pyi_hashes_file.write_text(
12541262
json.dumps(
12551263
dict(
12561264
zip(
12571265
[
1258-
str(f.relative_to(top_dir.parent))
1266+
str(f.relative_to(pyi_hashes_file.parent))
12591267
for f in file_paths
12601268
],
12611269
hashes,
@@ -1271,11 +1279,8 @@ def scan_all(
12711279
file_paths = list(map(Path, file_paths))
12721280
pyi_hashes_parent = file_paths[0].parent
12731281
while (
1274-
not any(
1275-
subfile.name == "pyi_hashes.json"
1276-
for subfile in pyi_hashes_parent.iterdir()
1277-
)
1278-
and pyi_hashes_parent.parent
1282+
pyi_hashes_parent.parent
1283+
and not (pyi_hashes_parent / "pyi_hashes.json").exists()
12791284
):
12801285
pyi_hashes_parent = pyi_hashes_parent.parent
12811286

@@ -1288,6 +1293,7 @@ def scan_all(
12881293
pyi_hashes[str(file_path.relative_to(pyi_hashes_parent))] = (
12891294
hashed_content
12901295
)
1296+
12911297
pyi_hashes_file.write_text(
12921298
json.dumps(pyi_hashes, indent=2, sort_keys=True) + "\n"
12931299
)

reflex/utils/telemetry.py

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import warnings
1010
from contextlib import suppress
1111
from datetime import datetime, timezone
12+
from typing import TypedDict
1213

1314
import httpx
1415
import psutil
1516

1617
from reflex import constants
1718
from reflex.config import environment
1819
from reflex.utils import console
20+
from reflex.utils.decorator import once_unless_none
21+
from reflex.utils.exceptions import ReflexError
1922
from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
2023

2124
UTC = timezone.utc
@@ -94,15 +97,39 @@ def _raise_on_missing_project_hash() -> bool:
9497
return not environment.REFLEX_SKIP_COMPILE.get()
9598

9699

97-
def _prepare_event(event: str, **kwargs) -> dict:
98-
"""Prepare the event to be sent to the PostHog server.
100+
class _Properties(TypedDict):
101+
"""Properties type for telemetry."""
99102

100-
Args:
101-
event: The event name.
102-
kwargs: Additional data to send with the event.
103+
distinct_id: int
104+
distinct_app_id: int
105+
user_os: str
106+
user_os_detail: str
107+
reflex_version: str
108+
python_version: str
109+
cpu_count: int
110+
memory: int
111+
cpu_info: dict
112+
113+
114+
class _DefaultEvent(TypedDict):
115+
"""Default event type for telemetry."""
116+
117+
api_key: str
118+
properties: _Properties
119+
120+
121+
class _Event(_DefaultEvent):
122+
"""Event type for telemetry."""
123+
124+
event: str
125+
timestamp: str
126+
127+
128+
def _get_event_defaults() -> _DefaultEvent | None:
129+
"""Get the default event data.
103130
104131
Returns:
105-
The event data.
132+
The default event data.
106133
"""
107134
from reflex.utils.prerequisites import get_cpu_info
108135

@@ -113,19 +140,12 @@ def _prepare_event(event: str, **kwargs) -> dict:
113140
console.debug(
114141
f"Could not get installation_id or project_hash: {installation_id}, {project_hash}"
115142
)
116-
return {}
117-
118-
stamp = datetime.now(UTC).isoformat()
143+
return None
119144

120145
cpuinfo = get_cpu_info()
121146

122-
additional_keys = ["template", "context", "detail", "user_uuid"]
123-
additional_fields = {
124-
key: value for key in additional_keys if (value := kwargs.get(key)) is not None
125-
}
126147
return {
127148
"api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
128-
"event": event,
129149
"properties": {
130150
"distinct_id": installation_id,
131151
"distinct_app_id": project_hash,
@@ -136,13 +156,55 @@ def _prepare_event(event: str, **kwargs) -> dict:
136156
"cpu_count": get_cpu_count(),
137157
"memory": get_memory(),
138158
"cpu_info": dataclasses.asdict(cpuinfo) if cpuinfo else {},
139-
**additional_fields,
140159
},
160+
}
161+
162+
163+
@once_unless_none
164+
def get_event_defaults() -> _DefaultEvent | None:
165+
"""Get the default event data.
166+
167+
Returns:
168+
The default event data.
169+
"""
170+
return _get_event_defaults()
171+
172+
173+
def _prepare_event(event: str, **kwargs) -> _Event | None:
174+
"""Prepare the event to be sent to the PostHog server.
175+
176+
Args:
177+
event: The event name.
178+
kwargs: Additional data to send with the event.
179+
180+
Returns:
181+
The event data.
182+
"""
183+
event_data = get_event_defaults()
184+
if not event_data:
185+
return None
186+
187+
additional_keys = ["template", "context", "detail", "user_uuid"]
188+
189+
properties = event_data["properties"]
190+
191+
for key in additional_keys:
192+
if key in properties or key not in kwargs:
193+
continue
194+
195+
properties[key] = kwargs[key]
196+
197+
stamp = datetime.now(UTC).isoformat()
198+
199+
return {
200+
"api_key": event_data["api_key"],
201+
"event": event,
202+
"properties": properties,
141203
"timestamp": stamp,
142204
}
143205

144206

145-
def _send_event(event_data: dict) -> bool:
207+
def _send_event(event_data: _Event) -> bool:
146208
try:
147209
httpx.post(POSTHOG_API_URL, json=event_data)
148210
except Exception:
@@ -151,7 +213,7 @@ def _send_event(event_data: dict) -> bool:
151213
return True
152214

153215

154-
def _send(event: str, telemetry_enabled: bool | None, **kwargs):
216+
def _send(event: str, telemetry_enabled: bool | None, **kwargs) -> bool:
155217
from reflex.config import get_config
156218

157219
# Get the telemetry_enabled from the config if it is not specified.
@@ -167,6 +229,7 @@ def _send(event: str, telemetry_enabled: bool | None, **kwargs):
167229
if not event_data:
168230
return False
169231
return _send_event(event_data)
232+
return False
170233

171234

172235
def send(event: str, telemetry_enabled: bool | None = None, **kwargs):
@@ -196,8 +259,6 @@ def send_error(error: Exception, context: str):
196259
Args:
197260
error: The error to send.
198261
context: The context of the error (e.g. "frontend" or "backend")
199-
200-
Returns:
201-
Whether the telemetry was sent successfully.
202262
"""
203-
return send("error", detail=type(error).__name__, context=context)
263+
if isinstance(error, ReflexError):
264+
send("error", detail=type(error).__name__, context=context)

reflex/utils/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,18 @@ def typehint_issubclass(
996996
for arg in args
997997
)
998998

999+
if is_literal(possible_subclass):
1000+
args = get_args(possible_subclass)
1001+
return all(
1002+
_isinstance(
1003+
arg,
1004+
possible_superclass,
1005+
treat_mutable_obj_as_immutable=treat_mutable_superclasss_as_immutable,
1006+
nested=2,
1007+
)
1008+
for arg in args
1009+
)
1010+
9991011
# Remove this check when Python 3.10 is the minimum supported version
10001012
if hasattr(types, "UnionType"):
10011013
provided_type_origin = (

tests/units/test_telemetry.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_send(mocker, event):
3636
httpx_post_mock = mocker.patch("httpx.post")
3737

3838
# Mock the read_text method of Path
39-
pathlib_path_read_text_mock = mocker.patch(
39+
mocker.patch(
4040
"pathlib.Path.read_text",
4141
return_value='{"project_hash": "78285505863498957834586115958872998605"}',
4242
)
@@ -45,5 +45,3 @@ def test_send(mocker, event):
4545

4646
telemetry._send(event, telemetry_enabled=True)
4747
httpx_post_mock.assert_called_once()
48-
49-
assert pathlib_path_read_text_mock.call_count == 2

tests/units/utils/test_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ def test_is_generic_alias(cls: type, expected: bool):
103103
(str, Literal["test", "value", 2, 3], True),
104104
(int, Literal["test", "value"], False),
105105
(int, Literal["test", "value", 2, 3], True),
106+
(Literal["test", "value"], str, True),
107+
(Literal["test", "value", 2, 3], str, False),
108+
(Literal["test", "value"], int, False),
109+
(Literal["test", "value", 2, 3], int, False),
106110
*[
107111
(NoReturn, super_class, True)
108112
for super_class in [int, float, str, bool, list, dict, object, Any]

0 commit comments

Comments
 (0)