reflex 0.8.0a6__py3-none-any.whl → 0.8.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

reflex/environment.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import concurrent.futures
6
6
  import dataclasses
7
7
  import enum
8
+ import importlib
8
9
  import inspect
9
10
  import multiprocessing
10
11
  import os
@@ -24,6 +25,7 @@ from typing import (
24
25
  )
25
26
 
26
27
  from reflex import constants
28
+ from reflex.plugins import Plugin
27
29
  from reflex.utils.exceptions import EnvironmentVarValueError
28
30
  from reflex.utils.types import GenericType, is_union, value_inside_optional
29
31
 
@@ -126,6 +128,48 @@ def interpret_path_env(value: str, field_name: str) -> Path:
126
128
  return Path(value)
127
129
 
128
130
 
131
+ def interpret_plugin_env(value: str, field_name: str) -> Plugin:
132
+ """Interpret a plugin environment variable value.
133
+
134
+ Args:
135
+ value: The environment variable value.
136
+ field_name: The field name.
137
+
138
+ Returns:
139
+ The interpreted value.
140
+
141
+ Raises:
142
+ EnvironmentVarValueError: If the value is invalid.
143
+ """
144
+ if "." not in value:
145
+ msg = f"Invalid plugin value: {value!r} for {field_name}. Plugin name must be in the format 'package.module.PluginName'."
146
+ raise EnvironmentVarValueError(msg)
147
+
148
+ import_path, plugin_name = value.rsplit(".", 1)
149
+
150
+ try:
151
+ module = importlib.import_module(import_path)
152
+ except ImportError as e:
153
+ msg = f"Failed to import module {import_path!r} for {field_name}: {e}"
154
+ raise EnvironmentVarValueError(msg) from e
155
+
156
+ try:
157
+ plugin_class = getattr(module, plugin_name, None)
158
+ except Exception as e:
159
+ msg = f"Failed to get plugin class {plugin_name!r} from module {import_path!r} for {field_name}: {e}"
160
+ raise EnvironmentVarValueError(msg) from e
161
+
162
+ if not inspect.isclass(plugin_class) or not issubclass(plugin_class, Plugin):
163
+ msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin."
164
+ raise EnvironmentVarValueError(msg)
165
+
166
+ try:
167
+ return plugin_class()
168
+ except Exception as e:
169
+ msg = f"Failed to instantiate plugin {plugin_name!r} for {field_name}: {e}"
170
+ raise EnvironmentVarValueError(msg) from e
171
+
172
+
129
173
  def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
130
174
  """Interpret an enum environment variable value.
131
175
 
@@ -181,6 +225,8 @@ def interpret_env_var_value(
181
225
  return interpret_path_env(value, field_name)
182
226
  if field_type is ExistingPath:
183
227
  return interpret_existing_path_env(value, field_name)
228
+ if field_type is Plugin:
229
+ return interpret_plugin_env(value, field_name)
184
230
  if get_origin(field_type) is list:
185
231
  return [
186
232
  interpret_env_var_value(
@@ -606,3 +652,73 @@ class EnvironmentVariables:
606
652
 
607
653
 
608
654
  environment = EnvironmentVariables()
655
+
656
+ try:
657
+ from dotenv import load_dotenv
658
+ except ImportError:
659
+ load_dotenv = None
660
+
661
+
662
+ def _paths_from_env_files(env_files: str) -> list[Path]:
663
+ """Convert a string of paths separated by os.pathsep into a list of Path objects.
664
+
665
+ Args:
666
+ env_files: The string of paths.
667
+
668
+ Returns:
669
+ A list of Path objects.
670
+ """
671
+ # load env files in reverse order
672
+ return list(
673
+ reversed(
674
+ [
675
+ Path(path)
676
+ for path_element in env_files.split(os.pathsep)
677
+ if (path := path_element.strip())
678
+ ]
679
+ )
680
+ )
681
+
682
+
683
+ def _load_dotenv_from_files(files: list[Path]):
684
+ """Load environment variables from a list of files.
685
+
686
+ Args:
687
+ files: A list of Path objects representing the environment variable files.
688
+ """
689
+ from reflex.utils import console
690
+
691
+ if not files:
692
+ return
693
+
694
+ if load_dotenv is None:
695
+ console.error(
696
+ """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
697
+ )
698
+ return
699
+
700
+ for env_file in files:
701
+ if env_file.exists():
702
+ load_dotenv(env_file, override=True)
703
+
704
+
705
+ def _paths_from_environment() -> list[Path]:
706
+ """Get the paths from the REFLEX_ENV_FILE environment variable.
707
+
708
+ Returns:
709
+ A list of Path objects.
710
+ """
711
+ env_files = os.environ.get("REFLEX_ENV_FILE")
712
+ if not env_files:
713
+ return []
714
+
715
+ return _paths_from_env_files(env_files)
716
+
717
+
718
+ def _load_dotenv_from_env():
719
+ """Load environment variables from paths specified in REFLEX_ENV_FILE."""
720
+ _load_dotenv_from_files(_paths_from_environment())
721
+
722
+
723
+ # Load the env files at import time if they are set in the ENV_FILE environment variable.
724
+ _load_dotenv_from_env()
reflex/event.py CHANGED
@@ -570,7 +570,7 @@ class JavascriptHTMLInputElement:
570
570
  class JavascriptInputEvent:
571
571
  """Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent."""
572
572
 
573
- target: JavascriptHTMLInputElement = JavascriptHTMLInputElement() # noqa: RUF009
573
+ target: JavascriptHTMLInputElement = JavascriptHTMLInputElement()
574
574
 
575
575
 
576
576
  @dataclasses.dataclass(
@@ -1025,6 +1025,22 @@ def set_focus(ref: str) -> EventSpec:
1025
1025
  )
1026
1026
 
1027
1027
 
1028
+ def blur_focus(ref: str) -> EventSpec:
1029
+ """Blur focus of specified ref.
1030
+
1031
+ Args:
1032
+ ref: The ref.
1033
+
1034
+ Returns:
1035
+ An event to blur focus on the ref
1036
+ """
1037
+ return server_side(
1038
+ "_blur_focus",
1039
+ get_fn_signature(blur_focus),
1040
+ ref=LiteralVar.create(format.format_ref(ref)),
1041
+ )
1042
+
1043
+
1028
1044
  def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec:
1029
1045
  """Select the id of a html element for scrolling into view.
1030
1046
 
@@ -2293,6 +2309,7 @@ class EventNamespace:
2293
2309
  back = staticmethod(back)
2294
2310
  window_alert = staticmethod(window_alert)
2295
2311
  set_focus = staticmethod(set_focus)
2312
+ blur_focus = staticmethod(blur_focus)
2296
2313
  scroll_to = staticmethod(scroll_to)
2297
2314
  set_value = staticmethod(set_value)
2298
2315
  remove_cookie = staticmethod(remove_cookie)
reflex/istate/data.py CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  import dataclasses
4
4
  from collections.abc import Mapping
5
+ from typing import TYPE_CHECKING
6
+ from urllib.parse import _NetlocResultMixinStr, urlsplit
5
7
 
6
8
  from reflex import constants
7
- from reflex.utils import format
9
+ from reflex.utils import console, format
8
10
  from reflex.utils.serializers import serializer
9
11
 
10
12
 
@@ -45,36 +47,40 @@ class _HeaderData:
45
47
  )
46
48
 
47
49
 
48
- @dataclasses.dataclass(frozen=True, init=False)
50
+ _HEADER_DATA_FIELDS = frozenset(
51
+ [field.name for field in dataclasses.fields(_HeaderData)]
52
+ )
53
+
54
+
55
+ @dataclasses.dataclass(frozen=True)
49
56
  class HeaderData(_HeaderData):
50
57
  """An object containing headers data."""
51
58
 
52
- def __init__(self, router_data: dict | None = None):
53
- """Initialize the HeaderData object based on router_data.
59
+ @classmethod
60
+ def from_router_data(cls, router_data: dict) -> "HeaderData":
61
+ """Create a HeaderData object from the given router_data.
54
62
 
55
63
  Args:
56
64
  router_data: the router_data dict.
65
+
66
+ Returns:
67
+ A HeaderData object initialized with the provided router_data.
57
68
  """
58
- super().__init__()
59
- if router_data:
60
- fields_names = [f.name for f in dataclasses.fields(self)]
61
- for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items():
62
- snake_case_key = format.to_snake_case(k)
63
- if snake_case_key in fields_names:
64
- object.__setattr__(self, snake_case_key, v)
65
- object.__setattr__(
66
- self,
67
- "raw_headers",
68
- _FrozenDictStrStr(
69
- **{
70
- k: v
71
- for k, v in router_data.get(
72
- constants.RouteVar.HEADERS, {}
73
- ).items()
74
- if v
75
- }
76
- ),
77
- )
69
+ return cls(
70
+ **{
71
+ snake_case_key: v
72
+ for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
73
+ if v
74
+ and (snake_case_key := format.to_snake_case(k)) in _HEADER_DATA_FIELDS
75
+ },
76
+ raw_headers=_FrozenDictStrStr(
77
+ **{
78
+ k: v
79
+ for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
80
+ if v
81
+ }
82
+ ),
83
+ )
78
84
 
79
85
 
80
86
  @serializer(to=dict)
@@ -90,6 +96,35 @@ def serialize_frozen_dict_str_str(obj: _FrozenDictStrStr) -> dict:
90
96
  return dict(obj._data)
91
97
 
92
98
 
99
+ class ReflexURL(str, _NetlocResultMixinStr):
100
+ """A class representing a URL split result."""
101
+
102
+ if TYPE_CHECKING:
103
+ scheme: str
104
+ netloc: str
105
+ path: str
106
+ query: str
107
+ fragment: str
108
+
109
+ def __new__(cls, url: str):
110
+ """Create a new ReflexURL instance.
111
+
112
+ Args:
113
+ url: the URL to split.
114
+
115
+ Returns:
116
+ A new ReflexURL instance.
117
+ """
118
+ (scheme, netloc, path, query, fragment) = urlsplit(url)
119
+ obj = super().__new__(cls, url)
120
+ object.__setattr__(obj, "scheme", scheme)
121
+ object.__setattr__(obj, "netloc", netloc)
122
+ object.__setattr__(obj, "path", path)
123
+ object.__setattr__(obj, "query", query)
124
+ object.__setattr__(obj, "fragment", fragment)
125
+ return obj
126
+
127
+
93
128
  @dataclasses.dataclass(frozen=True)
94
129
  class PageData:
95
130
  """An object containing page data."""
@@ -101,39 +136,30 @@ class PageData:
101
136
  full_raw_path: str = ""
102
137
  params: dict = dataclasses.field(default_factory=dict)
103
138
 
104
- def __init__(self, router_data: dict | None = None):
105
- """Initialize the PageData object based on router_data.
139
+ @classmethod
140
+ def from_router_data(cls, router_data: dict) -> "PageData":
141
+ """Create a PageData object from the given router_data.
106
142
 
107
143
  Args:
108
144
  router_data: the router_data dict.
145
+
146
+ Returns:
147
+ A PageData object initialized with the provided router_data.
109
148
  """
110
- if router_data:
111
- object.__setattr__(
112
- self,
113
- "host",
114
- router_data.get(constants.RouteVar.HEADERS, {}).get("origin", ""),
115
- )
116
- object.__setattr__(
117
- self, "path", router_data.get(constants.RouteVar.PATH, "")
118
- )
119
- object.__setattr__(
120
- self, "raw_path", router_data.get(constants.RouteVar.ORIGIN, "")
121
- )
122
- object.__setattr__(self, "full_path", f"{self.host}{self.path}")
123
- object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
124
- object.__setattr__(
125
- self, "params", router_data.get(constants.RouteVar.QUERY, {})
126
- )
127
- else:
128
- object.__setattr__(self, "host", "")
129
- object.__setattr__(self, "path", "")
130
- object.__setattr__(self, "raw_path", "")
131
- object.__setattr__(self, "full_path", "")
132
- object.__setattr__(self, "full_raw_path", "")
133
- object.__setattr__(self, "params", {})
149
+ host = router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
150
+ path = router_data.get(constants.RouteVar.PATH, "")
151
+ raw_path = router_data.get(constants.RouteVar.ORIGIN, "")
152
+ return cls(
153
+ host=host,
154
+ path=path,
155
+ raw_path=raw_path,
156
+ full_path=f"{host}{path}",
157
+ full_raw_path=f"{host}{raw_path}",
158
+ params=router_data.get(constants.RouteVar.QUERY, {}),
159
+ )
134
160
 
135
161
 
136
- @dataclasses.dataclass(frozen=True, init=False)
162
+ @dataclasses.dataclass(frozen=True)
137
163
  class SessionData:
138
164
  """An object containing session data."""
139
165
 
@@ -141,37 +167,84 @@ class SessionData:
141
167
  client_ip: str = ""
142
168
  session_id: str = ""
143
169
 
144
- def __init__(self, router_data: dict | None = None):
145
- """Initialize the SessionData object based on router_data.
170
+ @classmethod
171
+ def from_router_data(cls, router_data: dict) -> "SessionData":
172
+ """Create a SessionData object from the given router_data.
146
173
 
147
174
  Args:
148
175
  router_data: the router_data dict.
176
+
177
+ Returns:
178
+ A SessionData object initialized with the provided router_data.
149
179
  """
150
- if router_data:
151
- client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
152
- client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
153
- session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
154
- else:
155
- client_token = client_ip = session_id = ""
156
- object.__setattr__(self, "client_token", client_token)
157
- object.__setattr__(self, "client_ip", client_ip)
158
- object.__setattr__(self, "session_id", session_id)
180
+ return cls(
181
+ client_token=router_data.get(constants.RouteVar.CLIENT_TOKEN, ""),
182
+ client_ip=router_data.get(constants.RouteVar.CLIENT_IP, ""),
183
+ session_id=router_data.get(constants.RouteVar.SESSION_ID, ""),
184
+ )
159
185
 
160
186
 
161
- @dataclasses.dataclass(frozen=True, init=False)
187
+ @dataclasses.dataclass(frozen=True)
162
188
  class RouterData:
163
189
  """An object containing RouterData."""
164
190
 
165
191
  session: SessionData = dataclasses.field(default_factory=SessionData)
166
192
  headers: HeaderData = dataclasses.field(default_factory=HeaderData)
167
- page: PageData = dataclasses.field(default_factory=PageData)
193
+ _page: PageData = dataclasses.field(default_factory=PageData)
194
+ url: ReflexURL = dataclasses.field(default=ReflexURL(""))
195
+ route_id: str = ""
168
196
 
169
- def __init__(self, router_data: dict | None = None):
170
- """Initialize the RouterData object.
197
+ @property
198
+ def page(self) -> PageData:
199
+ """Get the page data.
200
+
201
+ Returns:
202
+ The PageData object.
203
+ """
204
+ console.deprecate(
205
+ "RouterData.page",
206
+ "Use RouterData.url instead",
207
+ deprecation_version="0.8.1",
208
+ removal_version="0.9.0",
209
+ )
210
+ return self._page
211
+
212
+ @classmethod
213
+ def from_router_data(cls, router_data: dict) -> "RouterData":
214
+ """Create a RouterData object from the given router_data.
171
215
 
172
216
  Args:
173
217
  router_data: the router_data dict.
218
+
219
+ Returns:
220
+ A RouterData object initialized with the provided router_data.
174
221
  """
175
- object.__setattr__(self, "session", SessionData(router_data))
176
- object.__setattr__(self, "headers", HeaderData(router_data))
177
- object.__setattr__(self, "page", PageData(router_data))
222
+ return cls(
223
+ session=SessionData.from_router_data(router_data),
224
+ headers=HeaderData.from_router_data(router_data),
225
+ _page=PageData.from_router_data(router_data),
226
+ url=ReflexURL(
227
+ router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
228
+ + router_data.get(constants.RouteVar.ORIGIN, "")
229
+ ),
230
+ route_id=router_data.get(constants.RouteVar.PATH, ""),
231
+ )
232
+
233
+
234
+ @serializer(to=dict)
235
+ def serialize_router_data(obj: RouterData) -> dict:
236
+ """Serialize a RouterData object to a dict.
237
+
238
+ Args:
239
+ obj: the RouterData object.
240
+
241
+ Returns:
242
+ A dict representation of the RouterData object.
243
+ """
244
+ return {
245
+ "session": obj.session,
246
+ "headers": obj.headers,
247
+ "page": obj._page,
248
+ "url": obj.url,
249
+ "route_id": obj.route_id,
250
+ }
@@ -17,7 +17,7 @@ class Constants(SimpleNamespace):
17
17
  """Tailwind constants."""
18
18
 
19
19
  # The Tailwindcss version
20
- VERSION = "tailwindcss@4.1.10"
20
+ VERSION = "tailwindcss@4.1.11"
21
21
  # The Tailwind config.
22
22
  CONFIG = "tailwind.config.js"
23
23
  # Default Tailwind content paths
@@ -156,7 +156,7 @@ class TailwindV4Plugin(TailwindPlugin):
156
156
  return [
157
157
  *super().get_frontend_development_dependencies(**context),
158
158
  Constants.VERSION,
159
- "@tailwindcss/postcss@4.1.10",
159
+ "@tailwindcss/postcss@4.1.11",
160
160
  ]
161
161
 
162
162
  def pre_compile(self, **context):
reflex/state.py CHANGED
@@ -1204,7 +1204,7 @@ class BaseState(EvenMoreBasicBaseState):
1204
1204
 
1205
1205
  def argsingle_factory(param: str):
1206
1206
  def inner_func(self: BaseState) -> str:
1207
- return self.router.page.params.get(param, "")
1207
+ return self.router._page.params.get(param, "")
1208
1208
 
1209
1209
  inner_func.__name__ = param
1210
1210
 
@@ -1212,7 +1212,7 @@ class BaseState(EvenMoreBasicBaseState):
1212
1212
 
1213
1213
  def arglist_factory(param: str):
1214
1214
  def inner_func(self: BaseState) -> list[str]:
1215
- return self.router.page.params.get(param, [])
1215
+ return self.router._page.params.get(param, [])
1216
1216
 
1217
1217
  inner_func.__name__ = param
1218
1218
 
@@ -2466,7 +2466,7 @@ class OnLoadInternalState(State):
2466
2466
  """
2467
2467
  # Do not app._compile()! It should be already compiled by now.
2468
2468
  load_events = prerequisites.get_and_validate_app().app.get_load_events(
2469
- self.router.page.path
2469
+ self.router._page.path
2470
2470
  )
2471
2471
  if not load_events:
2472
2472
  self.is_hydrated = True
reflex/utils/exec.py CHANGED
@@ -519,9 +519,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
519
519
 
520
520
  from granian.constants import Interfaces
521
521
  from granian.log import LogLevels
522
- from granian.server import MPServer as Granian
522
+ from granian.server import Server as Granian
523
523
 
524
- Granian(
524
+ from reflex.environment import _paths_from_environment
525
+
526
+ granian_app = Granian(
525
527
  target=get_app_instance_from_file(),
526
528
  factory=True,
527
529
  address=host,
@@ -533,8 +535,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
533
535
  reload_ignore_worker_failure=True,
534
536
  reload_ignore_patterns=HOTRELOAD_IGNORE_PATTERNS,
535
537
  reload_tick=100,
538
+ env_files=_paths_from_environment() or None,
536
539
  workers_kill_timeout=2,
537
- ).serve()
540
+ )
541
+
542
+ granian_app.serve()
538
543
 
539
544
 
540
545
  def run_backend_prod(
@@ -568,15 +573,37 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
568
573
  port: The app port
569
574
  loglevel: The log level.
570
575
  """
576
+ import os
577
+ import shlex
578
+
571
579
  from reflex.utils import processes
572
580
 
573
581
  app_module = get_app_instance()
574
582
 
575
- command = (
576
- ["uvicorn", *("--host", host), *("--port", str(port)), "--factory", app_module]
577
- if constants.IS_WINDOWS
578
- else ["gunicorn", "--preload", *("--bind", f"{host}:{port}"), f"{app_module}()"]
579
- )
583
+ if constants.IS_WINDOWS:
584
+ command = [
585
+ "uvicorn",
586
+ *("--host", host),
587
+ *("--port", str(port)),
588
+ "--factory",
589
+ app_module,
590
+ ]
591
+ else:
592
+ # Parse GUNICORN_CMD_ARGS for user overrides
593
+ env_args = []
594
+ if gunicorn_cmd_args := os.environ.get("GUNICORN_CMD_ARGS", ""):
595
+ env_args = shlex.split(gunicorn_cmd_args)
596
+
597
+ # Our default args, then env args (env args win on conflicts)
598
+ command = [
599
+ "gunicorn",
600
+ "--preload",
601
+ "--worker-class",
602
+ "uvicorn.workers.UvicornH11Worker",
603
+ *("--bind", f"{host}:{port}"),
604
+ *env_args,
605
+ f"{app_module}()",
606
+ ]
580
607
 
581
608
  command += [
582
609
  *("--log-level", loglevel.value),
@@ -27,6 +27,7 @@ def attach(
27
27
  package_name: str,
28
28
  submodules: set[str] | None = None,
29
29
  submod_attrs: dict[str, list[str]] | None = None,
30
+ **extra_mappings,
30
31
  ):
31
32
  """Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach.
32
33
  The lazy loader __getattr__ doesn't support tuples as list values. We needed to add
@@ -39,6 +40,7 @@ def attach(
39
40
  submodules : List of submodules to attach.
40
41
  submod_attrs : Dictionary of submodule -> list of attributes / functions.
41
42
  These attributes are imported as they are used.
43
+ extra_mappings: Additional mappings to resolve lazily.
42
44
 
43
45
  Returns:
44
46
  __getattr__, __dir__, __all__
@@ -60,9 +62,13 @@ def attach(
60
62
  attr: mod for mod, attrs in submod_attrs.items() for attr in attrs
61
63
  }
62
64
 
63
- __all__ = sorted(submodules | attr_to_modules.keys())
65
+ __all__ = sorted([*(submodules | attr_to_modules.keys()), *(extra_mappings or [])])
64
66
 
65
67
  def __getattr__(name: str): # noqa: N807
68
+ if name in extra_mappings:
69
+ submod_path, attr = extra_mappings[name].rsplit(".", 1)
70
+ submod = importlib.import_module(submod_path)
71
+ return getattr(submod, attr)
66
72
  if name in submodules:
67
73
  return importlib.import_module(f"{package_name}.{name}")
68
74
  if name in attr_to_modules:
reflex/utils/misc.py CHANGED
@@ -31,14 +31,13 @@ def get_module_path(module_name: str) -> Path | None:
31
31
  for i, part in enumerate(parts):
32
32
  potential_file = current_path / (part + ".py")
33
33
  potential_dir = current_path / part
34
- potential_init = current_path / part / "__init__.py"
35
34
 
36
35
  if potential_file.is_file():
37
36
  # We encountered a file, but we can't continue deeper
38
37
  if i == len(parts) - 1:
39
38
  return potential_file
40
39
  return None # Can't continue deeper
41
- if potential_dir.is_dir() and potential_init.is_file():
40
+ if potential_dir.is_dir():
42
41
  # It's a package, so we can continue deeper
43
42
  current_path = potential_dir
44
43
  else:
reflex/utils/processes.py CHANGED
@@ -15,6 +15,7 @@ from pathlib import Path
15
15
  from typing import Any, Literal, overload
16
16
 
17
17
  import click
18
+ import rich.markup
18
19
  from redis.exceptions import RedisError
19
20
  from rich.progress import Progress
20
21
 
@@ -61,8 +62,16 @@ def is_process_on_port(port: int) -> bool:
61
62
  Returns:
62
63
  Whether a process is running on the given port.
63
64
  """
65
+ # Test IPv4 localhost (127.0.0.1)
64
66
  with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
65
- return sock.connect_ex(("127.0.0.1", port)) == 0
67
+ ipv4_result = sock.connect_ex(("127.0.0.1", port)) == 0
68
+
69
+ # Test IPv6 localhost (::1)
70
+ with closing(socket.socket(socket.AF_INET6, socket.SOCK_STREAM)) as sock:
71
+ ipv6_result = sock.connect_ex(("::1", port)) == 0
72
+
73
+ # Port is in use if either IPv4 or IPv6 is listening
74
+ return ipv4_result or ipv6_result
66
75
 
67
76
 
68
77
  def change_port(port: int, _type: str) -> int:
@@ -273,7 +282,7 @@ def stream_logs(
273
282
  return
274
283
  try:
275
284
  for line in process.stdout:
276
- console.debug(line, end="", progress=progress)
285
+ console.debug(rich.markup.escape(line), end="", progress=progress)
277
286
  logs.append(line)
278
287
  yield line
279
288
  except ValueError: