133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""Miscellaneous functions for the experimental package."""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import inspect
|
|
import sys
|
|
import threading
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def get_module_path(module_name: str) -> Path | None:
|
|
"""Check if a module exists and return its path.
|
|
|
|
This function searches for a module by navigating through the module hierarchy
|
|
in each path of sys.path, checking for both .py files and packages with __init__.py.
|
|
|
|
Args:
|
|
module_name: The name of the module to search for (e.g., "package.submodule").
|
|
|
|
Returns:
|
|
The path to the module file if found, None otherwise.
|
|
"""
|
|
parts = module_name.split(".")
|
|
|
|
# Check each path in sys.path
|
|
for path in sys.path:
|
|
current_path = Path(path)
|
|
|
|
# Navigate through the module hierarchy
|
|
for i, part in enumerate(parts):
|
|
potential_file = current_path / (part + ".py")
|
|
potential_dir = current_path / part
|
|
|
|
if potential_file.is_file():
|
|
# We encountered a file, but we can't continue deeper
|
|
if i == len(parts) - 1:
|
|
return potential_file
|
|
return None # Can't continue deeper
|
|
if potential_dir.is_dir():
|
|
# It's a package, so we can continue deeper
|
|
current_path = potential_dir
|
|
else:
|
|
break # Path doesn't exist, break out of the loop
|
|
else:
|
|
return current_path / "__init__.py" # Made it through all parts
|
|
|
|
return None
|
|
|
|
|
|
async def run_in_thread(func: Callable) -> Any:
|
|
"""Run a function in a separate thread.
|
|
|
|
To not block the UI event queue, run_in_thread must be inside inside a rx.event(background=True) decorated method.
|
|
|
|
Args:
|
|
func: The non-async function to run.
|
|
|
|
Returns:
|
|
Any: The return value of the function.
|
|
|
|
Raises:
|
|
ValueError: If the function is an async function.
|
|
"""
|
|
if inspect.iscoroutinefunction(func):
|
|
msg = "func must be a non-async function"
|
|
raise ValueError(msg)
|
|
return await asyncio.get_event_loop().run_in_executor(None, func)
|
|
|
|
|
|
# Global lock for thread-safe sys.path manipulation
|
|
_sys_path_lock = threading.RLock()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def with_cwd_in_syspath():
|
|
"""Temporarily add current working directory to sys.path in a thread-safe manner.
|
|
|
|
This context manager temporarily prepends the current working directory to sys.path,
|
|
ensuring that modules in the current directory can be imported. The original sys.path
|
|
is restored when exiting the context.
|
|
|
|
Yields:
|
|
None
|
|
"""
|
|
with _sys_path_lock:
|
|
orig_sys_path = sys.path.copy()
|
|
sys.path.insert(0, str(Path.cwd()))
|
|
try:
|
|
yield
|
|
finally:
|
|
sys.path[:] = orig_sys_path
|
|
|
|
|
|
def preload_color_theme():
|
|
"""Create a script component that preloads the color theme to prevent FOUC.
|
|
|
|
This script runs immediately in the document head before React hydration,
|
|
reading the saved theme from localStorage and applying the correct CSS classes
|
|
to prevent flash of unstyled content.
|
|
|
|
Returns:
|
|
Script: A script component to add to App.head_components
|
|
"""
|
|
from reflex_components_core.el.elements.scripts import Script
|
|
|
|
# Create direct inline script content (like next-themes dangerouslySetInnerHTML)
|
|
script_content = """
|
|
// Only run in browser environment, not during SSR
|
|
if (typeof document !== 'undefined') {
|
|
try {
|
|
const theme = localStorage.getItem("theme") || "system";
|
|
const systemPreference = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
const resolvedTheme = theme === "system" ? systemPreference : theme;
|
|
|
|
// Apply theme immediately - blocks until complete
|
|
// Use classList to avoid overwriting other classes
|
|
document.documentElement.classList.remove("light", "dark");
|
|
document.documentElement.classList.add(resolvedTheme);
|
|
document.documentElement.style.colorScheme = resolvedTheme;
|
|
|
|
} catch (e) {
|
|
// Fallback to system preference on any error (resolve "system" to actual theme)
|
|
const fallbackTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
document.documentElement.classList.remove("light", "dark");
|
|
document.documentElement.classList.add(fallbackTheme);
|
|
document.documentElement.style.colorScheme = fallbackTheme;
|
|
}
|
|
}
|
|
"""
|
|
|
|
return Script.create(script_content)
|