Selaa lähdekoodia

Merge branch 'main' into release/reflex-0.7.6

Khaleel Al-Adhami 1 kuukausi sitten
vanhempi
säilyke
8dbd2767e3

+ 4 - 1
.github/workflows/pre-commit.yml

@@ -23,4 +23,7 @@ jobs:
         with:
           python-version: 3.13.2
           run-uv-sync: true
-      - run: uv run pre-commit run --all-files
+      - uses: actions/checkout@v4
+        with:
+          clean: false
+      - run: uv run pre-commit run --all-files --show-diff-on-failure

+ 1 - 1
pyi_hashes.json

@@ -29,7 +29,7 @@
   "reflex/components/el/element.pyi": "06ac2213b062119323291fa66a1ac19e",
   "reflex/components/el/elements/__init__.pyi": "280ed457675f3720e34b560a3f617739",
   "reflex/components/el/elements/base.pyi": "6e533348b5e1a88cf62fbb5a38dbd795",
-  "reflex/components/el/elements/forms.pyi": "dca85624142e170709dbecdbffdff4ee",
+  "reflex/components/el/elements/forms.pyi": "e05f3ed762ea47f37f32550f8b9105e5",
   "reflex/components/el/elements/inline.pyi": "33d9d860e75dd8c4769825127ed363bb",
   "reflex/components/el/elements/media.pyi": "addd6872281d65d44a484358b895432f",
   "reflex/components/el/elements/metadata.pyi": "974a86d9f0662f6fc15a5bb4b3a87862",

+ 6 - 0
reflex/components/el/elements/forms.py

@@ -572,6 +572,12 @@ class Select(BaseHTML):
     # Fired when the select value changes
     on_change: EventHandler[input_event]
 
+    # The controlled value of the select, read only unless used with on_change
+    value: Var[str]
+
+    # The default value of the select when initially rendered
+    default_value: Var[str]
+
 
 AUTO_HEIGHT_JS = """
 const autoHeightOnInput = (e, is_enabled) => {

+ 21 - 0
reflex/utils/decorator.py

@@ -18,6 +18,7 @@ def once(f: Callable[[], T]) -> Callable[[], T]:
     unset = object()
     value: object | T = unset
 
+    @functools.wraps(f)
     def wrapper() -> T:
         nonlocal value
         value = f() if value is unset else value
@@ -26,6 +27,26 @@ def once(f: Callable[[], T]) -> Callable[[], T]:
     return wrapper
 
 
+def once_unless_none(f: Callable[[], T | None]) -> Callable[[], T | None]:
+    """A decorator that calls the function once and caches the result unless it is None.
+
+    Args:
+        f: The function to call.
+
+    Returns:
+        A function that calls the function once and caches the result unless it is None.
+    """
+    value: T | None = None
+
+    @functools.wraps(f)
+    def wrapper() -> T | None:
+        nonlocal value
+        value = f() if value is None else value
+        return value
+
+    return wrapper
+
+
 P = ParamSpec("P")
 
 

+ 13 - 7
reflex/utils/pyi_generator.py

@@ -1250,12 +1250,20 @@ class PyiGenerator:
                         file_parent = file_parent.parent
                         top_dir = top_dir.parent
 
-                (top_dir.parent / "pyi_hashes.json").write_text(
+                pyi_hashes_file = top_dir / "pyi_hashes.json"
+                if not pyi_hashes_file.exists():
+                    while top_dir.parent and not (top_dir / "pyi_hashes.json").exists():
+                        top_dir = top_dir.parent
+                    another_pyi_hashes_file = top_dir / "pyi_hashes.json"
+                    if another_pyi_hashes_file.exists():
+                        pyi_hashes_file = another_pyi_hashes_file
+
+                pyi_hashes_file.write_text(
                     json.dumps(
                         dict(
                             zip(
                                 [
-                                    str(f.relative_to(top_dir.parent))
+                                    str(f.relative_to(pyi_hashes_file.parent))
                                     for f in file_paths
                                 ],
                                 hashes,
@@ -1271,11 +1279,8 @@ class PyiGenerator:
                 file_paths = list(map(Path, file_paths))
                 pyi_hashes_parent = file_paths[0].parent
                 while (
-                    not any(
-                        subfile.name == "pyi_hashes.json"
-                        for subfile in pyi_hashes_parent.iterdir()
-                    )
-                    and pyi_hashes_parent.parent
+                    pyi_hashes_parent.parent
+                    and not (pyi_hashes_parent / "pyi_hashes.json").exists()
                 ):
                     pyi_hashes_parent = pyi_hashes_parent.parent
 
@@ -1288,6 +1293,7 @@ class PyiGenerator:
                         pyi_hashes[str(file_path.relative_to(pyi_hashes_parent))] = (
                             hashed_content
                         )
+
                     pyi_hashes_file.write_text(
                         json.dumps(pyi_hashes, indent=2, sort_keys=True) + "\n"
                     )

+ 82 - 21
reflex/utils/telemetry.py

@@ -9,6 +9,7 @@ import platform
 import warnings
 from contextlib import suppress
 from datetime import datetime, timezone
+from typing import TypedDict
 
 import httpx
 import psutil
@@ -16,6 +17,8 @@ import psutil
 from reflex import constants
 from reflex.config import environment
 from reflex.utils import console
+from reflex.utils.decorator import once_unless_none
+from reflex.utils.exceptions import ReflexError
 from reflex.utils.prerequisites import ensure_reflex_installation_id, get_project_hash
 
 UTC = timezone.utc
@@ -94,15 +97,39 @@ def _raise_on_missing_project_hash() -> bool:
     return not environment.REFLEX_SKIP_COMPILE.get()
 
 
-def _prepare_event(event: str, **kwargs) -> dict:
-    """Prepare the event to be sent to the PostHog server.
+class _Properties(TypedDict):
+    """Properties type for telemetry."""
 
-    Args:
-        event: The event name.
-        kwargs: Additional data to send with the event.
+    distinct_id: int
+    distinct_app_id: int
+    user_os: str
+    user_os_detail: str
+    reflex_version: str
+    python_version: str
+    cpu_count: int
+    memory: int
+    cpu_info: dict
+
+
+class _DefaultEvent(TypedDict):
+    """Default event type for telemetry."""
+
+    api_key: str
+    properties: _Properties
+
+
+class _Event(_DefaultEvent):
+    """Event type for telemetry."""
+
+    event: str
+    timestamp: str
+
+
+def _get_event_defaults() -> _DefaultEvent | None:
+    """Get the default event data.
 
     Returns:
-        The event data.
+        The default event data.
     """
     from reflex.utils.prerequisites import get_cpu_info
 
@@ -113,19 +140,12 @@ def _prepare_event(event: str, **kwargs) -> dict:
         console.debug(
             f"Could not get installation_id or project_hash: {installation_id}, {project_hash}"
         )
-        return {}
-
-    stamp = datetime.now(UTC).isoformat()
+        return None
 
     cpuinfo = get_cpu_info()
 
-    additional_keys = ["template", "context", "detail", "user_uuid"]
-    additional_fields = {
-        key: value for key in additional_keys if (value := kwargs.get(key)) is not None
-    }
     return {
         "api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
-        "event": event,
         "properties": {
             "distinct_id": installation_id,
             "distinct_app_id": project_hash,
@@ -136,13 +156,55 @@ def _prepare_event(event: str, **kwargs) -> dict:
             "cpu_count": get_cpu_count(),
             "memory": get_memory(),
             "cpu_info": dataclasses.asdict(cpuinfo) if cpuinfo else {},
-            **additional_fields,
         },
+    }
+
+
+@once_unless_none
+def get_event_defaults() -> _DefaultEvent | None:
+    """Get the default event data.
+
+    Returns:
+        The default event data.
+    """
+    return _get_event_defaults()
+
+
+def _prepare_event(event: str, **kwargs) -> _Event | None:
+    """Prepare the event to be sent to the PostHog server.
+
+    Args:
+        event: The event name.
+        kwargs: Additional data to send with the event.
+
+    Returns:
+        The event data.
+    """
+    event_data = get_event_defaults()
+    if not event_data:
+        return None
+
+    additional_keys = ["template", "context", "detail", "user_uuid"]
+
+    properties = event_data["properties"]
+
+    for key in additional_keys:
+        if key in properties or key not in kwargs:
+            continue
+
+        properties[key] = kwargs[key]
+
+    stamp = datetime.now(UTC).isoformat()
+
+    return {
+        "api_key": event_data["api_key"],
+        "event": event,
+        "properties": properties,
         "timestamp": stamp,
     }
 
 
-def _send_event(event_data: dict) -> bool:
+def _send_event(event_data: _Event) -> bool:
     try:
         httpx.post(POSTHOG_API_URL, json=event_data)
     except Exception:
@@ -151,7 +213,7 @@ def _send_event(event_data: dict) -> bool:
         return True
 
 
-def _send(event: str, telemetry_enabled: bool | None, **kwargs):
+def _send(event: str, telemetry_enabled: bool | None, **kwargs) -> bool:
     from reflex.config import get_config
 
     # 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):
         if not event_data:
             return False
         return _send_event(event_data)
+    return False
 
 
 def send(event: str, telemetry_enabled: bool | None = None, **kwargs):
@@ -196,8 +259,6 @@ def send_error(error: Exception, context: str):
     Args:
         error: The error to send.
         context: The context of the error (e.g. "frontend" or "backend")
-
-    Returns:
-        Whether the telemetry was sent successfully.
     """
-    return send("error", detail=type(error).__name__, context=context)
+    if isinstance(error, ReflexError):
+        send("error", detail=type(error).__name__, context=context)

+ 12 - 0
reflex/utils/types.py

@@ -996,6 +996,18 @@ def typehint_issubclass(
             for arg in args
         )
 
+    if is_literal(possible_subclass):
+        args = get_args(possible_subclass)
+        return all(
+            _isinstance(
+                arg,
+                possible_superclass,
+                treat_mutable_obj_as_immutable=treat_mutable_superclasss_as_immutable,
+                nested=2,
+            )
+            for arg in args
+        )
+
     # Remove this check when Python 3.10 is the minimum supported version
     if hasattr(types, "UnionType"):
         provided_type_origin = (

+ 1 - 3
tests/units/test_telemetry.py

@@ -36,7 +36,7 @@ def test_send(mocker, event):
     httpx_post_mock = mocker.patch("httpx.post")
 
     # Mock the read_text method of Path
-    pathlib_path_read_text_mock = mocker.patch(
+    mocker.patch(
         "pathlib.Path.read_text",
         return_value='{"project_hash": "78285505863498957834586115958872998605"}',
     )
@@ -45,5 +45,3 @@ def test_send(mocker, event):
 
     telemetry._send(event, telemetry_enabled=True)
     httpx_post_mock.assert_called_once()
-
-    assert pathlib_path_read_text_mock.call_count == 2

+ 4 - 0
tests/units/utils/test_utils.py

@@ -103,6 +103,10 @@ def test_is_generic_alias(cls: type, expected: bool):
         (str, Literal["test", "value", 2, 3], True),
         (int, Literal["test", "value"], False),
         (int, Literal["test", "value", 2, 3], True),
+        (Literal["test", "value"], str, True),
+        (Literal["test", "value", 2, 3], str, False),
+        (Literal["test", "value"], int, False),
+        (Literal["test", "value", 2, 3], int, False),
         *[
             (NoReturn, super_class, True)
             for super_class in [int, float, str, bool, list, dict, object, Any]